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łowym | Co 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:
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średnievalalbo wywołanienew Function/Function, którego ciało nie jest statyczne, lubfn.toString()wstrzyknięte do<script>albo Workera. To ostrzeżenie zapowiadaReferenceError.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średnievallub dynamiczne wywołanienew Function/Function. Pojawia się także przy bezpiecznym statycznymeval('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.
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ę naReferenceErrorw czasie wykonania.DynamicCodeRenameRisknadal 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
- Zachowanie bezpośredniego eval: wzorzec rozpakowania IIFE ograniczający zasięg pominięcia.
- Wskazywanie funkcji: użycie
vmTargetFunctionsModedo włączania lub wyłączania ochrony na poziomie pojedynczych funkcji.
