拆解 Datadome Captcha WAF
我们一步步走一遍最新的 Datadome Captcha/Geetest WAF,来理解如何访问被它保护的站点。

我们一步步走一遍最新的 Datadome Captcha/Geetest WAF,来理解如何访问被它保护的站点。

你被拦截了!
下面是当你抓取一个受保护站点、请求被 Datadome Captcha WAF 拦截时会看到的响应:
<html lang="en">
<head>
<title>datadome.co</title>
<style>
...
</style>
</head>
<body style="margin:0">
<p id="cmsg">Please enable JS and disable any ad blocker</p>
<script data-cfasync="false">
var dd = {
'rt': 'c',
'cid': 'AHrlqAAAAAMAseGpalIFikoAyV2i2w==',
'hsh': '14D062F60A4BDE8CE8647DFC720349',
't': 'fe',
'qp': '',
's': 44330,
'e': 'a1dea4f437d7f7234cf4ab0017373492e7438d099a0632bc3dd4ac787bf0ffa3',
'host': 'geo.captcha-delivery.com',
'cookie': 'Y1XRfldQv9mO1z2TBDqSmbztAi_H6BR04bz14NTujEm~6G_7X6eHyn9i71KhHxsv86WUIZGokIBSyX9bulEARTINIkeWdhjKGUu4Rm7pkbnPpTfSwfC4ntcQPF6rcA5z'
}
</script>
<script data-cfasync="false" src="https://ct.captcha-delivery.com/c.js"></script>
</body>
</html>
利用返回的 dm 字典,被引入的那段 JavaScript 会生成真正的挑战 URL,并把它以 iframe 的形式注入到页面上。iframe 一嵌入,你要么会遇到需要解决的验证码,要么会看到"You have been blocked"的提示。这意味着两种情况之一:
- 你的 IP 地址被标记为不良。
- 你的请求流程或请求头偏离了正常浏览器的流程,触发了 Datadome 的机器人检测。
要避免这种情况,请务必先用真实浏览器测试你的代理,确认它们能解决挑战并浏览站点,然后在你的脚本里精确复现那次浏览器会话的请求顺序和请求头。
那个请求
解决挑战之后,会发出下面这个 GET 请求:

成功解决后,会返回一个 Datadome cookie。这个请求里的大多数值都只是从页面里解析出来的。唯一重要的是 ddCaptchaEncodedPayload,我们会在本文后面研究并拆解它。它本质上是一个字典,装着各种细节,被称为 signals(信号)。

Datadome 如何自我宣传
"关键不只在于是人还是机器人——而在于意图。我们的 AI 能实时检测恶意意图与合法意图,无论它来自机器人还是人类。"
他们公司自称是"网络欺诈防护领域的领导者",并夸口通过在全球 25+ 个接入点每天处理超过一万亿个信号,拦截了"数十亿"次攻击。它主打自家的高速 AI 引擎:例如 2025 年 4 月的一次发布声称,它能"在不到 2 毫秒内识别、归类、适应并响应流量"。
"大多数机器人检测工具依赖静态的、基于规则的逻辑,跟不上新出现的威胁。DataDome 的 AI 引擎不一样。它用有监督和无监督两种模型持续学习并实时适应,……"
营销材料常把这一点拿来和传统防御对比;博客里声称基于规则的 WAF"过时了","根本不是高级机器人的对手"。
在一篇博客里,DataDome 甚至称自己是"唯一以服务形式交付的机器人与在线欺诈防护方案"。
他们的 CEO 声称机器学习引擎"评估每一个请求",而不是依赖静态规则(来源:techcrunch.com)。
他们还宣称能"实时"阻止内容盗窃、账户接管、广告欺诈和 DDoS。
在我们开始深入分析之前,先记住一点:这家公司累计已获得 8100 万美元融资。
这个挑战的历史
这个 WAF 页面大约五年前首次出现,正值"球鞋抢购的黄金时代"。当它出现在 Slamjam、Starcow 这类站点上时,唯一的解决办法是通过 reCAPTCHA。

后来它被换成了 Geetest 的实现,再后来又换成了一个自定义的、Geetest 风格的滑块验证码或音频挑战。
音频这个选项从来都不理想,因为通过这种方式拿到的 cookie 往往信任分较低,会触发第二道挑战或很快被拦。
使用场景
目前 Datadome 仍被一些零售商、大多数欧洲足球队的购票网站,以及一些信用卡礼品卡网站使用。下面是一些例子:
- https://tickets.rolandgarros.com/
- https://www.pokemoncenter.com/
- https://www.liverpoolfc.com/tickets
- https://mygift.giftcardmall.com/
- https://www.giftcards.com/
- https://www.uefa.com/
- https://www.arsenal.com/
- https://www.vinted.com/
- https://www.thefork.com/
- https://www.etsy.com/
- https://www.soundcloud.com/
- https://www.seatgeek.com/(以及它旗下的所有网站)
名单还很长……
JavaScript 混淆
在 iframe 页面里,你会看到一长条压缩过的代码,里面装着验证码真正的 JavaScript。

