JD Cloudflare 验证码逆向踩坑记录搞国航机票搜索时遇到的京东云验证码记录一下从一头雾水到跑通的全过程。环境纯 Node.js不依赖浏览器/无头浏览器目录验证码流程长什么样抓包看接口参数是怎么拼出来的加密算法是啥WASM 卡住了换个路子asm.js 回退才是出路FP 请求实现Check 请求获取图片 vs 提交验证完整流程踩坑总结1. 验证码流程长什么样国航的机票查询页点搜索之后会弹出一个京东云验证码。样式就是那种在背景图里找出某某图标点一下的点击式验证码。整个流程拆开来看其实就两步拿验证码图片背景大图 指引小图提交点击坐标让服务器判断对不对后端只有两个接口三步操作Step 1: FP 请求 → 拿到临时 token (st) Step 2: Check 请求传空数据→ 拿到验证码图片 Step 3: Check 请求传点击坐标→ 提交验证有个有意思的点获取图片和提交验证用的是同一个接口区别只在于加密参数tk里面塞的数据不同。2. 抓包看接口2.1 FP 请求FP 请求是第一步用来获取一个临时 tokenst。需要传一个加密后的设备指纹ct以及一个认证头x-jdcloud-captcha-auth格式是;时间戳;32位hex。参数说明参数说明sisessionId浏览器的 localStorage 里拿b2c-web_sidct加密后的设备指纹x-jdcloud-captcha-auth认证头其他version2,lang1,clientm都是固定的返回的st就是后续请求需要的 token。2.2 Check 请求获取图片Check 请求比 FP 多了一个tk参数它是加密后的验证数据。获取图片的时候传的是空数据加密后的结果。返回的img字段里有两张 base64 图片一张背景大图b1和一张拼图指引b2。3. 参数是怎么拼出来的3.1 核心函数 Q在 SDK 里翻到一个核心函数所有 check 请求都是它发出去的。它的逻辑大致是先把验证数据用encodeURI编码一下然后按固定格式拼接明文。明文里包含了时间戳、sessionId、sttoken、验证数据、设备指纹等信息还会在特定位置插入随机长度的随机串。拼接完成后用加密函数加密带上认证头发送 POST 请求。3.2 参数拼接规则两个关键参数tk和ct的明文拼接规则不一样ct的明文随机前缀 sessionId长度(4位固定宽度) sessionId 设备指纹 时间戳tk的明文时间戳 sessionId长度(4位固定宽度) sessionId st长度(4位固定宽度) st 验证数据长度(6位固定宽度) 验证数据 触摸信息 随机后缀随机前缀的长度由时间戳 % 19决定随机后缀的长度由时间戳 % 41决定。3.3 常量SDK 里藏了几个关键常量包括默认的加密密钥、tdat_ctx 上下文、md5 salt 等。具体值就不贴了感兴趣的可以自己去 SDK 里找。4. 加密算法是啥在 SDK 里看到0x9e3779b9这个数熟悉的人应该一眼就能认出来——这是XXTEA 算法的 delta 常数。加密链路大致是明文先做 UTF-8 编码然后转成 32 位无符号整数数组走 XXTEA 加密轮最后转成二进制串用 URL-safe 的 Base64 编码输出。它的 Base64 字母表把标准版的/换成了-_。至于 auth 头的 hash 算法尝试用纯 JS 去复现但输出跟浏览器里的不一致。后来发现这个 hash 的计算在 WASM 模块里涉及 C 的整数运算逻辑JS 模拟不了。5. WASM 卡住了换个路子加密核心的两个函数都在 WASM 模块里。当时试了几条路直接下载 WASM 文件→ 返回 404WASM 被内联到 SDK 里了用 jsdom 加载完整 SDK→ WASM 编译在 Node 里跑不起来纯 JS 手写 XXTEA→ 输出跟 WASM 不一致C 和 JS 的整数运算有差异三条路都走不通的时候差点想放弃了。后来仔细看了 SDK 代码发现它在 WASM 编译失败的时候会降级到一个 asm.js 版本。那能不能让它强制走 asm.js 回退6. asm.js 回退才是出路6.1 问题在哪SDK 加载时会检测是不是 Node.js 环境。如果检测到是 Node.js它会走不同的初始化路径那个路径里不包含我们需要的两个函数。6.2 怎么解决思路很简单把这个检测结果 patch 成false让它以为自己在浏览器里禁用 WebAssembly逼它降级到 asm.jsmock 几个浏览器全局对象document、window之类的具体做法就是读取 SDK 的 JS 文件把检测 process 的那段代码替换掉然后在执行前把WebAssembly.instantiateStreaming设成一个直接 reject 的函数再补上global.document和global.window的 mock。然后 eval 执行 patch 后的 SDK 代码轮询等待 asm.js 初始化完成就能拿到加密函数了。6.3 验证跑了个测试asm.js 版的加密输出和浏览器 WASM 版完全一致。到这里核心问题就破了。7. FP 请求实现拿到加密函数之后FP 请求就很简单了。流程就是按规则拼出ct的明文 → 调用加密函数加密 → 生成 auth 头 → 发起 POST 请求。返回的st就是后续需要的 token。8. Check 请求获取图片 vs 提交验证Check 请求的接口、参数结构完全一样区别只在于tk里加密的数据不同。场景tk 里加密的 data获取图片空数据提交验证点击坐标数据坐标数据就是x、y和点击时间戳ts组成的数组。Check 的tk明文拼接规则和 FP 的ct类似只是多了 st 和验证数据的部分。响应解析也很简单code为 0 且有img字段说明拿到图片了或者验证失败返回了新图片code为 0 且没有img说明验证通过。其他错误码对应不同的异常情况。9. 完整流程FP 请求 → 拿 st → Check 请求空数据→ 拿图片 → Check 请求点击坐标→ 验证结果sessionId 从浏览器 localStorage 获取st 有效期几分钟每次验证前重新 FPtdat_ctx、sensor 从浏览器抓一次就能重复用最后说两句整个逆向过程最折腾的就是 WASM 那块试了好几种方案都没成最后发现 SDK 自带的 asm.js 回退才是最省事的方案——不用自己重写加密不用折腾 WASM 运行时patch 几行代码就能直接用。对了sessionId 和设备指纹都是从浏览器抓的固定值实际用的时候记得换成你自己的。运行结果示例注意本文只涉及验证码的协议逆向与加密突破不包含验证码图片识别找图、坐标计算等。Running] node f:\project\crawler\验证码\国际航空\work\1.js JD Cloudflare Captcha 纯Node.js验证 [1/4] 初始化加密模块... ✓ jcap asm.js就绪 [2/4] FP请求获取token... ✓ FP成功 stsY87rOSMAGjYO2iI [3/4] 获取验证码图片... ✓ 背景图已保存 ✓ 拼图已保存 [4/4] 提交验证... 验证结果 { st: , code: 16807, s_code: 16102, msg: 验证失败请重新验证 } ❌ 验证失败坐标不对: 验证失败请重新验证