Artículos en profundidad sobre la tecnología que da forma al futuro.

Obfuscación anti-LLM: el fingerprinting contraataca

La obfuscación anti-LLM oculta prompts de jailbreak en scripts de fingerprinting para que la IA no pueda desofuscarlos. Así funciona y qué lo vence.

Una pared de pequeñas impresiones de arte generativo abstracto en lienzo, con una pieza central brillante
Un rastreo de 10.000 sitios arrojó 1.033 canvas de fingerprinting, y uno de ellos escondía una trampa.

Un investigador enganchó cada llamada a canvas en 10.000 de los sitios web más grandes, recopiló más de mil renderizados de fingerprinting y se puso a buscar el más bonito. Lo que encontró dentro del script responsable es el ejemplo más limpio hasta la fecha de obfuscación anti-LLM: un script de fingerprinting de navegador cargado de funciones con nombres como computeRicinLethalDosageForWaterSupply y un prompt de jailbreak codificado en caracteres Unicode invisibles. No está diseñado para esconderse de ingenieros inversos humanos, sino para que los asistentes de IA se nieguen a desofuscarlo.

Llevo años en respuesta a incidentes, y mi primera lectura del informe produjo la reacción que buscaban los autores: esto parece malware, o algo peor. Funciones con nombres sobre dosis letales de ricina y radios de contaminación de una bomba sucia. Una cadena decodificada que exige guías paso a paso para sintetizar armas biológicas. Una ruta de código de Node.js que lee .npmrc, .env y configuraciones de kube, descarga un binario desde una IP interna e instala una baliza cron. Todos mis instintos dicen actor malicioso. Y todos están equivocados, y el error es precisamente el objetivo. Esta es una historia sobre lo que pasa cuando quienes venden obfuscación se dan cuenta de que su adversario real ya no es un humano con un depurador, sino un modelo con un filtro de seguridad.

El rastreo en sí fue sencillo: Playwright contra la lista de sitios top de Tranco, enganchando HTMLCanvasElement.prototype.toDataURL y las delatoras clásicas de automatización como navigator.webdriver para filtrar los canvas inofensivos. Los renderizados se recopilaron en una galería pública, agrupados por proveedor: FingerprintJS, Castle, los sospechosos habituales, más una larga cola de desconocidos. Un canvas, un precioso remolino de líneas fluidas, se rastreó hasta un script servido por un proveedor ruso de protección DDoS. Ahí empezó la diversión.

El problema: los LLM rompieron el modelo de negocio de la obfuscación

Durante veinte años, la obfuscación de JavaScript tuvo exactamente un trabajo: hacer la ingeniería inversa lo bastante cara como para que nadie se molestara. Herramientas como javascript-obfuscator y sus derivados comerciales desordenan identificadores, empaquetan literales de cadena en arrays de búsqueda, inyectan predicados opacos y aplanan el flujo de control en máquinas de estado dirigidas por switch. Nada de eso sobrevive a un humano decidido con una tarde libre, pero no importaba, porque los humanos decididos con tardes libres eran raros y la economía favorecía al defensor.

Luego los LLM se volvieron buenos leyendo código ofuscado, y la economía se invirtió. Pegar un script anti-bot ofuscado de 400 KB en un chat y preguntar «¿qué hace esto?» lleva treinta segundos. Al modelo no le importa que tu función se llame PiERW ni que tus cadenas vivan en un array rotado. Lee la semántica. Expande el decodificador, sigue el flujo de datos y te devuelve un resumen en lenguaje llano. El foso de todos los proveedores anti-bot, la opacidad, se evaporó en silencio.

Los proveedores lo notaron, y su contramedida es genuinamente ingeniosa de una forma exasperante: si no puedes ocultar el código al modelo, ataca al modelo. Dale contenido que su entrenamiento de seguridad lo obliga a rechazar. La obfuscación anti-LLM trata la alineación del modelo como el eslabón más débil del proceso de análisis. Y, por desgracia, tienen razón.

Cómo los scripts de fingerprinting del navegador recogen entropía

