diff --git a/har/1ms_dianxuan/PROGRESS.md b/har/1ms_dianxuan/PROGRESS.md index d1fd913..4b909c9 100644 --- a/har/1ms_dianxuan/PROGRESS.md +++ b/har/1ms_dianxuan/PROGRESS.md @@ -53,6 +53,9 @@ | 20 | **opcode 表可从该构建解释器源码推导**:主循环 `X[Y[Z++]]()` 的 `X` 就是 handler 数组,下标即 opcode;handler 规范化指纹可跨构建精确匹配 | 6/6 份构建 **58/58 handler 精确匹配**、0 未识别(`tcaptcha/tdc_opcodes.py`)| 确证 | | 21 | **密钥字闭包只写"被 XTEA 闭包捕获的那个数组"**;脚本同时算了两套 4 字(真 + 诱饵),靠**两层捕获链**区分:字闭包捕获的 B 帧寄存器 `w_src`,经"定义 B 的那个闭包"的 caps 映射到 G 帧后须 == XTEA 闭包捕获的寄存器 | core/root/1ms×3/实时新抓 全部选对;诱饵组按同规则被排除 | 确证 | | 22 | **纯 Python 静态提取 XTEA 密钥成功,零 Node** | `tcaptcha/tdc_key.py`:6/6 份构建与 Node 插桩 `extract_xtea_key` **逐字一致**;含 2026-10-11 现场新抓的 `tdc.js`(`har/1ms_dianxuan/live/tdc_fresh.js`,77967B,密钥 `[1397244740,1799507557,1399740484,1516463189]`)| 确证 | +| 23 | **纯 Python 密钥能解密 1ms 真实成功样本的 collect** | 用 `har/签到成功-1.har` 里那次 ec=0 之前的 tdc.js 提密钥 → 解出该次 collect 明文 `{"cd":[2560,"MacIntel",...` 1353B;且与 `models/success_collect_template.b64` **逐字节同构**(仅轨迹/时间戳/盐被回填)| 确证 | +| 24 | **1ms 真实成功链路已完整解析** | prehandle(带腾讯云登录 cookie) → tdc.js → getcapbysig → verify **ec=0**(ticket `tr03…`) → `POST /api/v1/mall/checkin`,body = `{captchaTicket:{provider,data:{ticket,randstr}}, sessionUuid, identity_context}`;`/api/v1/captcha/params` 的 body 带 `identity_context`(`emctx1..`) | 确证 | +| 25 | **真实浏览器对 verify 从不吃 12** | `点选.har`(同一 cookie/同一 aid) 三次 verify = 50/50/9;`签到成功-1.har` = 0。而我们**任何**客户端形态都是 12 → 12 是"请求方身份"判定,不是答案/载荷 | 确证 | --- @@ -83,6 +86,10 @@ | 轨迹 dt 太小是 12 的"直接原因" | 换成真实 ms 分布后仍 12(E6) | | **opcode 数值是跨构建固定的 VM 常量** | 6 份构建实测 pc0 的 `SET_STACK_LEN`/`JUMP` 数值全不同(见已知 #19);`tools/tdc_disasm.py` 里固化的表**只对 core_js 有效**,已改为"可注入按构建推导的表" | | **"密钥字必须靠 Node 插桩才能拿到"** | `tcaptcha/tdc_key.py` 纯 Python 零 Node 复算,6/6 构建与 Node 逐字一致(已知 #22)→ Node 降级为兜底 | +| **H3(captcha 域腾讯云登录 cookie)能解决 12** | 用**抓包里的真实 cookie 值**(uin/skey/qcmainCSRFToken/intl)重跑,仍 12(`scripts/experiment_h3_cookie_ab.py --only b`,2026-10-11)→ 与早期结论一致,H3 确凿排除 | +| **HTTP/TLS 传输指纹是 12 的自变量** | `curl_cffi`(impersonate=chrome: JA3/JA4 + HTTP/2 + Chrome 头序) + 真实 cookie,仍 12(`scripts/experiment_transport_ab.py --only b`)→ 传输层排除 | +| **请求头保真度是 12 的自变量** | 按真实抓包逐 endpoint 保真:资源 GET 用 `Sec-Fetch-Dest=script/image` + `Mode=no-cors` + `Storage-Access=active` + cookie,POST 用 `empty/cors` + Origin 且**不带** cookie;仍 12 | +| **collect 明文结构/取值是 12 的自变量** | 我们的 collect 明文与 1ms 真实成功样本**逐字节同构**(同 1353B、同字段序、同取值,仅回填轨迹/时间戳/盐),仍 12 | --- @@ -115,9 +122,13 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认 残留增强项(非阻塞):① 若服务端改了 handler 语义,`tdc_opcodes.derive_opcode_table` 会抛"指纹未识别",此时应更新参考指纹而非改定位逻辑;② 抗"多套诱饵数组 >2 层捕获"的 兜底(当前两层已覆盖实测全部 6 份)。 -1. **拿下 1ms 的 12**:做账本 §7.5 的**单变量实验** —— - (a) 给 captcha 域带上成功样本里的 `uin/skey/qcmainCSRFToken/intl` cookie(已做,H3 已排除); - (b) 登录态真页面触发 vs 未登录触发。二选一即可闭合。 +1. **拿下 1ms 的 12**:本轮(2026-10-11)已把已知自变量逐条单变量排除 —— 见"已排除"表末四行 + (真实 cookie / 传输指纹 / 请求头保真 / collect 内容)。**剩余两条可执行路径**: + (a) **保底(建议先做)**:账本 §2.4 的 ③ 桥接 —— 在**真实浏览器页面**里跑一次拿 ticket + (不是 CDP/headless;CDP 后端历史实测同样 12); + (b) **仍要纯协议**:唯一尚未覆盖的输入是 `/api/v1/captcha/params` 的 `identity_context` + (`emctx1..`) —— 但它只发给 **1ms 后端**、不进 Tencent 的 verify, + 因此**不能单独解释** 12;除非 1ms 后端用它把业务会话与 Tencent 侧绑定(需一次对照实验验证)。 2. **补齐新版明文字段语义**:对照 10+ 份实时模板做差分。 3. **实现 2408 图标点选**:按 1ms 图标类别重训 `yolo_detector` + `icon_classifier`。 4. **实现滑块 (2401/2402)**:模板匹配找缺口 + 生成滑动轨迹。 diff --git a/pyproject.toml b/pyproject.toml index 2039a60..23049a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,6 +3,7 @@ name = "hello-js-reverse-skill" version = "0.1.0" requires-python = ">=3.12" dependencies = [ + "curl-cffi>=0.16.3", "ddddocr>=1.6.1", "httpx>=0.28.1", "numpy>=2.5.3", diff --git a/scripts/experiment_transport_ab.py b/scripts/experiment_transport_ab.py new file mode 100644 index 0000000..1348f2b --- /dev/null +++ b/scripts/experiment_transport_ab.py @@ -0,0 +1,117 @@ +#!/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 --csrf --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()) diff --git a/tcaptcha/client.py b/tcaptcha/client.py index f50209c..d1108a9 100644 --- a/tcaptcha/client.py +++ b/tcaptcha/client.py @@ -32,6 +32,7 @@ from pathlib import Path from typing import Dict, List, Optional, Sequence, Tuple import httpx +from urllib.parse import urlsplit PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(PROJECT_ROOT) not in sys.path: @@ -87,6 +88,46 @@ def classify_challenge(dyn_show_info: dict, subcapclass: str = "") -> str: return KIND_UNKNOWN +def _site_origin(url: str) -> str: + """取 `scheme://host[:port]/` —— 页内