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