Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
datadome-vm — Reverse engineering the new Datadome VM 🔥 | Kitploit
Tools/GitHubGitHub/xkiian/datadome-vm
Dynamic Analysis (Sandboxing)Reverse EngineeringBinary AnalysisLearning & EducationAnti-BotCAPTCHA Bypass
GitHubxkiian/datadome-vm

datadome-vm

Reverse engineering the new Datadome VM 🔥

View Repository
12519617 months agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

DataDome VM Analysis

Summary

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:

  • Bytecode loading and decoding mechanisms
  • VM memory layout and architecture
  • A proof-of-concept disassembler
  • Control-flow analysis notes

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.

Background

On January 14, 2026, DataDome began shipping a new VM-based component in their client tag.

Deobfuscation

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.

Initial Analysis

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.

Bytecode Decoding

At the start of the Q.exports function, we can see how the bytecode is decoded:

  1. The input string is base64 decoded
  2. An array of length 129,263 is created
  3. Each index is checked against a specific range:
    • If the index falls within the range, the value is decoded
    • Otherwise, a random number is returned (using B(), a pseudo-random number generator) -> D holds the decoded bytecode with some random "noise"

VM Architecture

Scrolling down reveals the VM entry point: a function with two parameters A (the bytecode) and Q (an empty dictionary used for error handling).

Memory Layout

The most interesting aspect of this VM is its architecture: everything lives in a single array (A). This array contains:

  • The stack
  • Registers
  • Opcodes
  • The bytecode itself
  • The instruction pointer

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.

Helper Functions

The VM begins with a collection of helper functions that handle:

  • Reading typed values from the stack
  • Moving data between the stack and "registers"

VM Initialization

After the helper functions, the VM initializes core values:

  • All pointers (stack, instruction, frame base)
  • Exit flag
  • Last result register

Below the initialization are all the instruction handlers.

The Dispatcher Loop

The dispatcher is the main VM loop that runs until `exit_flag` is set:
  • I represents the current instruction
  • P is the actual offset into the array (accounting for obfuscation)
  • The loop sets the current instruction to current_opcode_handler and updates current_opcode_id

Opcode Implementation

How Opcodes Work

Here's a basic example of an opcode handler:

  1. Fetches an immediate value from the bytecode
  2. Retrieves the top value from the stack
  3. Performs an operation (e.g., %= or ^=)
  4. Calls the fetch() function at the end

Interesting Opcodes

Opcode 4919: Function/Closure Creation

One 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:

  1. Reads the expected argument count
  2. Captures variables from the current scope (closure)
  3. Creates a function that sets up a new stack frame with proper calling conventions
  4. Handles missing arguments by filling with undefined
  5. Saves the return address and frame pointer for proper returns

Opcode 5003: Dynamic Function Call

This 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:

  1. Pops the function and context from the stack
  2. Creates a wrapper that can be called with arguments
  3. Detects whether it's a constructor call (new) or regular call
  4. Applies the function with proper context and error handling
  5. Pushes the result back onto the stack

Opcode 4961: Object Literal Construction

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