feat(VM): 运行时反证通过 —— 静态导出的 cd 槽位序与真机 require 序逐位一致

做法: 真机 iframe 里包 Function.prototype.apply, 模块工厂调用形状实测为
4 参 [object,object,object,function](require 是最后一个), 按工厂实例分组记账;
再用新增 tools/cdp_script_source.js(Debugger.scriptParsed + getScriptSource)
取运行时真正执行的那份 tdc.js(不能用重新 fetch —— 同 URL 每次返回不同构建)。

结果: 静态 == 运行时 =
[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 项逐位相同 -> 槽位序=require序 这条链闭合; 之前语义对不上是标注启发式的问题。

附带: window.TDC(含 getData) 只在验证码 iframe, 可直接调用取证; cdp_eval 新增 --init-script。
账本新增已知 #35。
This commit is contained in:
yml2213
2026-10-12 00:24:13 +08:00
parent 50c0ca33c6
commit d2b3889132
5 changed files with 283 additions and 1 deletions
+158
View File
@@ -0,0 +1,158 @@
# -*- coding: utf-8 -*-
"""
实验 B —— 运行时反证「静态导出的 cd 槽位序」。
要回答的问题(har/1ms_dianxuan/PROGRESS.md §五 1(b) 残留点②):
`tools/tdc_layout.py` 从字节码静态推出的 require 顺序, 真的是**运行时**清单模块
请求模块的顺序吗? 如果一致, 槽位序=require 序 这条链就闭合了; 不一致则还有一层重排。
做法(只做观测, 不伪造环境):
1. 在真机页面里包一层 `Function.prototype.apply`。VM 的 `CALL_FUNC` 走 apply,
而模块工厂的调用形参固定是 (module, exports, require) —— 我们借这次调用的机会
把**该工厂专属的 require** 换成记账版本(每个工厂一个 group)。
2. 触发一次真实答题(走既有桥接), 让模块系统真正跑起来(清单模块在组装 collect 时被 require)。
3. 取回日志: 找出"distinct id 数最多"的那个 group = 清单模块;
与同一次会话抓到的 tdc.js 的静态导出串对比。
用法:
.venv/bin/python scripts/experiment_require_order.py
"""
from __future__ import annotations
import argparse
import importlib.util
import json
import subprocess
import sys
import time
from pathlib import Path
from typing import List
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_layout # noqa: E402
import tdc_program as tp # noqa: E402
TRAP_JS = r"""(() => {
if (window.__reqTrapInstalled) return {already: true};
window.__reqTrapInstalled = true;
window.__reqLog = [];
window.__reqGroup = 0;
const origApply = Function.prototype.apply;
Function.prototype.apply = function (thisArg, args) {
try {
// 模块工厂调用形参固定 (module, exports, require): 3 个实参, 第 3 个是函数
if (args && args.length === 3 && typeof args[2] === 'function' && args[1] && typeof args[1] === 'object') {
const req = args[2];
if (!req.__logged) {
const g = ++window.__reqGroup;
const wrapped = function (id) {
window.__reqLog.push([g, typeof id === 'number' ? id : String(id)]);
return req.apply(this, arguments);
};
wrapped.__logged = true;
return origApply.call(this, thisArg, [args[0], args[1], wrapped]);
}
}
} catch (e) { /* 观测器绝不影响原流程 */ }
return origApply.call(this, thisArg, args);
};
return {installed: true};
})()"""
READ_LOG = "(() => ({installed: !!window.__reqTrapInstalled, n: (window.__reqLog||[]).length, log: window.__reqLog||[]}))()"
def _bridge():
spec = importlib.util.spec_from_file_location("bridge_1ms", ROOT / "scripts" / "bridge_1ms_captcha.py")
mod = importlib.util.module_from_spec(spec)
sys.argv = ["experiment"]
spec.loader.exec_module(mod)
return mod
def main() -> int:
ap = argparse.ArgumentParser(description="运行时 require 顺序 vs 静态导出")
ap.add_argument("--rounds", type=int, default=8)
args = ap.parse_args()
b = _bridge()
cap_path = Path("/tmp/1ms_bridge/req_cap.json")
print("[1] 唤起 widget 并装 require 记账器 ...")
print(" ", b.cdp_eval(b.EXPR_ENSURE, b.PAGE)["result"])
time.sleep(2.0)
for ctx in (b.PAGE, b.IFRAME):
try:
print(f" {ctx}: {b.cdp_eval(TRAP_JS, ctx)['result']}")
except Exception as exc:
print(f" {ctx}: 装失败 {exc}")
print("[2] 同时抓包(取本会话的 tdc.js) + 答题触发模块系统 ...")
cap = subprocess.Popen(["node", str(ROOT / "tools" / "cdp_capture.js"),
"--seconds", "70", "--url-substr", "tdc.js,cap_union",
"--out", str(cap_path)],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(3)
p = subprocess.run([sys.executable, str(ROOT / "scripts" / "bridge_1ms_captcha.py"),
"--rounds", str(args.rounds), "--solves", "1"],
cwd=str(ROOT), capture_output=True, text=True, timeout=600)
tail = [ln for ln in p.stdout.splitlines() if ln.strip()][-4:]
print(" " + "\n ".join(tail))
cap.wait(timeout=120)
print("[3] 取运行时 require 日志 ...")
logs: List[list] = []
for ctx in (b.IFRAME, b.PAGE):
try:
r = b.cdp_eval(READ_LOG, ctx)["result"]
print(f" {ctx}: installed={r.get('installed')} 条数={r.get('n')}")
if r.get("log"):
logs += r["log"]
except Exception as exc:
print(f" {ctx}: 读失败 {exc}")
if not logs:
print("❌ 没拿到 require 日志(模块系统可能没跑到)")
return 1
groups: dict = {}
for g, mid in logs:
groups.setdefault(g, []).append(mid)
# 清单模块 = distinct id 最多的那个 group
best_g, best_seq = max(groups.items(), key=lambda kv: len(set(kv[1])))
runtime_seq = list(dict.fromkeys(best_seq))
print(f"[4] group 数={len(groups)} 清单候选 group={best_g} "
f"调用 {len(best_seq)} 次 / distinct {len(runtime_seq)}")
print(f" 运行时序列: {runtime_seq}")
print("[5] 静态导出(同一份构建)...")
if not cap_path.exists():
print("❌ 没抓到本会话的 tdc.js, 无法比对")
return 1
recs = json.load(open(cap_path, encoding="utf-8"))["records"]
tdcs = [r for r in recs if "tdc.js" in r["url"] and r.get("body")]
if not tdcs:
print("❌ 抓包里没有 tdc.js 响应体")
return 1
js_path = Path("/tmp/1ms_bridge/req_build.js")
js_path.write_text(tdcs[-1]["body"], encoding="utf-8")
prog = tp.load(str(js_path))
reg = dict(prog.module_registry(0))
_mid, _entry, static_seq = tdc_layout.find_manifest_module(prog)
print(f" 静态序列: {static_seq}")
ok = runtime_seq == static_seq
print(f"\n>>> 运行时 == 静态 ? {ok}")
if not ok:
print(f" 只在运行时有的 id: {sorted(set(runtime_seq) - set(static_seq))}")
print(f" 只在静态有的 id : {sorted(set(static_seq) - set(runtime_seq))}")
print(" (若只是顺序不同 -> 分发阶段有重排; 若集合都不同 -> 静态提取取错了模块)")
return 0
if __name__ == "__main__":
raise SystemExit(main())