实验 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(剩余自变量+收口实验)。
154 lines
7.3 KiB
Python
154 lines
7.3 KiB
Python
# -*- 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())
|