做法: 真机 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。
159 lines
6.3 KiB
Python
159 lines
6.3 KiB
Python
# -*- 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())
|