
Ingeniería inversa de la nueva VM de Datadome 🔥
Este repositorio documenta la primera versión pública de la máquina virtual (VM) de JavaScript en el navegador de DataDome, utilizada en su flujo CAPTCHA/intersticial. Este análisis cubre:
Nota: Este repositorio cubre solo una versión (estática) de la VM y está destinado a fines de investigación y análisis de seguridad. No incluye solucionadores dinámicos ni implementaciones de solucionadores en producción.
El 14 de enero de 2026, DataDome comenzó a distribuir un nuevo componente basado en VM en su etiqueta de cliente.
El código de la VM se ha extraído del desafío captcha en vm.js (disponible en este repositorio).
El primer paso fue desofuscar el script:
La ofuscación es sencilla: evaluar cada variable y reemplazarla con su valor real. Hay un script de desofuscación disponible en deobf.js.
Ejecutar el código desofuscado (out.js) en DevTools revela la salida esperada de la VM:

La salida es un objeto JSON que contiene dos números y una cadena. Ahora profundicemos en la implementación real de la VM.

Al inicio de la función Q.exports, podemos ver cómo se decodifica el bytecode:
B(), un generador de números pseudoaleatorios)D contiene el bytecode decodificado con algo de 'ruido' aleatorio
Desplazándose hacia abajo se revela el punto de entrada de la VM: una función con dos parámetros A (el bytecode) y Q (un diccionario vacío usado para el manejo de errores).
El aspecto más interesante de esta VM es su arquitectura: todo vive en un solo array (A). Este array contiene:
Este diseño refleja la arquitectura real de computadoras con regiones de memoria distintas. El siguiente paso es mapear cada desplazamiento para entender qué se almacena dónde:
var stack_pointer = 4593
var instruction_pointer = 4635
var frame_base_pointer = 4674
var last_result = 4633
var exit_flag = 4656
var current_opcode_handler = 4685
var current_opcode_id = 4675
var stack_offset = 124482
var vm_start = 5258
Con estos desplazamientos mapeados, la estructura de la VM se vuelve clara.

La VM comienza con una colección de funciones auxiliares que manejan:

Después de las funciones auxiliares, la VM inicializa los valores centrales:
Debajo de la inicialización están todos los manejadores de instrucciones.

