
Reverse engineering the new Datadome VM 🔥
This repository documents the first public version of DataDome's in-browser JavaScript virtual machine (VM) used in their CAPTCHA/interstitial flow. This analysis covers:
Note: This repository covers only one (static) VM version and is intended for security research and analysis purposes. It does not include dynamic solvers or production solver implementations.
On January 14, 2026, DataDome began shipping a new VM-based component in their client tag.
The VM code has been extracted from the captcha challenge into vm.js (available in this repository).
The first step was deobfuscating the script:
The obfuscation is straightforward: evaluate each variable and replace it with its actual value. A deobfuscation script is available in deobf.js.
Running the deobfuscated code (out.js) in DevTools reveals the VM's expected output:
The output is a JSON object containing two numbers and a string. Now let's dive into the actual VM implementation.
At the start of the Q.exports function, we can see how the bytecode is decoded:
B(), a pseudo-random number generator)
-> D holds the decoded bytecode with some random "noise"
Scrolling down reveals the VM entry point: a function with two parameters A (the bytecode) and Q (an empty dictionary used for error handling).
The most interesting aspect of this VM is its architecture: everything lives in a single array (A). This array contains:
This design mirrors real computer architecture with distinct memory regions. The next step is to map out each offset to understand what's stored where:
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
With these offsets mapped, the VM structure becomes clear.
The VM begins with a collection of helper functions that handle:
After the helper functions, the VM initializes core values:
Below the initialization are all the instruction handlers.
The dispatcher is the main VM loop that runs until `exit_flag` is set:
I represents the current instructionP is the actual offset into the array (accounting for obfuscation)current_opcode_handler and updates current_opcode_id
Here's a basic example of an opcode handler:
%= or ^=)fetch() function at the endOne of the most complex opcodes creates closures/functions:
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();
};
This opcode:
undefinedThis opcode creates a wrapper for function calls that handles both regular and constructor calls:
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();
};
This opcode:
new) or regular callA[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();
};