feat(collect): 按本次构建生成 collect 的工具链 + 回环回归测试

tools/tdc_collectgen.py: 静态导出槽位序 -> 按 #36 规则算「项->槽」-> 重排 -> 该构建密钥加密。
同一构建实测: 37 项 -> 38 槽, 明文 686B / collect 920 字符, 解密回环逐字节一致。

规则修正(重要):
  * kind=1 的串只按**括号外**逗号切槽 —— '[]'/'[360,360]'/'["zh-CN","zh"]' 内逗号不构成新槽
  * kind=2 的项**仍产出 1 槽**(r3 清空转存 r4, r4 在收尾 @4905 拼回)

新增 tcaptcha/tests/test_collectgen.py (已并入 selftest) 与 fixture:
  outer_commas / 槽数推算 == 真机槽数 / 重排是置换 / 加解密回环 / 不匹配必须报错。
账本新增已知 #37(工具链跑通) 与 #38(纯离线生成尚未打通, 诚实标注)。
This commit is contained in:
yml2213
2026-10-12 00:45:23 +08:00
parent 6460dd78d6
commit 6aaf6bc290
5 changed files with 328 additions and 1 deletions
+3 -1
View File
@@ -66,7 +66,9 @@
| 33 | **VM 程序结构层已吃透:顶层就是 62 个模块工厂的注册表** | 顶层 `@0` 做 `registry[id] = function(module, exports, require){...}`(`DUP; PUSH_IMM [id]; REVERSE_TOP2; JUMP -> CREATE_CLOSURE; SET_PROP`)。`tools/tdc_program.py --modules` 直接导出。修了两个真 bug:① 反汇编器漏了 `RET_TRUE`/`THROW` 是终结符(控制流穿过函数结尾 → 子程序归属全错);② 提升器终结符判定写死 opcode 数值 | 确证 |
| 34 | **`cd` 槽位序的来源已定位并可静态导出** | 装配链:清单模块 `arr[arr.length] = require(N)`(**追加,所以 require 顺序就是数组顺序**,已核)→ 分发模块按 `.on`/`.reset`/`.get` 分三数组 → 装配模块 `r3='{"cd":['` + `for(...) r3 += arr[i].get()`。线上构建实测 **37 个 require + 1 个字面量槽 = 38**,与真机 `cd` 槽数完全吻合;两份构建的 require 串不同(与 #31 一致)。工具:`tools/tdc_layout.py` | 确证(顺序)/ 语义标签部分 |
| 35 | **运行时反证通过:静态导出的 require 序 == 运行时真实 require 序(逐项一致)** | 在真机 iframe(`tdc.js` 实际执行的上下文,`window.TDC.getData` 就在这里)里包一层 `Function.prototype.apply`,模块工厂调用形状 = **4 参 `[object,object,object,function]`**(require 是最后一个),按工厂实例分组记账。触发一次刷新后取运行时串,用 `tools/cdp_script_source.js`(Debugger.scriptParsed + getScriptSource)取到**运行时真正执行的那份 tdc.js**(exactly-once,不能用重新 fetch 代替——#29 每次返回不同构建),两边串:<br>静态 = 运行时 = `[8,9,10,13,14,15,16,17,18,19,20,21,23,24,25,30,31,32,34,36,37,1,38,39,40,41,42,43,44,47,48,49,50,51,56,57,58]`(37 项,逐位相同)<br>→ 由此可判定:之前"部分槽语义对不上"是**标注启发式**的问题,**顺序本身正确**。工具:`scripts/experiment_require_order.py` | 确证 |
| 36 | **「清单项 → cd 槽」的精确规则(同一构建端到端校验确立)** | 装配循环(线上构建 `@4362`,370 条)分支树已读通:<br>`if(!item) continue;` `v,w,kind = item.get();`<br>· **`kind===2`(`@4820`)→ 不进 `cd`**:它把 `r3` 清空(`r3=""`)转存到 `r4`(`@4860`),`r4` 在收尾 `@4905` 被二次加工后另作他用(如随机串 `vXc29RxhvT34Q…`)<br>· `v===null/undefined`(`@4799`)→ `out += "null"`<br>· `typeof v==="number"`(`@4738`)→ `out += (v||0)`<br>· **`kind===1`(`@4724`)→ `out += v` 原样拼**:返回值是 JSON 碎片串,串里的逗号会**切出额外槽**(实测 `"641547489,1791732001"` → 2 槽)<br>· 其余(`@4703`)→ `out += '"' + v + '"'`<br>· 收尾 `@4753`:`typeof w === "number"` 才再 +1 槽(实测 kind=2 的第二元素是 null,不触发;此路径本次未命中)<br>→ **37 项 → 38 槽**,仅第 33 项(模块 1)占 2 槽。工具:`tools/tdc_slotmap.py`(`--items` 运行时取样 + `--plain` 明文对拍) | 确证 |
| 36 | **「清单项 → cd 槽」的精确规则(同一构建端到端校验确立)** | 装配循环(线上构建 `@4362`,370 条)分支树已读通:<br>`if(!item) continue;` `v,w,kind = item.get();`<br>· **`kind===2`(`@4820`)**:把 `r3` 清空转存 `r4`(`@4860`),`r4` 在收尾 `@4905` 二次加工后**拼回**,因此**仍产出 1 槽**(实测第 0 项,随机串 `vXc29RxhvT34Q…`)<br>· `v===null/undefined`(`@4799`)→ `out += "null"`<br>· `typeof v==="number"`(`@4738`)→ `out += (v||0)`<br>· **`kind===1`(`@4724`)→ `out += v` 原样拼**:返回值是 JSON 碎片串;**只按括号外的逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽,而 `"641547489,1791732001"` → 2 槽<br>· 其余(`@4703`)→ `out += '"' + v + '"'`<br>· 收尾 `@4753`:`typeof w === "number"` 才再 +1 槽(实测 kind=2 的第二元素是 null,不触发;此路径本次未命中)<br>→ **37 项 → 38 槽**,仅第 33 项(模块 1)占 2 槽。工具:`tools/tdc_slotmap.py`(`--items` 运行时取样 + `--plain` 明文对拍) | 确证 |
| 37 | **按构建生成 collect 的工具链已跑通(含回环自测)** | `tools/tdc_collectgen.py`:静态导出槽位序 → 按 #36 规则算「项→槽」→ 重排 → 该构建密钥加密。<br>实测(同一构建):37 项 → **38 槽**,明文 686B / collect 920 字符;**解密回环逐字节一致**,cd 与真机样本逐槽相同。<br>规则修正(重要):`kind=1` 的串**只按括号外逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽;`kind=2` 的项**仍产出 1 槽**(转存 r4 后在收尾 @4905 拼回)。<br>回归:`tcaptcha/tests/test_collectgen.py`(已并入 selftest) | 确证 |
| 38 | **纯离线生成还没打通(诚实标注)** | 当前 `--from-sample` 依赖**一份同机的真机明文**做重排;`--values`(语义名→值)需要调用方提供取值字典。**仍缺**:纯算地为每个语义名算出值(多数是静态环境量可配置,但 canvas/WebGL 指纹只能取自真实环境)。这一步完成前,不能声称"纯离线过风控"。 | 状态说明 |
---