Files
hello_js_reverse_skill/tools/tdc_layout.py
T
yml2213 c4fe365e8c feat(VM): 吃透 Chaos VM 程序结构层 —— 模块注册表 + collect 槽位表可静态导出
补上 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 全绿。
2026-10-12 00:13:27 +08:00

159 lines
6.7 KiB
Python
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.
# -*- coding: utf-8 -*-
"""
tdc_layout.py —— 静态导出 `collect` 明文的 **cd 槽位表**(纯算, 不跑 JS, 不跑环境)。
背景(这条链是 2026-10-11 排查 errorCode=12 时打通的, 见 har/1ms_dianxuan/PROGRESS.md #31/#32):
真机成功会话的 collect 明文解出来后, 发现 **5 份同日样本的 `cd` 槽位数(38)、取值集合、`sd`
全都一样, 但槽位顺序 5/5 互不相同** —— 也就是"哪些字段排在哪一槽"是**按构建随机化**的。
仓库里的 success_collect_template.b64 是**某一次**的顺序, 拿它发的 collect 会被服务端
按"本次构建的槽位表"读成自相矛盾的环境 -> errorCode=12。
槽位序从哪来(程序结构, 见 tools/tdc_program.py):
1. 顶层 @0 先把 62 个**模块工厂**按 id 填进一个数组 (registry[id] = function(module, exports, require));
2. 其中一个模块(下称"清单模块")返回一个**数组**, 它的构造方式是一串
r6[r6.length] = require(N)
也就是 **`cd` 的槽位顺序 = 这串 require 的顺序**;
3. 另一个模块负责把这些采集器按 `.on` / `.reset` / `.get` 分到三个数组, 再另有一个模块
遍历"有 .get() 的那些", 依次取 `get()` 的值拼成 `{"cd":[...],"sd":{...}}`。
所以本工具做的事:
* 找清单模块(= push(require(N)) 次数最多的那个模块);
* 读出它的 require 序列 -> 槽位表 [(槽位号, 模块 id, 工厂 entry), ...];
* 把每个槽位子模块(含其内部闭包)里的**环境属性字面量**列出来, 作为该槽的语义线索;
* 可选: 读一份已解密的真机 collect 明文, 与槽位表并排打印, 人工/程序核对。
用法:
# 只看槽位表
.venv/bin/python tools/tdc_layout.py tcaptcha/core_js/tdc_sample.js
# 和真机明文并排核对 (明文是怎么来的: 同会话 tdc.js 密钥解密 verify 的 collect)
.venv/bin/python tools/tdc_layout.py <tdc.js> --plain /tmp/real_collect_plain.txt
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
if str(ROOT / "tools") not in sys.path:
sys.path.insert(0, str(ROOT / "tools"))
import tdc_program as tp # noqa: E402
# 采集器用来读环境的属性名(用于给槽位打语义标签, 不是判定依据)
ENV_KEYS = [
"userAgent", "platform", "languages", "language", "hardwareConcurrency", "deviceMemory",
"colorDepth", "pixelDepth", "availHeight", "availWidth", "deviceXDPI", "logicalXDPI",
"fontSmoothingEnabled", "appVersion", "appName", "vendor", "cookieEnabled", "timezone",
"getTimezoneOffset", "toDataURL", "getParameter", "getExtension", "TCaptchaReferrer",
"webdriver", "referrer", "href", "width", "height", "innerWidth", "innerHeight",
"devicePixelRatio", "characterSet", "MAX_TEXTURE_SIZE", "WEBGL_debug_renderer_info",
]
def find_manifest_module(p: tp.Program) -> Tuple[int, int, List[int]]:
"""找"清单模块": 体内 `push(require(N))` 次数最多者。
返回 (模块 id, 工厂 entry, require 序列)。
"""
reg = dict(p.module_registry(0))
best = (0, -1, [])
for mid, entry in reg.items():
code = p.function_code(entry)
seq: List[int] = []
for k, inst in enumerate(code):
if inst["name"] != "PUSH_IMM" or not inst["operands"]:
continue
if k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM" \
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC":
seq.append(int(inst["operands"][0]))
if len(seq) > len(best[2]):
best = (mid, entry, seq)
return best
def transitive_strings(p: tp.Program, entry: int, depth: int = 3) -> List[str]:
"""模块自身 + 它内部创建的闭包(递归 depth 层)里的字面串。"""
seen, out = set(), []
frontier = {entry}
for _ in range(depth):
nxt = set()
for e in frontier:
if e in seen:
continue
seen.add(e)
for pc, s in p.strings:
if p.owner(pc) == e:
out.append(s)
for c in p.closures:
if p.owner(c.pc) == e:
nxt.add(c.target)
frontier = nxt
return out
def env_hints(strings: List[str]) -> List[str]:
hits = []
for s in strings:
for k in ENV_KEYS:
if k.lower() in s.lower() and k not in hits:
hits.append(k)
return hits
def main() -> int:
ap = argparse.ArgumentParser(description="静态导出 collect 的 cd 槽位表")
ap.add_argument("path")
ap.add_argument("--plain", default=None, help="已解密的真机 collect 明文 (json 文本)")
ap.add_argument("--json", default=None, help="把槽位表写成 JSON")
args = ap.parse_args()
p = tp.load(args.path)
reg = dict(p.module_registry(0))
mid, entry, seq = find_manifest_module(p)
print(f"文件 : {args.path}")
print(f"模块总数 : {len(reg)}")
print(f"清单模块 : registry[{mid}] = @{entry}")
print(f"槽位序列长度 : {len(seq)} (真机 cd 槽数 = 38, 差的部分是字面量槽)")
print(f"require 序列 : {seq}")
plain_cd: Optional[list] = None
if args.plain:
txt = open(args.plain, encoding="latin1").read().rstrip("\x00").strip()
obj = json.loads(txt)
plain_cd = obj.get("cd")
print(f"真机明文 cd 槽数: {len(plain_cd)}")
table = []
print("\n槽位 模块id 工厂entry 语义线索 真机值")
print("-" * 108)
for i, m in enumerate(seq):
tgt = reg.get(m)
strs = transitive_strings(p, tgt) if tgt else []
hints = env_hints(strs)
val = ""
if plain_cd is not None and i < len(plain_cd):
v = repr(plain_cd[i])
val = v if len(v) <= 34 else v[:34] + "..."
table.append({"slot": i, "module": m, "entry": tgt, "hints": hints})
print(f" {i:2d} {m:4d} @{tgt:<7} {str(hints)[:34]:<34} {val}")
if plain_cd is not None:
print(f"\n(真机 cd 里还有 {len(plain_cd) - len(seq)} 个槽不来自 require —— "
f"通常是字面量, 例如 cd[0] 的空串)")
if args.json:
Path(args.json).write_text(json.dumps({"manifestModule": mid, "entry": entry,
"slots": table}, ensure_ascii=False, indent=1),
encoding="utf-8")
print(f"槽位表 -> {args.json}")
return 0
if __name__ == "__main__":
raise SystemExit(main())