VM-Obfuskierung mit eval und new Function
Wie die VM-Obfuskierung mit dynamisch erzeugtem Code umgeht (direktes eval und der Function-Konstruktor), was in Bytecode überführt und was übersprungen wird, welche Warnungen der Obfuscator ausgibt und wie Sie einen ReferenceError zur Laufzeit diagnostizieren.
Warum das wichtig ist
Die VM-Obfuskierung kompiliert Funktionsrümpfe zu Bytecode, der über einen Interpreter in der Laufzeit ausgeführt wird.
Zusätzlich werden Bezeichner im umgebenden Scope umbenannt. Beide Transformationen vertragen sich schlecht mit Code, der
zur Laufzeit aus einer Zeichenkette erzeugt wird: eval(s), new Function(...s) und Function(...s). Referenziert der zur
Laufzeit erzeugte Code einen Bezeichner, den der Obfuscator umbenannt hat, erhalten Sie beim ersten Ausführen der erzeugten
Funktion ein Uncaught ReferenceError: <renamed-name> is not defined.
Der Obfuscator behandelt jedes Muster unterschiedlich. Die folgende Matrix ist die Kurzfassung; der Rest dieser Seite erläutert jede Zelle.
Was der Obfuscator tut – auf einen Blick
| Muster in Ihrem Quellcode | Was passiert |
|---|---|
eval('literal string') (der Rumpf ist ein String-Literal) | Läuft korrekt. Die Funktion, die diesen Aufruf enthält, verliert die VM-Bytecode-Umwandlung (direktes eval liest die umgebenden lokalen Variablen, die die VM nicht mehr vorhält, sobald eine Funktion zu Bytecode kompiliert wurde). |
eval(dynamicExpression) | Kann zur Laufzeit mit einem ReferenceError abstürzen. Die Funktion, die diesen Aufruf enthält – und jede darin definierte Funktion –, verliert ebenfalls die VM-Bytecode-Umwandlung. |
(0, eval)(s) / window.eval(s) (indirekt) | Läuft korrekt. Die Funktion, die diesen Aufruf enthält, wird normal in VM-Bytecode überführt. Indirektes eval sieht die umgebenden lokalen Variablen nicht, deshalb kann das Umbenennen es nicht beschädigen. |
new Function('a', 'b', 'return a + b') (alle Argumente sind String-Literale) | Läuft korrekt. Die Funktion, die diesen Aufruf enthält, wird normal in VM-Bytecode überführt. |
new Function(dynamicBody) / Function(dynamicBody) | Kann zur Laufzeit mit einem ReferenceError abstürzen. Die Funktion, die diesen Aufruf enthält – und jede darin definierte Funktion –, verliert ebenfalls die VM-Bytecode-Umwandlung. |
All diese Muster werden außerdem als nicht fatale Warnungen am Obfuskierungsergebnis gemeldet – die Warnungsformen und einen CI-Schnipsel finden Sie weiter unten unter Das Problem vor der Laufzeit erkennen.
Warum statisch und dynamisch unterschiedlich behandelt werden
eval(s) kann lokale Variablen der aufrufenden Funktion lesen und schreiben. Ist s ein String-Literal, kann der
Obfuscator den Rumpf zur Obfuskierungszeit parsen und die Bezeichner konsistent mit dem umgebenden Code umbenennen. Ist s
ein dynamischer Ausdruck, findet das Parsen erst zur Laufzeit statt – zu diesem Zeitpunkt sind die Bezeichner bereits
umbenannt, sodass der zur Laufzeit erzeugte Quelltext alte Namen referenziert, die es nicht mehr gibt.
new Function(s) funktioniert anders: Der Rumpf läuft immer so, als wäre er ganz oben in Ihrer Datei definiert worden – mit
Zugriff ausschließlich auf globale Variablen und nie auf die lokalen Variablen rund um den Aufruf. Das allein ist
unbedenklich – wenn Sie den Rumpf jedoch zusammensetzen, indem Sie einen umbenannten Bezeichner hineinkonkatenieren (etwa
über func.toString() einer Funktion, deren Innenleben der Obfuscator umgeschrieben hat), läuft die zur Laufzeit kompilierte
Funktion trotzdem in denselben ReferenceError.
Statisches new Function('return 42') birgt dieses Risiko nie: Der Rumpf ist eine schlichte Zeichenkette, die die
Umbenennung nie untersucht, und zur Laufzeit muss er nur die Globals sehen. Der Obfuscator lässt den Aufruf unverändert, und
die umgebende Funktion kommt weiterhin für die VM-Bytecode-Umwandlung infrage.
Der Fehler, den Sie zur Laufzeit sehen
Das übliche Symptom ist ein ReferenceError beim ersten Ausführen der dynamisch erzeugten Funktion:
TU ist hier ein umbenannter Bezeichner, den der Obfuscator im IIFE-Scope des Bundles eingeführt hat. Der dynamische Aufruf
von eval bzw. des Function-Konstruktors wertet einen Rumpf aus, der ihn referenziert – doch dieser Rumpf läuft in einem
Scope, in dem TU nicht definiert ist.
Das Problem vor der Laufzeit erkennen
Der Obfuscator gibt über die API nicht fatale Warnungen aus, sodass Sie diese Muster in der CI erkennen können, bevor Sie ausliefern. Zwei Warnungstypen sind hier relevant:
DynamicCodeRenameRisk– eine Funktion enthält einen dynamischen Aufruf voneval/new Function/Function, dessen Rumpf zur Laufzeit erzeugt wird.VMDynamicCodeSkipped– die VM-Bytecode-Umwandlung wurde für eine Funktion wegen eines der obigen Muster übersprungen. Enthält den Funktionsnamen (sofern verfügbar) und die Art des Konstrukts, das die Auslassung ausgelöst hat.
Wenn ein Warnungstyp in Ihrem Build erwartet wird und Sie ihn lieber an der Quelle stummschalten als in der CI herausfiltern
möchten, steuert die Option warnings (v7.8.0+), was getWarnings() ausgibt: 'none' unterdrückt alles, und eine Zuordnung
pro Typ wie { VMDynamicCodeSkipped: false } schaltet nur einen Typ stumm und behält die übrigen bei.
Workarounds
Auf indirektes eval umstellen (
(0, eval)(s))Nur für den eval-Fall nützlich. Indirektes eval läuft im globalen Scope und kann die umgebenden lokalen Variablen daher nicht sehen – aus demselben Grund kann es aber auch keine umbenannten Bezeichner referenzieren. Die Funktion, die den Aufruf enthält, bleibt in VM-Bytecode überführt.
Den Rumpf vollständig statisch machen
Wenn Sie den Rumpf bei
new Functionals ein einzelnes String- oder Template-Literal ohne Interpolationen ausdrücken können, birgt der Aufruf kein Umbenennungsrisiko, und die Funktion darum herum wird weiterhin in Bytecode überführt.new Function('a', 'b', 'return a + b')ist in Ordnung,new Function('return ' + expr)nicht.Den Aufruf in eine eigene Top-Level-Funktion verschieben und das IIFE auflösen
Jede Top-Level-Funktion wird unabhängig geprüft. Verlagern Sie den Aufruf zur dynamischen Codeerzeugung in eine eigene Top-Level-Funktion, verliert nur diese eine Funktion die VM-Bytecode-Umwandlung, statt dass sich die Auslassung durch ein Top-Level-IIFE zieht, das Ihr gesamtes Bundle umschließt.
Auf
vmTargetFunctionsMode: 'comment'umstellenOpt-in-Modus: Nur Funktionen, die mit
/* javascript-obfuscator:vm */markiert sind, werden in Bytecode überführt. Versehen Sie die Funktion, die den dynamischen Codeaufruf enthält, einfach nicht mit der Markierung und überführen Sie den Rest. Siehe Funktionen gezielt auswählen.Die Auslassung mit
vmForceCompileDynamicCode: trueüberschreiben (v6.14.0+)Notausgang als letztes Mittel. Ist die Option aktiviert, überführt der Obfuscator die umgebende Funktion dennoch in Bytecode und unterdrückt die Warnung
VMDynamicCodeSkipped. Setzen Sie sie nur ein, wenn Sie garantieren können, dass der zur Laufzeit erzeugte Rumpf nie einen Bezeichner referenziert, den der Obfuscator umbenennt – andernfalls tauschen Sie eine saubere Obfuskierung gegen einenReferenceErrorzur Laufzeit ein.DynamicCodeRenameRiskwird weiterhin ausgelöst, sodass Ihre CI darauf weiter prüfen kann. Im Dashboard ist dies der Schalter „Force Compile Dynamic Code“ in der Gruppe Überschreibungen des VM-Bereichs.
Verwandte Seiten
- VM-Obfuskierung – Verhalten von direktem eval – das Muster zum Auflösen des IIFE, mit dem Sie den Wirkungsradius der Auslassung begrenzen.
- VM-Obfuskierung – Funktionen gezielt auswählen – wie Sie mit
vmTargetFunctionsModepro Funktion ein- oder aussteigen.
