Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
datadome-vm — 逆向工程新的 Datadome VM 🔥 | Kitploit
工具/GitHubGitHub/xkiian/datadome-vm
动态分析 (沙盒)逆向工程二进制分析学习与教育反机器人CAPTCHA 绕过
GitHubxkiian/datadome-vm

datadome-vm

逆向工程新的 Datadome VM 🔥

查看仓库
125196个月前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

DataDome VM 分析

摘要

本仓库记录了 DataDome 在其 CAPTCHA/插页流程中使用的浏览器内 JavaScript 虚拟机(VM)的第一个公开版本。本分析涵盖:

  • 字节码加载与解码机制
  • VM 内存布局与架构
  • 一个概念验证的反汇编器
  • 控制流分析笔记

注意: 本仓库仅覆盖一个(静态)VM 版本,仅供安全研究与分析目的。它不包含动态求解器或生产求解器实现。

背景

2026 年 1 月 14 日,DataDome 开始在其客户端标签中推送一个新的基于 VM 的组件。

反混淆

VM 代码已从验证码挑战中提取并保存到 vm.js(可在此仓库中找到)。

第一步是反混淆脚本:

混淆方式很直接:评估每个变量并将其替换为实际值。反混淆脚本可在 deobf.js 中找到。

初步分析

在 DevTools 中运行反混淆后的代码(out.js)会显示 VM 的预期输出:

输出是一个 JSON 对象,包含两个数字和一个字符串。现在让我们深入实际的 VM 实现。

字节码解码

在 Q.exports 函数的开头,我们可以看到字节码是如何解码的:

  1. 输入字符串经过 base64 解码
  2. 创建一个长度为 129,263 的数组
  3. 每个索引检查是否在一个特定范围内:
    • 如果索引落在范围内,则解码该值
    • 否则,返回一个随机数(使用 B(),一个伪随机数生成器) -> D 保存解码后的字节码,其中包含一些随机“噪声”

VM 架构

往下滚动会看到 VM 入口点:一个带有两个参数 A(字节码)和 Q(用于错误处理的空字典)的函数。

内存布局

这个 VM 最有趣的方面是它的架构:所有内容都存在于一个单一数组(A)中。该数组包含:

  • 栈
  • 寄存器
  • 操作码
  • 字节码本身
  • 指令指针

这种设计模拟了真实计算机架构的不同内存区域。下一步是映射每个偏移量,以了解每个位置存储了什么:

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

映射完这些偏移量后,VM 结构就变得清晰了。

辅助函数

VM 以一组辅助函数开始,这些函数处理:

  • 从栈中读取类型化值
  • 在栈和“寄存器”之间移动数据

VM 初始化

在辅助函数之后,VM 初始化核心值:

  • 所有指针(栈、指令、帧基址)
  • 退出标志
  • 最后结果寄存器

初始化下方是所有指令处理函数。

调度循环

调度器是主 VM 循环,一直运行直到 `exit_flag` 被设置:
  • I 代表当前指令
  • P 是数组中实际偏移量(考虑混淆因素)
  • 该循环将当前指令设置为 current_opcode_handler,并更新 current_opcode_id

操作码实现

操作码如何工作

以下是一个操作码处理函数的基本示例:

  1. 从字节码中获取一个立即数
  2. 检索栈顶的值
  3. 执行一个操作(例如 %= 或 ^=)
  4. 最后调用 fetch() 函数

有趣的操作码

操作码 4919:函数/闭包创建

最复杂的操作码之一用于创建闭包/函数:

root@kitploit:~
A[4919] = function () {
    var Q = readUint8();  // 期望的参数数量
    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);  // 从当前作用域捕获变量
    }
    var h = A[instruction_pointer] + 3;  // 保存函数体的地址
    A[A[stack_pointer]++] = function (E) {
        // 调用时建立新的栈帧
        var e = A[stack_pointer] - E;
        while (E < Q) {
            A[e + E++] = undefined;  // 缺少的参数用 undefined 填充
        }
        A[stack_pointer] = e + Q;
        for (var D = 0; D < B.length; D++) {
            var g = B[D];
            A[A[stack_pointer]++] = g;  // 推入捕获的变量
        }
        A[e - 2] = A[frame_base_pointer];  // 保存旧帧指针
        A[e - 1] = A[instruction_pointer];  // 保存返回地址
        A[frame_base_pointer] = e;
        A[instruction_pointer] = h;  // 跳转到函数体
    };
    fetch();
};

该操作码:

  1. 读取期望的参数个数
  2. 从当前作用域捕获变量(闭包)
  3. 创建一个函数,该函数使用正确的调用约定建立新栈帧
  4. 用 undefined 填充缺失的参数
  5. 保存返回地址和帧指针,以便正确返回

操作码 5003:动态函数调用

该操作码创建一个用于函数调用的包装器,同时处理常规调用和构造函数调用:

