
Reverse Engineering der neuen Datadome VM 🔥
Dieses Repository dokumentiert die erste öffentliche Version der In-Browser-JavaScript-VM (virtuelle Maschine) von DataDome, die in deren CAPTCHA/Interstitial-Ablauf verwendet wird. Diese Analyse umfasst:
Hinweis: Dieses Repository behandelt nur eine (statische) VM-Version und ist für Sicherheitsforschung und Analysezwecke gedacht. Es enthält keine dynamischen Löser oder Produktionslöser-Implementierungen.
Am 14. Januar 2026 begann DataDome, eine neue VM-basierte Komponente in ihrem Client-Tag auszuliefern.
Der VM-Code wurde aus der Captcha-Herausforderung in vm.js extrahiert (in diesem Repository verfügbar).
Der erste Schritt war die Deobfuskierung des Skripts:
Die Obfuskierung ist unkompliziert: Jede Variable auswerten und durch ihren tatsächlichen Wert ersetzen. Ein Deobfuskierungs-Skript ist in deobf.js verfügbar.
Das Ausführen des deobfuskierten Codes (out.js) in DevTools zeigt die erwartete Ausgabe der VM:

Die Ausgabe ist ein JSON-Objekt, das zwei Zahlen und einen String enthält. Tauchen wir nun in die tatsächliche VM-Implementierung ein.

Am Anfang der Funktion Q.exports sehen wir, wie der Bytecode dekodiert wird:
B(), einem Pseudozufallszahlengenerator)
-> D enthält den dekodierten Bytecode mit etwas zufälligem "Rauschen"
Weiter unten befindet sich der VM-Einstiegspunkt: eine Funktion mit zwei Parametern A (der Bytecode) und Q (ein leeres Wörterbuch für die Fehlerbehandlung).
Der interessanteste Aspekt dieser VM ist ihre Architektur: alles befindet sich in einem einzigen Array (A). Dieses Array enthält:
Dieses Design spiegelt echte Computerarchitektur mit unterschiedlichen Speicherbereichen wider. Der nächste Schritt ist die Kartierung der einzelnen Offsets, um zu verstehen, was wo gespeichert ist:
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
Mit diesen kartierten Offsets wird die VM-Struktur klar.

Die VM beginnt mit einer Sammlung von Hilfsfunktionen, die Folgendes behandeln:

Nach den Hilfsfunktionen initialisiert die VM Kernwerte:
Unterhalb der Initialisierung befinden sich alle Instruktionshandler.

I repräsentiert die aktuelle InstruktionP ist der tatsächliche Offset in das Array (unter Berücksichtigung der Obfuskierung)current_opcode_handler und aktualisiert current_opcode_id
Hier ist ein einfaches Beispiel eines Opcode-Handlers:
%= oder ^=)fetch() aufEiner der komplexesten Opcodes erstellt Closures/Funktionen:
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();
};
Dieser Opcode:
undefined gefüllt werdenDieser Opcode erstellt einen Wrapper für Funktionsaufrufe, der sowohl normale als auch Konstruktoraufrufe behandelt:
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();
};
Dieser Opcode:
new) oder einen normalen Aufruf handeltA[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();
};
Dieser Opcode erstellt Objektliterale durch:
object[firstPop] = secondPopJede Instruktion endet mit dem Aufruf von fetch(), das die nächste Instruktion vorbereitet:
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;
}
Diese Funktion:
current_opcode_handler und current_opcode_idDies spiegelt die Dispatcher-Logik wider und erzeugt einen für VM-Architekturen typischen Fetch-Decode-Execute-Zyklus.
Zur Unterstützung der Analyse wurde ein Proof-of-Concept-Disassembler (disasm.js) entwickelt, der den Bytecode der VM in menschenlesbare Assembly umwandelt.
Der Disassembler arbeitet in zwei Durchläufen:
Der erste Durchlauf scannt den Bytecode, um alle Sprungziele zu identifizieren. Dies umfasst:
JMP_FWD, JMP_BACK)JZ, JNZ_KEEP, JZ_KEEP)Jede Zieladresse wird mit einem Label (z. B. L_0042) markiert, um den Kontrollfluss leichter verfolgen zu können.
Der zweite Durchlauf wandelt jede Instruktion in eine assembly-ähnliche Ausgabe um:
000042: fa 00 0a PUSH_IMM 10
000045: 19 00 19 PUSH_REG 25
000048: eb ADD
Jede Zeile enthält:
Einer der komplexeren Aspekte ist die Dekodierung von Immediate-Werten, die im Bytecode eingebettet sind. Die VM verwendet Typmarkierungen, um anzugeben, wie die folgenden Bytes zu interpretieren sind:
Einfache Typen (keine zusätzlichen Daten):
0x28 → true0x7D → false0x4C → null0x3D → undefinedKleine Ganzzahlen (0-127): Mit gesetztem High-Bit kodiert
0x85 → 5 (0x80 | 5)Strings: XOR-kodiert und null-terminiert
0x67, XOR-Schlüssel beginnt bei 1830x27, XOR-Schlüssel beginnt bei 46Numerische Typen:
0x6F + 1 Byte0x61 + 2 Bytes (Big-Endian)0x65 + 3 Bytes (Big-Endian)0x54 + 4 Bytes (Big-Endian)0x05 + 8 BytesDie XOR-Kodierung für Strings ist einfach, verhindert jedoch eine beiläufige Inspektion:
let str = '';
let xorKey = 183; // Initial key for ASCII strings
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
str += String.fromCharCode(ch);
}
Einige Opcodes erfordern eine benutzerdefinierte Handhabung:
CLOSURE (opcode 136): Erstellt Funktionen/Closures mit erfassten Variablen
Format: CLOSURE locals, capture_count, [capture_indices...], skip_offset
Der Skip-Offset zeigt über den Funktionskörper hinaus und ermöglicht der VM, die Funktionsdefinition während der linearen Ausführung zu überspringen.
PUSH_MULTI_IMM (opcode 96): Schiebt mehrere Werte auf einmal
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
...
Ich bin noch recht neu bei VMs, also nimm alles mit einer Prise Salz. KI wurde verwendet, um bei der Dokumentation des Codes und beim Schreiben von Teilen dieser Readme zu helfen (weil das Schreiben von Doku schmerzhaft ist).
Dies dient ausschließlich Bildungs- und Sicherheitsforschungszwecken. Keine Löser oder Umgehungen enthalten – nur Dokumentation, wie die VM funktioniert, weil es wirklich interessant ist.
DataDome: Wenn ihr das lest, hallo!!! Das ist nur mein Versuch, ein Stipendium zu bekommen 🙏. Bitte verklagt mich nicht, ich bin pleite asf. Wenn ihr Probleme mit diesem Repo habt, lasst es mich einfach wissen und wir können darüber reden :)
tut mir leid für die Leute, die dachten, ich würde über das Innere der VM sprechen ... das wird nicht passieren
SCHREIBT MIR KEINE DM UND FRAGT NACH EINER DATADOME-API, ICH WERDE EUCH NICHT HELFEN