Files
hello_js_reverse_skill/scripts/experiment_require_order.py
T
yml2213 344eea2194 refactor(结构): 分层重组 + 清除全部废弃路线
做了什么:
- 包改为分层: tcaptcha/{protocol,telemetry,solvers,ocr,adapters,models,tests}
  * protocol/  client.py  transport.py  proxy.py
  * telemetry/ crypto.py  values.py  key.py  opcodes.py
               disasm.py  program.py  layout.py  modules.py
  * solvers/   text_click.py  icon_click.py  icon_click_rotate.py  slider.py
  * ocr/       engines.py  slicer.py
  * adapters/  one_ms.py
- tools/ 里原本是**包运行时依赖**的 tdc_{disasm,program,layout,modules}.py 并入 telemetry/
  (放在 tools/ 属于分层倒置),tools/ 只留 CLI: tdc_key_pure.py / tdc_profile.py / cdp_*.js
- crypto.py 529 -> 340 行: 切除「成功样本模板回填」与「Node 插桩兜底」两条老路;
  generate_telemetry 只剩 python_values 一种模式,传别的会直接报错并说明原因
- 求解器改名分清: solver.py -> solvers/icon_click.py(client 实际在用的 YOLO+ResNet18 ONNX),
  icon_click_solver.py -> solvers/icon_click_rotate.py(仅测试在用,留作对照)
- pyproject: 去掉 ultralytics/onnx(训练栈已删),补上真正在用的 onnxruntime

删除了什么:
- 雨云整条线 + dist/rainyun_qinglong(104M 旧快照)
- 模板回填: models/success_collect_template.b64、tdc_worker.js、analyze_collect_layout.py
- 「补环境」时代浏览器 JS: chrome_fetch/manual_captcha/probe_captcha/solve_in_browser/tdc_browser
- 旧 VM 探索工具 10 个(tdc_collectgen/tdc_slotmap/tdc_lift/tdc_vm_run/tdc_query/tdc_probe 等)
  + 8 个基于它们的旧 verify 脚本
- 训练/标注/评估: train_classifier/train_detector/prepare_yolo/web_labeler/collector/
  siamese/evaluate/benchmark_verify
- tcaptcha/core_js/(60 个分析产物)、TOOLCHAIN_REPORT.md(整篇在描述已删工具)
- 3 个已无任何引用的 PyTorch 权重:icon_classifier.pt 43M / yolo_detector.pt 5.9M /
  siamese_matcher.pt 1.6M —— 线上只跑 onnx,tcaptcha/ 117M -> 66M

为什么:
- 项目后面要专门承载「滑块 / 文字点选 / 图标点选」三种求解器;扁平包 + 运行时依赖混在
  tools/ + 两条已废弃的 collect 生成路线,会让后续扩展持续踩坑

怎么验证:
- .venv/bin/python -m tcaptcha.selftest                    全绿
- scripts/verify/verify_offline.py                         逐槽全等 + 分段几何一致
- scripts/verify/verify_values.py                          4 份真机明文 152/152 逐槽全等
- scripts/verify/send_test.py --mode python_values         1ms 签到页文字点选 errorCode=0
- 回滚点: tag backup/pre-refactor-20261012
2026-10-12 12:34:57 +08:00

159 lines
6.4 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"))
from tcaptcha.telemetry import layout # noqa: E402
from tcaptcha.telemetry import 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())