root@kitploit:~
A[5003] = function () {
    var Q = A[--A[stack_pointer]];  // 弹出函数
    var B = A[--A[stack_pointer]];  // 弹出 'this' 上下文

    function E(e) {  // e = 参数个数
        var D = A[stack_pointer];
        var g = A.slice(D - e, D);  // 从栈中获取参数
        if (this instanceof E) {
            // 构造函数调用 (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 {
            // 常规函数调用
            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();
};

该操作码:

  1. 从栈中弹出函数和上下文
  2. 创建一个可以带参数调用的包装器
  3. 检测是构造函数调用(new)还是常规调用
  4. 使用正确的上下文和错误处理来应用函数
  5. 将结果推回栈中

操作码 4961:对象字面量构造

root@kitploit:~
A[4961] = function () {
    var Q = {};
    for (var E = readUint16(), e = 0; e < E; e++) {
        var D = A[--A[stack_pointer]];  // 第一次弹出
        var g = A[--A[stack_pointer]];  // 第二次弹出
        Q[D] = g;
    }
    A[A[stack_pointer]++] = Q;
    fetch();
};

该操作码通过以下方式构建对象字面量:

  1. 从字节码中读取属性对的数量
  2. 从栈中弹出成对的值(第一次弹出成为键)
  3. 构造一个对象:object[firstPop] = secondPop
  4. 将生成的对象推入栈中

Fetch 函数

每条指令以调用 fetch() 结束,该函数准备下一条指令:

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

该函数:

  1. 读取指令指针
  2. 从字节码中获取下一个操作码
  3. 递增指令指针
  4. 查找操作码处理函数
  5. 更新 current_opcode_handler 和 current_opcode_id

这镜像了调度器的逻辑,形成了 VM 架构中典型的取指-解码-执行循环。

反汇编器实现

为辅助分析,开发了一个概念验证的反汇编器(disasm.js),用于将 VM 的字节码转换为人类可读的汇编代码。

方法

反汇编器分两遍运行:

第一遍:标签发现

第一遍扫描字节码以识别所有跳转目标。包括:

  • 前向和后向跳转(JMP_FWD、JMP_BACK)
  • 条件跳转(JZ、JNZ_KEEP、JZ_KEEP)
  • 闭包边界(函数体及其结束点)

每个目标地址都用一个标签标记(例如 L_0042),以便更容易地跟踪控制流。

第二遍:反汇编

第二遍将每条指令转换为类似汇编的输出:

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

每行包括:

  • 地址:字节码中的十六进制偏移
  • 原始字节:组成指令的实际字节(用于验证),而且看起来特别酷
  • 操作码:指令的助记符名称
  • 操作数:解码后的操作数(寄存器编号、立即数、跳转目标)

值解码

较为复杂的方面之一是解码嵌入在字节码中的立即数。VM 使用类型标记来指示如何解释后续字节:

简单类型(无额外数据):

  • 0x28 → true
  • 0x7D → false
  • 0x4C → null
  • 0x3D → undefined

小整数(0-127):高位设置为1编码

  • 0x85 → 5 (0x80 | 5)

字符串:XOR 编码且以 null 结尾

  • ASCII 字符串:标记 0x67,XOR 密钥起始为 183
  • UTF-8 字符串:标记 0x27,XOR 密钥起始为 46

数值类型:

  • 8位有符号:0x6F + 1 字节
  • 16位有符号:0x61 + 2 字节(大端序)
  • 24位有符号:0x65 + 3 字节(大端序)
  • 32位有符号:0x54 + 4 字节(大端序)
  • IEEE 754 双精度:0x05 + 8 字节

字符串的 XOR 编码很简单,但可以防止随意检查:

root@kitploit:~
let str = '';
let xorKey = 183;  // ASCII 字符串的初始密钥
let ch;
while ((ch = readByte() ^ (xorKey++ & 0xFF)) !== 0) {
    str += String.fromCharCode(ch);
}

特殊操作码

某些操作码需要自定义处理:

CLOSURE(操作码 136):创建带有捕获变量的函数/闭包

root@kitploit:~
格式: CLOSURE locals, capture_count, [capture_indices...], skip_offset

跳转偏移指向函数体之后,允许 VM 在线性执行期间跳过函数定义。

PUSH_MULTI_IMM(操作码 96):一次性推入多个值

root@kitploit:~
格式: PUSH_MULTI_IMM count, val1, val2, ...

用法

root@kitploit:~
# 从文件反汇编
node disasm.js bytecode.txt

输出示例

root@kitploit:~
; DataDome VM 反汇编
; 字节码大小: 5428 字节
; VM 常量: 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
...

这种输出格式使得能够:

  • 通过跟随跳转标签来追踪执行流程
  • 通过 CLOSURE 操作码识别函数边界
  • 查看哪些值正在被推入和操作
  • 与实际 VM 实现进行交叉引用

备注

我对 VM 还不太熟悉,所以请对一切持保留态度。AI 被用于帮助编写代码文档和本 README 的部分内容(因为写文档很痛苦)。

免责声明

这纯粹出于教育/安全研究目的。不包含任何求解器或绕过方法——仅仅是为了记录 VM 的工作原理,因为确实很有趣。

DataDome:如果你们看到了这个,你们好!!!我只是想争取一份奖学金🙏。请不要起诉我,我穷得叮当响。如果你们对这个仓库有任何异议,请告诉我,我们可以谈谈 :)

抱歉让那些以为我会讨论 VM 内部的人失望了……那是不可能的。

不要私信我索要 DataDome API,我不会帮你的。

下载工具