Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
datadome-vm — Ingeniería inversa de la nueva VM de Datadome 🔥 | Kitploit
Herramientas/GitHubGitHub/xkiian/datadome-vm
Análisis Dinámico (Sandboxing)Ingeniería InversaAnálisis de BinariosAprendizaje y EducaciónAnti-BotBypass de CAPTCHA
GitHubxkiian/datadome-vm

datadome-vm

Ingeniería inversa de la nueva VM de Datadome 🔥

Ver Repositorio
1251910hace 6 mesesRevisado por Kitploit

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Análisis de la VM de DataDome

Resumen

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:

  • Mecanismos de carga y decodificación de bytecode
  • Diseño y arquitectura de la memoria de la VM
  • Un desensamblador de prueba de concepto
  • Notas de análisis de flujo de control

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.

Antecedentes

El 14 de enero de 2026, DataDome comenzó a distribuir un nuevo componente basado en VM en su etiqueta de cliente.

Desofuscación

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.

Análisis Inicial

Descargar herramienta

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.

Decodificación de Bytecode

Al inicio de la función Q.exports, podemos ver cómo se decodifica el bytecode:

  1. La cadena de entrada se decodifica en base64
  2. Se crea un array de longitud 129,263
  3. Cada índice se verifica con un rango específico:
    • Si el índice cae dentro del rango, el valor se decodifica
    • De lo contrario, se devuelve un número aleatorio (usando B(), un generador de números pseudoaleatorios)
  4. -> D contiene el bytecode decodificado con algo de 'ruido' aleatorio

Arquitectura de la VM

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).

Diseño de Memoria

El aspecto más interesante de esta VM es su arquitectura: todo vive en un solo array (A). Este array contiene:

  • La pila
  • Registros
  • Códigos de operación
  • El propio bytecode
  • El puntero de instrucción

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:

root@kitploit:~
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.

Funciones Auxiliares

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

  • Lectura de valores tipificados de la pila
  • Movimiento de datos entre la pila y los 'registros'

Inicialización de la VM

Después de las funciones auxiliares, la VM inicializa los valores centrales:

  • Todos los punteros (pila, instrucción, base de marco)
  • Bandera de salida
  • Registro del último resultado

Debajo de la inicialización están todos los manejadores de instrucciones.

El Bucle Despachador

El despachador es el bucle principal de la VM que se ejecuta hasta que exit_flag está configurado:

  • I representa la instrucción actual
  • P es el desplazamiento real en el array (teniendo en cuenta la ofuscación)
  • El bucle establece la instrucción actual en current_opcode_handler y actualiza current_opcode_id

Implementación de Códigos de Operación

Cómo Funcionan los Códigos de Operación

Aquí hay un ejemplo básico de un manejador de código de operación:

  1. Obtiene un valor inmediato del bytecode
  2. Recupera el valor superior de la pila
  3. Realiza una operación (por ejemplo, %= o ^=)
  4. Llama a la función fetch() al final

Códigos de Operación Interesantes

Opcode 4919: Creación de Función/Clausura

Uno de los códigos de operación más complejos crea clausuras/funciones:

root@kitploit:~
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:

  1. Lee el recuento de argumentos esperados
  2. Captura variables del ámbito actual (clausura)
  3. Crea una función que configura un nuevo marco de pila con las convenciones de llamada adecuadas
  4. Maneja argumentos faltantes llenándolos con undefined
  5. Guarda la dirección de retorno y el puntero de marco para retornos adecuados

Opcode 5003: Llamada Dinámica a Función

Este código de operación crea un envoltorio para llamadas a funciones que maneja tanto llamadas regulares como de constructor:

root@kitploit:~
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:

  1. Extrae la función y el contexto de la pila
  2. Crea un envoltorio que puede ser llamado con argumentos
  3. Detecta si es una llamada de constructor (new) o una llamada regular
  4. Aplica la función con el contexto adecuado y manejo de errores
  5. Empuja el resultado de vuelta a la pila

