Dokumentacja
/
Przepisy
/

Ukrywanie nazw funkcji przed analizą LLM

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ą __entry1 lub 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 przez addEventListener wywoł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 validateLicense do 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ę.