Ocultar los nombres de las funciones al análisis con LLM
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.
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
Tras la ofuscación VM, tanto validateLicense como checkExpiry sobreviven con su nombre en la salida.
Después: nombres ocultos tras una IIFE
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
__entry1o algo similar) y toda la lógica con significado permanece oculta.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 poraddEventListenerdesde 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
Con vmWrapTopLevelInitializers: true
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
validateLicensepara 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.
