Dokumentacja
/

Rozwiązywanie problemów

/

Obfuskacja VM a eval i new Function

Obfuskacja VM a eval i new Function

Jak obfuskacja VM obsługuje dynamiczne tworzenie kodu (bezpośredni eval i konstruktor Function), co trafia do kodu bajtowego, a co jest pomijane, jakie ostrzeżenia zgłasza obfuskator i jak diagnozować błąd ReferenceError w czasie wykonania.

Dlaczego to ważne

Obfuskacja VM kompiluje ciała funkcji do kodu bajtowego wykonywanego przez interpreter wbudowany w środowisko uruchomieniowe. Zmieniane są też nazwy identyfikatorów w otaczającym zakresie. Obie transformacje źle współgrają z kodem budowanym z ciągu znaków w czasie wykonania: eval(s), new Function(...s) i Function(...s). Jeśli kod zbudowany w czasie wykonania odwołuje się do identyfikatora, którego nazwę zmienił obfuskator, przy pierwszym uruchomieniu wygenerowanej funkcji otrzymasz Uncaught ReferenceError: <renamed-name> is not defined.

Obfuskator obsługuje każdy wzorzec inaczej. Poniższa tabela to wersja skrócona; dalsza część strony wyjaśnia każdy wiersz.

Co robi obfuskator w skrócie

Wzorzec w kodzie źródłowymCo się dzieje
eval('literal string') (ciało jest literałem tekstowym)Działa poprawnie. Funkcja zawierająca to wywołanie, a także każda funkcja zdefiniowana w jej wnętrzu, traci kompilację do kodu bajtowego VM (bezpośredni eval odczytuje otaczające zmienne lokalne, których VM nie zachowuje po skompilowaniu funkcji do kodu bajtowego).
eval(dynamicExpression)Może zakończyć się awarią w czasie wykonania z ReferenceError. Funkcja zawierająca to wywołanie, a także każda funkcja zdefiniowana w jej wnętrzu, również traci kompilację do kodu bajtowego VM.
(0, eval)(s) / window.eval(s) (pośrednio)Funkcja zawierająca to wywołanie jest normalnie kompilowana do kodu bajtowego VM. Pośredni eval działa w zakresie globalnym i nie widzi otaczających zmiennych lokalnych, więc zmienione nazwy zmiennych lokalnych nie mogą go zepsuć. Nadal może zawieść, jeśli wykonywany kod odwołuje się do zmiennej globalnej, której nazwę zmieniono lub którą usunięto.
new Function('a', 'b', 'return a + b') (wszystkie argumenty są literałami tekstowymi)Działa poprawnie. Funkcja zawierająca to wywołanie jest normalnie kompilowana do kodu bajtowego VM.
new Function(dynamicBody) / Function(dynamicBody)Może zakończyć się awarią w czasie wykonania z ReferenceError. Funkcja zawierająca to wywołanie, a także każda funkcja zdefiniowana w jej wnętrzu, również traci kompilację do kodu bajtowego VM.

Kompilator zachowawczo wykrywa również eval?.(code) i traktuje je jak bezpośredni eval. JavaScript definiuje tę formę opcjonalnego wywołania jako pośredni eval; wykrywanie przez kompilator nie zmienia tego zachowania języka.

Tylko niektóre z tych wzorców powodują niekrytyczne ostrzeżenia, i to wyłącznie wtedy, gdy wywołanie znajduje się wewnątrz funkcji: dynamiczne wywołanie eval, new Function lub Function zgłasza zarówno DynamicCodeRenameRisk, jak i VMDynamicCodeSkipped, statyczne eval('...') zgłasza tylko VMDynamicCodeSkipped, a pośredni eval i w pełni statyczne new Function nie zgłaszają niczego. Dynamiczne wywołanie na najwyższym poziomie pliku, poza jakąkolwiek funkcją, nigdy nie jest zgłaszane. Typy ostrzeżeń i fragment kodu dla CI opisuje sekcja Wykrywanie problemu przed uruchomieniem poniżej.

