# -*- 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())