Menyembunyikan Nama Fungsi dari Analisis LLM
Masalahnya
Anda mengaktifkan vmObfuscation: true, menjalankannya pada berkas yang mengandung fungsi seperti validateLicense, dan menyadari bahwa
keluaran hasil obfuskasi masih mengandung teks harfiah validateLicense - tubuhnya hilang, digantikan oleh bytecode, tetapi
namanya sendiri terpampang jelas di sana.
// Input
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
// Output - name is preserved, body is bytecode
function validateLicense(b) {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
}
Kalikan hal itu di seluruh basis kode nyata dan Anda akan mendapatkan daftar nama fungsi seperti validateLicense, decryptPayload,
processPayment, checkSubscription. Sebuah LLM tidak perlu memecahkan bytecode untuk memahami apa yang dilakukan program -
nama-nama itu saja sudah cukup baginya untuk menghasilkan ringkasan perilaku modul yang percaya diri dan akurat. Bytecode-nya
opak; daftar isinya tidak.
Mengapa obfuskasi VM mempertahankan nama-nama ini
Dengan vmTargetFunctionsMode: 'root' (bawaan), obfuscator mentransformasi tubuh setiap fungsi tingkat akar
menjadi bytecode VM tetapi sengaja membiarkan namanya. Deklarasi fungsi tingkat akar, secara semantik, adalah
sebuah binding pada scope di sekitarnya - untuk sebuah skrip itu berarti objek global, untuk sebuah modul itu berarti namespace
modul. Obfuscator tidak dapat mengganti namanya dengan aman karena ia tidak punya cara untuk mengetahui siapa lagi yang merujuknya: bundel lain,
sebuah <script> inline, sebuah atribut HTML onclick="validateLicense(...)", pencarian dinamis window['validateLicense'],
dan sebagainya.
Jadi konsekuensi yang diambil oleh mode bawaan adalah: lindungi implementasinya, pertahankan permukaan publiknya. Itu menjaga integrasi tetap utuh, tetapi juga berarti sebuah LLM mendapatkan indeks gratis dari setiap titik masuk.
Mengapa ini penting untuk rekayasa balik yang dibantu LLM
Seorang penyerang manusia yang dihadapkan pada beberapa ratus baris dispatch bytecode biasanya akan menyerah. Sebuah LLM yang diberi berkas yang sama tidak akan repot menyerang bytecode-nya sama sekali - ia akan membaca nama-namanya, memeriksa silang beberapa literal string yang dapat dilihatnya, dan mengeluarkan sesuatu seperti:
Ringkasan itu sudah cukup bagi penyerang untuk merencanakan pengelabuan yang tertarget tanpa pernah menyentuh VM. Nama-nama itulah kebocorannya.
Perbaikannya: bungkus kode Anda dalam IIFE
Cara paling sederhana dan paling andal untuk menghilangkan kebocoran ini adalah mendorong fungsi-fungsi sensitif Anda satu tingkat lebih dalam ke dalam pohon scope. Fungsi yang dideklarasikan di dalam fungsi lain bukan berada di tingkat akar, sehingga obfuscator bebas mengganti namanya dan menyerap deklarasinya ke dalam bytecode seperti pernyataan lainnya.
Sebuah IIFE (Immediately-Invoked Function Expression) adalah cara paling ringan untuk melakukannya - ia menambahkan satu fungsi pembungkus yang berjalan sekali dan tidak mengekspos apa pun berdasarkan nama.
Sebelum - nama terekspos
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
Setelah obfuskasi VM, baik validateLicense maupun checkExpiry tetap bertahan berdasarkan nama di keluaran.
Sesudah - nama disembunyikan di balik IIFE
(function () {
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
})();
Kini kedua deklarasi fungsi tersebut berada di dalam tubuh IIFE. IIFE itu sendiri adalah satu-satunya konstruksi tingkat akar, dan sebuah IIFE anonim tidak punya nama untuk dibocorkan. Setelah obfuskasi VM, seluruh tubuh - termasuk setiap deklarasi di dalamnya - diubah menjadi bytecode dan dienkode; tidak ada apa pun di dalam IIFE yang bertahan sebagai teks yang terbaca.
Bagaimana jika sebuah fungsi benar-benar harus menjadi global?
Terkadang sebuah fungsi memang merupakan titik masuk publik - event handler inline, callback JSONP, sebuah hook SDK pihak ketiga. Anda punya dua pilihan:
Ekspos sebuah trampolin tipis, simpan logikanya di dalam IIFE. Deklarasikan pembungkus global kecil yang tugasnya hanya memanggil implementasi yang ber-scope IIFE. Nama trampolin tetap bocor, tetapi tidak membawa informasi semantik - beri nama seperti
__entry1atau semacamnya - dan seluruh logika yang bermakna tetap tersembunyi.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token).Tulis ulang call site yang tidak Anda kendalikan. Jika global tersebut hanya ada karena sebuah
onclick="validateLicense(...)"inline membutuhkannya, ganti handler inline tersebut denganaddEventListenerdari dalam IIFE. HTML berhenti menyebut nama fungsi tersebut, fungsi tersebut berhenti perlu menjadi global, dan kebocorannya hilang sepenuhnya.
Inisialisator variabel tingkat atas: vmWrapTopLevelInitializers
Deklarasi fungsi bukan satu-satunya hal yang berada di akar sebuah berkas. Inisialisator variabel tingkat atas - konstanta
string, objek konfigurasi, tabel pencarian - sama-sama terbaca di keluaran secara bawaan. Sebuah baris seperti
const API_BASE = '/api/v2/license' memberi tahu LLM sebanyak yang diberikan function validateLicense.
Opsi vmWrapTopLevelInitializers (boolean, bawaan false) membungkus inisialisator tingkat atas yang memenuhi syarat di dalam IIFE sehingga
nilainya sendiri dihitung oleh bytecode VM saat runtime alih-alih terpampang di sumber sebagai literal.
Tanpa opsi tersebut
// Input
const MY_STRING = 'my-string';
// Output - string is visible
const MY_STRING = 'my-string';
Dengan vmWrapTopLevelInitializers: true
// Input
const MY_STRING = 'my-string';
// Output - initializer is now a VM call, the string lives inside bytecode
const MY_STRING = (() => {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
})();
Nama binding-nya (MY_STRING) tetap di tingkat akar dengan alasan yang sama seperti nama fungsi - sesuatu di luar berkas
mungkin merujuknya - tetapi nilai yang dipegangnya kini dihasilkan oleh VM dan tidak lagi muncul sebagai teks yang terbaca.
Ketika ini belum cukup
- Nama yang diimpor dari modul lain. Jika Anda membundel beberapa berkas dan satu modul mengekspor
validateLicenseuntuk diimpor modul lain, bundler akan tetap membuat nama tersebut terlihat di keluaran bundel dengan cara yang sama seperti fungsi tingkat akar terlihat. Bungkus bundel itu sendiri dalam IIFE (sebagian besar bundler dapat melakukan ini), atau pindahkan ekspor tersebut ke dalam IIFE dan ekspos ulang melalui trampolin yang tak bermakna.
