Ukrywanie nazw funkcji przed analizą LLM
Problem
Po włączeniu vmObfuscation: true i uruchomieniu obfuskacji na pliku zawierającym funkcję taką jak validateLicense
można zauważyć, że zobfuskowany kod wynikowy nadal zawiera dosłowny tekst validateLicense — ciało funkcji zniknęło,
zastąpione kodem bajtowym, ale sama nazwa pozostaje widoczna jak na dłoni.
// 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);
}
Pomnóż to przez rzeczywistą bazę kodu, a otrzymasz listę nazw funkcji takich jak validateLicense, decryptPayload,
processPayment, checkSubscription. Model LLM nie musi łamać kodu bajtowego, aby zrozumieć, co robi program —
same nazwy wystarczą, by wygenerował pewne i trafne podsumowanie działania modułu. Kod bajtowy jest
nieprzejrzysty; spis treści już nie.
Dlaczego obfuskacja VM zachowuje te nazwy
Przy vmTargetFunctionsMode: 'root' (wartość domyślna) obfuskator przekształca ciało każdej funkcji najwyższego
poziomu w kod bajtowy VM, ale celowo pozostawia nazwę bez zmian. Deklaracja funkcji najwyższego poziomu jest
semantycznie powiązaniem w otaczającym zasięgu — dla skryptu oznacza to obiekt globalny, dla modułu przestrzeń nazw
modułu. Obfuskator nie może jej bezpiecznie zmienić, ponieważ nie ma jak ustalić, kto jeszcze się do niej odwołuje: inna
paczka, wbudowany <script>, atrybut HTML onclick="validateLicense(...)", dynamiczne wyszukiwanie
window['validateLicense'] itd.
Kompromis przyjęty przez ustawienie domyślne brzmi zatem: chronić implementację, zachować publiczną powierzchnię. Dzięki temu integracja pozostaje nienaruszona, ale oznacza to również, że model LLM dostaje darmowy indeks każdego punktu wejścia.
Dlaczego ma to znaczenie dla inżynierii wstecznej wspieranej przez LLM
Człowiek-atakujący postawiony przed kilkuset liniami dyspozytora kodu bajtowego zwykle się poddaje. Model LLM, któremu poda się ten sam plik, w ogóle nie będzie próbował atakować kodu bajtowego — przeczyta nazwy, powiąże z nimi nieliczne widoczne literały tekstowe i wygeneruje coś w rodzaju:
To podsumowanie wystarczy atakującemu, by zaplanować ukierunkowane obejście, nawet nie dotykając VM. To nazwy są przeciekiem.
Rozwiązanie: opakuj kod w IIFE
Najprostszym i najbardziej niezawodnym sposobem na usunięcie tego przecieku jest przesunięcie wrażliwych funkcji o jeden poziom głębiej w drzewie zasięgów. Funkcje zadeklarowane wewnątrz innej funkcji nie są funkcjami najwyższego poziomu, więc obfuskator może swobodnie zmieniać ich nazwy i wchłaniać ich deklaracje do kodu bajtowego, tak jak każdą inną instrukcję.
IIFE (Immediately-Invoked Function Expression) jest najlżejszym sposobem, by to osiągnąć — dodaje pojedynczą funkcję opakowującą, która uruchamia się raz i nie udostępnia niczego pod nazwą.
Przed — nazwy odsłonięte
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();
}
});
Po obfuskacji VM zarówno validateLicense, jak i checkExpiry przetrwają w kodzie wynikowym pod swoimi nazwami.
Po — nazwy ukryte za 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();
}
});
})();
Teraz obie deklaracje funkcji znajdują się wewnątrz ciała IIFE. Samo IIFE jest jedyną konstrukcją najwyższego poziomu, a anonimowe IIFE nie ma nazwy, którą mogłoby wyciec. Po obfuskacji VM całe ciało — wraz z każdą znajdującą się w nim deklaracją — jest przekształcane w kod bajtowy i zakodowane; nic wewnątrz IIFE nie przetrwa jako czytelny tekst.
Co jeśli funkcja naprawdę musi być globalna?
Czasem funkcja rzeczywiście jest publicznym punktem wejścia — wbudowanym obsługiwaczem zdarzeń, wywołaniem zwrotnym JSONP, hookiem SDK innej firmy. Masz dwie możliwości:
Udostępnij cienki trampolinę, a logikę zostaw wewnątrz IIFE. Zadeklaruj mały globalny wrapper, którego jedynym zadaniem jest wywołanie implementacji o zasięgu IIFE. Nazwa trampoliny nadal wycieka, ale nie niesie żadnej informacji semantycznej — nazwij ją
__entry1lub podobnie — a cała istotna logika pozostaje ukryta.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token).Przepisz miejsce wywołania, nad którym nie masz kontroli. Jeśli zmienna globalna istnieje tylko dlatego, że wymaga jej wbudowane
onclick="validateLicense(...)", zastąp wbudowany obsługiwacz przezaddEventListenerwywoływane z wnętrza IIFE. HTML przestaje nazywać funkcję, funkcja przestaje musieć być globalna, a przeciek znika całkowicie.
Inicjalizatory zmiennych najwyższego poziomu: vmWrapTopLevelInitializers
Deklaracje funkcji nie są jedyną rzeczą, która żyje w korzeniu pliku. Inicjalizatory zmiennych najwyższego poziomu —
stałe tekstowe, obiekty konfiguracyjne, tablice odwzorowań — są domyślnie równie czytelne w kodzie wynikowym. Linia taka
jak const API_BASE = '/api/v2/license' mówi modelowi LLM tyle samo, co function validateLicense.
Opcja vmWrapTopLevelInitializers (typu logicznego, domyślnie false) opakowuje kwalifikujące się inicjalizatory
najwyższego poziomu w IIFE, dzięki czemu sama wartość jest obliczana przez kod bajtowy VM w czasie wykonania, zamiast
znajdować się w kodzie źródłowym jako literał.
Bez tej opcji
// Input
const MY_STRING = 'my-string';
// Output - string is visible
const MY_STRING = 'my-string';
Z 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);
})();
Nazwa powiązania (MY_STRING) nadal pozostaje na najwyższym poziomie z tego samego powodu, co nazwy funkcji — coś spoza
pliku może się do niej odwoływać — ale wartość, którą przechowuje, jest teraz produkowana przez VM i nie pojawia się
już jako czytelny tekst.
Kiedy to nie wystarcza
- Nazwy importowane z innych modułów. Jeśli budujesz paczkę z wielu plików, a jeden moduł eksportuje
validateLicensedo zaimportowania przez inny, bundler zachowa tę nazwę jako widoczną w spakowanym kodzie wynikowym tak samo, jak widoczne są funkcje najwyższego poziomu. Opakuj samą paczkę w IIFE (większość bundlerów potrafi to zrobić) albo przenieś eksport do wnętrza IIFE i ponownie udostępnij go przez pozbawioną znaczenia trampolinę.
