Funktionsnamen vor der LLM-Analyse verbergen
Das Problem
Sie haben vmObfuscation: true aktiviert, eine Datei verarbeitet, die eine Funktion wie validateLicense enthält, und
festgestellt, dass die obfuskierte Ausgabe weiterhin den wörtlichen Text validateLicense enthält: Der Funktionsrumpf ist
verschwunden und durch Bytecode ersetzt, aber der Name selbst steht gut sichtbar da.
Übertragen Sie das auf eine echte Codebasis, und Sie erhalten eine Liste von Funktionsnamen wie validateLicense,
decryptPayload, processPayment oder checkSubscription. Ein LLM muss den Bytecode nicht knacken, um zu verstehen, was
das Programm tut: Schon die Namen genügen ihm für eine selbstbewusste und zutreffende Zusammenfassung des Modulverhaltens.
Der Bytecode ist undurchsichtig, das Inhaltsverzeichnis nicht.
Warum die VM-Obfuskierung diese Namen beibehält
Mit vmTargetFunctionsMode: 'root' (der Standardeinstellung) wandelt der Obfuscator den Rumpf jeder Funktion auf
oberster Ebene in VM-Bytecode um, lässt den Namen aber bewusst unverändert. Eine Funktionsdeklaration auf oberster Ebene
ist semantisch eine Bindung im umgebenden Gültigkeitsbereich: bei einem Skript im globalen Objekt, bei einem Modul im
Modul-Gültigkeitsbereich (eine nicht exportierte Funktion auf oberster Ebene ist modullokal, nicht global, liegt in der Datei aber trotzdem auf oberster Ebene). Der Obfuscator kann sie nicht gefahrlos umbenennen, weil er nicht wissen kann, wer sonst noch darauf
verweist: ein anderes Bundle, ein Inline-<script>, ein HTML-Attribut onclick="validateLicense(...)", ein dynamischer
Zugriff über window['validateLicense'] usw.
Der Kompromiss der Standardeinstellung lautet also: die Implementierung schützen, die öffentliche Schnittstelle erhalten.
Dadurch bleiben Integrationen intakt, aber ein LLM erhält zugleich ein kostenloses Verzeichnis aller Einstiegspunkte. In
diesem Fall meldet das Obfuskierungsergebnis eine Warnung VMGlobalFunctionNamesNotRenamed mit den Namen, die lesbar
geblieben sind.
Warum das für LLM-gestütztes Reverse Engineering wichtig ist
Ein menschlicher Angreifer, der vor einigen hundert Zeilen Bytecode-Dispatch sitzt, gibt meist auf. Ein LLM, das dieselbe Datei erhält, greift den Bytecode gar nicht erst an: Es liest die Namen, gleicht sie mit den wenigen sichtbaren String-Literalen ab und liefert etwa Folgendes:
„Dieses Modul schaltet eine kostenpflichtige Funktion frei. validateLicense prüft ein signiertes Token, checkExpiry
weist abgelaufene Lizenzen zurück, und activateFeature entsperrt die Oberfläche, sobald die Prüfung bestanden ist. Die
Entschlüsselungshilfe steckt in decryptPayload.“
Diese Zusammenfassung reicht einem Angreifer, um eine gezielte Umgehung zu planen, ohne die VM je anzurühren. Die Namen sind das Leck.
Die Lösung: Code in eine IIFE einschließen
Der einfachste und robusteste Weg, dieses Leck zu schließen, besteht darin, Ihre sensiblen Funktionen eine Ebene tiefer in den Gültigkeitsbereichsbaum zu verschieben. Funktionen, die innerhalb einer anderen Funktion deklariert sind, liegen nicht auf oberster Ebene. Der Obfuscator kann sie daher frei umbenennen und ihre Deklarationen wie jede andere Anweisung in Bytecode aufnehmen.
Eine IIFE (Immediately-Invoked Function Expression, sofort ausgeführter Funktionsausdruck) ist dafür der leichtgewichtigste Weg: Sie fügt eine einzige umschließende Funktion hinzu, die einmal ausgeführt wird und nichts unter einem Namen preisgibt.
Vorher: Namen sichtbar
Nach der VM-Obfuskierung bleiben sowohl validateLicense als auch checkExpiry in der Ausgabe namentlich erhalten.
Nachher: Namen hinter einer IIFE verborgen
Jetzt befinden sich beide Funktionsdeklarationen im Rumpf der IIFE. Die IIFE selbst ist das einzige Konstrukt auf oberster Ebene, und eine anonyme IIFE hat keinen Namen, der etwas verraten könnte. Nach der VM-Obfuskierung sind die Funktionsnamen umbenannt und die Rümpfe in Bytecode umgewandelt.
Was sich geändert hat. Die Funktionen sind nicht mehr als globale Werte erreichbar (genau darüber sind ihre Namen
durchgesickert). Sie lassen sich weiterhin aufrufen, allerdings nur innerhalb derselben IIFE. Musste tatsächlich etwas
validateLicense von außen aufrufen, erreicht es die Funktion nicht mehr; siehe das Trampolin-Muster
weiter unten. War das nicht der Fall, haben Sie nichts verloren.
Exportierte Namen, reservierte Bezeichner, Strings und beobachtbares Verhalten können sichtbar bleiben. Testen Sie Integrationen nach dem Einschließen, insbesondere globale Werte, Modulexporte und Code, der Funktionsnamen auswertet.
Ein Kompromiss, den Sie kennen sollten: Enthält der Rumpf der IIFE ein direktes eval oder einen Aufruf von
new Function(...) / Function(...) mit dynamischem Rumpf, überspringt die VM-Obfuskierung diese gesamte Funktion samt allem, was darin verschachtelt ist
(eine Warnung VMDynamicCodeSkipped), weil der zur Laufzeit erzeugte Quelltext auf Bezeichner verweisen könnte, die der
Obfuscator umbenannt hat. Der Code, den Sie gerade in die IIFE verschoben haben, fällt dann auf die gewöhnliche Obfuskierung
zurück und verliert seinen Bytecode-Schutz. Verwenden Sie indirektes eval - (0, eval)(...) - oder lesen Sie die Optionen unter
Verhalten von direktem eval nach.
Was, wenn eine Funktion wirklich global sein muss?
Manchmal ist eine Funktion tatsächlich ein öffentlicher Einstiegspunkt: ein Inline-Event-Handler, ein JSONP-Callback oder ein Hook für ein Drittanbieter-SDK. Sie haben zwei Möglichkeiten:
Ein schlankes Trampolin bereitstellen und die Logik in der IIFE belassen. Deklarieren Sie einen kleinen globalen Wrapper, dessen einzige Aufgabe es ist, die Implementierung innerhalb der IIFE aufzurufen. Der Name des Trampolins ist weiterhin sichtbar, trägt aber keine inhaltliche Information (nennen Sie es etwa
__entry1), und die gesamte aussagekräftige Logik bleibt verborgen.Die Aufrufstelle umschreiben. Existiert der globale Wert nur, weil ein Inline-Handler
onclick="validateLicense(...)"ihn benötigt, ersetzen Sie diesen durchaddEventListenerinnerhalb der IIFE. Das HTML nennt die Funktion dann nicht mehr, die Funktion muss nicht mehr global sein, und das Leck verschwindet vollständig.
Variableninitialisierer auf oberster Ebene: vmWrapTopLevelInitializers
Funktionsdeklarationen sind nicht das Einzige, was auf der obersten Ebene einer Datei steht. Variableninitialisierer auf
oberster Ebene, also String-Konstanten, Konfigurationsobjekte und Nachschlagetabellen, sind in der Ausgabe ebenso lesbar,
wenn sie reines JavaScript bleiben. Eine Zeile wie const API_BASE = '/api/v2/license' verrät einem LLM genauso viel wie
function validateLicense.
Die Option vmWrapTopLevelInitializers (boolesch, Standardwert false; die aktuellen VM-Voreinstellungen aktivieren sie)
schließt geeignete Initialisierer auf oberster Ebene in eine IIFE ein, sodass der Wert selbst zur Laufzeit von VM-Bytecode
berechnet wird, statt als Literal im Quelltext zu stehen. Im Root-Modus werden Initialisierer, die reines JavaScript bleiben, mit der
Warnung VMTopLevelInitializerNotVirtualized gemeldet.
Ohne die Option
Mit vmWrapTopLevelInitializers: true
Der Name der Bindung (MY_STRING) liegt aus demselben Grund wie Funktionsnamen weiterhin auf oberster Ebene, denn Code
außerhalb der Datei könnte darauf verweisen. Der Wert, den sie enthält, wird jetzt aber von der VM erzeugt und erscheint
nicht mehr als lesbarer Text.
Die Option wirkt nur, wenn vmTargetFunctionsMode auf 'root' (der Standardeinstellung) steht und vmAsyncExecutor
ausgeschaltet ist. Im Kommentarmodus hat
sie keine Wirkung, und es wird keine Warnung gemeldet. Im Root-Modus unter vmAsyncExecutor (das nur asynchrone Funktionen
virtualisiert) hat sie ebenfalls keine Wirkung, und der Build meldet VMTopLevelInitializerNotVirtualized für die Initialisierer, die als reines
JavaScript verbleiben.
Enthält eine Datei keine Funktion, die die VM virtualisieren kann, meldet das Ergebnis VMNoFunctionsToVirtualize.
Schließen Sie den zu schützenden Code in eine Funktion ein, oder wählen Sie sensible Funktionen im Kommentarmodus explizit
aus.
Wenn das nicht ausreicht
- Aus anderen Modulen importierte Namen. Wenn Sie mehrere Dateien bündeln und ein Modul
validateLicenseexportiert, damit ein anderes es importiert, lässt der Bundler diesen Namen in der gebündelten Ausgabe genauso sichtbar wie Funktionen auf oberster Ebene. Schließen Sie das Bundle selbst in eine IIFE ein (die meisten Bundler können das), oder verschieben Sie den Export in eine IIFE und stellen Sie ihn über ein nichtssagendes Trampolin wieder bereit. - Die Laufzeit bleibt beobachtbar. Obfuskierung erhöht den Aufwand, Code zu verstehen und zu verändern; sie kann nicht garantieren, dass Reverse Engineering unmöglich ist. Halten Sie Geheimnisse und maßgebliche Sicherheitsentscheidungen auf dem Server.
Greifen Sie nicht zuerst zu renameGlobals. Die Option existiert und benennt Bezeichner auf oberster Ebene um, kann
aber nicht wissen, welche dieser Bezeichner von außerhalb der Datei referenziert werden (andere Bundles, Inline-HTML,
dynamische Zugriffe). Wenn Sie sie einschalten, bricht das Integrationen oft auf subtile Weise. Das Einschließen in eine
IIFE ist sicherer: Es benennt nichts Globales um, sondern verhindert lediglich globale Werte, die Sie gar nicht brauchten.