美化之后,这个文件暴露出一套相当简单的混淆,基于数组和函数的技巧构建。

它相当长,因为它实际上由八个独立的模块/文件组成,如所附图片所示。


每个文件都有一个特定、组织有序的用途,并包含它自己那套函数。
循环-Switch 结构混淆
一个值得注意的点,是他们"混淆" for 循环和 switch 语句的独特方式。它完全没意义、逆起来也轻而易举,但这种手法我以前没见过。

它做了什么
代码没有写成:
case 23
而是构建了一个二维查找表 s,并通过 s[x][y] 来驱动 switch 的状态,所以每个 case 看起来像:
case s[263][471]:
其中生成这张表的函数长这样:
// dynamic table function generation
var s = function (e, t) {
var a, n;
for (t = [], e = 0; e < 128; e++) t[e] = new Array(512);
for (a = 0; a < 512; a++) for (n = 0; n < 128; n++) t[n][a] = t[Ze(128, a, 337, 163, 349, 30, n)];
return t[30];
}();
// and his helper
function Ze(e, t, a, n, c, i, r) {
return (t * c ^ r * n ^ i * a) >>> 0 & e - 1;
}
**在运行时检查这张表
**如果你在浏览器控制台里打印 s[yyy][zzz],你得到的不是一个数字,而是一个 Array 对象(那张查找矩阵的其中一行)。

**为什么这是一种愚蠢的混淆
**通过在沙箱里(例如 Node 的 vm)运行相同的表生成函数和映射函数,你可以重建整张查找表,从而破掉这个混淆。
// 1. Spin up a sandbox and rebuild the exact same table
const vm = require('vm');
const context = vm.createContext({});
vm.runInContext(`
// dynamic table generation and his helper
`, context);
// 2. Build a reverse map from each row object back to its [x,y] indices
const s = context.s;
const rowMap = new Map();
s.forEach((row, x) => {
row.forEach((cell, y) => {
// we key off the *exact* Array instance stored at s[x][y]
rowMap.set(cell, [x, y]);
});
});
// 3. Helper to recover the original numeric state
function stateLocation(x, y) {
return rowMap.get(s[x][y]);
}
// 4. Proof it works:
console.log(stateLocation(263, 471)); // e.g. 42
**把它清理干净
**基于这个逻辑,一次简单的 AST 遍历就能把每个 case s[x][y] 替换回原本的数字字面量,还原成正常的 switch 语句。

又快又简单,但是个不错的案例。
脚本的动态性
一家融资超过 8000 万美元的公司,居然至今还没有为每个会话引入一个完全动态的脚本,这挺奇怪的,而目前大多数"大牌"反机器人厂商都是这么做的。
他们做的,是给文件实现了一套每日轮换,也就是说 iframe 的脚本每天都会在某个特定时间点变一次。
这套轮换大约一年前引入,除非有重大更新或补丁,否则版本号会保持不变,只有几个静态 ID 被改动,用来标识正在解决的是具体哪一个挑战。
动态的信号名
几个月前引入了一项重大改动——先是在验证码挑战上,后来在拦截页上也上了:signals 使用动态键名。每天,signals 字典里的键都会变成一个随机的六位字符串。如果你不能正确匹配它们,解出来就是无效的。

这是一个巧妙的手法,把大多数解题 API 提供商挡在了门外,但过去五年里,有人本可以"以防万一"把每天的脚本都存下来。然后他们就能做一次快速的语义搜索,识别出被重命名的信号、新增或删除的条目,或其他改动。:P

目前,这套动态键机制把大多数提供商挤出了局,或者逼他们转向基于浏览器的方案,在我看来这是最糟糕的交付服务方式,原因如下:
- 解决耗时慢、资源占用重
- 被锁死在特定的 user agent、浏览器、操作系统版本、时间戳或指纹上,逼着客户必须精确匹配
- 开发者往往完全不知道反机器人在检查什么,导致调试或修复站点专有问题很困难
- 每次挑战更新都需要新的浏览器补丁,导致长期或关键项目不稳定
这一切都源于没能找出一种可扩展的、每天抓取并识别信号名的方法。
WASM 挑战:boring_challenge

