feat(VM): 吃透 Chaos VM 程序结构层 —— 模块注册表 + collect 槽位表可静态导出

补上 VM_SPEC §5/§6 自己点出的缺口("不是 VM 的问题, 而是模块系统 + 宿主调用序列")。

新增 tools/tdc_program.py(程序结构分析器):
  * closures / entries / 字面串折叠 / 边界感知的函数体切分
  * module_registry(): 识别顶层 "registry[id] = factory" 装配模式 -> 62 个模块工厂
  * call_graph / shared_bodies / find_strings / --func 导出自身指令

修掉两个真 bug:
  * tdc_disasm: RET_TRUE / THROW 未当终结符 -> 控制流穿过函数结尾线性走下去,
    把父函数续段误判给夹在中间的兄弟函数(@4940 曾误归 @4362, 实为 @4160)
  * tdc_lift: 终结符判定写死 opcode 数值 19/49 -> 改为按语义名

新增 tools/tdc_layout.py(静态导出 cd 槽位表):
  装配链已完全厘清:
    顶层 = 62 个模块工厂的注册表(工厂形参 (module, exports, require))
    -> 清单模块 arr[arr.length] = require(N)  <<< 这串 require 的顺序就是 cd 槽位序
    -> 分发模块按 .on/.reset/.get 分三数组
    -> 装配模块 r3='{"cd":[' + for(...) r3 += r8.get() + '],"sd":{...}'
  跨构建实测: core_js 的 require 串与线上构建的串**不同**(槽位序按构建随机化, 与 #31 一致);
  语义对位已验证若干槽(userAgent->cd[3], href->cd[4], languages->cd[5] 命中)。

VM_SPEC 新增 §9 程序结构层(模块系统 / 入口 / collect 装配 / 为何决定 12)。
selftest 全绿。
This commit is contained in:
yml2213
2026-10-12 00:13:27 +08:00
parent c55f833c66
commit c4fe365e8c
7 changed files with 749 additions and 17 deletions
+195
View File
@@ -0,0 +1,195 @@
# -*- coding: utf-8 -*-
"""
决定性实验 A —— 把真机页面**组好但没发出**的 verify 包, 分别交给 Python / 浏览器 代发。
要回答的问题 (har/1ms_dianxuan/PROGRESS.md §五 1(a)):
12 到底是"**谁在发这个 HTTP**"造成的, 还是"**发出去的包**"造成的?
做法:
1. 在真机页面的 **顶层页 + 验证码 iframe** 里 hook 掉 XMLHttpRequest.send / fetch,
让腾讯 widget 照常组包, 但请求**不真正发出** (拦在 `cap_union_new_verify`)。
2. 用 CDP 真实鼠标把验证码答完 -> widget 组包 -> 被拦下, 我们拿到**原封不动的** body。
3. 把这个 body 发给 `cap_union_new_verify`:
--send-by python : httpx 按浏览器同款头发出 (实验组)
--send-by browser : 同 iframe 里 fetch 发出 (对照组: 证明这个包本身有效且未被消耗)
4. 判读:
* 两组都 0 -> "谁在发"无关, 12 只剩"包内容"(H-LAYOUT) -> 下一步去取槽位表
* python 12, 浏览器 0 -> 存在"会话↔请求方"绑定, 需换实验 A'
* 浏览器也非 0 -> 拦截本身污染了会话, 实验无效
用法:
.venv/bin/python scripts/experiment_relay_verify.py --send-by python
.venv/bin/python scripts/experiment_relay_verify.py --send-by browser
"""
from __future__ import annotations
import argparse
import base64
import importlib.util
import json
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import httpx # noqa: E402
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
TRAP_JS = r"""(() => {
if (window.__vtrapInstalled) return {already: true};
window.__vtrapInstalled = true;
window.__vtrap = null;
const isVerify = (u) => typeof u === 'string' && u.indexOf('cap_union_new_verify') >= 0;
const origOpen = XMLHttpRequest.prototype.open;
const origSend = XMLHttpRequest.prototype.send;
const origSRH = XMLHttpRequest.prototype.setRequestHeader;
XMLHttpRequest.prototype.open = function (m, u) {
this.__vUrl = u; this.__vMethod = m; this.__vHdrs = {};
return origOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.setRequestHeader = function (k, v) {
try { if (isVerify(this.__vUrl)) this.__vHdrs[k] = v; } catch (e) {}
return origSRH.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function (body) {
if (isVerify(this.__vUrl)) {
window.__vtrap = { via: 'xhr', url: String(this.__vUrl), method: this.__vMethod || 'POST',
headers: this.__vHdrs || {}, body: String(body), at: Date.now() };
return; // 故意不发出
}
return origSend.apply(this, arguments);
};
const origFetch = window.fetch;
if (typeof origFetch === 'function') {
window.fetch = function (input, init) {
const u = (typeof input === 'string') ? input : (input && input.url);
if (isVerify(u)) {
const body = (init && init.body) || '';
window.__vtrap = { via: 'fetch', url: String(u), method: (init && init.method) || 'GET',
headers: (init && init.headers) || {}, body: String(body), at: Date.now() };
return Promise.reject(new Error('__vtrap_blocked__'));
}
return origFetch.apply(this, arguments);
};
}
return {installed: true};
})()"""
READ_TRAP = "(() => ({trap: window.__vtrap || null, installed: !!window.__vtrapInstalled}))()"
def send_by_python(url: str, body: str, headers: dict, want_origin: bool) -> dict:
h = {
"Content-Type": headers.get("Content-Type",
"application/x-www-form-urlencoded; charset=UTF-8"),
"Accept": headers.get("Accept", "application/json, text/javascript, */*; q=0.01"),
"Referer": "https://turing.captcha.gtimg.com/",
"User-Agent": ("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"),
"sec-ch-ua-platform": '"macOS"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua": '"Google Chrome";v="155", "Chromium";v="155", "Not(A:Brand";v="24"',
}
for k, v in (headers or {}).items():
h[k] = v
if want_origin:
h["Origin"] = "https://turing.captcha.gtimg.com"
with httpx.Client(timeout=20.0, trust_env=False) as c:
r = c.post(url, content=body.encode("utf-8"), headers=h)
return {"status": r.status_code, "text": r.text, "sent_headers": h}
def main() -> int:
ap = argparse.ArgumentParser(description="把真机组好的 verify 包交给 Python/浏览器 代发")
ap.add_argument("--send-by", choices=["python", "browser"], default="python")
ap.add_argument("--rounds", type=int, default=8, help="最多刷几轮去等一个文字点选题")
ap.add_argument("--origin", action="store_true", help="Python 代发时附带 Origin 头")
args = ap.parse_args()
b = _bridge()
# 1) 唤起 widget
ens = b.cdp_eval(b.EXPR_ENSURE, b.PAGE)["result"]
print(f"[1] 唤起 widget: {ens}")
time.sleep(2.0)
# 2) 装陷阱 (顶层页 + iframe; 谁发都拦得住)
for ctx in (b.PAGE, b.IFRAME):
try:
print(f"[2] 装陷阱 {ctx}: {b.cdp_eval(TRAP_JS, ctx)['result']}")
except Exception as exc:
print(f"[2] 装陷阱 {ctx} 失败: {exc}")
# 3) 跑桥接去答题 (回调必定超时 —— 因为 verify 被我们拦下了, 这正是预期)
print("[3] 让真机把题答完 (verify 会被拦下, 所以回调超时属预期) ...")
p = subprocess.run([sys.executable, str(ROOT / "scripts" / "bridge_1ms_captcha.py"),
"--only", "text", "--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()][-8:]
print(" " + "\n ".join(tail))
# 4) 取包
trap = None
for ctx in (b.IFRAME, b.PAGE):
try:
r = b.cdp_eval(READ_TRAP, ctx)["result"]
except Exception as exc:
print(f"[4] 读陷阱 {ctx} 失败: {exc}")
continue
print(f"[4] {ctx}: installed={r.get('installed')} trap={'有' if r.get('trap') else '无'}")
if r.get("trap"):
trap = r["trap"]
break
if not trap:
print("❌ 没拦到 verify 包 (可能本轮没抽到可解的题, 或 iframe 被重建)")
return 1
body = trap["body"]
print(f"[4] 拦到 verify 包: via={trap['via']} url={trap['url'][:80]} body={len(body)}B")
print(f" headers={trap.get('headers')}")
# 5) 代发
if args.send_by == "python":
print("[5] Python(httpx) 代发同一包 ...")
res = send_by_python(trap["url"], body, trap.get("headers") or {}, args.origin)
else:
print("[5] 浏览器(同 iframe) 代发同一包 ...")
js = ("(async()=>{const body=atob('%s');"
"const r=await fetch('%s',{method:'POST',headers:"
"{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8',"
"'Accept':'application/json, text/javascript, */*; q=0.01'},body});"
"return {status:r.status, text: await r.text()};})()"
% (base64.b64encode(body.encode()).decode(), trap["url"]))
tmp = Path("/tmp/1ms_bridge/relay_browser.js")
tmp.parent.mkdir(parents=True, exist_ok=True)
tmp.write_text(js, encoding="utf-8")
res = b.cdp_eval(js, b.IFRAME)["result"]
print(f"[6] 代发结果: status={res.get('status')}")
print(f" body={str(res.get('text'))[:400]}")
try:
code = json.loads(res.get("text") or "{}").get("errorCode")
except Exception:
code = None
print(f"\n>>> errorCode = {code} (sender={args.send_by})")
print(">>> 判读: 0 => '谁在发'无关, 12 只剩包内容(H-LAYOUT); 12 => 还需排查请求方绑定")
return 0
if __name__ == "__main__":
raise SystemExit(main())