Ukrywanie nazw funkcji przed analizą LLM
Problem
Włączasz vmObfuscation: true, uruchamiasz obfuskację pliku zawierającego funkcję taką jak validateLicense i zauważasz, że
zobfuskowany wynik nadal zawiera dosłowny tekst validateLicense. Ciało funkcji zniknęło, zastąpione kodem bajtowym, ale
sama nazwa pozostaje na widoku.
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ą mu do przygotowania pewnego siebie i trafnego podsumowania działania modułu. Kod bajtowy jest nieprzejrzysty,
ale spis treści już nie.
Dlaczego obfuskacja VM zachowuje te nazwy
Przy vmTargetFunctionsMode: 'root' (domyślnie) obfuskator przekształca ciało każdej funkcji najwyższego poziomu
w kod bajtowy VM, ale celowo nie rusza jej nazwy. Deklaracja funkcji najwyższego poziomu jest semantycznie wiązaniem
w otaczającym zakresie: w skrypcie oznacza to obiekt globalny, a w module zakres modułu (nieeksportowana funkcja najwyższego poziomu ma zakres
modułu, a nie globalny, ale w pliku nadal jest funkcją poziomu głównego). Obfuskator nie może
bezpiecznie zmienić jej nazwy, ponieważ nie ma jak ustalić, kto jeszcze się do niej odwołuje: inny bundle, osadzony
<script>, atrybut HTML onclick="validateLicense(...)", dynamiczne odwołanie window['validateLicense'] itd.
Ustawienie domyślne przyjmuje więc kompromis: chroni implementację, zachowuje publiczny interfejs. Dzięki temu integracje
nadal działają, ale oznacza to też, że LLM dostaje za darmo indeks wszystkich punktów wejścia. W takiej sytuacji wynik
obfuskacji zgłasza ostrzeżenie VMGlobalFunctionNamesNotRenamed z listą nazw, które pozostały czytelne.
Dlaczego ma to znaczenie przy inżynierii wstecznej wspomaganej przez LLM
Człowiek, który stanie przed kilkuset wierszami dispatchera kodu bajtowego, zwykle się poddaje. LLM, który dostanie ten sam plik, w ogóle nie będzie atakować kodu bajtowego: przeczyta nazwy, zestawi je z kilkoma widocznymi literałami tekstowymi i napisze coś w rodzaju:
„Ten moduł blokuje płatną funkcję. validateLicense weryfikuje podpisany token, checkExpiry odrzuca wygasłe
licencje, a activateFeature odblokowuje interfejs po pomyślnym sprawdzeniu. Funkcja pomocnicza do odszyfrowywania
znajduje się w decryptPayload.”
Takie podsumowanie wystarczy atakującemu do zaplanowania celowanego obejścia bez dotykania VM. Wyciekiem są nazwy.
Rozwiązanie: opakuj kod w IIFE
Najprostszym i najpewniejszym sposobem usunięcia tego wycieku jest przeniesienie wrażliwych funkcji o jeden poziom głębiej w drzewie zakresów. Funkcje zadeklarowane wewnątrz innej funkcji nie są funkcjami najwyższego poziomu, więc obfuskator może swobodnie zmienić ich nazwy i wchłonąć ich deklaracje do kodu bajtowego jak każdą inną instrukcję.
IIFE (Immediately-Invoked Function Expression, natychmiast wywoływane wyrażenie funkcyjne) to najlżejszy sposób, aby to osiągnąć: dodaje jedną funkcję opakowującą, która wykonuje się raz i niczego nie udostępnia z nazwy.
Przed: nazwy są widoczne
Po obfuskacji VM zarówno validateLicense, jak i checkExpiry zachowują w wyniku swoje nazwy.
Po: nazwy ukryte w IIFE
Teraz obie deklaracje funkcji znajdują się w ciele IIFE. Samo IIFE jest jedyną konstrukcją najwyższego poziomu, a anonimowe IIFE nie ma nazwy, która mogłaby wyciec. Po obfuskacji VM nazwy funkcji zostają zmienione, a ich ciała przekształcone w kod bajtowy.
Co się zmieniło. Funkcje nie są już dostępne jako zmienne globalne (to właśnie przez to wyciekały ich nazwy). Nadal można
je wywoływać, tyle że z wnętrza tego samego IIFE. Jeśli coś rzeczywiście musiało wywoływać validateLicense z zewnątrz,
teraz już do niej nie dotrze; zobacz wzorzec trampoliny poniżej. Jeśli nic
tego nie robiło, niczego nie tracisz.
Eksportowane nazwy, zastrzeżone identyfikatory, ciągi znaków i obserwowalne zachowanie mogą pozostać widoczne. Po opakowaniu przetestuj integracje, zwłaszcza zmienne globalne, eksporty modułów i kod, który sprawdza nazwy funkcji.
Kompromis, o którym warto wiedzieć: jeśli ciało IIFE zawiera bezpośredni eval albo wywołanie new Function(...) / Function(...)
z dynamicznym ciałem, obfuskacja VM pomija całą tę funkcję i wszystko, co jest w niej zagnieżdżone (ostrzeżenie
VMDynamicCodeSkipped), ponieważ kod źródłowy budowany w czasie wykonania może odwoływać się do identyfikatorów, których
nazwy zmienił obfuskator. Kod, który właśnie przeniesiono do IIFE, wraca wtedy do zwykłej obfuskacji i traci ochronę kodu
bajtowego. Użyj pośredniego eval - (0, eval)(...) - albo zobacz dostępne możliwości na stronie
Bezpośrednie wywołanie eval() wyłącza obfuskację VM.
Co jeśli funkcja naprawdę musi być globalna?
Czasem funkcja rzeczywiście jest publicznym punktem wejścia: osadzoną obsługą zdarzenia, callbackiem JSONP, hookiem SDK zewnętrznego dostawcy. Masz wtedy dwie możliwości:
Udostępnij cienką trampolinę, a logikę zostaw w IIFE. Zadeklaruj niewielki globalny wrapper, którego jedynym zadaniem jest wywołanie implementacji z zakresu IIFE. Nazwa trampoliny nadal wycieka, ale nie niesie żadnej informacji semantycznej (nazwij ją
__entry1lub podobnie), a cała istotna logika pozostaje ukryta.Przepisz miejsce wywołania. Jeśli zmienna globalna istnieje tylko dlatego, że potrzebuje jej osadzone
onclick="validateLicense(...)", zastąp osadzoną obsługę zdarzenia wywołaniemaddEventListenerz wnętrza IIFE. HTML przestaje wymieniać nazwę funkcji, funkcja przestaje musieć być globalna, a wyciek całkowicie znika.
Inicjalizatory zmiennych najwyższego poziomu: vmWrapTopLevelInitializers
Deklaracje funkcji to nie jedyne, co znajduje się na najwyższym poziomie pliku. Inicjalizatory zmiennych najwyższego poziomu,
czyli stałe tekstowe, obiekty konfiguracyjne i tablice wyszukiwania, są w wyniku równie czytelne, gdy pozostają zwykłym
JavaScriptem. Wiersz taki jak const API_BASE = '/api/v2/license' mówi modelowi LLM tyle samo co function validateLicense.
Opcja vmWrapTopLevelInitializers (wartość logiczna, domyślnie false; obecne presety VM ją włączają) opakowuje
kwalifikujące się inicjalizatory najwyższego poziomu w IIFE, dzięki czemu sama wartość jest obliczana w czasie wykonania
przez kod bajtowy VM, zamiast znajdować się w kodzie źródłowym jako literał. W trybie root inicjalizatory, które pozostają zwykłym
JavaScriptem, są zgłaszane w ostrzeżeniu VMTopLevelInitializerNotVirtualized.
Bez tej opcji
Z vmWrapTopLevelInitializers: true
Nazwa wiązania (MY_STRING) nadal jest na najwyższym poziomie z tego samego powodu co nazwy funkcji (coś spoza pliku może się
do niej odwoływać), ale przechowywana w niej wartość jest teraz wytwarzana przez VM i nie pojawia się już jako czytelny tekst.
Działa tylko wtedy, gdy vmTargetFunctionsMode ma wartość 'root' (domyślnie), a vmAsyncExecutor jest wyłączony. W trybie komentarzy
opcja nie ma żadnego efektu i nie jest zgłaszane żadne ostrzeżenie. W trybie root przy vmAsyncExecutor (który wirtualizuje
wyłącznie funkcje asynchroniczne) również nie ma efektu, a build zgłasza VMTopLevelInitializerNotVirtualized dla inicjalizatorów pozostawionych w zwykłym JavaScripcie.
Jeśli plik nie zawiera żadnej funkcji, którą VM mogłaby zwirtualizować, wynik zgłasza VMNoFunctionsToVirtualize; opakuj kod,
który chcesz chronić, w funkcję albo użyj trybu komentarzy, aby jawnie wskazać wrażliwe funkcje.
Kiedy to nie wystarcza
- Nazwy importowane z innych modułów. Jeśli łączysz w bundle wiele plików, a jeden moduł eksportuje
validateLicense, aby inny mógł ją zaimportować, bundler pozostawi tę nazwę widoczną w wyniku tak samo, jak widoczne są funkcje najwyższego poziomu. Opakuj w IIFE sam bundle (większość bundlerów to potrafi) albo przenieś eksport do IIFE i udostępnij go ponownie przez trampolinę o nic nieznaczącej nazwie. - Środowisko uruchomieniowe nadal jest obserwowalne. Obfuskacja zwiększa wysiłek potrzebny do zrozumienia i modyfikacji kodu, ale nie gwarantuje, że inżynieria wsteczna jest niemożliwa. Sekrety i wiążące decyzje dotyczące bezpieczeństwa trzymaj na serwerze.
Nie sięgaj najpierw po renameGlobals. Ta opcja istnieje i zmieni nazwy identyfikatorów najwyższego poziomu, ale nie ma
jak ustalić, do których z nich odwołuje się kod spoza pliku (inne bundle, osadzony HTML, dynamiczne odwołania). Jej włączenie
często psuje integracje w subtelny sposób. Opakowanie w IIFE jest bezpieczniejsze: nie zmienia nazw niczego globalnego,
po prostu przestaje tworzyć zmienne globalne, których nie potrzebujesz.
