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