Dokumentation
/
Rezepte
/

Funktionsnamen vor der LLM-Analyse verbergen

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 __entry1 oder ä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 ein addEventListener aus 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 validateLicense exportiert, 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.