最近,作为其中一个信号的一部分,引入了一个基于 WASM 的挑战。主函数名为 boring_challenge,本质上是逼你的浏览器反复运行一个用 Rust 编译的小型状态机,直到它吐出一个数字。
- 把一个紧凑的 Wasm blob 做 Base64 解码,并同步编译成一个 WebAssembly.Module。
- 设置 wasm-bindgen 的 imports(用 [undefined, null, true, false] 扩充一个 externref 表)。
- 实例化该模块(它会通过自己的 __wbindgen_start 把一些全局变量清零)。
- 挑一个随机的 32 位种子(在 1000 万到 2000 万之间)和一个并发提示(你的 CPU 核心数)。
- 调用 boring_challenge(BigInt(seed), BigInt(concurrency)),它会跳进一个庞大的、嵌套的循环,里面全是位运算、XOR、移位、循环移位和魔术常量——全部硬编码在一个混淆过的、基于查找表的状态机里。
- 只有当那个状态机最终到达一个终止值时才退出,返回一个 64 位结果,你的脚本再把它转换回一个 Number。
这里没有真正的"验证码",只是纯粹的工作量证明(proof-of-work)。服务器可以在自己这边廉价地重新调用同一个函数来验证你确实做了这份苦工,但你的浏览器必须烧掉 CPU 周期来解它。

它逆起来毫不费力(我们只需把它反编译并自己运行)。它基本上更像是一种 "CPU 税",而不是真正的指纹,也许是为了给无头浏览器制造麻烦而引入的(我猜?)。
你可以在我开源的仓库里了解更多
动态哈希挑战
在 WASM 引入之后,最新引入的"特性"是一个动态哈希挑战,其中一些已经在其他信号里提供过的浏览器细节,被组装成一个列表,然后用一个特定的动态挑战哈希做哈希。
得到的列表随后送进一条动态的运算链,看起来像这样:
((((((inputValue[0] >>> 0 ^ 555683) >>> 0 >> 4 >>> 0)
+ (((956305 & inputValue[1] >>> 0) >>> 0 & (inputValue[2] >>> 0)
+ (inputValue[1] >>> 0) >>> 0) >>> 0) >>> 0)
+ ((2488139776 ^ (inputValue[1] >>> 0 << 1 >>> 0)
+ (inputValue[0] >>> 0 >> 6 >>> 0) >>> 0) >>> 0) >>> 0
^ (((699654 + (inputValue[0] >>> 0) >>> 0) - 1111820 >>> 0)
- ((968080 - (inputValue[0] >>> 0) >>> 0 ^ 864233) >>> 0) >>> 0)
- ((((792526 & inputValue[2] >>> 0) >>> 0
| (298058 & inputValue[1] >>> 0) >>> 0) >>> 0)
- (((inputValue[1] >>> 0) + (inputValue[1] >>> 0) >>> 0
^ 622410 - (inputValue[1] >>> 0) >>> 0) >>> 0) >>> 0) >>> 0) >>> 0
& ((11187856 + (410903 + (inputValue[1] >>> 0) >>> 0) >>> 0)
+ (((inputValue[0] >>> 0 | 0) >>> 0 ^ 706164
- (inputValue[1] >>> 0) >>> 0) >>> 0) >>> 0 << 9 >>> 0)
- (((441672 - (inputValue[2] >>> 0) >>> 0)
+ ((inputValue[0] >>> 0) - (inputValue[2] >>> 0) >>> 0) >>> 0)
+ (((inputValue[1] >>> 0) - 617963 >>> 0)
+ ((inputValue[2] >>> 0) - 798365 >>> 0) >>> 0) >>> 0 >> 8 >>> 0) >>> 0) >>> 0
^ 351641146) >>> 0
这是个有意思的思路,但不幸的是它相当容易识别和提取,而且因为它完全依赖基于数学的计算,简单地求值一下就够了。

脚本的完整性校验
直到今天,都没有加入任何完整性校验,这让为了调试目的去打补丁变得很容易。
更多地理解各版本的变化
出于学习目的,看看我那些如今已经过时的、开源的 DataDome 去混淆模块(拦截页和验证码都有):
- Datadome-Deobfuscator(2023 年 10 月)
- Datadome-Interstitial-Deobfuscator(2024 年 1 月)
- Datadome-Captcha-Deobfuscator(2024 年 1 月)
图片解题

Datadome Captcha 返回的图片,只用几个滤镜和一些数学运算就能相当轻松、快速地解出来。我已经开源了一个仍然可用的 Python 基础脚本供你上手:Datadome-GeeTest-Captcha-Solver

轨迹计算
真正的关键挑战,是确保你提供的移动轨迹匹配 DataDome 的预期。
DataDome 用两个列表来分析轨迹
- _initialCoordsList 捕获从页面加载到点击滑块按钮之间的移动
- _coordsList 装的是滑动过程中的移动
这两个列表随后被计算成 31 个信号,基于曲率、长度、直线度以及其他指标,用来标记异常输入。

