
Reverse engineering della nuova VM Datadome 🔥
Questo repository documenta la prima versione pubblica della macchina virtuale JavaScript (VM) di DataDome utilizzata nel loro flusso CAPTCHA/interstiziale. Questa analisi copre:
Nota: Questo repository copre solo una versione (statica) della VM ed è inteso per scopi di ricerca e analisi sulla sicurezza. Non include risolutori dinamici o implementazioni di risolutori di produzione.
Il 14 gennaio 2026, DataDome ha iniziato a distribuire un nuovo componente basato su VM nel loro tag client.
Il codice della VM è stato estratto dalla sfida captcha in vm.js (disponibile in questo repository).
Il primo passo è stato deoffuscare lo script:
L'offuscamento è semplice: valutare ogni variabile e sostituirla con il suo valore effettivo. Uno script di deoffuscamento è disponibile in deobf.js.
Eseguendo il codice deoffuscato (out.js) in DevTools si rivela l'output atteso della VM:
L'output è un oggetto JSON contenente due numeri e una stringa. Ora immergiamoci nell'implementazione vera e propria della VM.
All'inizio della funzione Q.exports, possiamo vedere come viene decodificato il bytecode:
B(), un generatore di numeri pseudo-casuali)
-> D contiene il bytecode decodificato con del "rumore" casuale
Scorrendo verso il basso si rivela il punto di ingresso della VM: una funzione con due parametri A (il bytecode) e Q (un dizionario vuoto usato per la gestione degli errori).
L'aspetto più interessante di questa VM è la sua architettura: tutto vive in un singolo array (A). Questo array contiene:
Questo design rispecchia l'architettura reale dei computer con regioni di memoria distinte. Il passo successivo è mappare ogni offset per capire cosa è memorizzato dove:
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 questi offset mappati, la struttura della VM diventa chiara.
La VM inizia con una raccolta di funzioni di supporto che gestiscono:
Dopo le funzioni di supporto, la VM inizializza i valori principali:
Sotto l'inizializzazione ci sono tutti i gestori di istruzioni.
Il dispatcher è il ciclo principale della VM che viene eseguito finché exit_flag non viene impostato:
I rappresenta l'istruzione correnteP è l'offset effettivo nell'array (tenendo conto dell'offuscamento)current_opcode_handler e aggiorna current_opcode_id
Ecco un esempio base di un gestore di opcode:
%= o ^=)fetch() alla fineUno degli opcode più complessi crea closure/funzioni:
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();
};
Questo opcode:
undefinedQuesto opcode crea un wrapper per le chiamate di funzione che gestisce sia le chiamate regolari che quelle costruttore:
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();
};
Questo opcode:
new) o regolareA[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();
};
Questo opcode costruisce letterali oggetto:
object[firstPop] = secondPopOgni istruzione si conclude chiamando fetch(), che prepara l'istruzione successiva:
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;
}
Questa funzione:
current_opcode_handler e current_opcode_idQuesto rispecchia la logica del dispatcher, creando un ciclo fetch-decode-execute tipico delle architetture VM.
Per facilitare l'analisi, è stato sviluppato un disassemblatore proof-of-concept (disasm.js) per convertire il bytecode della VM in assembly leggibile dall'uomo.
Il disassemblatore opera in due passate:
La prima passata scansiona il bytecode per identificare tutti i target di salto. Questo include:
JMP_FWD, JMP_BACK)JZ, JNZ_KEEP, JZ_KEEP)Ogni indirizzo target è marcato con un'etichetta (ad es., L_0042) per rendere più facile seguire il flusso di controllo.
La seconda passata converte ogni istruzione in un output simile all'assembly:
000042: fa 00 0a PUSH_IMM 10
000045: 19 00 19 PUSH_REG 25
000048: eb ADD
Ogni riga include:
Uno degli aspetti più complessi è la decodifica dei valori immediati incorporati nel bytecode. La VM utilizza marcatori di tipo per indicare come interpretare i byte successivi:
Tipi semplici (nessun dato aggiuntivo):
0x28 → true0x7D → false0x4C → null0x3D → undefinedInteri piccoli (0-127): Codificati con il bit alto impostato
0x85 → 5 (0x80 | 5)Stringhe: Codificate con XOR e terminate da null
0x67, chiave XOR inizia a 1830x27, chiave XOR inizia a 46Tipi numerici:
0x6F + 1 byte0x61 + 2 byte (big-endian)0x65 + 3 byte (big-endian)0x54 + 4 byte (big-endian)0x05 + 8 byteLa codifica XOR per le stringhe è semplice ma impedisce un'ispezione casuale:
let str = '';
let xorKey = 183; // Initial key for ASCII strings
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
str += String.fromCharCode(ch);
}
Alcuni opcode richiedono una gestione personalizzata:
CLOSURE (opcode 136): Crea funzioni/closure con variabili catturate
Format: CLOSURE locals, capture_count, [capture_indices...], skip_offset
L'offset di salto punta oltre il corpo della funzione, permettendo alla VM di saltare la definizione della funzione durante l'esecuzione lineare.
PUSH_MULTI_IMM (opcode 96): Spinge più valori contemporaneamente
Format: 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
...
Questo formato di output rende possibile:
Sono ancora abbastanza nuovo alle VM, quindi prendete tutto con le pinze. L'IA è stata usata per aiutare a documentare il codice e scrivere parti di questo readme (perché scrivere documentazione è doloroso).
Questo è puramente per scopi educativi/ricerca sulla sicurezza. Nessun risolutore o bypass incluso - solo documentazione di come funziona la VM perché è genuinamente interessante.
DataDome: se state leggendo questo, ciao!!! Questo è solo un mio tentativo di ottenere una borsa di studio 🙏. Per favore non denunciatemi, sono al verde. Se avete problemi con questo repo fatemelo sapere e possiamo parlarne :)
mi dispiace per le persone che pensavano che avrei parlato dell'interno della vm... non succederà
NON INVIATEMI DM CHIEDENDO UN'API DATADOME, NON VI AIUTERÒ