Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
datadome-vm — Reverse Engineering der neuen Datadome VM 🔥 | Kitploit
Tools/GitHubGitHub/xkiian/datadome-vm
Dynamische Analyse (Sandboxing)Reverse EngineeringBinäranalyseLernen & BildungAnti-BotCAPTCHA-Umgehung
GitHubxkiian/datadome-vm

datadome-vm

Reverse Engineering der neuen Datadome VM 🔥

Repository anzeigen
1251910vor 6 MonatenVon Kitploit geprüft

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

DataDome VM-Analyse

Zusammenfassung

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:

  • Mechanismen zum Laden und Dekodieren von Bytecode
  • VM-Speicherlayout und -Architektur
  • Ein Proof-of-Concept-Disassembler
  • Notizen zur Kontrollflussanalyse

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.

Hintergrund

Am 14. Januar 2026 begann DataDome, eine neue VM-basierte Komponente in ihrem Client-Tag auszuliefern.

Deobfuskierung

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.

Erste Analyse

Der Dispatcher ist die Haupt-VM-Schleife, die läuft, bis `exit_flag` gesetzt ist:
Tool herunterladen

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.

Bytecode-Dekodierung

Am Anfang der Funktion Q.exports sehen wir, wie der Bytecode dekodiert wird:

  1. Der Eingabestring wird base64-dekodiert
  2. Ein Array der Länge 129.263 wird erstellt
  3. Jeder Index wird gegen einen bestimmten Bereich geprüft:
    • Wenn der Index in den Bereich fällt, wird der Wert dekodiert
    • Andernfalls wird eine Zufallszahl zurückgegeben (mithilfe von B(), einem Pseudozufallszahlengenerator) -> D enthält den dekodierten Bytecode mit etwas zufälligem "Rauschen"

VM-Architektur

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

Speicherlayout

Der interessanteste Aspekt dieser VM ist ihre Architektur: alles befindet sich in einem einzigen Array (A). Dieses Array enthält:

  • Den Stack
  • Register
  • Opcode
  • Den Bytecode selbst
  • Den Instruktionszeiger

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:

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

Mit diesen kartierten Offsets wird die VM-Struktur klar.

Hilfsfunktionen

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

  • Einlesen typisierter Werte vom Stack
  • Verschieben von Daten zwischen dem Stack und "Registern"

VM-Initialisierung

Nach den Hilfsfunktionen initialisiert die VM Kernwerte:

  • Alle Zeiger (Stack, Instruktion, Frame-Basis)
  • Exit-Flag
  • Letztes Ergebnisregister

Unterhalb der Initialisierung befinden sich alle Instruktionshandler.

Die Dispatcher-Schleife

  • I repräsentiert die aktuelle Instruktion
  • P ist der tatsächliche Offset in das Array (unter Berücksichtigung der Obfuskierung)
  • Die Schleife setzt die aktuelle Instruktion auf current_opcode_handler und aktualisiert current_opcode_id

Opcode-Implementierung

Wie Opcodes funktionieren

Hier ist ein einfaches Beispiel eines Opcode-Handlers:

  1. Holt einen unmittelbaren Wert aus dem Bytecode
  2. Holt den obersten Wert vom Stack
  3. Führt eine Operation aus (z. B. %= oder ^=)
  4. Ruft am Ende die Funktion fetch() auf

Interessante Opcodes

Opcode 4919: Funktions-/Closure-Erstellung

Einer der komplexesten Opcodes erstellt Closures/Funktionen:

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

Dieser Opcode:

  1. Liest die erwartete Argumentanzahl
  2. Erfasst Variablen aus dem aktuellen Gültigkeitsbereich (Closure)
  3. Erstellt eine Funktion, die einen neuen Stack-Frame mit korrekten Aufrufkonventionen einrichtet
  4. Behandelt fehlende Argumente, indem sie mit undefined gefüllt werden
  5. Speichert die Rücksprungadresse und den Frame-Zeiger für ordnungsgemäße Rückgaben

Opcode 5003: Dynamischer Funktionsaufruf

Dieser Opcode erstellt einen Wrapper für Funktionsaufrufe, der sowohl normale als auch Konstruktoraufrufe behandelt:

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

