Dokumentacja
/
Przepisy
/

Obfuskacja VM a eval i new Function

Obfuskacja VM a eval i new Function

Jak obfuskacja VM obsługuje bezpośrednie wywołania eval i konstruktora 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 ma znaczenie

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

Obfuskator obsługuje każdy z tych wzorców inaczej. Poniższa tabela to wersja skrócona; reszta tej strony wyjaśnia każdą komórkę.

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 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ę błędem ReferenceError w czasie wykonania. Funkcja zawierająca to wywołanie — oraz każda funkcja zdefiniowana wewnątrz niej — również traci kompilację do kodu bajtowego VM.
(0, eval)(s) / window.eval(s) (pośredni)Działa poprawnie. Funkcja zawierająca to wywołanie jest normalnie kompilowana do kodu bajtowego VM. Pośredni eval nie widzi otaczających zmiennych lokalnych, więc zmiana nazw nie może go zepsuć.
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ę błędem ReferenceError w czasie wykonania. Funkcja zawierająca to wywołanie — oraz każda funkcja zdefiniowana wewnątrz niej — również traci kompilację do kodu bajtowego VM.

Wszystkie te wzorce są też zgłaszane jako niekrytyczne ostrzeżenia w wyniku obfuskacji — zobacz Wykrywanie problemu przed uruchomieniem poniżej, aby poznać kształt ostrzeżeń i fragment kodu do CI.

Dlaczego kod statyczny i dynamiczny są traktowane inaczej

eval(s) może odczytywać i zapisywać zmienne lokalne funkcji, w której został wywołany. Gdy s jest literałem tekstowym, obfuskator potrafi sparsować ciało na etapie 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 — a wtedy 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) działa inaczej — ciało zawsze wykonuje się tak, jakby było zdefiniowane na początku pliku, z dostępem wyłącznie do zmiennych globalnych i nigdy do zmiennych lokalnych wokół wywołania. Samo w sobie jest to bezpieczne — ale jeśli ciało powstaje przez sklejenie w nim zmienionej nazwy identyfikatora (np. przez func.toString() funkcji, której wnętrze obfuskator przepisał), funkcja skompilowana w czasie wykonania i tak natrafi na ten sam rodzaj błędu ReferenceError.

Statyczne new Function('return 42') nigdy nie niesie tego ryzyka — ciało jest zwykłym tekstem, którego mechanizm zmiany nazw nigdy nie analizuje, a w czasie wykonania musi ono widzieć tylko zmienne globalne. Obfuskator zostawia wywołanie na miejscu, a otaczająca funkcja nadal kwalifikuje się do kompilacji do kodu bajtowego VM.

Błąd widoczny w czasie wykonania

Typowym objawem jest ReferenceError przy pierwszym uruchomieniu dynamicznie zbudowanej funkcji:

browser console

Błąd

TU to tutaj zmieniona nazwa identyfikatora, którą obfuskator wprowadził wewnątrz zasięgu IIFE paczki. Dynamiczne wywołanie eval / konstruktora Function wykonuje ciało, które się do niej odwołuje, ale ciało to działa w zasięgu, w którym TU jest niezdefiniowane.

Wykrywanie problemu przed uruchomieniem

Obfuskator zgłasza przez API niekrytyczne ostrzeżenia, dzięki czemu te wzorce można wychwycić w CI jeszcze przed wydaniem. Istotne są dwa typy ostrzeżeń:

  • DynamicCodeRenameRisk — funkcja zawiera dynamiczne wywołanie eval / new Function / Function, którego ciało powstaje w czasie wykonania.
  • VMDynamicCodeSkipped — kompilacja funkcji do kodu bajtowego VM została pominięta z powodu jednego z powyższych wzorców. Zawiera nazwę funkcji (jeśli jest dostępna) oraz rodzaj konstrukcji, która spowodowała pominięcie.

ci-build.mjs

JavaScript

Jeśli dany typ ostrzeżenia jest w danym procesie budowania oczekiwany i wygodniej wyciszyć je u źródła niż filtrować w CI, opcja warnings (v7.8.0+) steruje tym, co zgłasza getWarnings()'none' wycisza wszystko, a mapa dla poszczególnych typów, taka jak { VMDynamicCodeSkipped: false }, wycisza tylko jeden typ, zachowując pozostałe.

Obejścia

  • Przejście na pośredni eval ((0, eval)(s))

    Przydatne tylko w przypadku eval. Pośredni eval działa w zasięgu globalnym, więc nie widzi otaczających zmiennych lokalnych — ale z tego samego powodu nie może też odwołać się do zmienionych nazw identyfikatorów. Funkcja zawierająca wywołanie pozostaje skompilowana do kodu bajtowego VM.

  • Uczynienie ciała w pełni statycznym

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

  • Przeniesienie wywołania do własnej funkcji najwyższego poziomu i rozpakowanie IIFE

    Każda funkcja najwyższego poziomu jest sprawdzana niezależnie. Wyniesienie wywołania budującego kod dynamicznie do własnej funkcji najwyższego poziomu sprawia, że tylko ta jedna funkcja traci kompilację do kodu bajtowego VM, zamiast kaskadowego pominięcia całego IIFE najwyższego poziomu opakowującego całą paczkę.

  • Przejście na vmTargetFunctionsMode: 'comment'

    Tryb opt-in — do kodu bajtowego trafiają tylko funkcje oznaczone /* javascript-obfuscator:vm */. Wystarczy nie oznaczać funkcji zawierającej wywołanie budujące kod dynamicznie, a resztę skompilować do kodu bajtowego. Zobacz Wskazywanie funkcji.

  • Wymuszenie kompilacji mimo pominięcia za pomocą vmForceCompileDynamicCode: true (v6.14.0+)

    Furtka ostatniej szansy. Po włączeniu obfuskator mimo wszystko kompiluje otaczającą funkcję do kodu bajtowego i wycisza ostrzeżenie VMDynamicCodeSkipped. Należy jej używać tylko wtedy, gdy można zagwarantować, że ciało budowane w czasie wykonania nigdy nie odwołuje się do identyfikatora, którego nazwę obfuskator zmienia — w przeciwnym razie czysta obfuskacja zostaje zamieniona na ReferenceError w czasie wykonania. DynamicCodeRenameRisk nadal się zgłasza, więc CI może dalej na nim bramkować. W panelu jest to przełącznik „Force Compile Dynamic Code” w grupie Nadpisania sekcji VM.

Powiązane strony