Files
hello_js_reverse_skill/scripts/experiment_transport_ab.py
T
yml2213 06c4348163 test(1ms): 逐条单变量排除 errorCode=12 的已知自变量 (cookie / 传输指纹 / 头保真 / collect)
做了什么
- 新增 tcaptcha/http_transport.py: curl_cffi(Chrome) 传输层壳, 对上层暴露 httpx.Client 同款
  get/post/close; TencentCaptchaClient 增加 transport="httpx"|"curl" + impersonate 参数。
- client.py 按真实抓包(har/签到成功-1.har)逐 endpoint 保真请求头:
  * prehandle/tdc.js/getcapbysig: Sec-Fetch-Dest=script/image + Mode=no-cors +
    Storage-Access=active + captcha 域 cookie, Referer 为 site-origin / gtimg;
  * getsig/verify: empty/cors + Origin gtimg, 且 **不带** cookie (SameSite=Lax 实测如此)。
  cookie 由全局头改为按调用点附加 (此前 verify 也带, 与真实浏览器不符)。
- 新增 scripts/experiment_transport_ab.py: 只改传输层的单变量 A/B。

为什么 (AGENTS §2.1 / §5.7)
- 抓包里 1ms 的真机成功样本(errorCode=0)与真机失败样本(50/50/9)证明: 12 是"请求方身份"判定,
  与答案/载荷无关 —— 需要把客户端与真机的每一处可测差异逐条单变量掉。

怎么验证 (2026-10-11 实测, 全部仍 12)
- 真实 cookie 值 (uin/skey/qcmainCSRFToken/intl) -> 12 (H3 用真值再次排除)
- curl_cffi impersonate=chrome (JA3/JA4 + HTTP/2 + 头序) + 真实 cookie -> 12 (传输层排除)
- curl_cffi + 逐 endpoint 头保真 (Sec-Fetch-*/cookie 位置) -> 12 (头保真排除)
- 我们的 collect 明文与 1ms 真实成功样本逐字节同构 (同 1353B/同字段序/同取值) -> 12 (内容排除)
- 反向: 用纯 Python 提取的密钥成功解密 1ms 真实成功样本的 collect -> 密钥链路对真机样本成立
- .venv/bin/python -m tcaptcha.selftest 全绿

账本 har/1ms_dianxuan/PROGRESS.md: 已知 #23~#25; 已排除表新增四行; 下一步改为 (a) 真机桥接保底 /
(b) identity_context 研究项。
2026-10-11 23:01:24 +08:00

118 lines
4.8 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
传输指纹单变量实验: errorCode=12 是否由 HTTP/TLS 指纹决定?
依据: AGENTS.md §2.1 (用服务端响应画生死线) / §5.6 (先怀疑自己的工具) / ANALYSIS.md §7.6。
已确证的事实 (2026-10-11):
* 同一 aid (1ms 195672344) + 同一套腾讯云登录 cookie + 同一 prehandle 参数下,
**真实浏览器** (har/签到成功-1.har, 点选.har) 对 cap_union_new_verify 只得到
50 / 9 / 0 —— **从不 12**;
* 本客户端 (httpx, HTTP/1.1 + OpenSSL) 在不同 cookie / IP / 时序 / 后端下**恒为 12**;
* 答案正确 (OCR 后验 1.0)、XTEA 密钥为实时构建真密钥 → 12 与载荷内容无关。
→ 剩下唯一被测到的差异: **"谁在发这个 HTTP"** (JA3/JA4 + HTTP/2 + 头序 + 头大小写)。
本脚本**只改传输层**, 其余 (cookie / 参数 / 题型 / 求解器) 完全相同:
A) transport=httpx (基线)
B) transport=curl (curl_cffi impersonate=chrome)
判读:
A=12 且 B=0 -> 12 = 传输指纹问题; 纯算流程只需换传输层即可通过
A=12 且 B=12 -> 传输指纹也不够, 需回到"真实浏览器页面上下文"(H1/H2) 或 collect 内容
用法:
.venv/bin/python scripts/experiment_transport_ab.py --both \
--uin 100012108023 --skey <skey> --csrf <qcmainCSRFToken> --intl 1
"""
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from tcaptcha.client import KIND_TEXT_CLICK, TencentCaptchaClient # noqa: E402
TRANSPORTS = {"a": ("httpx", None), "b": ("curl", "chrome")}
def run_once(tag: str, transport: str, impersonate, cookies, args) -> dict:
client = TencentCaptchaClient(
aid=args.aid,
entry_url=args.entry_url,
referer=args.referer,
wb=args.wb,
accept_kinds=(KIND_TEXT_CLICK,),
telemetry_mode=args.telemetry,
proxy=args.proxy,
cookies=cookies,
transport=transport,
impersonate=impersonate,
)
try:
result = client.solve(max_retries=args.retries, max_refreshes=args.refreshes)
finally:
client.close()
raw = result.get("raw_response") or {}
print(f"\n[{tag}] transport={transport} impersonate={impersonate} "
f"success={result['success']} errorCode={raw.get('errorCode')} "
f"ticket={(raw.get('ticket') or '')[:24]}")
if result.get("error"):
print(f" error={result['error']}")
return result
def main() -> int:
ap = argparse.ArgumentParser(description="传输指纹单变量实验 (httpx vs curl_cffi)")
ap.add_argument("--aid", default="195672344")
ap.add_argument("--entry-url", default="https://1ms.run/console/personal/benefits/check-in")
ap.add_argument("--referer", default="https://1ms.run/")
ap.add_argument("--wb", default="1")
ap.add_argument("--retries", type=int, default=3)
ap.add_argument("--refreshes", type=int, default=20, help="换题上限 (1ms 会随机下发滑块/图标/文字)")
ap.add_argument("--telemetry", default="python_keyed", choices=["python_keyed", "python"])
ap.add_argument("--proxy", default=None)
ap.add_argument("--uin", default="")
ap.add_argument("--skey", default="")
ap.add_argument("--csrf", dest="csrf", default="", help="qcmainCSRFToken")
ap.add_argument("--intl", default="1")
ap.add_argument("--both", action="store_true", help="先跑 A(httpx) 再跑 B(curl)")
ap.add_argument("--only", choices=["a", "b"], default="b")
args = ap.parse_args()
cookies = {k: v for k, v in {
"uin": args.uin, "skey": args.skey, "qcmainCSRFToken": args.csrf, "intl": args.intl,
}.items() if v}
print(f"[*] cookie 数量={len(cookies)} 代理={args.proxy} telemetry={args.telemetry}")
plan = ["a", "b"] if args.both else [args.only]
summary = {}
for which in plan:
transport, impersonate = TRANSPORTS[which]
tag = f"{which.upper()} {transport}"
r = run_once(tag, transport, impersonate, cookies or None, args)
summary[which] = str((r.get("raw_response") or {}).get("errorCode"))
print("\n=== 判读 ===")
print(json.dumps({"A_httpx": summary.get("a"), "B_curl_chrome": summary.get("b")},
ensure_ascii=False))
if args.both:
a, b = summary.get("a"), summary.get("b")
if a == "12" and b == "0":
print("=> 传输指纹是 12 的自变量: 换 curl_cffi(Chrome 指纹) 即可通过")
elif a == "12" and b == "12":
print("=> 传输指纹不足: 回到 H1/H2(真实浏览器页面上下文) 或 collect 内容")
else:
print("=> 结果非典型, 需人工判读")
return 0
if __name__ == "__main__":
raise SystemExit(main())