Dieser Opcode:

  1. Holt die Funktion und den Kontext vom Stack
  2. Erstellt einen Wrapper, der mit Argumenten aufgerufen werden kann
  3. Erkennt, ob es sich um einen Konstruktoraufruf (new) oder einen normalen Aufruf handelt
  4. Wendet die Funktion mit korrektem Kontext und Fehlerbehandlung an
  5. Schiebt das Ergebnis zurück auf den Stack

Opcode 4961: Objektliteral-Konstruktion

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

Dieser Opcode erstellt Objektliterale durch:

  1. Einlesen der Anzahl der Eigenschaftspaare aus dem Bytecode
  2. Holen von Paaren vom Stack (der erste Pop wird zum Schlüssel)
  3. Erstellen eines Objekts: object[firstPop] = secondPop
  4. Schieben des resultierenden Objekts auf den Stack

Die Fetch-Funktion

Jede Instruktion endet mit dem Aufruf von fetch(), das die nächste Instruktion vorbereitet:

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

Diese Funktion:

  1. Liest den Instruktionszeiger
  2. Holt den nächsten Opcode aus dem Bytecode
  3. Erhöht den Instruktionszeiger
  4. Schlägt den Opcode-Handler nach
  5. Aktualisiert current_opcode_handler und current_opcode_id

Dies spiegelt die Dispatcher-Logik wider und erzeugt einen für VM-Architekturen typischen Fetch-Decode-Execute-Zyklus.

Disassembler-Implementierung

Zur Unterstützung der Analyse wurde ein Proof-of-Concept-Disassembler (disasm.js) entwickelt, der den Bytecode der VM in menschenlesbare Assembly umwandelt.

Ansatz

Der Disassembler arbeitet in zwei Durchläufen:

Durchlauf 1: Label-Erkennung

Der erste Durchlauf scannt den Bytecode, um alle Sprungziele zu identifizieren. Dies umfasst:

  • Vorwärts- und Rückwärtssprünge (JMP_FWD, JMP_BACK)
  • Bedingte Sprünge (JZ, JNZ_KEEP, JZ_KEEP)
  • Closure-Grenzen (Funktionskörper und deren Endpunkte)

Jede Zieladresse wird mit einem Label (z. B. L_0042) markiert, um den Kontrollfluss leichter verfolgen zu können.

Durchlauf 2: Disassemblierung

Der zweite Durchlauf wandelt jede Instruktion in eine assembly-ähnliche Ausgabe um:

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

Jede Zeile enthält:

  • Adresse: Hexadezimaler Offset im Bytecode
  • Rohbytes: Die tatsächlichen Bytes der Instruktion (nützlich zur Überprüfung) und sieht irgendwie krass aus
  • Opcode: Mnemonischer Name der Instruktion
  • Argumente: Dekodierte Operanden (Registernummern, Immediate, Sprungziele)

Wertedekodierung

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

Kleine Ganzzahlen (0-127): Mit gesetztem High-Bit kodiert

  • 0x85 → 5 (0x80 | 5)

Strings: XOR-kodiert und null-terminiert

  • ASCII-Strings: Marker 0x67, XOR-Schlüssel beginnt bei 183
  • UTF-8-Strings: Marker 0x27, XOR-Schlüssel beginnt bei 46

Numerische Typen:

  • 8-Bit vorzeichenbehaftet: 0x6F + 1 Byte
  • 16-Bit vorzeichenbehaftet: 0x61 + 2 Bytes (Big-Endian)
  • 24-Bit vorzeichenbehaftet: 0x65 + 3 Bytes (Big-Endian)
  • 32-Bit vorzeichenbehaftet: 0x54 + 4 Bytes (Big-Endian)
  • IEEE 754 Double: 0x05 + 8 Bytes

Die XOR-Kodierung für Strings ist einfach, verhindert jedoch eine beiläufige Inspektion:

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

Spezielle Opcodes

Einige Opcodes erfordern eine benutzerdefinierte Handhabung:

CLOSURE (opcode 136): Erstellt Funktionen/Closures mit erfassten Variablen

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

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

Verwendung

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

Beispielausgabe

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

Anmerkungen

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

Haftungsausschluss

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