Funktionsnamen vor der LLM-Analyse verbergen
Das Problem
Sie haben vmObfuscation: true aktiviert, es auf eine Datei mit einer Funktion wie validateLicense angewendet und
festgestellt, dass die obfuskierte Ausgabe den Text validateLicense weiterhin wörtlich enthält – der Rumpf ist
verschwunden und durch Bytecode ersetzt, aber der Name steht offen sichtbar da.
// Input
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
// Output - name is preserved, body is bytecode
function validateLicense(b) {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
}
Rechnen Sie das auf eine echte Codebasis hoch, und Sie erhalten eine Liste von Funktionsnamen wie validateLicense,
decryptPayload, processPayment, checkSubscription. Ein LLM muss den Bytecode gar nicht knacken, um zu verstehen, was
das Programm tut – die Namen allein genügen ihm für eine selbstbewusste, treffende Zusammenfassung des Modulverhaltens. Der
Bytecode ist undurchsichtig, das Inhaltsverzeichnis nicht.
Warum die VM-Obfuskierung diese Namen beibehält
Mit vmTargetFunctionsMode: 'root' (dem Standard) wandelt der Obfuscator den Rumpf jeder Funktion auf Root-Ebene in
VM-Bytecode um, lässt den Namen aber bewusst unangetastet. Eine Funktionsdeklaration auf Root-Ebene ist semantisch eine
Bindung im umgebenden Scope – bei einem Skript ist das das globale Objekt, bei einem Modul der Modul-Namensraum. Der
Obfuscator kann sie nicht gefahrlos umbenennen, weil er nicht wissen kann, wer sie sonst noch referenziert: ein anderes
Bundle, ein Inline-<script>, ein HTML-Attribut onclick="validateLicense(...)", ein dynamischer Zugriff
window['validateLicense'] und so weiter.
Der Kompromiss des Standardverhaltens lautet also: die Implementierung schützen, die öffentliche Oberfläche erhalten. Das hält die Integration intakt, bedeutet aber auch, dass ein LLM einen kostenlosen Index sämtlicher Einstiegspunkte erhält.
Warum das für LLM-gestütztes Reverse Engineering wichtig ist
Ein menschlicher Angreifer, der ein paar hundert Zeilen Bytecode-Dispatch vor sich hat, gibt meist auf. Ein LLM, dem dieselbe Datei vorgelegt wird, greift den Bytecode gar nicht erst an – es liest die Namen, gleicht sie mit den wenigen sichtbaren String-Literalen ab und liefert etwas wie:
Diese Zusammenfassung genügt einem Angreifer, um eine gezielte Umgehung zu planen, ohne die VM auch nur anzufassen. Die Namen sind das Leck.
Die Lösung: Verpacken Sie Ihren Code in ein IIFE
Der einfachste und robusteste Weg, dieses Leck zu schließen, besteht darin, Ihre sensiblen Funktionen eine Ebene tiefer in den Scope-Baum zu schieben. Funktionen, die innerhalb einer anderen Funktion deklariert werden, liegen nicht auf Root-Ebene, sodass der Obfuscator sie frei umbenennen und ihre Deklarationen wie jede andere Anweisung in den Bytecode aufnehmen kann.
Ein IIFE (Immediately-Invoked Function Expression) ist der leichtgewichtigste Weg dorthin – es fügt eine einzige umhüllende Funktion hinzu, die einmal läuft und nichts unter einem Namen nach außen gibt.
Vorher – Namen liegen offen
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
Nach der VM-Obfuskierung überleben sowohl validateLicense als auch checkExpiry namentlich in der Ausgabe.
Nachher – Namen hinter einem IIFE verborgen
(function () {
function validateLicense(token) {
const decoded = decodeBase64(token);
return verifySignature(decoded);
}
function checkExpiry(license) {
return Date.now() < license.expiresAt;
}
document.querySelector('#activate').addEventListener('click', () => {
const token = document.querySelector('#token').value;
if (validateLicense(token)) {
unlockUI();
}
});
})();
Nun liegen beide Funktionsdeklarationen im Rumpf des IIFE. Das IIFE selbst ist das einzige Konstrukt auf Root-Ebene, und ein anonymes IIFE hat keinen Namen, der verraten werden könnte. Nach der VM-Obfuskierung wird der gesamte Rumpf – einschließlich jeder Deklaration darin – in Bytecode umgewandelt und codiert; nichts im Inneren des IIFE überlebt als lesbarer Text.
Was, wenn eine Funktion wirklich global sein muss?
Manchmal ist eine Funktion tatsächlich ein öffentlicher Einstiegspunkt – ein Inline-Event-Handler, ein JSONP-Callback, ein Hook eines Drittanbieter-SDKs. Dann haben Sie zwei Möglichkeiten:
Ein schlankes Trampolin nach außen geben und die Logik im IIFE belassen. Deklarieren Sie einen kleinen globalen Wrapper, dessen einzige Aufgabe darin besteht, die im IIFE liegende Implementierung aufzurufen. Der Name des Trampolins liegt weiterhin offen, transportiert aber keinerlei inhaltliche Information – nennen Sie ihn
__entry1oder ähnlich –, und die gesamte aussagekräftige Logik bleibt verborgen.var __entry1; (function () { function validateLicense(token) { /* … */ } __entry1 = validateLicense; })(); // Outside code calls __entry1(token) instead of validateLicense(token).Die Aufrufstelle umschreiben, die Sie nicht kontrollieren. Existiert der Global nur, weil ein Inline-
onclick="validateLicense(...)"ihn braucht, ersetzen Sie den Inline-Handler durch einaddEventListeneraus dem IIFE heraus. Das HTML nennt die Funktion dann nicht mehr, die Funktion muss nicht mehr global sein, und das Leck verschwindet vollständig.
Initialisierer von Top-Level-Variablen: vmWrapTopLevelInitializers
Funktionsdeklarationen sind nicht das Einzige, was auf der Wurzelebene einer Datei lebt. Initialisierer von
Top-Level-Variablen – String-Konstanten, Konfigurationsobjekte, Nachschlagetabellen – sind in der Ausgabe standardmäßig
genauso gut lesbar. Eine Zeile wie const API_BASE = '/api/v2/license' verrät einem LLM ebenso viel wie
function validateLicense.
Die Option vmWrapTopLevelInitializers (boolesch, Standard false) verpackt geeignete Top-Level-Initialisierer in ein IIFE,
sodass der Wert selbst zur Laufzeit von VM-Bytecode berechnet wird, statt als Literal im Quelltext zu stehen.
Ohne die Option
// Input
const MY_STRING = 'my-string';
// Output - string is visible
const MY_STRING = 'my-string';
Mit vmWrapTopLevelInitializers: true
// Input
const MY_STRING = 'my-string';
// Output - initializer is now a VM call, the string lives inside bytecode
const MY_STRING = (() => {
return vmq_1bac70(0x5, [], undefined, undefined, undefined, this);
})();
Der Name der Bindung (MY_STRING) bleibt aus demselben Grund auf Root-Ebene, aus dem auch Funktionsnamen dort bleiben –
etwas außerhalb der Datei könnte ihn referenzieren –, aber der Wert, den sie hält, wird nun von der VM erzeugt und
erscheint nicht mehr als lesbarer Text.
Wenn das nicht ausreicht
- Importierte Namen aus anderen Modulen. Wenn Sie mehrere Dateien bündeln und ein Modul
validateLicenseexportiert, damit ein anderes es importieren kann, hält der Bundler diesen Namen in der gebündelten Ausgabe genauso sichtbar wie Funktionen auf Root-Ebene. Verpacken Sie das Bundle selbst in ein IIFE (die meisten Bundler können das), oder verschieben Sie den Export in ein IIFE und geben Sie ihn über ein nichtssagendes Trampolin wieder nach außen.
