Documentation
/
Recettes
/

Masquer les noms de fonctions à l'analyse par LLM

Masquer les noms de fonctions à l'analyse par LLM

Le problème

Vous avez activé vmObfuscation: true, appliqué l'obfuscation à un fichier contenant une fonction comme validateLicense, et constaté que la sortie obfusquée contient toujours le texte littéral validateLicense - le corps a bien disparu, remplacé par du bytecode, mais le nom, lui, s'affiche en clair.

// 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);
}

Multipliez cela à l'échelle d'une vraie base de code et vous obtenez une liste de noms de fonctions comme validateLicense, decryptPayload, processPayment, checkSubscription. Un LLM n'a pas besoin de casser le bytecode pour comprendre ce que fait le programme - les noms suffisent à lui faire produire un résumé sûr et exact du comportement du module. Le bytecode est opaque ; la table des matières, non.

Pourquoi l'obfuscation VM conserve ces noms

Avec vmTargetFunctionsMode: 'root' (valeur par défaut), l'obfuscateur transforme le corps de chaque fonction de niveau racine en bytecode VM, mais laisse délibérément le nom intact. Une déclaration de fonction de niveau racine est, sémantiquement, une liaison dans la portée environnante - pour un script, cela signifie l'objet global ; pour un module, l'espace de noms du module. L'obfuscateur ne peut pas la renommer sans risque, car il n'a aucun moyen de savoir qui d'autre la référence : un autre bundle, un <script> en ligne, un attribut HTML onclick="validateLicense(...)", une recherche dynamique window['validateLicense'], etc.

Le compromis retenu par défaut est donc : protéger l'implémentation, préserver la surface publique. Cela évite de casser l'intégration, mais cela offre aussi à un LLM un index gratuit de tous les points d'entrée.

Pourquoi cela compte face à la rétroconception assistée par LLM

Un attaquant humain confronté à quelques centaines de lignes de répartition de bytecode abandonnera généralement. Un LLM à qui l'on soumet le même fichier ne cherchera même pas à attaquer le bytecode - il lira les noms, recoupera les rares littéraux de chaîne encore visibles, et produira quelque chose comme :

Ce résumé suffit à un attaquant pour préparer un contournement ciblé sans jamais toucher à la VM. Ce sont les noms qui fuitent.

La solution : encapsuler votre code dans une IIFE

Le moyen le plus simple et le plus robuste de supprimer cette fuite consiste à descendre vos fonctions sensibles d'un cran dans l'arbre des portées. Les fonctions déclarées à l'intérieur d'une autre fonction ne sont pas de niveau racine : l'obfuscateur est donc libre de les renommer et d'absorber leurs déclarations dans le bytecode, comme n'importe quelle autre instruction.

Une IIFE (Immediately-Invoked Function Expression) est le moyen le plus léger d'y parvenir - elle n'ajoute qu'une seule fonction englobante, exécutée une fois et n'exposant aucun nom.

Avant - noms exposés

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();
    }
});

Après obfuscation VM, validateLicense et checkExpiry survivent tous deux par leur nom dans la sortie.

Après - noms masqués derrière une IIFE

(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();
        }
    });
})();

Les deux déclarations de fonctions vivent désormais dans le corps de l'IIFE. L'IIFE est la seule construction de niveau racine, et une IIFE anonyme n'a aucun nom à divulguer. Après obfuscation VM, tout son corps - y compris chaque déclaration qu'il contient - est converti en bytecode puis encodé ; rien de ce qui se trouve à l'intérieur de l'IIFE ne subsiste sous forme de texte lisible.

Et si une fonction doit vraiment être globale ?

Il arrive qu'une fonction soit réellement un point d'entrée public - un gestionnaire d'événement en ligne, un callback JSONP, un point d'accroche d'un SDK tiers. Vous avez alors deux possibilités :

  • Exposer un fin trampoline et garder la logique à l'intérieur de l'IIFE. Déclarez un petit wrapper global dont le seul rôle est d'appeler l'implémentation confinée dans l'IIFE. Le nom du trampoline fuite toujours, mais il ne véhicule aucune information sémantique - appelez-le __entry1 ou similaire - et toute la logique porteuse de sens reste masquée.

    var __entry1;
    (function () {
        function validateLicense(token) { /* … */ }
        __entry1 = validateLicense;
    })();
    // Outside code calls __entry1(token) instead of validateLicense(token).
  • Réécrire le site d'appel que vous ne contrôlez pas. Si la globale n'existe que parce qu'un onclick="validateLicense(...)" en ligne en a besoin, remplacez ce gestionnaire en ligne par un addEventListener déclaré depuis l'IIFE. Le HTML cesse de nommer la fonction, la fonction cesse d'avoir besoin d'être globale, et la fuite disparaît entièrement.

Initialiseurs de variables de premier niveau : vmWrapTopLevelInitializers

Les déclarations de fonctions ne sont pas les seules choses à vivre à la racine d'un fichier. Les initialiseurs de variables de premier niveau - constantes de chaîne, objets de configuration, tables de correspondance - sont tout aussi lisibles dans la sortie par défaut. Une ligne comme const API_BASE = '/api/v2/license' en dit autant à un LLM que function validateLicense.

L'option vmWrapTopLevelInitializers (booléen, false par défaut) enveloppe les initialiseurs de premier niveau éligibles dans une IIFE, de sorte que la valeur elle-même est calculée par le bytecode VM à l'exécution au lieu de figurer en clair dans le source.

Sans l'option

// Input
const MY_STRING = 'my-string';

// Output - string is visible
const MY_STRING = 'my-string';

Avec 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);
})();

Le nom de la liaison (MY_STRING) reste de niveau racine pour la même raison que les noms de fonctions - quelque chose à l'extérieur du fichier pourrait le référencer - mais la valeur qu'il contient est désormais produite par la VM et n'apparaît plus sous forme de texte lisible.

Quand cela ne suffit pas

  • Noms importés depuis d'autres modules. Si vous regroupez plusieurs fichiers et qu'un module exporte validateLicense pour qu'un autre l'importe, le bundler conservera ce nom visible dans la sortie groupée, exactement comme les fonctions de niveau racine le sont. Encapsulez le bundle lui-même dans une IIFE (la plupart des bundlers savent le faire), ou déplacez l'export dans une IIFE et réexposez-le via un trampoline au nom insignifiant.