Files
hello_js_reverse_skill/tcaptcha/VM_SPEC.md
T
yml2213 2e7aaff9ec feat(vm): 自上而下重构工具链 —— 新增提升器(lift)与查询CLI(query)
做了什么:
- 新增 tools/tdc_lift.py: 字节码 -> 可读伪代码 (AGENTS.md §3.3 步骤5)
  * 表达式栈还原 (算术/比较/调用/属性)
  * 寄存器引用惯用法还原 (PUSH_IMM_ARRAY n + MAKE_REG_PROP_REF -> rN[key])
  * 控制流 label/goto/if; CREATE_CLOSURE 递归展开成具名函数
- 新增 tools/tdc_query.py: who rN / at @pc / funcs / grep RE (对齐 §4.2 验收)
- 新增 tools/tdc_keytrace.js: 用真 VM 当神谕, 定位某值在哪段字节码诞生
- VM_SPEC §7/§8 重写为"自上而下五件套 + 路线", 并把 tdc_vm_run.py 标注为归档(打地鼠陷阱)
- 修正 lift 的 [n]->rN 归一 与 标签刷屏

为什么:
- 之前用 Python 复刻 JS 运行时去"跑"VM = §2.3 铁律2 的打地鼠陷阱, 收敛极慢
- §3.3 步骤5 要的是"读指令流得到算法; 闭包递归展开" -> 提升(lift)才是正路
- §2.1 最后一步: 读出算法后用目标语言原生重写, 跳过 VM/环境层

怎么验证:
- lift 首次把 XTEA 轮读成可读式: r2=(r2+2654435769); r?+=(((r6<<4)^(r6>>>5))+r6)^(r2+(r7[((r2>>>11)&3)]+855311))
- query 三个验收问题均可秒答: who r2 / at @9285 / funcs
2026-10-11 22:15:43 +08:00