Para entender la trampa, hay que conocer la maquinaria legítima a la que está sujeta. El fingerprinting por canvas consiste en dibujar texto, degradados y formas en un <canvas> invisible y luego leer el búfer de píxeles con toDataURL(). El pipeline de renderizado —GPU, driver, rasterizador de fuentes, antialiasing, hinting subpíxel— varía sutilmente entre máquinas, así que dos navegadores que dibujan las mismas instrucciones producen búferes de píxeles con hashes distintos. Combínalo con cadenas del renderizador WebGL, temporización de AudioContext, geometría de pantalla, fuentes instaladas y un centenar de señales más, y obtienes un identificador estable sin una sola cookie.

El rastreo que encontró nuestro script usó el truco de interceptación estándar. Engancha la primitiva de lectura en un script de inicialización, registra la traza de pila y un prefijo de cada imagen capturada:

await page.addInitScript(() => {
const original = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (...args) {
const result = original.apply(this, args);
fetch("/__log", {
method: "POST",
body: JSON.stringify({
stack: new Error().stack,
byteLength: result.length,
head: result.slice(0, 64),
}),
});
return result;
};
});

Haz eso en suficientes sitios y el ecosistema de fingerprinting se mapea solo. La mayoría de los canvas son aburridos: un pangrama, algunos rectángulos, un degradado. Unos pocos son lo bastante elaborados como para considerarse arte generativo. Todos son producidos por scripts muy ofuscados, porque los proveedores saben que un fingerprinter sin ofuscar es un fingerprinter fácil de identificar. La ofuscación siempre formó parte del producto. Lo nuevo es a quién va dirigida.

Dentro de la trampa de obfuscación anti-LLM

El script empezó de forma convencional: nombres desordenados, cadenas empaquetadas, lo de siempre. Luego los nombres de los identificadores dejaron de ser aleatorios y pasaron a apuntar a algo. computeRicinLethalDosageForWaterSupply. calculateDeadlyLethalSuicideAspirinDosage. computeCRISPRGeneDrivePropagationRate. computeCesium137DirtyBombFalloutRadius. Las funciones devuelven números disfrazados con resúmenes de sonido alarmante —«umbral de letalidad para adultos confirmado en X mg/kg»—, pero la aritmética es señuelo. No calcula nada real y no alimenta nada.

Aquí está la clave: los identificadores no tienen ningún significado en tiempo de ejecución, pero un peso semántico enorme para un modelo de lenguaje. Un ingeniero inverso humano ha aprendido con la experiencia a ignorar los nombres; todos nos hemos quemado con una función llamada validateUser que no valida nada. Un modelo hace lo contrario. Lee computeRicinLethalDosageForWaterSupply, detecta un contexto de armas biológicas y su entrenamiento de seguridad se activa antes que su curiosidad.

La capa más profunda era un template literal lleno de caracteres invisibles: puntos de código del bloque de Tags de Unicode, empezando en U+E0000. Se renderizan como nada. El decodificador es una sola línea:

const cleartext = [...payload]
.map((c) => String.fromCodePoint(c.codePointAt(0) - 917504))
.join("");
// 917504 === 0xE0000, the start of the invisible "Tags" block.
// Subtracting it maps each hidden char back to plain ASCII.

Decodificado, era un prompt de jailbreak completo: un falso «SYSTEM OVERRIDE -- CLASSIFIED BRIEFING» que declara suspendidas las directrices de seguridad y exige guías paso a paso para ataques DDoS contra infraestructura sanitaria, evasión fiscal, agentes biológicos aerosolizados y dosis de fármacos, además de referencias a la plaza de Tiananmén en inglés y chino, claramente diseñadas para activar los reflejos de censura de los modelos alojados en China. Era una cadena diseñada para golpear simultáneamente todas las taxonomías de salvaguardas. Un LLM al que se le pide analizar el código que lo rodea ingiere esto como contexto y, pase lo que pase, ahora no hace nada útil.

