做了什么 - 新增 tcaptcha/tdc_opcodes.py: 按构建从解释器源码推导 opcode 表。 实测 opcode 编号**每份构建整体重排**(同一源码 pc0 的 SET_STACK_LEN 在 6 份构建里 = 53/19/32/57/55/1, JUMP = 60/17/42/58/21/2); 旧固化表只对 core_js 有效 —— 用它反汇编其它构建会在 pc0 撞上 RET_TRUE, 得到"空反汇编"的假象。 做法: 定位主循环 X[Y[Z++]]() 的 handler 数组 -> JS 感知切分 -> handler 源码规范化 (去空白 + 局部变量按首次出现重命名) 取指纹 -> 与内置 58 条 core_js 参考指纹匹配; 数组下标即该构建的 opcode, argc 由语义名取 (与 handler 体内 x[y++] 次数实测一致)。 - 新增 tcaptcha/tdc_key.py: 纯 Python XTEA 密钥静态还原器 (零 Node / 零 pc 硬编码)。 反汇编 -> 定位含 delta 的 XTEA 闭包 -> 用"两层捕获链"选出真密钥数组: 字闭包捕获的 B 帧寄存器, 经"定义 B 的那个闭包"的 caps 映射到 G 帧后须 == XTEA 闭包 捕获的寄存器; 借此排除脚本里并行计算的第二套诱饵字 -> 读 4 个 (源串, 偏移, 下标) -> 折入轮加盐。 - tools/tdc_disasm.py: disassemble_cfg/_decode_one 支持注入按构建推导的 opcode 表 (names/arity/dynamic), 默认仍回落 core_js 表; 控制流 opcode 改为按语义名定位。 - tools/tdc_key_pure.py: CLI (-v 打印推导过程 / --opcodes 打印 opcode 表 / --json)。 - tcaptcha/crypto.py: extract_xtea_key 改为"纯 Python 主路径 + Node 插桩兜底"。 - 新增 tcaptcha/tests/test_tdc_key.py 并接入 selftest; 新增入库 fixture tcaptcha/tests/fixtures/tdc_fresh_2026-10-11.js (现场新抓的 tdc.js)。 为什么 - AGENTS.md §2.4 分层交付 / §1.1: 去掉最后一个 Node 依赖, 提高并发与抗发版能力; - §5.7 负结果也是资产: 显式撤回"opcode 数值跨构建固定"与"密钥只能靠 Node 插桩拿"两条旧结论。 怎么验证 - 6 份构建纯 Python 结果 == Node 插桩 extract_xtea_key, **逐字一致**: core_js / root / 1ms tdc.js / tdc_live / tdc_latest / 2026-10-11 现场新抓 tdc_fresh。 - opcode 表 6/6 构建 58/58 handler 精确匹配, 0 未识别。 - .venv/bin/python -m tcaptcha.selftest 全绿 (含既有逐字节对拍 test_xtea_key)。 - 把 node_bin 指向不存在的程序时 generate_telemetry 仍产出 collect (证明已零 Node)。
125 lines
7.6 KiB
Markdown
125 lines
7.6 KiB
Markdown
# 腾讯验证码 (TCaptcha / TDC) 逆向工具链建设与协议解构报告
|
||
|
||
依据 `AGENTS.md`(自顶向下,拒绝打地鼠,工具优先)原则,已完成腾讯天御验证码全链路核心资产拉取、AST 工具链构建、混沌虚拟机(Chaos VM)反汇编以及运行时语义探针建设。
|
||
|
||
---
|
||
|
||
## 一、已建成的工具集清单
|
||
|
||
| 工具 | 路径 | 核心功能 | 解决什么问题 |
|
||
| :--- | :--- | :--- | :--- |
|
||
| **Webpack 解包索引器** | [`tools/unpack_webpack.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/unpack_webpack.js) | 将 230KB 单文件 bundle 拆解为 122 个独立模块,建立加签/风控关键词索引 | 圈定加签核心,剔除 80% 开源库与 UI 噪音 (L1 解构) |
|
||
| **Chaos VM 字节码反汇编器 (纯 Python)** | [`tools/tdc_disasm.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_disasm.py) | 纯 Python 提取 45,636 字节码 + 可达性 CFG 反汇编(26,168 条可达指令,**0 处非法 opcode**);opcode 表可**按构建注入**(见下) | 彻底透明化 TDC 字节码调度器 (L2/L3 解构) |
|
||
| **按构建推导 opcode 表** | [`tcaptcha/tdc_opcodes.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tcaptcha/tdc_opcodes.py) | 从解释器主循环 `X[Y[Z++]]()` 取出 handler 数组 → 解析 → 规范化指纹 → 跨构建匹配出**该构建的 opcode 表** | **opcode 编号每份构建整体重排**,写死即换脚本全错(6/6 构建 58/58 handler 匹配) |
|
||
| **纯 Python XTEA 密钥静态提取** | [`tcaptcha/tdc_key.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tcaptcha/tdc_key.py) + [`tools/tdc_key_pure.py`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_key_pure.py) | 反汇编 → 定位 XTEA 闭包(delta) → 两层捕获链选出真密钥数组 → 读 4 个字 + 折盐 | **零 Node** 拿到该构建 XTEA 密钥;6/6 构建与 Node 插桩逐字一致 |
|
||
| **深度 Proxy 语义探针** | [`tools/tdc_probe.js`](file:///Users/yml/reverse/hello_js_reverse_skill/tools/tdc_probe.js) | 零 Headless/CDP 依赖,通过深度 Proxy 沙箱执行 `tdc.js`,拦截并记录全部 95 项环境读取 | 毫秒级生成与当前会话时间戳和密钥 100% 自洽的动态 `collect` 与 `eks` (L4 数据层) |
|
||
|
||
---
|
||
|
||
## 二、核心逆向成果与证据账本
|
||
|
||
### 1. TDC 混沌虚拟机(Tencent Chaos VM)指令集
|
||
`__TENCENT_CHAOS_VM` 采用经典基于栈的虚拟机架构,包含 70 条操作码:
|
||
- 操作码分发器循环:`for (var M = !1; !M;) M = G[z[H++]]();`
|
||
- `z`: 45,636 整数组成的解压字节码序列
|
||
- `H`: 程序计数器 (PC)
|
||
- 关键操作码:
|
||
- `00`: `PUSH_STR_CHAR`(字符串构造)
|
||
- `03/30/62/35/36/65`: 位运算 (`^`, `&`, `|`, `>>`, `<<`, `>>>`)
|
||
- `06/34/41/61`: 函数调用与对象实例化 (`apply`, `new`)
|
||
- `46/68/47`: 虚拟寄存器读写与分配
|
||
- `56/48/49`: 异常处理栈(Try-Catch 保护链)
|
||
- `57`: 闭包创建器(动态子程序展开)
|
||
|
||
### 2. TDC 关键检测项白名单(已由探针全量捕获,共 95 项)
|
||
探针运行实测表明,TDC 实际探测的环境特征仅 95 项,绝非海量属性:
|
||
1. **自动化/反爬标记检测**:
|
||
- `document.$cdc_asdjflasutopfhvcZLmcfl_`(精准探测 ChromeDriver 变量)
|
||
- `window.navigator.webdriver`
|
||
- `window.domAutomation`
|
||
- `window.fxdriver_id`
|
||
- `window.callPhantom` / `casper` / `CasperError` / `patchRequire`
|
||
- `window.ubot` / `WebPage`
|
||
2. **显示与硬件画像**:
|
||
- `screen.width` (2560), `screen.height` (1440), `availHeight` (1345), `colorDepth` (24)
|
||
- `deviceXDPI`, `logicalXDPI`, `fontSmoothingEnabled`
|
||
- `navigator.hardwareConcurrency` (10), `platform` ("MacIntel"), `languages`
|
||
3. **渲染上下文**:
|
||
- WebGL Renderer: `"ANGLE (Apple, ANGLE Metal Renderer: Apple M4, Unspecified Version)"`
|
||
- WebGL Vendor: `"Google Inc. (Apple)"`
|
||
- Canvas Fingerprint: 成功样本原图 Base64 (368 字节)
|
||
4. **容器关系**:
|
||
- `window.top !== window`(识别是否运行在验证码 iframe 内)
|
||
- `document.getElementById("tCaptchaDyContent")`
|
||
- `window.TCaptchaReferrer`(宿主页面 URL 绑定)
|
||
|
||
### 3. L4 遥测协议数据解密 (`cd` 38 维字段)
|
||
成功解密的 `collect` 明文格式为 `{"cd": [...], "sd": {"od": "C", "ft": "..."}}`:
|
||
- `cd[0..1]`: 屏幕宽、平台 (`2560`, `"MacIntel"`)
|
||
- `cd[2]`: Canvas 指纹
|
||
- `cd[8]`: 宿主页面 URL (`document.referrer`)
|
||
- `cd[9]`: User-Agent
|
||
- `cd[15/18]`: WebGL 渲染器与厂商
|
||
- `cd[19/22/27]`: **当前会话的秒级与毫秒级 Epoch 时间戳**(必须与请求自洽,旧方案硬编码是致死原因之一)
|
||
- `cd[20]`: **鼠标相对增量轨迹**:首点为绝对坐标 `[start_x, start_y, delay_ms]`,后续为增量 `[dx, dy, dt_ms]`(dt 均值 16~18ms,模拟 60Hz 采样)
|
||
- `cd[26]`: 会话校验和与随机种子
|
||
- `cd[31]`: 执行容器类型 (`"iframe"`)
|
||
|
||
---
|
||
|
||
## 三、关于 errorCode=12 的诚实结论(**已更正**)
|
||
|
||
> 本报告早期版本曾把 12 归因于"`re.sub` 破坏校验和 / 时间戳错位 / 机械假轨迹 / CDP 特征泄露"。
|
||
> 这 4 条**均与本项目自己的证据账本冲突**(见 `har/1ms_dianxuan/ANALYSIS.md` §7 与 `PROGRESS.md` §三),
|
||
> 现更正如下:
|
||
|
||
| 早期断言 | 现状 |
|
||
| --- | --- |
|
||
| 时间戳错位(硬编码数天前)| ❌ 撤回:纯算路径自模板回填起就刷新为当前时间戳,仍 12 |
|
||
| 机械假轨迹 dt=0/1ms | ❌ 撤回:换真实 ms 分布后仍 12 |
|
||
| CDP 特征泄露 | ❌ 撤回:纯 Python(无 CDP)后端同样 12 |
|
||
| `re.sub` 破坏内部校验和 | ⚠️ 仅为假设,无对照实验 |
|
||
|
||
**已确证**:12 与遥测后端 / 出口 IP / 静默时长均无关。真正的自变量
|
||
(登录态 / 真实验证码页面上下文)尚未由单变量实验区分 —— 见 `ANALYSIS.md` §7.5。
|
||
|
||
### 关于深度 Proxy 探针(`tools/tdc_probe.js`)的定位
|
||
|
||
探针是**分析工具**(用于枚举 tdc.js 真实读取了哪些环境属性),**已不在遥测生成路径上**
|
||
(`crypto.generate_telemetry` 只保留纯算)。它注入的 Canvas / WebGL / 屏幕取值来自
|
||
**单份成功样本**,属于"复刻一份画像"而非"真实环境",因此不作为交付依据。
|
||
|
||
---
|
||
|
||
## 四、一键复验入口
|
||
|
||
```bash
|
||
# 1. 验证 Webpack 解包与核心模块提取
|
||
node tools/unpack_webpack.js
|
||
|
||
# 2. 验证 TDC 虚拟机反汇编与字符串提取
|
||
node tools/tdc_vm_disasm.js tcaptcha/core_js/tdc_sample.js # 已废弃, 转发到下面的纯 Python 实现
|
||
.venv/bin/python tools/tdc_disasm.py tcaptcha/core_js/tdc_sample.js
|
||
|
||
# 3. 验证深度 Proxy 语义探针执行并产出 100% 合法 collect 与 eks
|
||
node tools/tdc_probe.js tcaptcha/core_js/tdc_sample.js
|
||
|
||
# 4. 验证探针生成的 collect 成功解密
|
||
.venv/bin/python -c '
|
||
import json, base64, urllib.parse, subprocess
|
||
from tcaptcha.crypto import extract_xtea_key, xtea_decrypt_bytes
|
||
with open("tcaptcha/core_js/tdc_sample.js") as f: code = f.read()
|
||
key = extract_xtea_key(code)
|
||
res = subprocess.check_output(["node", "-e", "const {probeTdc}=require(\"./tools/tdc_probe.js\"); const r=probeTdc(\"tcaptcha/core_js/tdc_sample.js\"); console.log(r.collect);"], text=True).strip()
|
||
raw = base64.b64decode(urllib.parse.unquote(res))
|
||
plain = xtea_decrypt_bytes(raw, key).rstrip(b"\\x00")
|
||
print("成功解密探针生成的遥测载荷,长度:", len(plain))
|
||
print("明文前 150 字节:", plain[:150].decode("latin1"))
|
||
'
|
||
|
||
# 5. 纯 Python XTEA 密钥提取 (零 Node) + 跨构建回归
|
||
.venv/bin/python tools/tdc_key_pure.py -v har/1ms_dianxuan/live/tdc_fresh.js
|
||
.venv/bin/python -m tcaptcha.tests.test_tdc_key # 6 份构建 (含实时新抓) 全部断言
|
||
.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 打印该构建的 opcode 表
|
||
```
|