241 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 腾讯天御 Chaos VM 规范(纯粹从源码/反汇编fact整理)
> 目的:把 TDC 的 JSVMP(`__TENCENT_CHAOS_VM`)从"读不懂"变成"有确定规格",供纯 Python 复现与静态提取使用。
> 事实来源:`tcaptcha/core_js/tdc_sample.js` 的 VM 源码(AST 导出)+ `tools/tdc_disasm.py` 的正确反汇编。
> 工具:`tools/tdc_disasm.py`(提取+CFG 反汇编)、`tools/tdc_vm_run.py`(纯 Python 解释器,WIP)。
---
## 1. 架构总览
VM 就是一个**基于栈的字节码解释器**,入口签名(源码原样):
```js
function __TENCENT_CHAOS_VM(H, z, c, E, I, D, U, C) {
var g, A = !E,
B = (H = +H, z = z || [0], E = E || [[this], [{}]], I = I || {}, []),
w = null;
function Q() { return function (A, B, w) { return new (Function.bind.apply(A, B))(); }.apply(null, arguments); }
/* ... 定义 58 个 handler,装配成 G 数组 ... */
for (var G = [ /* handlers */ ], M = !1; !M; ) M = G[z[H++]]();
}
```
| 形参 | 含义 |
| --- | --- |
| `H` | 程序计数器 PC(字节码下标) |
| `z` | 字节码整数数组(45636 个) |
| `c` | **宿主全局对象**(页面里就是 `window`)—— `.apply(c, ...)` 的默认 this |
| `E` | **寄存器区 + 求值栈**(同一个数组!)默认 `[[this],[{}]]` |
| `I D U C` | 其它透传参数(本样本用不到) |
| `B` | try 栈(异常帧) |
| `w` | 异常标志(非空即"有异常") |
主循环:`M = G[z[H++]]()` —— 取 `z[H++]` 当 opcode 下标调用 handler;**handler 返回真值时循环结束**(对应 `RET_TRUE`=19、`IS_EXCEPTION`=1 在异常时)。
---
## 2. 数据模型(关键,之前所有混乱都出在这里)
1. **`E` 同时是"寄存器区"和"求值栈"**:`E[0..n-1]` 是寄存器,`E[n..]` 是栈。
2. **寄存器是"单元数组"**:寄存器 `i` 的**值**是 `E[i][0]`,而 `E[i]` 本身是个数组(`ALLOC_REG i` → `E[i] = []`)。
3. **"寄存器引用"惯用法**:
- `PUSH_IMM_ARRAY i`(op 31)压入字面数组 `[i]`(这里 `i` 是**寄存器号**,不是数组元素);
- `MAKE_REG_PROP_REF`(op 32)`A=E.pop(); E.push([E[E.pop()][0], A])` → 得到 `[E[i][0], key]`,即"**对寄存器值的属性引用**"。
4. **引用 = 两元素数组 `[base, key]`**;`GET_PROP`(59)/`SET_PROP`(14) 负责解引用。
5. 栈操作:`DUP`(2)、`SWAP n`(17,`n` 是相对栈顶的偏移)、`POP`(51)、`REVERSE_TOP2`(54)、`SET_STACK_LEN n`(53,**直接截断/扩张 E 的长度**)、`REPLACE_TOP_IMM v`(64)。
> ⚠️ 因为 `E` 一物两用、寄存器有"单元数组"这层壳、且引用是普通二元数组,**任何"按寄存器号做正则匹配"的做法都会失效** —— 必须做真正的数据流/执行。
---
## 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,说明**解码失步**(旧线性反汇编器就是这么坏的)。
| opcode | argc | handler 源码(VM 内 G 数组第 i 项) |
| --- | --- | --- |
| 0 | 1 | `function () { E[E.length - 1] += String.fromCharCode(z[H++]); }` |
| 1 | 0 | `function () { return !!w; }` |
| 2 | 0 | `function () { E.push(E[E.length - 1]); }` |
| 3 | 0 | `function () { E[E.length - 2] = E[E.length - 2] ^ E.pop(); }` |
| 4 | 0 | `function () { E[E.length - 2] = E[E.length - 2] === E.pop(); }` |
| 5 | 0 | `function () { E[E.length - 2] = E[E.length - 2] % E.pop(); }` |
| 6 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : []; E.length -= A, E.push(E.pop().apply(c, B)); }` |
| 7 | - | _(空洞: 无 handler)_ |
| 8 | 0 | `function () { E[E.length - 1] = c[E[E.length - 1]]; }` |
| 9 | 0 | `function () { var A = E.pop(), B = E.pop(); E.push([B[0][B[1]], A]); }` |
| 10 | 0 | `function () { E.push(typeof E.pop()); }` |
| 11 | 0 | `function () { var A = E.pop(); E.push(delete A[0][A[1]]); }` |
| 12 | 0 | `function () { E.push(!E.pop()); }` |
| 13 | 0 | `function () { E[E.length - 2] = E[E.length - 2] == E.pop(); }` |
| 14 | 0 | `function () { var A = E[E.length - 2]; A[0][A[1]] = E[E.length - 1]; }` |
| 15 | 0 | `function () { E[E.length - 2] = E[E.length - 2] > E.pop(); }` |
| 16 | - | _(空洞: 无 handler)_ |
| 17 | 1 | `function () { var A = z[H++], B = E[E.length - 2 - A]; E[E.length - 2 - A] = E.pop(), E.push(B); }` |
| 18 | 0 | `function () { E[E.length - 2] = E[E.length - 2] in E.pop(); }` |
| 19 | 0 | `function () { return !0; }` |
| 20 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >= E.pop(); }` |
| 21 | - | _(空洞: 无 handler)_ |
| 22 | 1 | `function () { E.push(z[H++]); }` |
| 23 | 0 | `function () { E[E.length - 2] = E[E.length - 2] * E.pop(); }` |
| 24 | - | _(空洞: 无 handler)_ |
| 25 | 0 | `function () { E.push(E[E.pop()[0]][0]); }` |
| 26 | 0 | `function () { E[E.length - 1].length ? E.push(E[E.length - 1].shift(), !0) : E.push(undefined, !1); }` |
| 27 | - | _(空洞: 无 handler)_ |
| 28 | 0 | `function () { E.push([c, E.pop()]); }` |
| 29 | 0 | `function () { E.push(null); }` |
| 30 | 0 | `function () { E[E.length - 2] = E[E.length - 2] & E.pop(); }` |
| 31 | 1 | `function () { E.push([z[H++]]); }` |
| 32 | 0 | `function () { var A = E.pop(); E.push([E[E.pop()][0], A]); }` |
| 33 | 0 | `function () { E[E.length - 2] = E[E.length - 2] - E.pop(); }` |
| 34 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : [], A = (E.length -= A, E.pop()); E.push(A[0][A[1]].apply(A[0], B)); }` |
| 35 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >> E.pop(); }` |
| 36 | 0 | `function () { E[E.length - 2] = E[E.length - 2] << E.pop(); }` |
| 37 | 1 | `function () { var A = z[H++]; E[E.length - 1] && (H = A); }` |
| 38 | 0 | `function () { var A, B = []; for (A in E.pop()) B.push(A); E.push(B); }` |
| 39 | 0 | `function () { E[E.length - 2] = E[E.length - 2] + E.pop(); }` |
| 40 | - | _(空洞: 无 handler)_ |
| 41 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : [], A = (E.length -= A, B.unshift(null), E.pop()); E.push(Q(A[0][A[1]], B)); }` |
| 42 | 0 | `function () { E.push(undefined); }` |
| 43 | - | _(空洞: 无 handler)_ |
| 44 | - | _(空洞: 无 handler)_ |
| 45 | 0 | `function () { E.push(!1); }` |
| 46 | 0 | `function () { E[E[E.length - 2][0]][0] = E[E.length - 1]; }` |
| 47 | 1 | `function () { var A = z[H++]; E[A] = E[A] === undefined ? [] : E[A]; }` |
| 48 | 0 | `function () { B.pop(); }` |
| 49 | 0 | `function () { throw E[E.length - 1]; }` |
| 50 | - | _(空洞: 无 handler)_ |
| 51 | 0 | `function () { E.pop(); }` |
| 52 | 0 | `function () { E[E.length - 2] = E[E.length - 2] / E.pop(); }` |
| 53 | 1 | `function () { E.length = z[H++]; }` |
| 54 | 0 | `function () { E.push([E.pop(), E.pop()].reverse()); }` |
| 55 | 0 | `function () { E.push(""); }` |
| 56 | 2 | `function () { B.push([z[H++], E.length, z[H++]]); }` |
| 57 | 6 | `function () { for (var w = z[H++], Q = [], A = z[H++], B = z[H++], G = [], M = 0; M < A; M++) Q[z[H++]] = E[z[H++]]; for (M = 0; M < B; M++) G[M] = z[H++]; E.push(function g() { var A = Q.slice(0); A[0] = [this], A[1] = [arguments …` |
| 58 | - | _(空洞: 无 handler)_ |
| 59 | 0 | `function () { var A = E.pop(); E.push(A[0][A[1]]); }` |
| 60 | 1 | `function () { H = z[H++]; }` |
| 61 | 1 | `function () { var A = z[H++], B = A ? E.slice(-A) : []; E.length -= A, B.unshift(null), E.push(Q(E.pop(), B)); }` |
| 62 | 0 | `function () { E[E.length - 2] = E[E.length - 2] \| E.pop(); }` |
| 63 | - | _(空洞: 无 handler)_ |
| 64 | 1 | `function () { E[E.length - 1] = z[H++]; }` |
| 65 | 0 | `function () { E[E.length - 2] = E[E.length - 2] >>> E.pop(); }` |
| 66 | 0 | `function () { E.push(!0); }` |
| 67 | - | _(空洞: 无 handler)_ |
| 68 | 1 | `function () { E.push(E[z[H++]][0]); }` |
| 69 | 0 | `function () { w = null; }` |
---
## 4. 闭包与调用约定
### 4.1 `CREATE_CLOSURE`(57) —— 操作数布局(动态)
```
@pc: 57 target capN parN [reg,reg]*capN [reg]*parN
```
handler 语义(源码):
```js
for (var w = z[H++], Q = [], A = z[H++], B = z[H++], G = [], M = 0; M < A; M++) Q[z[H++]] = E[z[H++]];
for (M = 0; M < B; M++) G[M] = z[H++];
E.push(function g() {
var A = Q.slice(0);
A[0] = [this], A[1] = [arguments], A[2] = [g];
for (var B = 0; B < G.length && B < arguments.length; B++) 0 < G[B] && (A[G[B]] = [arguments[B]]);
return __TENCENT_CHAOS_VM(w, z, c, A, I, D, U, C);
});
```
- `Q[capRegIdx] = E[parentRegIdx]` —— **捕获是按"寄存器号 → 寄存器号"成对出现的**(`caps=[r7=r8]` 表示新闭包的 r7 ← 父闭包 r8,**共享同一单元对象**);
- `G` 是形参寄存器号表;
- **调用闭包 = 以 `target` 为新 PC、带着"捕获拷贝 + this/arguments/g"重新进入 VM**。
> 🔑 由此得出一条反汇编铁律:**`CREATE_CLOSURE` 的 `target` 是"被调用时才执行"的代码,必须作为反汇编遍历根**,否则大半个程序都算不到(这是旧工具的第二处致命缺陷)。
### 4.2 调用类 opcode
| op | 名字 | this 绑定 | 说明 |
| --- | --- | --- | --- |
| 6 | CALL_FUNC n | `c`(全局) | `E.push(E.pop().apply(c, B))` |
| 34 | CALL_METHOD n | 引用基对象 | `A[0][A[1]].apply(A[0], B)` |
| 61 | NEW_FUNC n | — | 用 `Q(...)` 做 `new` |
| 41 | NEW_METHOD n | — | 同上,参数前插 `null` |
| 28 | BIND_THIS | — | `E.push([c, E.pop()])` 造"以 c 为基"的引用 |
### 4.3 其它结构
- **异常**:`PUSH_TRY`(56) 压 `[catchPC, E.length, catchVarReg]` 到 `B`;`POP_TRY`(48) 弹;`IS_EXCEPTION`(1) 检查 `w`;`CLEAR_EXCEPTION`(69) `w=null`;`THROW`(49)。
- **属性**:`GET_GLOBAL_PROP`(8) `c[E[-1]]`;`MAKE_PROP_REF`(9) 通用引用;`FOR_IN_KEYS`(38)。
- **字符串**:`PUSH_EMPTY_STR`(55) 压 `""`,随后一串 `PUSH_STR_CHAR`(0) 逐字符追加 —— 所以**字面串在字节码里是一串 <256 的立即数**(折叠即可还原,`tdc_disasm.py` 已做)。
---
## 5. 引导流程(bootstrap,实测)
```
@00000: SET_STACK_LEN 3
@00002: ALLOC_REG 2
@00004: JUMP @1518
...
@01518: CREATE_CLOSURE target=@7 caps=[] params=[p0->r3] <- 模块工厂(之一)
@01523: PUSH_EMPTY_STR "Array" ... GET_GLOBAL_PROP / NEW_FUNC ...
...
(很快 RET_TRUE 返回)
```
**我们的 Python 解释器跑 739 步后 `RET_TRUE` 返回,且寄存器里没有可用的模块闭包** —— 说明:
> **tdc.js 的顶层只"注册/装配"模块;真正做 XTEA、生成密钥的代码,是在宿主调用进模块时才执行的**(浏览器里最终由 `TDC.getData()` 触发;`tools/tdc_probe.js` 也是这么驱动的)。
---
## 6. 宿主接口与"如何驱动"(纯 Python 路线的真正缺口)
- JS 侧调用是 `__TENCENT_CHAOS_VM(0, <zCall 结果>, window)` → **`c = window`**。
- VM 本身不碰环境;**环境访问全部通过 `c`**(`GET_GLOBAL_PROP` / `CALL_METHOD` 的基对象)。
- 因此"能跑出 collect"靠的是:**页面加载 → 模块系统装配 → 宿主调用模块入口 → 模块内部用 `c` 读环境**。
- 所以纯 Python 静态路线要补的不是 VM,而是**模块系统 + 宿主调用序列**。
---
## 7. 工具链映射(对齐 AGENTS.md §3.2 五件套)
> 目标(§4.2 验收):能秒答「**这个寄存器/字段谁写的**」「**这段代码在哪**」「**有多少子程序**」。
| # | 环节 | 工具 | 输入 → 输出 | 状态 |
| --- | --- | --- | --- | --- |
| ① | 清点 inventory | `tcaptcha/VM_SPEC.md` §3 + `tools/tdc_disasm.py` | tdc.js → opcode 表(70槽/58 handler/arity) + 字节码 | ✅ |
| ② | 提取 disasm | `tools/tdc_disasm.py` | tdc.js → 可达性 CFG 反汇编(0 非法 opcode) | ✅ |
| ③ | 运行时追踪 trace | `tools/tdc_trace.js` | 跑一次 → 真值 PC 序列(用于逐条对拍) | ✅ |
| ④ | **执行序渲染 lift** | `tools/tdc_lift.py` | 字节码 → **可读伪代码**(表达式栈还原 + 控制流 label/goto + **闭包递归展开**) | ✅ |
| ⑤ | 查询 CLI query | `tools/tdc_query.py` | `who rN` / `at @pc` / `funcs` / `grep RE` | ✅ |
| 辅助 | 神谕定位 | `tools/tdc_keytrace.js` | 驱动真 VM,定位"某个值在哪段字节码诞生" | ✅ |
| 归档 | ~~复刻 JS 运行时~~ | `tools/tdc_vm_run.py` | 用 Python 重写 JS 语义去"跑"VM | 🗄 归档为储备(**打地鼠陷阱**,§2.3 铁律2) |
**lift 实例(XTEA 轮,直接可读)**:
```
@09285:
r? = (r? + ((((r6 << 4) ^ (r6 >>> 5)) + r6) ^ (r2 + (r7[(3 & r2)] + 855311))));
r2 = (r2 + 2654435769); // sum += DELTA
if (([6] == ((0 >>> 11) & 3))) goto @9398;
if ((3 == ((r2 >>> 11) & 3))) goto @9361;
```
(教科书 XTEA:`v += (((v<<4)^(v>>>5)) + v) ^ (sum + K[(sum>>>11)&3])`;`855311` 即该构建的轮加盐。)
---
## 8. 下一步路线(自上而下)
**已完成的 L3 解构**:①字节码提取 ②正确反汇编 ③全部 opcode 语义 ④提升器(闭包递归展开)
⑤查询 CLI ⑥密钥形态与流向定位(`caps=[r7=r8]`、XTEA 轮 `reg7[(sum>>>11)&3]`)。
**下一步(不改路线)**:
1. 用 ④ 的 lilted 产物 **读** 密钥例程(不再"模拟")→ 用 Python 原生重写(§2.1 最后一步);
2. 与 `extract_xtea_key`(Node 神谕)逐字对齐后,替换掉最后一个 Node 依赖;
3. 把 lift 的栈模型补全(`SWAP/SET_STACK_LEN/REPLACE_TOP_IMM` 的表达式还原),消除残留的 `r<?>`。