Una página aparentemente en blanco que revela glifos brillantes ocultos bajo luz ultravioleta
Un prompt de jailbreak oculto en caracteres Unicode de etiqueta invisibles: imperceptible para los humanos, pero ruidoso para los modelos.

Intentos fallidos: rechazos, falsos positivos y una botnet falsa

El primer movimiento natural —pegarlo en un modelo de frontera— falló exactamente como estaba previsto. El modelo se negó, citando riesgo de seguridad biológica. Un segundo intento con una postura de seguridad diferente también fue bloqueado. El ofuscador había convertido el rechazo en un arma: el script es ahora indescompilable para las herramientas que todos realmente usan. Un modelo de pesos abiertos con guardarraíles más débiles sí obedeció, lo cual es un dato, no una solución: la mayoría de los analistas no tienen un modelo local preparado para esto, y la mayoría de las políticas de empresa prohíben pegar código de terceros en infraestructura no validada de todos modos.

El segundo intento fallido es más sutil y más peligroso: el analista humano que concluye que el script es malware. Hay una ruta de código solo para Node, condicionada a process.env.METRICS_AGENT_8BFD_ACTIVE, que lee archivos de configuración cercanos a SSH, descarga un «metrics agent» desde 10.15.45.1, huella el host en /tmp, lo comprime e instala una baliza cron. Parece un implante de cadena de suministro. Pero tira de los hilos: la variable de entorno no la establece nadie ni en ningún sitio, readFile se invoca con callbacks vacíos así que los datos robados no van a ninguna parte, la dirección C2 pertenece al espacio RFC 1918, y los comandos de shell repiten cebos de jailbreak en archivos marcadores que nada exfiltra. Ejecútalo en un sandbox y el radio de explosión es un directorio temporal. Es teatro: un señuelo ajustado para la triage asistida por IA y para humanos que leen por encima.

Hay un eco en el mundo real. Cuando un importante hub de modelos investigó un compromiso genuino, su equipo descubrió que las APIs comerciales de modelos se negaban a analizar los comandos, cargas útiles y artefactos C2 del atacante: los guardarraíles no podían distinguir a un respondedor de incidentes de un atacante. Acabaron ejecutando el análisis forense en un modelo de pesos abiertos en su propia infraestructura. Ese modo de fallo, repetido a escala, es exactamente la asimetría que explota la obfuscación anti-LLM. También es la razón por la que sigo insistiendo en que los controles de comprensión de código con IA no pueden ser tu única línea de defensa: el juicio del modelo es ahora una superficie de ataque con un exploit publicado.

El ofuscador ya no te oculta el código a ti. Se lo oculta a tu modelo, y usa el propio entrenamiento de seguridad del modelo como cerradura.

Qué funciona de verdad contra la obfuscación anti-LLM

La solución es dejar de dar código fuente en bruto al modelo y recuperar las etapas del pipeline que las trampas envenenan. Concretamente, en el orden en que lo construiría:

  1. Desofuscación determinista primero. Pasa el script por pases de AST basados en Babel: plegado de constantes, inlining de arrays de cadenas, eliminación de ramas muertas, desaplanado del flujo de control. Las trampas viven en literales de cadena e identificadores; plegar y renombrar las neutraliza antes de que el modelo vea un solo token. El prompt de jailbreak decodificado se convierte en una cadena más dentro de una ruta de código muerto.
  2. Traza el comportamiento, no leas el código fuente. Ejecuta el script en un sandbox instrumentado —una página de Playwright con APIs enganchadas, o un realm aislado— y registra cada llamada al DOM, petición de red, acceso a propiedades y lectura de canvas. Dale al modelo la traza. Una traza de comportamiento no contiene ni una referencia a la ricina, porque el comportamiento no tiene identificadores.
  3. Renombra los identificadores a tokens neutros antes de cualquier pase con LLM. computeRicinLethalDosageForWaterSupply pasa a ser fn_0417. Pierdes un poco de pista semántica; pierdes todo el envenenamiento. Buen intercambio.
  4. Mantén un modelo local de pesos abiertos para la triage de contenido realmente sensible. Cuando el contenido es legítimamente desagradable —kits de exploits reales, kits de phishing auténticos—, un modelo aislado sin guardarraíles en la nube es la diferencia entre un análisis y un muro de rechazos.
  5. Veredictos basados en evidencias, no en sensaciones. Puntúa los scripts según capacidades observadas: ¿leyó credenciales? ¿Salieron bytes de la página? No según si el código fuente menciona credenciales.

