做了什么:
- 包改为分层: 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
159 lines
6.4 KiB
Python
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())
|