VM-Obfuskierung mit eval und new Function
Wie die VM-Obfuskierung mit dynamisch erzeugtem Code (direktem eval und dem Function-Konstruktor) umgeht, was zu Bytecode kompiliert 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 von einem Interpreter in der Laufzeit ausgeführt wird. Zusätzlich
werden die Bezeichner im umgebenden Gültigkeitsbereich umbenannt. Beide Transformationen vertragen sich schlecht mit Code, der zur
Laufzeit aus einem String erzeugt wird: eval(s), new Function(...s) und Function(...s). Verweist der zur Laufzeit erzeugte Code auf einen
Bezeichner, den der Obfuscator umbenannt hat, erhalten Sie beim ersten Ausführen der erzeugten Funktion
Uncaught ReferenceError: <renamed-name> is not defined.
Der Obfuscator behandelt jedes Muster unterschiedlich. Die folgende Tabelle ist die Kurzfassung; der Rest dieser Seite erläutert jede Zeile.
Was der Obfuscator tut, auf einen Blick
| Muster in Ihrem Quellcode | Was passiert |
|---|---|
eval('literal string') (Rumpf ist ein String-Literal) | Läuft korrekt. Die Funktion, die diesen Aufruf enthält, und jede darin definierte Funktion werden nicht mehr zu VM-Bytecode kompiliert (direktes eval liest die umgebenden lokalen Variablen, die die VM nicht erhält, sobald eine Funktion zu Bytecode kompiliert ist). |
eval(dynamicExpression) | Kann zur Laufzeit mit ReferenceError abstürzen. Die Funktion, die diesen Aufruf enthält, und jede darin definierte Funktion werden ebenfalls nicht mehr zu VM-Bytecode kompiliert. |
(0, eval)(s) / window.eval(s) (indirekt) | Die Funktion, die diesen Aufruf enthält, wird normal zu VM-Bytecode kompiliert. Indirektes eval läuft im globalen Gültigkeitsbereich und sieht keine umgebenden lokalen Variablen, daher können umbenannte lokale Variablen es nicht beschädigen. Es kann dennoch fehlschlagen, wenn der ausgewertete Code auf eine globale Variable verweist, die umbenannt oder entfernt wurde. |
new Function('a', 'b', 'return a + b') (alle Argumente sind String-Literale) | Läuft korrekt. Die Funktion, die diesen Aufruf enthält, wird normal zu VM-Bytecode kompiliert. |
new Function(dynamicBody) / Function(dynamicBody) | Kann zur Laufzeit mit ReferenceError abstürzen. Die Funktion, die diesen Aufruf enthält, und jede darin definierte Funktion werden ebenfalls nicht mehr zu VM-Bytecode kompiliert. |
Der Compiler erkennt außerdem eval?.(code) vorsichtshalber und behandelt es wie direktes eval. JavaScript definiert diese
Form mit optionalem Aufruf als indirektes eval; die Erkennung durch den Compiler ändert an diesem Sprachverhalten nichts.
Nur einige dieser Muster erzeugen nicht fatale Warnungen, und nur dann, wenn der Aufruf innerhalb einer Funktion steht: Ein dynamischer
eval-, new Function- oder Function-Aufruf meldet sowohl DynamicCodeRenameRisk als auch VMDynamicCodeSkipped, ein statisches
eval('...') meldet nur VMDynamicCodeSkipped, und indirektes eval sowie ein vollständig statisches new Function melden nichts. Ein
dynamischer Aufruf auf der obersten Ebene einer Datei, außerhalb jeder Funktion, wird nie gemeldet. Die Warnungstypen und ein CI-Snippet finden Sie
unten unter Das Problem vor der Laufzeit erkennen.
Das Überspringen setzt sich durch IIFEs fort. Umschließt eine IIFE auf oberster Ebene Ihr gesamtes Bundle und verwendet irgendeine Funktion darin
dynamisches eval oder new Function, wird die gesamte IIFE von der VM-Bytecode-Kompilierung ausgenommen. Unter
Verhalten von direktem eval finden Sie das Muster zum Auflösen der IIFE, das die Auswirkungen
begrenzt.
Warum statisch und dynamisch unterschiedlich behandelt werden
eval(s) kann lokale Variablen der Funktion, in der es aufgerufen wird, lesen und schreiben. Ist s ein String-Literal, kann der
Obfuscator den Rumpf zum Zeitpunkt der Obfuskierung 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 Quellcode auf alte Namen verweist, die es nicht mehr gibt.
new Function(s) und indirektes eval funktionieren anders: Der Rumpf läuft immer im globalen Gültigkeitsbereich und hat nur Zugriff auf globale
Variablen, nie auf die lokalen Variablen rund um den Aufruf. Sie können nicht von umbenannten lokalen Variablen abhängen, aber dennoch fehlschlagen, wenn
sie auf globale Variablen verweisen, die umbenannt oder entfernt wurden, oder wenn Sie den Rumpf durch Verketten mit einem umbenannten Bezeichner
zusammensetzen (z. B. über func.toString() einer Funktion, deren Inneres der Obfuscator umgeschrieben hat).
Ein statischer Rumpf, der nur seine eigenen Parameter verwendet, etwa new Function('a', 'b', 'return a + b'), hat keine solche
Abhängigkeit: Der Rumpf ist ein einfacher String, den die Umbenennung nie untersucht. Der Obfuscator belässt den Aufruf, und die
umgebende Funktion kann weiterhin zu VM-Bytecode kompiliert werden. Eine andere eval-Syntax allein macht beliebigen
dynamischen Code nicht sicher: Prüfen Sie die Warnungen und testen Sie das fertige Bundle.
Der Fehler, den Sie zur Laufzeit sehen
Das typische Symptom ist ein ReferenceError beim ersten Ausführen der dynamisch erzeugten Funktion:
TU ist hier ein umbenannter Bezeichner, den der Obfuscator im IIFE-Gültigkeitsbereich des Bundles eingeführt hat. Der dynamische eval-
bzw. Function-Konstruktor-Aufruf wertet einen Rumpf aus, der darauf verweist, doch dieser Rumpf läuft in einem Gültigkeitsbereich, in dem TU nicht definiert ist.
Das Problem vor der Laufzeit erkennen
Der Obfuscator gibt über die API nicht fatale Warnungen aus, damit Sie einige dieser Muster in der CI abfangen können, bevor Sie ausliefern. Zwei Warnungstypen sind relevant, und beide werden nur für Aufrufe innerhalb einer Funktion gemeldet:
DynamicCodeRenameRisk- eine Funktion erzeugt zur Laufzeit Code aus einem String: ein direktesevaloder einnew Function- /Function-Aufruf, dessen Rumpf nicht statisch ist, oderfn.toString(), das in ein<script>oder einen Worker eingefügt wird. Diese Warnung sagt einenReferenceErrorvoraus.VMDynamicCodeSkipped- die VM-Bytecode-Kompilierung wurde für eine Funktion und jede darin definierte Funktion übersprungen, weil sie ein direktesevaloder einen dynamischennew Function- /Function-Aufruf enthält. Sie wird auch für ein sicheres statischeseval('literal')ausgelöst und signalisiert daher verlorenen VM-Schutz statt eines Absturzes zur Laufzeit. Enthält den Funktionsnamen (sofern verfügbar) und das Konstrukt, das das Überspringen ausgelöst hat.
Das npm-Paket javascript-obfuscator stellt in seinem Ergebnis keine Warnungen bereit, daher liest ein CI-Gate sie aus der API-Antwort:
Die Nachrichten result und chunk_end enthalten ein Array warnings. Das folgende Beispiel verwendet
readObfuscationResponse(), den Stream-Reader aus der API-Referenz, und lässt den Build nur bei
DynamicCodeRenameRisk fehlschlagen; nehmen Sie VMDynamicCodeSkipped in den Filter auf, wenn auch der Verlust des VM-Schutzes für eine
Funktion das Release blockieren soll.
Wenn ein Warnungstyp in Ihrem Build erwartet wird und Sie ihn lieber an der Quelle stummschalten, statt ihn in der CI zu filtern, steuert die
Option warnings (v7.8.0+), welche Warnungen ausgegeben werden: 'none' unterdrückt alles, und eine Zuordnung pro Typ
wie { VMDynamicCodeSkipped: false } schaltet nur einen Typ stumm und behält die übrigen bei.
Umgehungen
Auf indirektes eval (
(0, eval)(s)) umstellenNur für den
eval-Fall nützlich. Indirektes eval läuft im globalen Gültigkeitsbereich und sieht daher keine umgebenden lokalen Variablen; aus demselben Grund kann es auch nicht auf umbenannte lokale Variablen verweisen. Die Funktion, die den Aufruf enthält, bleibt VM-Bytecode. Der ausgewertete Code darf dennoch nicht von globalen Variablen abhängen, die der Obfuscator umbenennt.Den Rumpf vollständig statisch machen
Wenn Sie bei
new Functionden Rumpf als ein einziges String-Literal bzw. Template-Literal ohne Interpolationen ausdrücken können, birgt der Aufruf kein Umbenennungsrisiko, und die umgebende Funktion wird weiterhin zu Bytecode kompiliert.new Function('a', 'b', 'return a + b')ist in Ordnung,new Function('return ' + expr)nicht.Den Aufruf in eine eigene Funktion auf oberster Ebene verschieben und die IIFE auflösen
Jede Funktion auf oberster Ebene wird unabhängig geprüft. Verschieben Sie den Aufruf mit dynamischem Code in eine eigene Funktion auf oberster Ebene, verliert nur diese eine Funktion die VM-Bytecode-Kompilierung, statt dass sich das Überspringen durch eine IIFE auf oberster Ebene fortsetzt, die Ihr gesamtes Bundle umschließt.
Auf
vmTargetFunctionsMode: 'comment'umstellenOpt-in-Modus: Nur Funktionen, die mit
/* javascript-obfuscator:vm */markiert sind, werden zu Bytecode kompiliert. Annotieren Sie die Funktion, die den Aufruf mit dynamischem Code enthält, nicht, und markieren Sie die übrigen. Erhalten Sie diese Kommentare bis zum Obfuskierungsschritt. Siehe Funktionen gezielt auswählen.Das Überspringen mit
vmForceCompileDynamicCode: trueübersteuern (v6.14.0+)Notausgang als letztes Mittel. Ist die Option aktiviert, kompiliert der Obfuscator die umgebende Funktion trotzdem zu Bytecode und unterdrückt die Warnung
VMDynamicCodeSkipped. Abhängigkeiten von Gültigkeitsbereichen oder umbenannten Bezeichnern kann sie nicht reparieren: Verwenden Sie sie nur, wenn Sie garantieren können, dass der zur Laufzeit erzeugte Rumpf nie auf einen Bezeichner verweist, den der Obfuscator umbenennt. Andernfalls tauschen Sie eine saubere Obfuskierung gegen einenReferenceErrorzur Laufzeit.DynamicCodeRenameRiskwird weiterhin ausgelöst, sodass die CI weiterhin darauf prüfen kann. Im Dashboard ist dies der Schalter „Force Compile Dynamic Code“ in der Gruppe Überschreibungen des VM-Bereichs.
Verwandte Seiten
- Verhalten von direktem eval - das Muster zum Auflösen der IIFE, das die Auswirkungen des Überspringens begrenzt.
- Funktionen gezielt auswählen -
vmTargetFunctionsModeverwenden, um Funktionen einzeln ein- oder auszuschließen.
