腾讯防水墙是腾讯自研的验证码服务,广泛应用于 QQ、微信生态以及大量第三方接入站点。它提供多种验证类型:滑动拼图、文字点选、空间推理等。
本文以腾讯防水墙的文字点选验证码为目标。当用户触发需要验证的操作后,页面会弹出一个验证码 iframe,要求用户"请依次点击"指定的三个汉字。验证通过后返回 ticket,前端携带 ticket 完成业务请求。
验证码运行在跨域 iframe turing.captcha.gtimg.com 中,核心逻辑由一个名为 tdc.js 的脚本驱动。这个脚本负责采集用户的浏览器指纹、鼠标轨迹等行为数据,加密后生成一个 collect 字段提交给服务端验证。
如果你打开 tdc.js 看一眼,会发现它只有几行代码——但这几行代码定义了一个完整的栈式字节码虚拟机,腾讯称之为 CHAOS VM。所有真正的业务逻辑(指纹采集、加密算法、数据组装)都编译成了字节码,运行在这个 VM 内部。你看不到任何业务代码——没有 function encrypt(),没有 key = [...],一切都藏在一个巨大的 Base64 字符串里。
但 VM 保护本身不是最难的部分。真正的核心难点是:tdc.js 每次加载都会变。
每次请求 2bfK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6@1N6i4u0A6L8X3N6Q4x3X3g2U0j5i4m8@1j5$3S2S2i4K6u0W2M7h3y4D9L8%4g2V1i4K6u0W2j5$3!0E0i4K6u0r3N6r3c8U0i4K6u0W2K9Y4x3`.,服务端都会动态生成一份全新的 tdc.js。变化的内容包括:
这意味着你不能做一次分析然后硬编码结果——上一秒分析出来的 key、模块顺序、opcode 映射,下一秒全部作废。
面对 tdc.js 的 VM 保护,一个常见的思路是"补环境":用 Node.js 或 jsdom 搭建一个模拟的浏览器环境,直接执行 tdc.js,调用 TDC.getData(true) 拿到结果。
这个思路确实能跑通——但它是一个黑盒方案。你不知道 tdc.js 内部在做什么,不知道它采集了哪些指纹,不知道哪些环境变量会影响结果。这带来几个问题:
我们选择了另一条路:白盒还原。完全理解 tdc.js 的内部逻辑,用 Python 从零构造 collect。这条路更难,但一旦走通,每个参数都在掌控之中。
用纯 Python 还原 TDC.getData(true) 生成的 collect 加密字段,实现端到端的自动化流程:
整个过程不需要运行任何 JavaScript,不需要浏览器环境,完全在 Python 中完成。
用户触发需要验证的操作后,服务端返回验证码弹出指令,前端随即加载腾讯验证码 SDK(TCaptcha.js),弹出一个跨域 iframe。
这个 iframe 的域名是 turing.captcha.gtimg.com,与宿主页面跨域隔离。验证码的所有逻辑——图片展示、用户交互、行为采集、数据加密——都在这个 iframe 内部完成。这也给逆向分析带来了额外的挑战:你不能在主页面的 console 中直接访问验证码的变量和函数,必须切换到 iframe 上下文才行。
阶段 1:预处理(prehandle)
返回内容包括:
阶段 2:tdc.js 加载
tdc.js 下载后通过 blob URL 加载到 iframe 中。加载完成后,iframe 的 window 上会出现一个 TDC 对象,暴露 4 个方法:
tdc.js 加载后立即调用 core.mInit() 开始采集行为数据(鼠标移动、键盘事件等)。
阶段 3:用户交互
用户看到验证码图片,依次点击指定的汉字。每次点击的坐标被记录为 ans 参数。与此同时,TDC 持续采集鼠标移动轨迹。
阶段 4:提交验证
用户点击完成后,前端脚本 dy-ele.js 执行以下操作:
阶段 5:响应处理
提交验证时有 8 个参数,但需要逆向的实际上只有 2 个:
一个有意思的设计:服务端如何知道 XTEA 加密的密钥?
答案藏在 eks 字段里。eks 是一个 Base64 编码的 176 字节数据,实质上是 XTEA key + offsets + 元数据的加密信封。服务端生成 tdc.js 时,随机生成 XTEA key,用服务端主密钥加密后写入 tdc.js 源码中的一个全局变量。客户端原样回传 eks,服务端用主密钥解密即可还原 key。
这种设计实现了完全无状态的验证架构——服务端不需要存储任何 key-session 映射关系。
key 的生命周期与 tdc.js 绑定:页面加载时生成新 key,整个验证会话内不变(包括多次重试和刷新题目)。只有页面刷新重新加载 tdc.js 才会产生新 key。
打开 tdc.js,格式化后只有约 226 行。前几行是普通 JS 赋值(Date 代理函数、eks 常量),核心是一个名为 __TENCENT_CHAOS_VM 的函数,它定义了一个完整的栈式字节码虚拟机。
VM 的主循环极其简洁:
三个核心变量:
每次循环取出 k[N] 作为操作码索引,调用 U 中对应的 handler 函数执行操作。handler 返回 true 时虚拟机停止。
字节码和字符串表的编码方式也很巧妙:
Q 参数是一个大数组:["NgMBAjQ0NgYB...", 偏移1, 值1, 偏移2, 值2, ...]。Base64 解码后,每个字符的 charCode 作为操作码或操作数,字符串表按偏移位置插入指令流。最终产出一个约 45000 个元素的指令数组。
VM 的操作码表 U 是一个数组,每个元素是一个 handler 函数。问题是:每次加载 tdc.js,操作码在数组中的索引位置会变。比如"加法"操作这次在 U[19],下次可能在 U[37]。
但 handler 函数的代码本身不变。我们通过分析每个 handler 的代码特征来识别它的语义:
58 个操作码涵盖了一个完整语言的基本操作:算术运算、位运算、比较、跳转、函数定义与调用、属性访问、数组操作等。每次拿到新的 tdc.js,自动识别一遍即可。
有了操作码映射,就可以将指令数组反汇编成可读的汇编代码。
反汇编器采用递归下降策略:
以 demo01 版本为例,反汇编产出 214 个函数、24000+ 条指令。一个典型的反汇编片段:
反汇编只是第一步。24000 条指令中,大部分是 37 个采集器模块的代码,真正的加密核心只有几百条。AI 在这一步的价值是快速定位和理解:
整个 VM 逆向过程是一个"AI 精读 + 人工验证"的循环:AI 提出伪代码假设,人在浏览器中打断点验证。
在 45000 个指令元素中,有一个数字极具辨识度:2654435769,即 0x9E3779B9——这是 TEA 系列加密算法的标志性 delta 常量,来源于黄金分割比。
在指令数组中搜索这个值,直接定位到 XTEA 核心函数(demo01 中是 func_9181,约 255 条指令)。从这个函数向上追溯调用链:
Key 每次都变,但提取 key 的方法是不变的。这是"变化中找不变"这条主线在加密层面的体现。
XTEA 的 key 是 4 个 int32,由 4 个字符串通过小端序打包生成。每次 tdc.js 变化的是字符串内容和偏移值,但打包公式的代码结构不变:
难点在于真假混淆。主加密函数有 14 个子函数参与 key 构建,其中只有 4 个写入真实 key buffer(buf_A),其余 10 个写入诱饵 buffer(buf_B)。真实和诱饵使用完全相同的字符串(如 "SUGU" 同时出现在 buf_A 和 buf_B),仅凭字符串无法区分。
而且,真实 buffer 的 scope 索引每个版本都不同——版本 A 中 M[9] 是真实 key,版本 B 中 M[8] 才是。不能硬编码。
不变的判别规则:虽然索引在变,但 XTEA 函数必须引用真实的 key buffer 才能加密。所以我们从 XTEA 函数的 scope 声明出发,逆向追踪它引用的是哪个 buffer,再映射到父函数中找到写入该 buffer 的子函数——这条 scope chain 追踪逻辑是跨版本稳定的。
用同样的思路处理 XTEA 的运行时偏移值(每次加密时给特定 key 索引加的额外偏移):偏移值本身每次不同,但它在字节码中的位置模式不变——总是作为 PUSH_IMM 指令嵌入在 XTEA 循环的条件分支中。搜索 XTEA 函数范围内符合特征的立即数,即可自动提取。
我们用 6 个不同版本的 tdc.js 验证了这套自动提取方案的正确性。
在动手实现之前,我们搜索了中英文社区(看雪、CSDN、知乎、GitHub、cn-sec.com 等),确认没有人公开过从字节码中自动静态提取 key 的方法。现有方案要么需要手动断点抓取(每次操作浏览器),要么走补环境路线(不提取 key,直接跑 tdc.js)。
我们的方案是:纯静态分析字节码,自动提取 key。不需要浏览器,不需要 Node.js,从 tdc.js 文本直接计算出 key 值。
collect 不是简单地"加密一段 JSON"。它由 4 段密文直接拼接而成:
chunk1、trajectory、chunk2 在加密前用空格 pad 到 24 字节对齐(确保 Base64 编码后没有 = 填充符),sd 不 pad。4 段密文的 Base64 之间没有分隔符,直接拼接。
这个设计很聪明:因为每段都 pad 到 24 字节对齐,Base64 编码后没有 =,4 段拼起来看起来就是一个完整的 Base64 字符串。但解密时也可以整体 decode + decrypt,因为 XTEA 是 ECB 模式(每 8 字节独立加密)。
cd 数组是 collect 的主体,包含了 37 个采集器模块的输出。这些模块采集各种浏览器指纹和环境信息:
37 个模块中:
这是整个项目中最核心也最曲折的部分——它完美体现了"变化中找不变"的主题。
问题:tdc.js 每次加载时,37 个模块的 ID 随机重新分配。上一秒"模块 13"是 user_agent,下一秒"模块 13"可能变成 timezone。要正确构造 cd 数组,你必须知道每个模块 ID 这次对应什么类型——而这个映射关系每次都不同。
这是补环境方案完全无法解决的问题。补环境可以跑出一个 collect,但它不知道 cd 数组中第 5 个位置是 user_agent 还是 screen_width。一旦你需要控制指纹值(比如随机化 UA 或屏幕分辨率来避免被检测),就必须知道每个位置填什么。
失败的尝试 v1-v3
我们尝试了 3 个版本的静态分析器,思路是"追踪每个模块 .get 函数的返回值来源"。从字节码中确实能找到正确的 JS API 字符串(如 "userAgent"、"characterSet"),但位置映射全部错误——类型识别是对的,分配给了错误的模块。
根因是 VM 的动态语义(变量自增、共享引用、运行时 scope 修改)无法通过纯静态分析推断。
突破:代码变了位置,但没变内容
失败三次后,我们重新审视了问题,发现了一个关键洞察:
tdc.js 的"动态化"是重新排列,不是重新生成。
模块的 ID 变了、位置变了,但每个模块的代码逻辑完全不变。采集 userAgent 的模块,不管被分配到 ID 13 还是 ID 57,它的字节码里一定包含字符串 "userAgent"。采集 timezone 的模块,一定包含 "getTimezoneOffset"。
这就是不变量。
基于这个洞察,我们放弃了"追踪返回值"的思路,转而用字符串集合 + 结构特征作为指纹来识别模块类型:
对于没有区分性字符串的通用模块(如两个都只有简单赋值的模块),用 .get 函数的指令数 + entry 函数的指令数 + 是否包含特定指令来区分。
结果:3 个 tdc.js 版本、37 个模块、全部正确识别,无一 unknown。从 v1 的全部错位到指纹方案的全部正确,关键不是更复杂的分析,而是换了一个更好的问题——不再问"这个模块返回什么值",而是问"这个模块的代码长什么样"。
知道了每个模块 ID 对应的类型后,还需要知道它们在 cd 数组中的排列顺序。
这个顺序由编排函数决定。编排函数的字节码结构是固定的:按 require_seq 顺序依次调用每个模块,将模块对象 push 到数组中。从编排函数的字节码中提取所有 PUSH_IMM mid 指令,就得到了完整的模块调用顺序。
结合指纹识别结果,每个位置的类型就确定了。再根据类型查表填入对应的指纹值,就能生成正确的 cd JSON。
func_4310 是将 37 个模块的输出组装成 JSON 字符串并分段加密的核心函数,逻辑如下:
几个细节:
验证码要求用户依次点击 3 个汉字,过程中 TDC 会采集完整的鼠标移动轨迹。轨迹格式:
坐标系是相对于验证码容器 tCaptchaDyContent(360×360 像素),使用 event.offsetX/Y。注意这与 ans 参数的坐标系不同——ans 使用的是原始图片坐标(672×480)。
轨迹生成用贝塞尔曲线模拟真实鼠标移动:
验证码图片中,需要点击的汉字以黄色高亮显示在背景图上。识别流程:
当前瓶颈是形近字的误识别。例如"边"和"芭"、"笆"在低分辨率下容易混淆。PaddleOCR 的识别准确率在 85% 左右,这直接影响了整体验证通过率。后续可以换用更强的 OCR 模型(如基于 Transformer 的方案)来优化。
如果每次请求都用相同的浏览器指纹,很容易被服务端检测到异常。我们实现了指纹随机化:
每次请求随机选择一款设备画像,再随机组合 Chrome 版本(130-145)和 macOS 版本,生成一致的 User-Agent、sec-ch-ua headers 和环境变量。所有相关的指纹值(屏幕尺寸、WebGL 渲染器、CPU 核心数等)都从同一个画像中取,确保自洽。
验证码提交前需要解决一个 PoW(工作量证明):给定 prefix 和目标 MD5 前缀,找到一个 nonce 使得 MD5(prefix + nonce) 以目标前缀开头。
通常几千到几万次尝试即可完成,耗时几十毫秒。
full_flow.py 是整个流程的编排器:
整个流程纯 Python 实现,不依赖浏览器环境,单次执行约 2-3 秒(主要耗时在网络请求和 OCR)。
传统 JS 逆向的工作流是:打开 DevTools → 搜索代码 → 手动打断点 → 触发操作 → 观察变量 → 记录到笔记。这个过程效率低、容易遗漏,且完全依赖人的经验。
AI 编码助手(如 Claude Code、Cursor、Copilot)擅长分析代码、识别模式、快速迭代方案。但它们有一个致命短板:无法直接操控浏览器调试器。AI 可以读反汇编代码并还原伪代码,但无法自己打个断点去验证。
市面上也有一些浏览器自动化的 MCP 工具(如 Playwright MCP、Puppeteer MCP),但它们的设计目标是UI 自动化测试,不是逆向分析。它们能点击按钮、填写表单、截取截图,但不能设断点、不能查看调用栈、不能 Hook 函数、不能在断点处求值表达式——而这些恰恰是 JS 逆向最核心的操作。
JS Reverse MCP 是专门为 JS 逆向分析设计的 MCP 服务器。它将 Chrome DevTools Protocol(CDP)的完整调试能力暴露为标准化的 MCP 工具,让 AI 能像经验丰富的逆向工程师一样操作 DevTools。
JS Reverse MCP 提供了 30+ 个工具,覆盖 JS 逆向分析的完整链路:
脚本分析:列出页面所有 JS 脚本、在所有源码中搜索关键字、获取脚本源码(支持压缩代码的字符偏移定位)
断点调试:设置行断点、条件断点、XHR/Fetch 断点。支持 set_breakpoint_on_text —— 直接搜索代码文本自动设置断点,特别适合压缩代码(整个文件在一行上)
执行控制:暂停/恢复执行、step over / step into / step out。在断点处用 evaluate_on_callframe 求值任意表达式
函数追踪:hook_function 拦截全局函数或对象方法,记录每次调用的参数和返回值。trace_function 更强大——它能追踪 webpack/rollup 打包的模块内部函数,使用条件断点(logpoint)实现零侵入式日志
网络分析:查看请求详情和响应内容、获取请求发起的 JavaScript 调用栈(快速定位哪段代码触发了某个 API 请求)
运行时检查:深度检查对象结构和原型链、获取 cookies/localStorage/sessionStorage、监控 DOM 事件、截取页面截图
JS Reverse MCP 有几个在逆向场景中至关重要、但其他 AI 工具完全不具备的能力:
这是本项目中最关键的能力之一。腾讯验证码运行在跨域 iframe turing.captcha.gtimg.com 中,与宿主页面完全隔离。普通的浏览器自动化工具(Playwright MCP、Puppeteer MCP)只能操作主页面上下文,无法进入跨域 iframe 的 JS 执行环境。
JS Reverse MCP 通过 CDP 的 Runtime.executionContextCreated 事件追踪所有 frame 的执行上下文,支持自由切换:
如果没有这个能力,AI 根本无法接触到验证码的核心对象 TDC,更无法验证加密逻辑的正确性。在整个项目的动态验证阶段,几乎每一次浏览器操作都需要先 select_frame(1) 切换到 iframe。
现代网站普遍使用 navigator.webdriver、window.chrome 等属性检测自动化浏览器。Puppeteer 和 Playwright 默认会被检测到,导致页面行为异常(验证码无限弹出、数据采集结果异常等)。
JS Reverse MCP 支持连接到用户已运行的 Chrome 实例,而不是自己启动一个受控的浏览器:
这意味着:
这对于分析验证码这类场景至关重要——腾讯防水墙的 tdc.js 有 27 项自动化检测(包括 navigator.webdriver、document.$cdc_asdjflasutopfhvcZLmcfl_ ChromeDriver 标记等),如果被检测到,采集到的行为数据会包含异常标记,服务端直接拒绝。
Playwright/Puppeteer MCP 只能执行页面级脚本(相当于在 console 中输入代码)。但 JS 逆向中最有价值的操作是在断点处检查局部变量和闭包变量——这些变量在全局作用域中不可见。
JS Reverse MCP 的 evaluate_on_callframe 可以在暂停的调用帧上下文中求值任意表达式:
在本项目中,XTEA 的 key buffer 存在于 VM 的闭包 scope 中,从全局作用域完全不可见。没有断点处求值的能力,就无法验证从字节码中静态提取的 key 是否正确。
这个工具贯穿了整个逆向分析过程:
请求分析阶段:用 list_network_requests 过滤 XHR/Fetch 请求,找到 cap_union_new_verify 接口。用 get_network_request 查看请求参数,识别出 collect、eks 等加密字段。
入口定位阶段:用 break_on_xhr("cap_union_new_verify") 设置 XHR 断点 → 触发验证 → get_paused_info() 获取完整的 JavaScript 调用栈,直接从请求倒推到加密函数的入口。
Key 验证阶段:在 XTEA 函数入口设置条件断点 → 用 evaluate_on_callframe 在断点处提取运行时的 key 和 scope 变量 → 与 Python 静态提取的结果对比验证。
iframe 切换:验证码运行在跨域 iframe 中。用 list_frames 列出所有 frame → select_frame(1) 切换到 tcaptcha_iframe_dy → 然后就可以在 iframe 上下文中执行脚本,直接调用 TDC.getData(true)。
Hook 拦截:用 hook_function("encodeURIComponent") 拦截 URL 编码函数,抓取 collect 加密后、URL 编码前的原始数据。用 hook_function("String.fromCharCode") 捕获 XTEA 加密输出的字节拆分过程(298 次调用 = 298 个 int32 = 1192 字节密文)。
源码搜索:用 search_in_sources("2654435769") 在所有加载的脚本中搜索 XTEA delta 常量,虽然 tdc.js 通过 blob URL 加载(CDP 无法直接列出),但搜索功能仍能覆盖。
GitHub 地址:8adK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6*7K9r3W2*7K9s2g2G2k6r3g2E0j5h3!0Q4x3V1k6B7M7#2)9J5k6s2u0W2N6X3g2J5M7$3g2Q4x3X3c8E0j5%4l9`.
配置到 AI 编码助手:
Cursor 和 VS Code Copilot 也支持,在 MCP 设置中添加相同的配置即可。
一个完整的 AI 驱动逆向分析流程:
整个过程中,你只需要操作浏览器触发请求,AI 负责所有的调试和分析工作。
回顾整个项目,AI 在以下方面提供了实质性帮助:
字节码精读与伪代码还原。面对 24000+ 条反汇编指令,AI 能快速将指令序列还原为可读的伪代码。一个 300 条指令的函数,人工分析可能需要半天,AI 几分钟就能给出伪代码初稿。
模式识别。从 14 个 key 构建子函数中识别真实 vs 诱饵的判别规则,从 37 个模块的字节码中提取指纹特征——这类"在大量相似代码中找差异"的任务,AI 比人高效得多。
方案迭代速度。模块分析器从 v1 到 v2 到 v3 再到最终的指纹方案,每次迭代都涉及重构核心逻辑。AI 让每次迭代的成本从"几天"降到了"几小时"。
跨文件关联。追踪 scope chain(从 XTEA 函数的 M[7] → parent 的 M[X] → encrypt 的 M[Y])、函数调用链(mGetData → get_collected → encrypt → XTEA)、模块注册映射(PUSH_IMM mid → FUNC_DEF)——这类需要在多个文件间跳转关联的任务,AI 的工作记忆是关键优势。
JS 位运算语义。+ 不截断但 << 截断的差异,AI 在第一次实现时没有正确处理。这类"语言规范的细微之处"需要人工提醒和验证。
VM 动态行为推断。LOAD_DYN(动态加载变量)、共享引用赋值、运行时 scope 修改——这些只有在 VM 实际执行时才能确定的行为,AI 的静态分析无法推断。模块分析器 v1-v3 的失败就是因为试图用静态分析解决本质上是动态的问题。
创造性突破。从"逐个分析每个模块返回什么值"到"用指纹识别模块类型"的思路转换,是一个关键的创造性跳跃。AI 在已有思路上迭代很快,但提出全新思路的能力仍有限。这个转换是人工观察到"模块代码语义不变,只是 ID 变了"后提出的。
回顾整个项目,核心方法论可以概括为一句话:在每次都变的代码中,找到跨版本不变的结构特征,然后基于这些不变量构建自动化提取管线。
这条路比补环境难得多,但收益也大得多:每个参数都在掌控中,可以精确控制指纹值、随机化设备画像、构造逼真的行为轨迹。
腾讯 CHAOS VM 的动态化设计确实增加了逆向成本——你不能做一次分析就硬编码结果,必须实现自动化的解析管线。但它的软肋在于:动态化是"重新排列",不是"重新生成"。模块代码不变,只是换了 ID 和位置,这给了指纹识别的机会。
如果未来对模块代码本身也进行变换(如指令替换、插入花指令、变量名混淆),指纹识别的难度会大幅增加。但这也会增加服务端的生成成本和 JS 体积。攻防永远是成本的博弈。
AI 工具的介入正在改变这个博弈的平衡点。过去需要一个经验丰富的逆向工程师花几周完成的工作,现在一个了解基本原理的开发者配合 AI 和专用工具(如 JS Reverse MCP)可以在几天内完成。
| 变化项 |
说明 |
影响 |
| XTEA 加密密钥 |
4 个 int32 的 key,每次完全不同 |
无法硬编码 key |
| key 的运行时偏移 |
加密时给特定 key 索引加的偏移值,每次不同 |
必须动态提取 |
| 操作码索引 |
VM 的 58 个操作码在 handler 数组中的位置打乱 |
无法硬编码 opcode 映射 |
| 37 个模块的 ID |
每个采集器模块的注册 ID 随机重新分配 |
无法按 ID 判断模块类型 |
| 模块的调用顺序 |
cd 数组中模块的排列顺序随机打乱 |
无法硬编码字段位置 |
| key 构建函数的 scope |
真实/诱饵 key buffer 的索引每次不同 |
无法硬编码判别规则 |
prehandle 获取会话 → 动态下载并解析 tdc.js → OCR 识别验证码文字
→ 生成鼠标轨迹 → 构造 collect → 求解 PoW → 提交 verify → 获取 ticket
prehandle 获取会话 → 动态下载并解析 tdc.js → OCR 识别验证码文字
→ 生成鼠标轨迹 → 构造 collect → 求解 PoW → 提交 verify → 获取 ticket
GET https://turing.captcha.qcloud.com/cap_union_prehandle
参数: aid=xxx, ua=Base64(UserAgent), ...
GET https://turing.captcha.qcloud.com/cap_union_prehandle
参数: aid=xxx, ua=Base64(UserAgent), ...
GET https://turing.captcha.qcloud.com/tdc.js?app_data={sid}&t={random}
GET https://turing.captcha.qcloud.com/tdc.js?app_data={sid}&t={random}
window.TDC = {
setData: core.mSet,
getData: core.mGetData,
getInfo: getInfoFunc,
clearTc: core.mClear
}
window.TDC = {
setData: core.mSet,
getData: core.mGetData,
getInfo: getInfoFunc,
clearTc: core.mClear
}
TDC.setData({ft: "qf_7Pf__H"});
collect = decodeURIComponent(TDC.getData(true));
eks = TDC.getInfo().info;
pow_answer = solvePow(pow_cfg);
POST /cap_union_new_verify {
collect, eks, tlg: collect.length,
ans: JSON.stringify(clicks),
sess, pow_answer, pow_calc_time, ...
}
TDC.setData({ft: "qf_7Pf__H"});
collect = decodeURIComponent(TDC.getData(true));
eks = TDC.getInfo().info;
pow_answer = solvePow(pow_cfg);
POST /cap_union_new_verify {
collect, eks, tlg: collect.length,
ans: JSON.stringify(clicks),
sess, pow_answer, pow_calc_time, ...
}
| 参数 |
难度 |
说明 |
collect |
核心难点 |
TDC.getData(true) 生成的加密行为数据 |
eks |
简单提取 |
tdc.js 源码中的硬编码常量,正则提取即可 |
ans |
OCR 问题 |
用户点击坐标,明文 JSON |
pow_answer |
简单 |
MD5 碰撞,几行代码 |
sess |
透传 |
从上一次响应中获取 |
tlg |
无需逆向 |
collect 字符串长度 |
pow_calc_time |
无需逆向 |
PoW 计算耗时 |
aid |
固定值 |
接入方分配的 ID |
for (var E = !1; !E;) E = U[k[N++]]();
for (var E = !1; !E;) E = U[k[N++]]();
__TENCENT_CHAOS_VM(0, function(Q) {
var B = Q[0],
F = Q[1],
U = P(B),
...
})
__TENCENT_CHAOS_VM(0, function(Q) {
var B = Q[0],
F = Q[1],
U = P(B),
...
})
patterns = {
'ADD': lambda code: 'b+a' in code or 'a+b' in code,
'SUB': lambda code: 'b-a' in code,
'MUL': lambda code: 'b*a' in code,
'SHL': lambda code: '<<' in code and '>>>' not in code,
'USHR': lambda code: '>>>' in code,
'PUSH_IMM': lambda code: 'k[N++]' in code and len(code) < 50,
...
}
patterns = {
'ADD': lambda code: 'b+a' in code or 'a+b' in code,
'SUB': lambda code: 'b-a' in code,
'MUL': lambda code: 'b*a' in code,
'SHL': lambda code: '<<' in code and '>>>' not in code,
'USHR': lambda code: '>>>' in code,
'PUSH_IMM': lambda code: 'k[N++]' in code and len(code) < 50,
...
}
func_9181 (XTEA 核心) ← 被 func_7313 (主加密函数) 调用
func_7313 ← 被 func_10005 (导出包装) 调用
func_10005 ← 被 func_6186 (mGetData) 调用
func_6186 = TDC.getData 的实际实现
func_9181 (XTEA 核心) ← 被 func_7313 (主加密函数) 调用
func_7313 ← 被 func_10005 (导出包装) 调用
func_10005 ← 被 func_6186 (mGetData) 调用
func_6186 = TDC.getData 的实际实现
key[index] = Σ (charCodeAt(string[i]) - offset) << (8 * i)
key[index] = Σ (charCodeAt(string[i]) - offset) << (8 * i)
collect = base64(xtea(chunk1_padded))
+ base64(xtea(trajectory_padded))
+ base64(xtea(chunk2_padded))
+ base64(xtea(sd_str))
collect = base64(xtea(chunk1_padded))
+ base64(xtea(trajectory_padded))
+ base64(xtea(chunk2_padded))
+ base64(xtea(sd_str))
| 类型 |
采集内容 |
值示例 |
| screen_fingerprint |
屏幕综合信息 |
"1512-982-900-30-*-*-|-*" |
| user_agent |
navigator.userAgent |
"Mozilla/5.0..." |
| canvas_fingerprint |
Canvas 2D 指纹 |
Base64 字符串 |
| webgl_renderer |
WebGL 渲染器 |
"ANGLE..." |
| hardwareConcurrency |
CPU 核心数 |
14 |
| timezone |
时区偏移 |
"+08" |
| webdriver_detect |
自动化检测 |
0 |
| trajectory |
鼠标轨迹 |
轨迹数组(加密后的密文) |
| ... |
... |
... |
fingerprint_rules = {
'user_agent': {'userAgent'},
'canvas_fingerprint': {'getContext', '2d'},
'webgl_renderer': {'UNMASKED_RENDERER_WEBGL'},
'timezone': {'getTimezoneOffset'},
'webdriver_detect': {'$cdc_asdjflasutopfhvcZLmcfl_'},
'webrtc_ip': {'RTCPeerConnection'},
...
}
fingerprint_rules = {
'user_agent': {'userAgent'},
'canvas_fingerprint': {'getContext', '2d'},
'webgl_renderer': {'UNMASKED_RENDERER_WEBGL'},
'timezone': {'getTimezoneOffset'},
'webdriver_detect': {'$cdc_asdjflasutopfhvcZLmcfl_'},
'webrtc_ip': {'RTCPeerConnection'},
...
}
output = '{"cd":['
first = True
for module in cd_array:
if not module: continue
value, secondary, type_flag = module.get()
if not first: output += ","
first = False
if type_flag == 2:
encrypted = base64(xtea(pad_to_24(output)))
traj_output = encrypted + value
output = ""
elif value is None:
output += "null"
elif isinstance(value, (int, float)):
output += str(value or 0)
elif type_flag == 1:
output += value
else:
output +=
if isinstance(secondary, (int, float)):
output += "," + str(secondary or 0)
output += "],"
traj_output += base64(xtea(pad_to_24(output)))
output = '{"cd":['
first = True
for module in cd_array:
if not module: continue
value, secondary, type_flag = module.get()
if not first: output += ","
first = False
if type_flag == 2:
encrypted = base64(xtea(pad_to_24(output)))
traj_output = encrypted + value
传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 1天前
被执着的猫编辑
,原因: