feat(密钥): 纯 Python 静态提取 XTEA 密钥 —— 去掉最后一个 Node 依赖 (6/6 构建与神谕逐字一致)
做了什么 - 新增 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)。
This commit is contained in:
+29
-3
@@ -51,10 +51,36 @@ function __TENCENT_CHAOS_VM(H, z, c, E, I, D, U, C) {
|
||||
|
||||
## 3. 完整 opcode 表(70 槽 / 58 handler / 12 空洞)
|
||||
|
||||
`argc` = handler 体内 `z[H++]` 次数(= 固定操作数字节数);`57` 是动态长度(见 §4)。
|
||||
**空洞 opcode**:`7, 16, 21, 24, 27, 40, 43, 44, 50, 58, 63, 67` —— 它们**永远不会**出现在可执行字节码里;一旦在反汇编里看到空洞 opcode,说明**解码失步**(旧线性反汇编器就是这么坏的)。
|
||||
`argc` = handler 体内 `z[H++]` 次数(= 固定操作数字节数);动态长度的那个是 `CREATE_CLOSURE`(见 §4)。
|
||||
|
||||
| opcode | argc | handler 源码(VM 内 G 数组第 i 项) |
|
||||
> ⚠️ **本节表格是"core_js 那一份构建"的编号,不能当跨构建常量用(重要修正,见 §3.1)。**
|
||||
> 下面的 `opcode` 列只对 `tcaptcha/core_js/tdc_sample.js` 成立。
|
||||
> **空洞 opcode**:`7, 16, 21, 24, 27, 40, 43, 44, 50, 58, 63, 67` —— 它们**永远不会**出现在可执行字节码里;
|
||||
> 一旦在反汇编里看到空洞 opcode,说明**解码失步**(旧线性反汇编器就是这么坏的)。
|
||||
|
||||
### 3.1 opcode 编号**每份构建都会整体重排**(实测,5+1 份构建)
|
||||
|
||||
> **推翻早期结论**:早期把上面的 opcode 数值当成 VM 常量("`RET_TRUE`=19、`SET_STACK_LEN`=53…")。
|
||||
> 实测是错的:**同一份源码编译出的字节码,pc0 处的 `SET_STACK_LEN` 在 6 份构建里分别是
|
||||
> 53 / 19 / 32 / 57 / 55 / 1**,`JUMP` 分别是 60 / 17 / 42 / 58 / 21 / 2。
|
||||
> 也就是说 **opcode → 语义 的映射表每份脚本都重新洗牌**;写死数值 = 换一份脚本全错
|
||||
> (这也是为什么早先用 core 表去反汇编其它构建会得到"`RET_TRUE` 在 pc0 ⇒ 反汇编为空"的假象)。
|
||||
|
||||
**正确做法**:不在字节码上死磕,而是**从该构建自己的解释器源码里推导 opcode 表** ——
|
||||
见 `tcaptcha/tdc_opcodes.py`:
|
||||
|
||||
1. 定位解释器主循环 `X[Y[Z++]]()`,取出**操作码处理器数组变量** `X`;
|
||||
2. 解析该数组字面量(JS 感知的括号/字符串/正则切分)得到 70 个槽位(含空洞);
|
||||
3. 把每个 handler 源码**规范化**(去空白 + 局部变量按首次出现重命名 `v1,v2,…`,
|
||||
保留 JS 关键字/内建名与属性名)得到与命名无关的"指纹";
|
||||
4. 用内置的 58 条参考指纹(取自 core_js,即本表)反查每个槽位的语义名 ——
|
||||
**数组下标即该构建的 opcode**;`argc` 直接按语义名取(与 handler 体内 `x[y++]` 次数实测一致)。
|
||||
|
||||
> 交叉验证:6/6 份构建全部 **58/58 handler 精确匹配**、0 未识别。
|
||||
> 因此 `tools/tdc_disasm.py` / `tools/tdc_lift.py` 现在都支持**注入按构建推导的 opcode 表**
|
||||
> (`disassemble_cfg(z, names=…, arity=…, dynamic=…)`);不传才回落到本表(core_js)。
|
||||
|
||||
| opcode | argc | handler 源码(core_js 的 VM 内 G 数组第 i 项) |
|
||||
| --- | --- | --- |
|
||||
| 0 | 1 | `function () { E[E.length - 1] += String.fromCharCode(z[H++]); }` |
|
||||
| 1 | 0 | `function () { return !!w; }` |
|
||||
|
||||
Reference in New Issue
Block a user