Files
hello_js_reverse_skill/tcaptcha/VM_SPEC.md
T
yml2213 70f2ece32c docs(vm): 新增 Chaos VM 规范 + WIP 纯 Python 解释器
做了什么:
- 新增 tcaptcha/VM_SPEC.md: 从源码/反汇编事实整理 VM 全套规格
  * §1 架构(VM(H,z,c,E,I,D,U,C), c=window, 主循环 M=G[z[H++]]())
  * §2 数据模型(E 一物两用=寄存器区+求值栈; 寄存器单元 E[i][0]; 引用=二元数组 [base,key])
  * §3 完整 opcode 表(70 槽/58 handler/12 空洞, 附 handler 源码与 argc, 由 AST 自动导出)
  * §4 闭包与调用(CREATE_CLOSURE 动态布局, caps 共享单元, CALL_FUNC/METHOD 的 this 绑定)
  * §5 引导流程(实测顶层只注册模块) §6 宿主与驱动(c 即 window, 环境全走 c)
  * §7 工具链映射 §8 解释器路线图(已完成①-④, 未完成⑤模块系统+宿主驱动)
- 新增 tools/tdc_vm_run.py: 58 个 opcode 的具体解释器 + 宽松 JS 宿主 (WIP, 跑通顶层引导)

为什么:
- 之前"没头绪"的根因是数据模型(E 一物两用/寄存器单元/引用二元数组), 必须写成确定规格
- 并定位真正的卡点: 不在 VM 而在"模块系统 + 宿主调用序列"

怎么验证:
- tools/tdc_vm_run.py 跑 739 步后 RET_TRUE (仅顶层引导), 与 CFG 可达性一致
2026-10-11 21:59:06 +08:00

226 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. 工具链映射
| 工具 | 作用 | 状态 |
| --- | --- | --- |
| `tools/tdc_disasm.py` | 纯 Python:提取字节码 + 可达性 CFG 反汇编(0 非法 opcode) | ✅ 可用(canonical) |
| `tools/tdc_vm_run.py` | 纯 Python:58 个 opcode 的具体解释器 + 宽松 JS 宿主 | 🚧 WIP(跑通顶层引导,未达密钥) |
| `tools/tdc_probe.js` | 深度 Proxy 沙箱驱动真实 tdc.js(**分析参考**,非交付路径) | 分析工具 |
| `tcaptcha/tdc_worker.js` | Node 插桩抓 XTEA 密钥常量(当前唯一 Node 依赖) | 可用·有缓存 |
---
## 8. 解释器路线图(要"彻底理顺"必须补齐的点)
1. **模块表**:记录每个 `CREATE_CLOSURE` 工厂的 `target`,识别"注册 vs 调用"。
2. **模块系统语义**:复刻 `require/exports` 式的装配顺序(谁在何时被调用)。
3. **宿主 `c` 的最小可用面**:`Object/Array/String/Math/JSON/RegExp/Date` + 事件/DOM 桩(严格只补"模块初始化真正读到的",不盲目补)。
4. **驱动入口**:复刻页面初始化序列,直到进入 XTEA 那段闭包。
5. **抓取产物**:寄存器中出现的"4 个可打印字符拼成的 32bit 数组" = XTEA 密钥(已确证形态)。
6. **校验**:与 `extract_xtea_key`(Node)结果逐字对齐,对齐后才敢替换。
> 已完成:①字节码提取 ②正确反汇编 ③全部 opcode 语义 ④密钥形态与流向定位(`caps=[r7=r8]`,XTEA 轮 `reg7[(sum>>>11)&3]`,本构建轮加盐 `723726`/`855311`)。
> 未完成:⑤模块系统 + 宿主驱动(本路线图的 1–4)。