Pominięcie przenosi się na całe IIFE. Jeśli IIFE najwyższego poziomu opakowuje cały bundle, a dowolna funkcja w jego wnętrzu używa dynamicznego eval lub new Function, całe IIFE zostaje pominięte przy kompilacji do kodu bajtowego VM. Wzorzec rozpakowania IIFE, który ogranicza zasięg szkód, opisuje strona Zachowanie bezpośredniego eval.

Dlaczego przypadki statyczne i dynamiczne są traktowane inaczej

eval(s) może odczytywać i zapisywać zmienne lokalne funkcji, w której zostało wywołane. Gdy s jest literałem tekstowym, obfuskator może sparsować ciało podczas obfuskacji i zmienić nazwy identyfikatorów spójnie z otaczającym kodem. Gdy s jest wyrażeniem dynamicznym, parsowanie następuje dopiero w czasie wykonania, kiedy nazwy identyfikatorów są już zmienione, więc kod zbudowany w czasie wykonania odwołuje się do starych nazw, które już nie istnieją.

new Function(s) i pośredni eval działają inaczej: ciało zawsze wykonuje się w zakresie globalnym, z dostępem wyłącznie do zmiennych globalnych i nigdy do zmiennych lokalnych wokół wywołania. Nie mogą więc zależeć od zmienionych nazw zmiennych lokalnych, ale nadal mogą zawieść, jeśli odwołują się do zmiennych globalnych, których nazwy zmieniono lub które usunięto, albo jeśli budujesz ciało, doklejając do niego identyfikator o zmienionej nazwie (np. przez func.toString() funkcji, której wnętrze obfuskator przepisał).

Statyczne ciało korzystające wyłącznie z własnych parametrów, np. new Function('a', 'b', 'return a + b'), nie ma takiej zależności: ciało jest zwykłym ciągiem znaków, którego mechanizm zmiany nazw nigdy nie analizuje. Obfuskator pozostawia wywołanie bez zmian, a otaczająca funkcja nadal może zostać skompilowana do kodu bajtowego VM. Sama zmiana składni eval nie sprawia, że dowolny dynamiczny kod staje się bezpieczny: sprawdzaj ostrzeżenia i testuj końcowy bundle.

Błąd widoczny w czasie wykonania

Typowym objawem jest ReferenceError przy pierwszym uruchomieniu dynamicznie zbudowanej funkcji:

Text

TU to tutaj identyfikator o zmienionej nazwie, wprowadzony przez obfuskator w zakresie IIFE bundle'a. Dynamiczne wywołanie eval lub konstruktora Function wykonuje ciało, które się do niego odwołuje, ale to ciało działa w zakresie, w którym TU nie jest zdefiniowane.

Wykrywanie problemu przed uruchomieniem

Obfuskator zgłasza przez API niekrytyczne ostrzeżenia, dzięki którym możesz wychwycić część tych wzorców w CI przed wdrożeniem. Istotne są dwa typy ostrzeżeń i oba są zgłaszane tylko dla wywołań wewnątrz funkcji:

  • DynamicCodeRenameRisk: funkcja buduje kod z łańcucha w czasie wykonania: bezpośredni eval albo wywołanie new Function / Function, którego ciało nie jest statyczne, lub fn.toString() wstrzyknięte do <script> albo Workera. To ostrzeżenie zapowiada ReferenceError.
  • VMDynamicCodeSkipped: kompilacja do kodu bajtowego VM została pominięta dla funkcji i każdej funkcji zdefiniowanej w jej wnętrzu, ponieważ zawiera ona bezpośredni eval lub dynamiczne wywołanie new Function / Function. Pojawia się także przy bezpiecznym statycznym eval('literal'), więc sygnalizuje utratę ochrony VM, a nie awarię w czasie wykonania. Zawiera nazwę funkcji (jeśli jest dostępna) i konstrukcję, która spowodowała pominięcie.