El despachador es el bucle principal de la VM que se ejecuta hasta que exit_flag está configurado:
I representa la instrucción actualP es el desplazamiento real en el array (teniendo en cuenta la ofuscación)current_opcode_handler y actualiza current_opcode_id
Aquí hay un ejemplo básico de un manejador de código de operación:
%= o ^=)fetch() al finalUno de los códigos de operación más complejos crea clausuras/funciones:
A[4919] = function () {
var Q = readUint8(); // Number of expected arguments
var B = [];
for (var E = readUint8(), D = 0; D < E; D++) {
var g = readUint8();
var a = A[A[frame_base_pointer] + g];
B.push(a); // Capture variables from current scope
}
var h = A[instruction_pointer] + 3; // Save address of function body
A[A[stack_pointer]++] = function (E) {
// Set up new stack frame when called
var e = A[stack_pointer] - E;
while (E < Q) {
A[e + E++] = undefined; // Fill missing arguments with undefined
}
A[stack_pointer] = e + Q;
for (var D = 0; D < B.length; D++) {
var g = B[D];
A[A[stack_pointer]++] = g; // Push captured variables
}
A[e - 2] = A[frame_base_pointer]; // Save old frame pointer
A[e - 1] = A[instruction_pointer]; // Save return address
A[frame_base_pointer] = e;
A[instruction_pointer] = h; // Jump to function body
};
fetch();
};
Este código de operación:
undefinedEste código de operación crea un envoltorio para llamadas a funciones que maneja tanto llamadas regulares como de constructor:
A[5003] = function () {
var Q = A[--A[stack_pointer]]; // POP function
var B = A[--A[stack_pointer]]; // POP 'this' context
function E(e) { // e = argument count
var D = A[stack_pointer];
var g = A.slice(D - e, D); // Get arguments from stack
if (this instanceof E) {
// Constructor call (new E(...))
g.unshift(null);
var h = Function.prototype.bind.apply(Q, g);
A[stack_pointer] -= e;
try {
a = new h();
} catch (A) {
a = A.message;
}
A[A[stack_pointer]++] = a;
} else {
// Regular function call
var t;
try {
t = Q.apply(B, g);
} catch (A) {
t = A.message;
}
A[stack_pointer] -= e + 2;
A[A[stack_pointer]++] = t;
}
}
A[A[stack_pointer]++] = E;
fetch();
};
Este código de operación:
new) o una llamada regularA[4961] = function () {
var Q = {};
for (var E = readUint16(), e = 0; e < E; e++) {
var D = A[--A[stack_pointer]]; // First POP
var g = A[--A[stack_pointer]]; // Second POP
Q[D] = g;
}
A[A[stack_pointer]++] = Q;
fetch();
};
Este código de operación construye literales de objeto:
object[firstPop] = secondPopCada instrucción concluye llamando a fetch(), que prepara la siguiente instrucción:
function fetch() {
var Q = A[instruction_pointer];
var B = A[vm_start + Q];
A[instruction_pointer] = Q + 1;
var E = A[4783 + B];
A[current_opcode_handler] = E;
A[current_opcode_id] = B;
}
Esta función:
current_opcode_handler y current_opcode_idEsto refleja la lógica del despachador, creando un ciclo de búsqueda-decodificación-ejecución típico de arquitecturas de VM.
Para ayudar en el análisis, se desarrolló un desensamblador de prueba de concepto (disasm.js) para convertir el bytecode de la VM en ensamblador legible por humanos.
El desensamblador opera en dos pasos:
El primer paso escanea el bytecode para identificar todos los destinos de salto. Esto incluye:
JMP_FWD, JMP_BACK)JZ, JNZ_KEEP, JZ_KEEP)Cada dirección de destino se marca con una etiqueta (por ejemplo, L_0042) para facilitar el seguimiento del flujo de control.
El segundo paso convierte cada instrucción en una salida similar a ensamblador:
000042: fa 00 0a PUSH_IMM 10
000045: 19 00 19 PUSH_REG 25
000048: eb ADD
Cada línea incluye:
Uno de los aspectos más complejos es la decodificación de valores inmediatos incrustados en el bytecode. La VM utiliza marcadores de tipo para indicar cómo interpretar los siguientes bytes:
Tipos simples (sin datos adicionales):
0x28 → true0x7D → false0x4C → null0x3D → undefinedEnteros pequeños (0-127): Codificados con el bit alto establecido
0x85 → 5 (0x80 | 5)Cadenas: Codificadas con XOR y terminadas en nulo
0x67, clave XOR comienza en 1830x27, clave XOR comienza en 46Tipos numéricos:
0x6F + 1 byte0x61 + 2 bytes (big-endian)0x65 + 3 bytes (big-endian)0x54 + 4 bytes (big-endian)0x05 + 8 bytesLa codificación XOR para cadenas es sencilla pero evita la inspección casual:
let str = '';
let xorKey = 183; // Initial key for ASCII strings
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
str += String.fromCharCode(ch);
}
Algunos códigos de operación requieren manejo personalizado:
CLOSURE (código 136): Crea funciones/clausuras con variables capturadas
Formato: CLOSURE locales, num_capturas, [índices_captura...], desplazamiento_salto
El desplazamiento de salto apunta más allá del cuerpo de la función, permitiendo que la VM salte la definición de la función durante la ejecución lineal.
PUSH_MULTI_IMM (código 96): Empuja múltiples valores a la vez
Formato: PUSH_MULTI_IMM count, val1, val2, ...
# Disassemble from file
node disasm.js bytecode.txt
; DataDome VM Disassembly
; Bytecode size: 5428 bytes
; VM Constants: VM_START=5258, OPCODE_BASE=4783
000000: 88 01 00 04 CLOSURE locals=1, captures=[0, 4], body=L_0006, end=L_0a3f
L_0006:
000006: fa 67 ... PUSH_IMM "window"
00001f: 2b PUSH_WINDOW
000020: 02 SET
000021: fa 67 ... PUSH_IMM "navigator"
00003a: 19 00 00 PUSH_REG 0
00003d: fa 67 ... PUSH_IMM "navigator"
000056: ee GET
000057: 02 SET
...
Este formato de salida permite:
Todavía soy bastante nuevo en las VM, así que toma todo con pinzas. Se ha utilizado IA para ayudar a documentar el código y escribir partes de este readme (porque escribir documentación es doloroso).
Esto es puramente con fines educativos/investigación de seguridad. No se incluyen solucionadores ni bypasses, solo documentar cómo funciona la VM porque es genuinamente interesante.
DataDome: si estás leyendo esto, ¡¡¡hola!!! Esto es solo yo tratando de obtener una beca 🙏. Por favor, no me demandes, estoy quebrado. Si tienes algún problema con este repo, solo házmelo saber y podemos hablar de ello :)
perdón a las personas que pensaron que iba a hablar sobre el interior de la vm... eso no va a pasar
NO ME ESCRIBAS POR DM PIDIENDO UNA API DE DATADOME, NO TE AYUDARÉ