Dokumentacja
/
Przepisy
/

Ukrywanie nazw funkcji przed analizą LLM

Ukrywanie nazw funkcji przed analizą LLM

Pro

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.

JavaScript

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

JavaScript

Po obfuskacji VM zarówno validateLicense, jak i checkExpiry zachowują w wyniku swoje nazwy.

Po: nazwy ukryte w IIFE

JavaScript

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ą __entry1 lub podobnie), a cała istotna logika pozostaje ukryta.

    JavaScript

  • Przepisz miejsce wywołania. Jeśli zmienna globalna istnieje tylko dlatego, że potrzebuje jej osadzone onclick="validateLicense(...)", zastąp osadzoną obsługę zdarzenia wywołaniem addEventListener z 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

JavaScript

Z vmWrapTopLevelInitializers: true

JavaScript

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.