Pakiet npm javascript-obfuscator nie udostępnia ostrzeżeń w swoim wyniku, więc bramka w CI odczytuje je z odpowiedzi API: komunikaty result i chunk_end zawierają tablicę warnings. Poniższy przykład używa readObfuscationResponse(), czytnika strumienia z Dokumentacji API, i przerywa build tylko przy DynamicCodeRenameRisk; dodaj VMDynamicCodeSkipped do filtra, jeśli utrata ochrony VM w funkcji również powinna blokować wydanie.

Code

Jeśli dany typ ostrzeżenia jest w Twoim buildzie oczekiwany i wolisz wyciszyć go u źródła, zamiast filtrować w CI, opcja warnings (v7.8.0+) określa, które ostrzeżenia są zgłaszane: 'none' wycisza wszystko, a mapa per typ, taka jak { VMDynamicCodeSkipped: false }, wycisza tylko jeden typ, zachowując pozostałe.

Obejścia

  • Przejdź na pośredni eval ((0, eval)(s))

    Przydatne tylko w przypadku eval. Pośredni eval działa w zakresie globalnym, więc nie widzi otaczających zmiennych lokalnych, a z tego samego powodu nie może też odwoływać się do zmiennych lokalnych o zmienionych nazwach. Funkcja zawierająca wywołanie pozostaje skompilowana do kodu bajtowego VM. Wykonywany kod nadal nie może zależeć od zmiennych globalnych, których nazwy zmienia obfuskator.

  • Uczyń ciało w pełni statycznym

    W przypadku new Function, jeśli możesz wyrazić ciało jako pojedynczy literał tekstowy lub literał szablonowy bez interpolacji, wywołanie nie niesie ryzyka związanego ze zmianą nazw, a otaczająca je funkcja nadal jest kompilowana do kodu bajtowego. new Function('a', 'b', 'return a + b') jest w porządku; new Function('return ' + expr) już nie.

  • Przenieś wywołanie do osobnej funkcji najwyższego poziomu i rozpakuj IIFE

    Każda funkcja najwyższego poziomu jest sprawdzana niezależnie. Przeniesienie wywołania dynamicznego kodu do osobnej funkcji najwyższego poziomu sprawia, że kompilację do kodu bajtowego VM traci tylko ta jedna funkcja, zamiast pominięcia obejmującego całe IIFE najwyższego poziomu opakowujące Twój bundle.

  • Przełącz na vmTargetFunctionsMode: 'comment'

    Tryb jawnego wyboru: do kodu bajtowego kompilowane są tylko funkcje oznaczone /* javascript-obfuscator:vm */. Nie oznaczaj funkcji zawierającej wywołanie dynamicznego kodu, a oznacz pozostałe. Zachowaj te komentarze aż do kroku obfuskacji. Zobacz Wskazywanie funkcji.

  • Wymuś kompilację mimo pominięcia za pomocą vmForceCompileDynamicCode: true (v6.14.0+)

    Furtka awaryjna na ostateczność. Po włączeniu obfuskator mimo wszystko kompiluje otaczającą funkcję do kodu bajtowego i wycisza ostrzeżenie VMDynamicCodeSkipped. Nie naprawia to zależności od zakresu ani od zmienionych identyfikatorów: używaj tej opcji tylko wtedy, gdy możesz zagwarantować, że ciało budowane w czasie wykonania nigdy nie odwołuje się do identyfikatora, którego nazwę zmienia obfuskator. W przeciwnym razie zamieniasz czystą obfuskację na ReferenceError w czasie wykonania. DynamicCodeRenameRisk nadal jest zgłaszane, więc CI może dalej na nim blokować build. W panelu jest to przełącznik „Force Compile Dynamic Code” w grupie Overrides sekcji VM.

Powiązane strony