Files
hello_js_reverse_skill/scripts/experiment_layout_verify.py
T
yml2213 50734a854b test(12): H-LAYOUT 与「包内容」两个假设均被证伪 —— 剩余自变量收敛到「谁在发 HTTP」
实验 A/B (scripts/experiment_layout_verify.py --solve, 真解题):
  按本次构建槽位序 vs 仓库旧模板 -> 两组 9/9 全 12  => 槽位序不是自变量
严格对照(决定性): 同一批取值、同一长度 920、只颠倒槽位序 -> 6/6 全 12

实验 C: 本会话真机真实 collect(TDC.getData(true), 1012 字符) 原样交 Python 发
  -> 4/4 全 12  => 包内容不是自变量

附带关键事实: 12 只在「答案正确」后才判; 答错一律 50/9。
  滑块题型下按点选坐标答(必错)两组都是 50, 无区分度; 换成真解后立刻全 12。
  这解释了早期多轮对照为何无效 —— 今后做 12 的 A/B 必须真解题。

账本: §三 新增两条证伪; 新增已知 #39(12 的判定前提) 与 #40(剩余自变量+收口实验)。
2026-10-12 00:52:01 +08:00

154 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
实验 C —— 用「按本次构建生成的 collect」打一次完整 verify,检验 12 是否消失。
背景(PROGRESS.md #31/#32/#34/#35/#36/#37):
槽位序按构建随机化;仓库里的模板是**某一次**的顺序;
`tools/tdc_collectgen.py` 已能按本次构建重排并加密(回环自洽)。
这一发要回答:
* 用「本次构建的槽位序」发出的 collect,aid 195672344 是否 **12 -> 0**?
- 若 0 -> H-LAYOUT 成立(12 的根因就是槽位序),这条链彻底闭合
- 若仍 12 -> 槽位序**不是** 12 的自变量,H-LAYOUT 需要改,
但我们已经知道该查什么(转去查请求方/会话绑定,即实验 A)
⚠️ 样本要求(#31): 重排需要一份**同机**的真机明文(取值集合相同、只有顺序不同)。
本脚本的 --sample 就是它;本次样本恰好与本次构建同序,因此这一发是"原序复现"。
用法:
.venv/bin/python scripts/experiment_layout_verify.py \\
--build /tmp/build.js --sample /tmp/sample_cd.json --kinds /tmp/items3.json
"""
from __future__ import annotations
import argparse
import json
import random
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))
if str(ROOT / "tools") not in sys.path:
sys.path.insert(0, str(ROOT / "tools"))
import tdc_collectgen as cg # noqa: E402
import tdc_layout # noqa: E402
import tdc_program as tp # noqa: E402
from tcaptcha.client import TencentCaptchaClient # noqa: E402
from tcaptcha.crypto import extract_eks, extract_xtea_key # noqa: E402
def one_verify(client, ch, tkid, collect: str, eks: str, pts) -> dict:
ans = json.dumps([{"elem_id": i + 1, "type": "DynAnswerType_POS",
"data": f"{p[0]},{p[1]}"} for i, p in enumerate(pts)],
separators=(",", ":"))
body = {"ans": ans, "collect": collect, "tlg": str(len(collect)),
"eks": eks, "sess": ch.sess, "tkid": tkid,
"pow_answer": "", "pow_calc_time": "1"}
r = client.http_client.post(
"/cap_union_new_verify", data=body,
headers={"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Accept": "application/json, text/javascript, */*; q=0.01",
"Origin": "https://turing.captcha.gtimg.com",
"Referer": "https://turing.captcha.gtimg.com/"})
return r.json()
def main() -> int:
ap = argparse.ArgumentParser(description="用按构建生成的 collect 打 verify(带对照组)")
ap.add_argument("--build", required=True, help="本次会话的 tdc.js 路径")
ap.add_argument("--sample", required=True, help="同机的真机 cd(json: {cd:[...]} 或 [...])")
ap.add_argument("--kinds", default=None, help="运行时取样的 items json(提供 kind/v0)")
ap.add_argument("--aid", default="195672344")
ap.add_argument("--entry-url", default="https://1ms.run/console/personal/benefits/check-in")
ap.add_argument("--n", type=int, default=12, help="每种 collect 各打几次(统计用)")
ap.add_argument("--solve", action="store_true",
help="真解题: 按题型用本地求解器算答案(12 只在答案正确时才判, 见说明)")
args = ap.parse_args()
js = open(args.build, encoding="utf-8", errors="replace").read()
prog = tp.load(args.build)
_mid, _entry, seq = tdc_layout.find_manifest_module(prog)
key = extract_xtea_key(js)
eks = extract_eks(js)
kinds, v0s = cg.load_shape(args.kinds, seq) if args.kinds else (None, None)
spec = cg.SlotSpec(seq, kinds, v0s)
sample = cg.load_sample_cd(args.sample)
cd = cg.reorder(sample, spec)
collect = cg.encrypt_plain(cg.build_plain(cd), key)
print(f"[1] 构建 : {args.build}")
print(f" 清单模块 : registry[{_mid}] 项 {len(seq)}")
print(f" 槽位规格 : {spec.describe()}")
print(f" 密钥 : {key}")
print(f" eks : {len(eks)} 字符")
print(f"[2] collect : {len(collect)} 字符 (tlg={len(collect)}) 槽数 {len(cd)}")
# 走一次完整协议: prehandle -> tdc(已在手) -> getcapbysig -> 解 -> verify, 但**替换 collect**
tkid = str(random.randint(100000000, 999999999))
client = TencentCaptchaClient(aid=args.aid, entry_url=args.entry_url,
referer="https://1ms.run/")
# ⚠️ 12 是"答案正确之后"的风控判定, 答错一律 50/9, 两组就没区分度。
# 所以 --solve 时按题型真解; 只统计**可解**的轮次。
def solve_points(ch):
if not args.solve:
return [(120, 90), (180, 150), (240, 210)]
if ch.kind in ("text", "text_click"):
res = client.text_solver.solve_ex(client.fetch_image(ch.img_url, 1),
ch.instruction)
return res.points if res.ok else None
if ch.kind in ("icon", "icon_click"):
bg = client.fetch_image(ch.img_url, 1)
pr = client.fetch_image(ch.img_url, 0)
if bg and pr:
return client.icon_solver.solve(bg, pr) or None
return None
return None # 滑块: 本脚本不处理(用 --no-solve 时才有意义)
# 对照组: 仓库旧模板(某一次的槽位序), 同一份构建/同一 session 流程
from tcaptcha.crypto import generate_telemetry
# 对照组只需"仓库旧模板的槽位序", 轨迹用固定点即可(不影响槽位序这一自变量)
tm = generate_telemetry(js, [(120, 90), (180, 150), (240, 210)],
aid=args.aid, user_agent=client.user_agent,
referrer=client.referer)
tally = {"new(按构建)": {}, "old(旧模板)": {}}
try:
solved = 0
for rnd in range(1, args.n + 1):
ch, _cfg, _sid = client.prehandle(tkid)
pts = solve_points(ch)
if not pts:
print(f" [{rnd:02d}] 跳过 (kind={ch.kind}, 本脚本不处理)")
continue
solved += 1
print(f" [{rnd:02d}] kind={ch.kind} pts={pts}")
for tag, collect in (("new(按构建)", collect), ("old(旧模板)", tm["collect"])):
v = one_verify(client, ch, tkid, collect, eks, pts)
code = str(v.get("errorCode"))
tally[tag][code] = tally[tag].get(code, 0) + 1
print(f" [{rnd:02d}] {tag:12s} -> {code}")
print(f"\n (有效轮次: {solved})")
for tag, d in tally.items():
print(f" {tag:12s} 分布: {d}")
new12 = tally["new(按构建)"].get("12", 0)
old12 = tally["old(旧模板)"].get("12", 0)
print()
if new12 == 0 and old12 > 0:
print(">>> ✅ 按构建生成后 12 消失(对照组仍有 12) -> H-LAYOUT 成立, 槽位序就是根因")
elif new12 and old12:
print(">>> ❌ 两组都有 12 -> 槽位序不是 12 的自变量, H-LAYOUT 需修正")
elif not new12 and not old12:
print(">>> ⚠️ 两组都没有 12(多为 50/9=答案未命中) -> 本轮题型/条件下风控未介入, "
"需要换成能答对的题型或加大样本再判")
return 0
finally:
client.close()
if __name__ == "__main__":
raise SystemExit(main())