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łowym | Co 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:
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łanieeval/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.
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 naReferenceErrorw czasie wykonania.DynamicCodeRenameRisknadal 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
- Obfuskacja VM — Zachowanie bezpośredniego eval — wzorzec rozpakowania IIFE ograniczający zasięg skutków pominięcia.
- Obfuskacja VM — Wskazywanie funkcji — używanie
vmTargetFunctionsModedo włączania lub wyłączania na poziomie pojedynczych funkcji.
