今天分析的是tk新版验证码 版本 3.0.72 先给大家分享datawasm解密的分析与实现吧本次看的页面是这个版本aHR0cHM6Ly9zZjE2LXdlYnNpdGUtbG9naW4ubmV1dHJhbC50dHdzdGF0aWMuY29tL29iai90aWt0b2tfd2ViX2xvZ2luX3N0YXRpYy9vZWMtY2FwdGNoYS10dHdlYi9jYXB0Y2hhL3NnLzMuMC43Mi8xLjAuMC45MzIvaW5kZXguaHRtbAo实际页面能正常打开页面里会加载对应的captcha.js还有滑块背景图和缺口图。1. 先别急着解先确认 data 是谁一开始我没有直接去猜算法而是先看网络请求。验证码页面会先请求captcha/get响应里有一个data字段。这个字段看起来就是一段 base64直接JSON.parse肯定不行。这里先记住一点captcha/get.data不是最终提交的captchaBody它只是服务端下发的题目配置。解出来以后它大概长这样{ challenge_code: 99999, codifica: true, cyfreso: 32, id: cabcdeaf01373400bcdffdaded2b2e06c90deb033d0c90582c48975c86fca82a, mode: slide, question: { url1: 背景图地址, url2: 拼图图地址, tip_x: 0, tip_y: 58 }, version: 2 }所以这里的目标很明确我们只是要把captcha/get.data还原成题目 JSON不是一步到位生成提交参数。2. 在源码里搜关键字这种混淆后的captcha.js很长一上来硬读基本会迷路。围绕几个关键词搜。我优先搜这些decryptData cmd_clientDecrypt decData JSON.parse(s.decData) captcha/get很快能看到一个比较关键的位置wasm 模块导出了cmd_clientDecrypt。看到这里基本就能判断data的核心解密逻辑在 wasm 里JS 只是包了一层调用。3. 看 JS 是怎么调用 wasm 的继续往下追decryptData能看到它底层其实就是ccall(cmd_clientDecrypt, ...)。这里有个细节它不是一次调用完事而是先调一次拿输出长度再申请输出缓冲区最后第二次调用拿decData。这一步给了两个很重要的信息输入是atob(data)后的二进制内容。输出字段叫decData后面会被当成 JSON 字符串处理。也就是说浏览器里真正的数据流不是“data 直接 parse”而是base64 data - atob - wasm decryptData - decData - JSON.parse4. 完整链路再往调用方看就能看到captcha/get返回以后页面确实是这么处理的这个位置最关键因为它把前面几个点串起来了c.data - atob(c.data) - a.decryptData(...) - s.decData - JSON.parse(s.decData)到这里其实已经能证明captcha/get.data是 wasm 解密出来的 JSON。后面本地复现只是在把 wasm 里的逻辑还原成 Python。5. 本地复现它做的事情可以拆成几步1. base64 解码 data 2. 读取包头 magic raw[0:2] method raw[2:4] seed raw[4:36] body raw[36:] 3. 检查 magic 是否是 0x00ac 4. 检查 method 是否是 20 5. 用 seed 固定 mask 派生 AES key / iv 6. AES-256-CBC 解密 body 7. 去 PKCS#7 padding 8. UTF-8 decode得到 JSON 字符串包结构是这样的magic 2 bytes 小端固定 0x00ac method 2 bytes 小端当前是 20 seed 32 bytes 用来派生 key/iv ciphertext 剩余部分 AES-CBC 密文真正派生 key 的地方是d1 SHA512(seed) d2 SHA512(d1 || method20_mask) key d2[0:32] iv d2[32:48]这里最容易踩坑的是prefix、seed、ciphertext的切片位置。只要偏 1 个字节后面 AES 能跑但 padding 或 JSON 一定会炸。6. data 解完以后不等于提交完成这个点也很重要。captcha/get.data解出来只是题目配置比如id、mode、背景图、缺口图、tip_x/tip_y。真正提交的时候页面还会把轨迹、时间、环境字段组装成对象t最后再做一层 outer 加密变成captchaBody。源码里能看到后面还有一段前者是这篇文章讲的data wasm 解密。后者是另外一层提交加密里面还牵涉reply/mm/mp/mu轨迹、tm/ir/ugt/tk时间链路、detRes、环境字段等。整个排查过程可以简单记成这条线看到 captcha/get.data 是 base64 - 页面源码搜 decryptData / decData - 发现 decryptData 调 wasm 的 cmd_clientDecrypt - 继续找调用方 - 看到 a.decryptData(atob(c.data)) - 看到 JSON.parse(s.decData) - 本地按包结构复现 - 解出 id/mode/question闭环这个闭环比单纯看算法更重要。因为只要页面版本变了也可以按同样方法重新找1. 先确认 captcha.js 版本。 2. 搜 captcha/get、decryptData、decData。 3. 看 data 是否仍然 atob 后进 wasm。 4. 看输出是否仍然 JSON.parse。 5. 本地用新样本跑 decrypt_data.py 验证。
某海外验证码分析(1)data的wasm算法解密
发布时间:2026/6/24 2:16:55
今天分析的是tk新版验证码 版本 3.0.72 先给大家分享datawasm解密的分析与实现吧本次看的页面是这个版本aHR0cHM6Ly9zZjE2LXdlYnNpdGUtbG9naW4ubmV1dHJhbC50dHdzdGF0aWMuY29tL29iai90aWt0b2tfd2ViX2xvZ2luX3N0YXRpYy9vZWMtY2FwdGNoYS10dHdlYi9jYXB0Y2hhL3NnLzMuMC43Mi8xLjAuMC45MzIvaW5kZXguaHRtbAo实际页面能正常打开页面里会加载对应的captcha.js还有滑块背景图和缺口图。1. 先别急着解先确认 data 是谁一开始我没有直接去猜算法而是先看网络请求。验证码页面会先请求captcha/get响应里有一个data字段。这个字段看起来就是一段 base64直接JSON.parse肯定不行。这里先记住一点captcha/get.data不是最终提交的captchaBody它只是服务端下发的题目配置。解出来以后它大概长这样{ challenge_code: 99999, codifica: true, cyfreso: 32, id: cabcdeaf01373400bcdffdaded2b2e06c90deb033d0c90582c48975c86fca82a, mode: slide, question: { url1: 背景图地址, url2: 拼图图地址, tip_x: 0, tip_y: 58 }, version: 2 }所以这里的目标很明确我们只是要把captcha/get.data还原成题目 JSON不是一步到位生成提交参数。2. 在源码里搜关键字这种混淆后的captcha.js很长一上来硬读基本会迷路。围绕几个关键词搜。我优先搜这些decryptData cmd_clientDecrypt decData JSON.parse(s.decData) captcha/get很快能看到一个比较关键的位置wasm 模块导出了cmd_clientDecrypt。看到这里基本就能判断data的核心解密逻辑在 wasm 里JS 只是包了一层调用。3. 看 JS 是怎么调用 wasm 的继续往下追decryptData能看到它底层其实就是ccall(cmd_clientDecrypt, ...)。这里有个细节它不是一次调用完事而是先调一次拿输出长度再申请输出缓冲区最后第二次调用拿decData。这一步给了两个很重要的信息输入是atob(data)后的二进制内容。输出字段叫decData后面会被当成 JSON 字符串处理。也就是说浏览器里真正的数据流不是“data 直接 parse”而是base64 data - atob - wasm decryptData - decData - JSON.parse4. 完整链路再往调用方看就能看到captcha/get返回以后页面确实是这么处理的这个位置最关键因为它把前面几个点串起来了c.data - atob(c.data) - a.decryptData(...) - s.decData - JSON.parse(s.decData)到这里其实已经能证明captcha/get.data是 wasm 解密出来的 JSON。后面本地复现只是在把 wasm 里的逻辑还原成 Python。5. 本地复现它做的事情可以拆成几步1. base64 解码 data 2. 读取包头 magic raw[0:2] method raw[2:4] seed raw[4:36] body raw[36:] 3. 检查 magic 是否是 0x00ac 4. 检查 method 是否是 20 5. 用 seed 固定 mask 派生 AES key / iv 6. AES-256-CBC 解密 body 7. 去 PKCS#7 padding 8. UTF-8 decode得到 JSON 字符串包结构是这样的magic 2 bytes 小端固定 0x00ac method 2 bytes 小端当前是 20 seed 32 bytes 用来派生 key/iv ciphertext 剩余部分 AES-CBC 密文真正派生 key 的地方是d1 SHA512(seed) d2 SHA512(d1 || method20_mask) key d2[0:32] iv d2[32:48]这里最容易踩坑的是prefix、seed、ciphertext的切片位置。只要偏 1 个字节后面 AES 能跑但 padding 或 JSON 一定会炸。6. data 解完以后不等于提交完成这个点也很重要。captcha/get.data解出来只是题目配置比如id、mode、背景图、缺口图、tip_x/tip_y。真正提交的时候页面还会把轨迹、时间、环境字段组装成对象t最后再做一层 outer 加密变成captchaBody。源码里能看到后面还有一段前者是这篇文章讲的data wasm 解密。后者是另外一层提交加密里面还牵涉reply/mm/mp/mu轨迹、tm/ir/ugt/tk时间链路、detRes、环境字段等。整个排查过程可以简单记成这条线看到 captcha/get.data 是 base64 - 页面源码搜 decryptData / decData - 发现 decryptData 调 wasm 的 cmd_clientDecrypt - 继续找调用方 - 看到 a.decryptData(atob(c.data)) - 看到 JSON.parse(s.decData) - 本地按包结构复现 - 解出 id/mode/question闭环这个闭环比单纯看算法更重要。因为只要页面版本变了也可以按同样方法重新找1. 先确认 captcha.js 版本。 2. 搜 captcha/get、decryptData、decData。 3. 看 data 是否仍然 atob 后进 wasm。 4. 看输出是否仍然 JSON.parse。 5. 本地用新样本跑 decrypt_data.py 验证。