补上 VM_SPEC §5/§6 自己点出的缺口("不是 VM 的问题, 而是模块系统 + 宿主调用序列")。
新增 tools/tdc_program.py(程序结构分析器):
* closures / entries / 字面串折叠 / 边界感知的函数体切分
* module_registry(): 识别顶层 "registry[id] = factory" 装配模式 -> 62 个模块工厂
* call_graph / shared_bodies / find_strings / --func 导出自身指令
修掉两个真 bug:
* tdc_disasm: RET_TRUE / THROW 未当终结符 -> 控制流穿过函数结尾线性走下去,
把父函数续段误判给夹在中间的兄弟函数(@4940 曾误归 @4362, 实为 @4160)
* tdc_lift: 终结符判定写死 opcode 数值 19/49 -> 改为按语义名
新增 tools/tdc_layout.py(静态导出 cd 槽位表):
装配链已完全厘清:
顶层 = 62 个模块工厂的注册表(工厂形参 (module, exports, require))
-> 清单模块 arr[arr.length] = require(N) <<< 这串 require 的顺序就是 cd 槽位序
-> 分发模块按 .on/.reset/.get 分三数组
-> 装配模块 r3='{"cd":[' + for(...) r3 += r8.get() + '],"sd":{...}'
跨构建实测: core_js 的 require 串与线上构建的串**不同**(槽位序按构建随机化, 与 #31 一致);
语义对位已验证若干槽(userAgent->cd[3], href->cd[4], languages->cd[5] 命中)。
VM_SPEC 新增 §9 程序结构层(模块系统 / 入口 / collect 装配 / 为何决定 12)。
selftest 全绿。
333 lines
19 KiB
Markdown
333 lines
19 KiB
Markdown
# 腾讯天御 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++]` 次数(= 固定操作数字节数);动态长度的那个是 `CREATE_CLOSURE`(见 §4)。
|
||
|
||
> ⚠️ **本节表格是"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; }` |
|
||
| 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<?>`。
|
||
|
||
|
||
---
|
||
|
||
## 9. 程序结构层:模块系统 / 入口 / collect 装配(2026-10-11 补,工具 `tools/tdc_program.py`)
|
||
|
||
§5/§6 曾把缺口写成「不是 VM 的问题,而是模块系统 + 宿主调用序列」。本节把它补成确定规格。
|
||
|
||
### 9.1 顶层就是「模块注册表装配」
|
||
|
||
```
|
||
@0000: SET_STACK_LEN 3; ALLOC_REG 2; JUMP @1518
|
||
@1518: r3 = CREATE_CLOSURE(target=@7, params=[p0->r3]) // VM 外层包装
|
||
...
|
||
@1538: PUSH_IMM [0]; REVERSE_TOP2; JUMP -> @2085
|
||
@2085: CREATE_CLOSURE(target=@1544, params=[p0->r3,p1->r4,p2->r5]); SET_PROP // registry[0] = 工厂@1544
|
||
@2096: PUSH_IMM [1]; REVERSE_TOP2; JUMP -> @2914
|
||
@2914: CREATE_CLOSURE(target=@2102, ...); SET_PROP // registry[1] = 工厂@2102
|
||
... // 共 62 个
|
||
```
|
||
|
||
即源码级的 `registry[id] = function (module, exports, require) { ... }`(CommonJS)。
|
||
**模块工厂的形参固定为 (r3=module, r4=exports, r5=require)**;模块体内 `r5(N)` 就是 `require(N)`。
|
||
|
||
> `tools/tdc_program.py --modules` 直接导出这张表。
|
||
|
||
### 9.2 代码布局的坑(把工具改对了)
|
||
|
||
- 函数体是**不连续**的:父函数的初始化序列与子函数体交错排布,父函数用 `JUMP` 跳过子函数体。
|
||
所以「按 pc 大小猜归属」全错(实测把父函数续段误判给夹在中间的兄弟函数)。
|
||
正确做法:**边界感知走法** —— 从入口走控制流,遇到**另一个函数入口**就停下。
|
||
- 反汇编器曾漏掉终结符:`RET_TRUE`/`THROW` 之后**没有 fall-through**,否则控制流会穿过函数结尾
|
||
线性走下去,把后续所有代码都算进当前函数。已修(按语义名定位,不写死 opcode 数值)。
|
||
|
||
### 9.3 collect 的装配链(`{"cd":[...],"sd":{...}}` 是怎么出来的)
|
||
|
||
1. **清单模块**:某个模块返回一个数组,构造方式是接连的
|
||
|
||
```
|
||
DUP?; PUSH_IMM_ARRAY [6]; "length" ... ; LOAD_REG_IMM [5]; PUSH_IMM [N]; CALL_FUNC [1]; SET_PROP
|
||
```
|
||
|
||
语义即 `arr[arr.length] = require(N)`。**这串 require 的顺序就是 `cd` 的槽位顺序。**
|
||
(core_js 那份:37 个 require + 1 个字面量槽;线上构建实测是**另一串** id。)
|
||
2. **分发模块**:一个闭包把清单按 `.on` / `.reset` / `.get` 分进三个数组
|
||
(`caps=[r5=r9,r6=r12,r7=r13,r8=r14]`,即 出=r12/r13/r14)。
|
||
3. **装配模块**:`r3 = '{"cd":['`,然后
|
||
|
||
```
|
||
for (r7 = 0; r7 < r15.length; r7++) { r8 = r15[r7]; v = r8.get(); ... 追加 r3 ... }
|
||
r3 += '],'; ... r4["cd"] = r3
|
||
```
|
||
|
||
即**按数组顺序**调每个采集器 `.get()` 并序列化 → `cd` 槽位序 = 清单数组顺序。
|
||
|
||
### 9.4 为什么这件事决定 errorCode=12
|
||
|
||
- 槽位序**按构建随机化**(5 份同日样本 5/5 互不相同,见 `har/1ms_dianxuan/PROGRESS.md` #31)。
|
||
- 仓库里的 `models/success_collect_template.b64` 是**某一次**的顺序
|
||
(其指纹与 `签到成功-1.har` 那一次逐槽一致,#32)。
|
||
- 拿旧顺序发的 collect,会被服务端按**本次构建**的槽位表解释成「自相矛盾的环境」(例如
|
||
`cd[1]` 该是 `[360,360]` 却是 `"MacIntel"`)→ 风控判 12。
|
||
- 这正是「换 IP / 换遥测后端 / 换传输指纹 / 换 cookie / 长静默全都无效」的原因:
|
||
它们**都不动槽位序**。
|
||
|
||
> 因此纯算路线必须**按本次构建**导出槽位表(`tools/tdc_layout.py`),
|
||
> 并用**本次 prehandle 的 sid** 去取那份 tdc.js(真机 URL 还多带 `appid=<aid>`)。
|