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:
@@ -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())
|
||||
Reference in New Issue
Block a user