老实说,他们的检查既严格又有效,能拦住大多数基于浏览器的自动化脚本,不过一个喂了大量数据的好 ML 模型照样能搞定 :P。
为了帮上这部分工作,我开源了 Datadome-Movements-Display,它会在计算之前把原始的移动列表可视化出来,让你把自己生成的模式和真实浏览器数据做对比。
信号加密:ddCaptchaEncodedPayload
在把一个信号加入最终 payload 时,会用到一种基于网站哈希的自定义编码方法。
e(`THcQWT`, i.left)
e(`ds6frg`, i.right)
e(`zzUlTA`, i.up)
e(`uJkDwZ`, i.down);
加密流程概述

**初始化
**系统用一个哈希、一个客户端标识符(cid)和一个可选的 salt 来播种。这些参数初始化一个伪随机数生成器(PRNG),驱动后续的各个步骤。
**缓冲区构建
**每个信号(键值对)都被混淆并与 PRNG 生成的字节做 XOR。标记字节({、})和分隔符(:)也被做 XOR 混淆并追加进去,用来划分结构。
**用第二轮 PRNG 做 XOR
**整个缓冲区再经历一次额外的 XOR,用的是从 cid 和 salt 派生出的第二个 PRNG 序列。这种双层做法确保每个字节的最终值都以错综复杂的方式依赖于多个动态参数。
**类似 Base64 的自定义编码
**XOR 之后,缓冲区用一种类似 Base64 的自定义算法编码成字符串。还会施加一些额外的、用一个递减 salt 值做的 XOR 运算。一个独特的字符映射保证了 URL 安全的传输,进一步增加了解密难度。
解密流程只要你搞懂了这些步骤,把它们逆过来就行。
为了不流失对技术细节不感兴趣的读者,我不会讲得太深,我已经把加解密逻辑的一次深入剖析开源了,NodeJS 和 Python 都有:
那么 Datadome 到底该怎么解?
我们一步步走一遍这个流程。我假设你用的是基于 requests 的方案,因为基于浏览器的方案每次更新都需要不断打补丁、资源占用高,而且对正在发生的事几乎没有掌控力。
- 从初始响应中识别并构造挑战 URL。
- 加载挑战页面。
- 从加载出的挑战里解析所需的细节。
- 提取、排序并解读动态键。
- 解决图片挑战。
- 组装你的 signals 字典、浏览器细节、已解决的挑战,以及计算出的移动轨迹。
- 加密 payload。
- 把它发到 DataDome 端点。
- 取回 cookie(如果解决成功的话)。
- 重新加载目标页面。
结论
总的来说,这只是一个案例研究。我并不是说 DataDome 是个糟糕的反机器人——他们的一些技术很独特、很有意思,即便很容易绕过——而是想强调他们的 WAF 有多"廉价"、多容易,并对他们从投资人那里拿到的数百万美元花到哪去了提出疑问。
也许他们该把这篇文章撤下来:How to Bypass DataDome (And Why It's Not That Simple)
需要 DataDome 绕过方案?

如果你的项目需要一个可靠的 DataDome 绕过方案,就找真正懂这项技术的专家。我的公司 TakionAPI 提供专业的反机器人绕过 API,对 DataDome 和其他机器人防御系统都有经过验证的效果。
你可以在这里查看并运行视频里的示例文件。
不用再操心自己去理解、逆向、解决挑战,也不用再操心每天把它维护到最新。一次简单的 API 调用就全搞定。
我们提供免费试用、示例实现和上手协助,让整个过程既简单又顺畅。查看我们直白的文档,在这里开始试用,或者对于定制开发和支持,在 Discord 上联系我们。
访问 TakionAPI.tech,获取真正的、高质量的反机器人绕过方案,我们知道自己在做什么。
与我联系
如果你喜欢这篇文章,在 GitHub 和 Medium 上关注我,每当我发帖或开源什么东西时你都会收到通知。
来源
- TechCrunch
- Datadome website
- How to Bypass DataDome (And Why It's Not That Simple) — Datadome 的宣传
- datadome-wasm
- datadome-encryption
- datadome-encryption-python
- Datadome-GeeTest-Captcha-Solver
- Datadome-Movements-Display
- TakionAPI
- TakionAPI DOCS
- Datadome-Deobfuscator (2023 年 10 月 — 已过时)
- Datadome-Interstitial-Deobfuscator (2024 年 1 月 — 已过时)
- Datadome-Captcha-Deobfuscator* (2024 年 1 月 — 已过时)*
- Datadome-Interstital-Encryptor (已过时)
- How to detect, block and manage DataDome (已过时)