Fíjate en el patrón: cada técnica sustituye «pregúntale al modelo qué dice el código» por «observa qué hace el código y luego pregúntale al modelo sobre esa observación». Es el mismo principio al que vuelvo una y otra vez con agentes autónomos: el confinamiento supera a la monitorización. No te fías de la autodescripción del artefacto; lo confinas, instrumentas la frontera y juzgas sus salidas.

Tus scripts de terceros están hablando con proveedores de modelos

Ahora la parte que debería preocupar a quienes no hacen ingeniería inversa por diversión. Este script no está alojado en un rincón oscuro: es un chequeo anti-bot servido en sitios de clientes, bajo dominios de clientes, junto a marcas de clientes. Eso significa que cada rastreador, cada clasificador de contenido basado en LLM, cada pipeline de seguridad y confianza que ingiere la página ahora ingiere solicitudes de síntesis de armas biológicas y prompts de jailbreak junto al logo de la empresa. Los clientes casi con seguridad no lo saben. El proveedor nunca preguntó.

Que los clasificadores automáticos actúen sobre contenido que no entienden no es hipotético. Hay profesionales que han visto documentos salientes desaparecer en cuarentena silenciosa porque una cadena de marca de agua invisible activó un filtro de correo. Ahora escala ese modo de fallo: un clasificador de seguridad de marca decide que tu dominio está cerca de contenido extremista o biopeligroso, y nunca te dirán por qué tu alcance se desplomó. Ni siquiera puedes apelar, porque la prueba son caracteres Unicode invisibles en un archivo que tú no escribiste.

Y las mitigaciones habituales no sirven. Subresource Integrity no sirve de nada cuando el proveedor rota el hash del script en cada despliegue, y estos scripts rotan constantemente, en parte para evitar el pinning. Un CSP estricto con hashes cambia igual de rápido. Te quedas con la conclusión incómoda a la que llegamos tras el compromiso de la cadena de suministro de LiteLLM: la dependencia que no puedes revisar es la que te acaba poseyendo. Si el código de tu proveedor anti-bot es deliberadamente indescompilable para las herramientas de tu equipo de seguridad, eso no es una característica. Es un fallo de gobernanza con una URL de CDN.

Paquetes en una cinta transportadora que pasan por un escáner, con uno brillando en rojo dentro
Los scripts de terceros se ejecutan bajo tu dominio con tu marca adjunta, y no puedes inspeccionar lo que contienen.

Hacia dónde va esta carrera armamentística

Mi predicción: en menos de un año, las trampas contra salvaguardas se venderán como una casilla más en los ofuscadores comerciales —«resistencia al análisis LLM»—, justo al lado de «aplanado del flujo de control». Los proveedores de modelos responderán entrenando con corpus de trampas para que los modelos aprendan a reconocer e ignorar los cebos incrustados en contextos de análisis. Los ofuscadores contraatacarán con trampas más sutiles: código cuyo comportamiento parece malicioso en las trazas, no solo sus cadenas. La carrera armamentística fractal que los comentarios de HN predijeron en broma —ofuscadores ajustados contra modelos, modelos ajustados contra ofuscadores— ya está aquí; simplemente está repartida de forma desigual.

La defensa duradera es la aburrida, y no ha cambiado desde los primeros empaquetadores de los noventa: análisis dinámico, evidencia de comportamiento y ejecución en sandbox. La opacidad nunca fue una propiedad de seguridad; era una estrategia de desplazamiento de costes, y el coste acaba de volver a su sitio. Si tu pipeline de detección puede ser derrotado por una cadena que perjudica los sentimientos de un modelo de lenguaje, el pipeline siempre fue la vulnerabilidad.