Documentación
/
Recetas
/

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

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

Pro

El problema

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

JavaScript

Multiplica eso por una base de código real y obtendrás una lista de nombres de funciones como validateLicense, decryptPayload, processPayment o checkSubscription. Un LLM no necesita descifrar el bytecode para entender qué 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 predeterminado), el ofuscador transforma el cuerpo de cada función de nivel raíz en bytecode de la VM, pero deja deliberadamente intacto el nombre. Una declaración de función de nivel raíz es, semánticamente, un enlace en el ámbito circundante: en un script, eso significa el objeto global, y en un módulo significa el ámbito del módulo (una función de nivel superior no exportada tiene ámbito de módulo, no es global, pero sigue siendo de nivel raíz en el archivo). El ofuscador no puede renombrarla de forma segura porque no tiene manera de saber quién más hace referencia a ella: otro bundle, un <script> en línea, un atributo HTML onclick="validateLicense(...)", una búsqueda dinámica window['validateLicense'], etc.

Así que la contrapartida que asume el valor predeterminado es: proteger la implementación y conservar la superficie pública. Eso mantiene intactas las integraciones, pero también significa que un LLM obtiene gratis un índice de todos los puntos de entrada. Cuando esto ocurre, el resultado de la ofuscación informa de una advertencia VMGlobalFunctionNamesNotRenamed que enumera los nombres que siguieron siendo legibles.

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

Un atacante humano que se enfrente a unos cientos de líneas de dispatch de bytecode normalmente se rendirá. Un LLM al que se le dé el mismo archivo ni siquiera se molestará en atacar el bytecode: leerá los nombres, cruzará las pocas cadenas literales que pueda ver y producirá algo como:

"Este módulo controla el acceso a una función de pago. validateLicense verifica un token firmado, checkExpiry rechaza las licencias caducadas y activateFeature desbloquea la interfaz una vez superada la comprobación. La función auxiliar de descifrado está en decryptPayload."

Ese resumen basta para que un atacante planifique una elusión dirigida sin tocar nunca la VM. La filtración son los nombres.

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

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

Una IIFE (Immediately-Invoked Function Expression, expresión de función invocada inmediatamente) 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 nombre.

Antes: nombres expuestos

JavaScript

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

Después: nombres ocultos tras una IIFE

JavaScript

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, los nombres de las funciones se renombran y los cuerpos se convierten en bytecode.

Qué ha cambiado. Las funciones ya no son accesibles como variables globales (que es lo que filtraba sus nombres). Siguen pudiendo llamarse, pero solo desde dentro de la misma IIFE. Si algo necesitaba de verdad llamar a validateLicense desde fuera, ya no puede alcanzarla; consulta el patrón trampolín más abajo. Si nada lo hacía, no has perdido nada.

Los nombres exportados, los identificadores reservados, las cadenas y el comportamiento observable pueden seguir siendo visibles. Prueba las integraciones después de envolver el código, sobre todo las variables globales, las exportaciones de módulos y el código que inspecciona nombres de funciones.

Una contrapartida que debes conocer: si el cuerpo de la IIFE contiene un eval directo, o una llamada a new Function(...) / Function(...) con un cuerpo dinámico, la ofuscación VM omite esa función entera y todo lo que tiene anidado (con una advertencia VMDynamicCodeSkipped), porque el código fuente construido en tiempo de ejecución podría hacer referencia a identificadores que el ofuscador renombró. El código que acabas de mover dentro de la IIFE vuelve entonces a la ofuscación normal y pierde su protección con bytecode. Usa un eval indirecto, (0, eval)(...), o consulta Comportamiento de eval directo para ver las opciones.

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

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

  • Expón un trampolín mínimo y mantén la lógica dentro de la IIFE. Declara un pequeño envoltorio global cuyo único cometido sea llamar a la implementación con ámbito en la IIFE. El nombre del trampolín sigue filtrándose, pero no aporta información semántica (llámalo __entry1 o algo similar) y toda la lógica con significado permanece oculta.

    JavaScript

  • Reescribe el punto de llamada. Si la variable global solo existe porque la necesita un onclick="validateLicense(...)" en línea, 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 filtración desaparece por completo.

Inicializadores de variables de nivel superior: vmWrapTopLevelInitializers

Las declaraciones de funciones 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 cuando siguen siendo JavaScript plano. Una línea como const API_BASE = '/api/v2/license' le dice a un LLM tanto como function validateLicense.

La opción vmWrapTopLevelInitializers (booleana, false de forma predeterminada; los preajustes de VM actuales la activan) envuelve los inicializadores de nivel superior elegibles en una IIFE para que el propio valor lo calcule el bytecode de la VM en tiempo de ejecución en lugar de quedar en el código como un literal. En el modo root, los inicializadores que permanecen en JavaScript plano se notifican con una advertencia VMTopLevelInitializerNotVirtualized.

Sin la opción

JavaScript

Con vmWrapTopLevelInitializers: true

JavaScript

El nombre del enlace (MY_STRING) sigue siendo de nivel raíz por el mismo motivo que los nombres de las funciones (algo fuera del archivo podría hacer referencia a él), pero el valor que contiene ahora lo produce la VM y ya no aparece como texto legible.

Solo tiene efecto cuando vmTargetFunctionsMode es 'root' (el valor predeterminado) y vmAsyncExecutor está desactivado. En el modo comment la opción no tiene ningún efecto y no se notifica ninguna advertencia. En el modo root con vmAsyncExecutor (que solo virtualiza funciones async) tampoco tiene efecto, y la compilación informa VMTopLevelInitializerNotVirtualized para los inicializadores que quedan en JavaScript sin transformar.

Si un archivo no contiene ninguna función que la VM pueda virtualizar, el resultado informa de VMNoFunctionsToVirtualize; envuelve en una función el código que quieras proteger, o usa el modo comment para seleccionar explícitamente las funciones sensibles.

Cuándo 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, igual que las funciones de nivel raíz. 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.
  • La ejecución sigue siendo observable. La ofuscación aumenta el esfuerzo necesario para entender y modificar el código; no puede garantizar que la ingeniería inversa sea imposible. Mantén los secretos y las decisiones de seguridad determinantes en el servidor.

No recurras primero a renameGlobals. Existe, y renombrará los identificadores de nivel raíz, pero no tiene forma de saber a cuáles de esos identificadores se hace referencia desde fuera del archivo (otros bundles, HTML en línea, búsquedas dinámicas). Activarla suele romper las integraciones de formas sutiles. Envolver en una IIFE es más seguro: no renombra nada global, simplemente deja de crear variables globales que no necesitabas.