Opcode 4961: Construcción de Literal de Objeto

root@kitploit:~
A[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:

  1. Leyendo el número de pares de propiedades del bytecode
  2. Extrayendo pares de la pila (la primera extracción se convierte en la clave)
  3. Construyendo un objeto: object[firstPop] = secondPop
  4. Empujando el objeto resultante a la pila

La Función Fetch

Cada instrucción concluye llamando a fetch(), que prepara la siguiente instrucción:

root@kitploit:~
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:

  1. Lee el puntero de instrucción
  2. Obtiene el siguiente código de operación del bytecode
  3. Incrementa el puntero de instrucción
  4. Busca el manejador del código de operación
  5. Actualiza current_opcode_handler y current_opcode_id

Esto refleja la lógica del despachador, creando un ciclo de búsqueda-decodificación-ejecución típico de arquitecturas de VM.

Implementación del Desensamblador

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.

Enfoque

El desensamblador opera en dos pasos:

Paso 1: Descubrimiento de Etiquetas

El primer paso escanea el bytecode para identificar todos los destinos de salto. Esto incluye:

  • Saltos hacia adelante y hacia atrás (JMP_FWD, JMP_BACK)
  • Saltos condicionales (JZ, JNZ_KEEP, JZ_KEEP)
  • Límites de clausura (cuerpos de funciones y sus puntos finales)

Cada dirección de destino se marca con una etiqueta (por ejemplo, L_0042) para facilitar el seguimiento del flujo de control.

Paso 2: Desensamblado

El segundo paso convierte cada instrucción en una salida similar a ensamblador:

root@kitploit:~
000042:  fa 00 0a           PUSH_IMM 10
000045:  19 00 19           PUSH_REG 25
000048:  eb                 ADD

Cada línea incluye:

  • Dirección: Desplazamiento hexadecimal en el bytecode
  • Bytes brutos: Los bytes reales que componen la instrucción (útil para verificación) y no sé, se ve duro así
  • Código de operación: Nombre mnemónico de la instrucción
  • Argumentos: Operandos decodificados (números de registro, inmediatos, destinos de salto)

Decodificación de Valores

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 → true
  • 0x7D → false
  • 0x4C → null
  • 0x3D → undefined

Enteros pequeños (0-127): Codificados con el bit alto establecido

  • 0x85 → 5 (0x80 | 5)

Cadenas: Codificadas con XOR y terminadas en nulo

  • Cadenas ASCII: marcador 0x67, clave XOR comienza en 183
  • Cadenas UTF-8: marcador 0x27, clave XOR comienza en 46

Tipos numéricos:

  • 8 bits con signo: 0x6F + 1 byte
  • 16 bits con signo: 0x61 + 2 bytes (big-endian)
  • 24 bits con signo: 0x65 + 3 bytes (big-endian)
  • 32 bits con signo: 0x54 + 4 bytes (big-endian)
  • Doble IEEE 754: 0x05 + 8 bytes

La codificación XOR para cadenas es sencilla pero evita la inspección casual:

root@kitploit:~
let str = '';
let xorKey = 183;  // Initial key for ASCII strings
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}

Códigos de Operación Especiales

Algunos códigos de operación requieren manejo personalizado:

CLOSURE (código 136): Crea funciones/clausuras con variables capturadas

root@kitploit:~
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

root@kitploit:~
Formato: PUSH_MULTI_IMM count, val1, val2, ...

Uso

root@kitploit:~
# Disassemble from file
node disasm.js bytecode.txt

Ejemplo de Salida

root@kitploit:~
; 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:

  • Rastrear el flujo de ejecución siguiendo las etiquetas de salto
  • Identificar los límites de funciones mediante códigos de operación CLOSURE
  • Ver exactamente qué valores se están empujando y manipulando
  • Hacer referencias cruzadas con la implementación real de la VM

Notas

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).

Descargo de responsabilidad

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É