Documentación
/
Recetas
/

Ocultar los nombres de las funciones al análisis con LLM

Ocultar los nombres de funciones del análisis por LLM

El problema

Activaste vmObfuscation: true, lo ejecutaste sobre un archivo que contiene una función como validateLicense y observaste que la salida ofuscada sigue conteniendo el texto literal validateLicense: el cuerpo ha desaparecido, reemplazado por bytecode, pero el nombre en sí sigue ahí a la vista.

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

Multiplica eso en una base de código real y obtienes una lista de nombres de funciones como validateLicense, decryptPayload, processPayment, checkSubscription. Un LLM no necesita descifrar el bytecode para entender lo que hace el programa: los nombres por sí solos le bastan para producir un resumen seguro y preciso del comportamiento del módulo. El bytecode es opaco; el índice de contenidos no.

Por qué la ofuscación VM conserva estos nombres

Con vmTargetFunctionsMode: 'root' (el valor por defecto), el ofuscador transforma el cuerpo de cada función de nivel raíz en bytecode de la VM, pero deja deliberadamente el nombre intacto. Una declaración de función de nivel raíz es, semánticamente, un enlace en el ámbito circundante: para un script eso significa el objeto global, para un módulo significa el espacio de nombres del módulo. El ofuscador no puede renombrarlo de forma segura porque no tiene manera de saber quién más lo referencia: otro bundle, un <script> en línea, un atributo HTML onclick="validateLicense(...)", una búsqueda dinámica window['validateLicense'], etc.

Así que la compensación que hace el valor por defecto es: proteger la implementación, preservar la superficie pública. Eso mantiene la integración intacta, pero también significa que un LLM obtiene gratis un índice de cada punto de entrada.

Por qué esto importa para la ingeniería inversa asistida por LLM

Un atacante humano ante unas pocas centenas de líneas de despacho de bytecode normalmente se rendirá. Un LLM al que se le da el mismo archivo no lo hará: ni siquiera intentará atacar el bytecode; leerá los nombres, cruzará los pocos literales de cadena que puede ver y emitirá algo como:

Ese resumen basta para que un atacante planee una elusión dirigida sin llegar a tocar la VM. Los nombres son la fuga.

La solución: envuelve tu código en una IIFE

La forma más sencilla y robusta de eliminar esta fuga es empujar tus funciones sensibles un nivel más adentro del árbol de ámbitos. Las funciones declaradas dentro de otra función no son de nivel raíz, así que el ofuscador es libre de renombrarlas y absorber sus declaraciones en el bytecode como cualquier otra sentencia.

Una IIFE (Immediately-Invoked Function Expression) es la forma más ligera de hacerlo: añade una única función envolvente que se ejecuta una vez y no expone nada por su nombre.

Antes - nombres expuestos

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

Tras la ofuscación VM, tanto validateLicense como checkExpiry sobreviven por su nombre en la salida.

Después - nombres ocultos tras una 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();
        }
    });
})();

Ahora ambas declaraciones de función viven dentro del cuerpo de la IIFE. La propia IIFE es la única construcción de nivel raíz, y una IIFE anónima no tiene nombre que filtrar. Tras la ofuscación VM, todo el cuerpo —incluida cada declaración de su interior— se convierte a bytecode y se codifica; nada dentro de la IIFE sobrevive como texto legible.

¿Y si una función realmente necesita ser global?

A veces una función realmente es un punto de entrada público: un manejador de eventos en línea, un callback JSONP, un enlace de un SDK de terceros. Tienes dos opciones:

  • Expón un trampolín ligero y mantén la lógica dentro de la IIFE. Declara un pequeño wrapper global cuyo único trabajo sea llamar a la implementación con ámbito de la IIFE. El nombre del trampolín sigue filtrándose, pero no lleva información semántica: nómbralo __entry1 o similar, y toda la lógica significativa permanece oculta.

    var __entry1;
    (function () {
        function validateLicense(token) { /* … */ }
        __entry1 = validateLicense;
    })();
    // Outside code calls __entry1(token) instead of validateLicense(token).
  • Reescribe el punto de llamada que no controlas. Si la global solo existe porque un onclick="validateLicense(...)" en línea la necesita, sustituye el manejador en línea por addEventListener desde dentro de la IIFE. El HTML deja de nombrar la función, la función deja de necesitar ser global y la fuga desaparece por completo.

Inicializadores de variables de nivel superior: vmWrapTopLevelInitializers

Las declaraciones de función no son lo único que vive en la raíz de un archivo. Los inicializadores de variables de nivel superior —constantes de cadena, objetos de configuración, tablas de búsqueda— son igual de legibles en la salida por defecto. Una línea como const API_BASE = '/api/v2/license' le dice a un LLM tanto como function validateLicense.

La opción vmWrapTopLevelInitializers (booleana, por defecto false) envuelve los inicializadores de nivel superior elegibles en una IIFE, de modo que el valor en sí lo calcula el bytecode de la VM en tiempo de ejecución en lugar de aparecer en el código fuente como un literal.

Sin la opción

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

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

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

El nombre del enlace (MY_STRING) sigue siendo de nivel raíz por la misma razón que los nombres de función lo son —algo fuera del archivo podría referenciarlo—, pero el valor que contiene ahora lo produce la VM y ya no aparece como texto legible.

Cuando esto no es suficiente

  • Nombres importados de otros módulos. Si empaquetas varios archivos y un módulo exporta validateLicense para que otro lo importe, el bundler mantendrá ese nombre visible en la salida empaquetada del mismo modo que las funciones de nivel raíz son visibles. Envuelve el propio bundle en una IIFE (la mayoría de los bundlers pueden hacerlo), o mueve la exportación dentro de una IIFE y vuelve a exponerla mediante un trampolín sin significado.