做了什么: - ANALYSIS.md §7.1 新增 E8: captcha 域"带/不带"腾讯云登录 cookie, 单变量 A/B 均为 12 - ANALYSIS.md §7.4 假设表更新: H3 标注为已排除, 只剩 H1/H2 - ANALYSIS.md §4.3 更正: extract_eks 改通用匹配后实测 248 字符, 与真实浏览器一致 - PROGRESS.md §三 已排除表新增 H3 一行 为什么: - 彻底关闭"换 cookie/cookie 同源"这条猜测路线, 避免重复投入 怎么验证: - 单变量 A/B: scripts/experiment_h3_cookie_ab.py --both -> A=12, B=12 (同一次运行, 题目 2404 求解 posterior=1.000 全对, collect_len=1856)
464 lines
25 KiB
Markdown
464 lines
25 KiB
Markdown
# 毫秒 1ms.run 腾讯天御「文字点选」协议分析
|
||
|
||
分析样本:`/Users/yml/Desktop/抓包/毫秒/点选.har`(20 条记录,WebInspector 导出)
|
||
分析时间:2026-10-11
|
||
目标题型:`subcapclass = 2404`(腾讯天御 **文字点选**,提示语为「请依次点击:甲 乙 丙」)
|
||
|
||
---
|
||
|
||
## 一、请求链
|
||
|
||
抓包中与验证码相关的记录共 12 条,可还原为 **1 个业务侧引导请求 + 11 个腾讯验证码请求**:
|
||
|
||
```
|
||
① POST https://1ms.run/api/v1/captcha/params ← 业务侧:取验证码配置
|
||
body: {"scene":"checkin","identity_context":"emctx1.<b64>.<b64>"}
|
||
resp: {"code":0,"data":{"enabled":true,"provider":"tencent",
|
||
"params":{"captchaAppId":195672344},
|
||
"sessionUuid":"01a12a04-e5e7-726b-b00c-2f54b2e1c167"}}
|
||
|
||
② GET https://turing.captcha.qcloud.com/cap_union_prehandle ← 建立会话 + 首题
|
||
?aid=195672344&protocol=https&accver=1&showtype=popup&ua=<base64 UA>
|
||
&noheader=1&fb=1&aged=0&enableAged=0&enableDarkMode=0&grayscale=1
|
||
&clientype=2&cap_cd=&uid=&lang=zh-cn
|
||
&entry_url=https%3A%2F%2F1ms.run%2Fconsole%2Fpersonal%2Fbenefits%2Fcheck-in
|
||
&elder_captcha=0&js=%2Ftcaptcha-frame.ba387dd1.js&login_appid=
|
||
&wb=1&tkid=407017912&subsid=1&callback=_aq_260580&sess=
|
||
resp: _aq_260580({... "capclass":"1","subcapclass":"2404","sess":"s0Rr8i01...",
|
||
"sid":"1793111442254233600",
|
||
"data":{"comm_captcha_cfg":{"tdc_path":"/tdc.js?app_data=<sid>&t=<rand>",
|
||
"pow_cfg":{"prefix":"a79f541e7c1ca571#",
|
||
"md5":"0264f7a4329e2b6f9f9076c53294c277"}},
|
||
"dyn_show_info":{"instruction":"请依次点击:虫 呈 般 ",
|
||
"bg_elem_cfg":{"size_2d":[672,480],
|
||
"click_cfg":{"mark_style":"inc_number",
|
||
"data_type":["DynAnswerType_POS"]},
|
||
"img_url":"/cap_union_new_getcapbysig?img_index=1&image=...&sess=..."}}}})
|
||
|
||
③ GET .../tdc.js?app_data=1793111442254233600&t=1309148276 ← 会话专属混淆脚本
|
||
④ GET .../cap_union_new_getcapbysig?img_index=1&image=...&sess=... ← 672x480 背景图
|
||
|
||
── 第 1 次作答 ──
|
||
⑤ POST .../cap_union_new_verify (ans + collect + eks + sess + pow_*) → errorCode 50
|
||
⑥ POST .../cap_union_new_getsig (sess + tkid) → 新 sess + 新题
|
||
⑦ GET .../cap_union_new_getcapbysig?... 第 2 张图
|
||
── 第 2 次作答 ── ⑧ verify → errorCode 9
|
||
⑨ getsig ⑩ getcapbysig 第 3 张图
|
||
── 第 3 次作答 ── ⑪ verify → errorCode 50
|
||
⑫ getsig ⑬ getcapbysig 第 4 张图(抓包结束时未提交)
|
||
```
|
||
|
||
> 抓包中的 `y.clarity.ms/collect`(微软 Clarity 埋点)、`collector.entmesh.mliev.com`
|
||
> 与验证码无关,属于站点自身的监控上报,本文不纳入实现。
|
||
|
||
---
|
||
|
||
## 二、会话状态机(关键!错一个就永远失败)
|
||
|
||
`sess` 在三处出现,且**互不相同**,必须严格区分:
|
||
|
||
| 位置 | 说明 |
|
||
| --- | --- |
|
||
| `prehandle` 响应体的 `sess` | **会话主 sess**,首次 `verify` 必须用它 |
|
||
| `dyn_show_info.bg_elem_cfg.img_url` 里的 `sess=` | 只作用于**取图**,与 verify 用的 sess 不是同一个 |
|
||
| `verify` 响应的 `sess` | 上一轮失败后的**新会话 sess** |
|
||
| `getsig` 请求体 `sess` | 用的是上一步 `verify` 响应里的新 sess(不是会话主 sess) |
|
||
| `getsig` 响应体的 `sess` | 下一轮的会话 sess,下一轮 verify 用它 |
|
||
|
||
实测(`点选.har`):
|
||
|
||
```
|
||
prehandle.sess = s0Rr8i01...(482) ← verify#1 用它
|
||
verify#1 resp.sess = s0TMSpmIpMCH3...(482) ← getsig#1 用它
|
||
getsig#1 resp.sess = s0VSknLJwQHTwRC...(494) ← verify#2 用它,同时也是该轮 img_url 的 sess
|
||
verify#2 resp.sess = s05b9OQJg2z6nx...(494) ← getsig#2 用它
|
||
getsig#2 resp.sess = s0nrIxkexDWO0Rgd...(482) ← verify#3 用它
|
||
```
|
||
|
||
**题型在会话内固定**:对同一会话连续 `getsig`,返回的 `subcapclass` 不变(实测滑块会话
|
||
连发 8 次 getsig 全是滑块)。想换题型只能**重新 `prehandle` 开新会话**。
|
||
|
||
---
|
||
|
||
## 三、参数分类
|
||
|
||
### 3.1 固定/常量参数
|
||
|
||
| 参数 | 值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `protocol` | `https` | 固定 |
|
||
| `accver` | `1` | 协议版本 |
|
||
| `showtype` | `popup` | 弹窗模式 |
|
||
| `noheader` / `fb` | `1` / `1` | 固定 |
|
||
| `aged` / `enableAged` / `enableDarkMode` | `0` | 固定 |
|
||
| `grayscale` | `1` | 灰度模式开启 |
|
||
| `clientype` | `2` | PC 端 |
|
||
| `lang` | `zh-cn` | 语言 |
|
||
| `js` | `/tcaptcha-frame.ba387dd1.js` | 验证码 frame 脚本名 |
|
||
| `wb` | `1` | 1ms.run 侧取值(雨云为 2) |
|
||
| `subsid` | `1` | 抓包中为 1(雨云实现里随机 1~5,实测不影响) |
|
||
|
||
### 3.2 每会话动态参数
|
||
|
||
| 参数 | 生成规律 |
|
||
| --- | --- |
|
||
| `aid` | 业务侧下发:`1ms.run/api/v1/captcha/params` → `captchaAppId = 195672344` |
|
||
| `tkid` | 0~9 数字串,9 位;**同一会话内所有请求复用同一个值**(抓包:407017912) |
|
||
| `callback` | `_aq_` + 6 位随机数;仅 `prehandle` 使用(JSONP) |
|
||
| `entry_url` | 触发页 URL(1ms 为签到页),URL-encoded |
|
||
| `ua` | 请求 UA 的 base64(抓包解码 = `Chrome/155.0.0.0` on macOS) |
|
||
| `sid` / `app_data` | `prehandle` 响应返回,仅用于拼 `tdc_path` |
|
||
| `sess` | `prehandle` / `getsig` / `verify` 三级传递,见第二节 |
|
||
| `image` | 服务端下发在 `img_url` 中的图片令牌,逐题变化 |
|
||
|
||
### 3.3 动态参数变化规律(多题对比)
|
||
|
||
| 题号 | `image` | `instruction` | `ans` | `pow_answer` | `pow_calc_time` |
|
||
| --- | --- | --- | --- | --- | --- |
|
||
| 1 | `026409000000001300000015aa3f011427ce` | 请依次点击:虫 呈 般 | `162,249` / `104,37` / `561,156` | `a79f541e7c1ca571#16447` | 46 |
|
||
| 2 | `026409000090e146000000159f1391142763` | 请依次点击:佰 衬 稗 | `426,47` / `332,156` / `517,332` | 同题 1 | 46 |
|
||
| 3 | `0264090000857147000000159f13911427e7` | 请依次点击:簿 蚕 沧 | `498,136` / `45,118` / `252,146` | 同题 1 | 46 |
|
||
|
||
结论:
|
||
* **`pow_answer` / `pow_calc_time` 在整个会话内恒定** —— 它来自 `prehandle` 的 `pow_cfg`,
|
||
是一次性的工作量证明,而不是每轮刷新。
|
||
* **`collect` / `eks` 在整个会话内恒定**(三轮 verify 的 `collect` 长度都是 1784 且字节一致)
|
||
—— 它们由 `tdc.js` 在页面初始化时一次性生成,之后每次 verify 复用,**不随后续点击坐标变化**。
|
||
* `ans` 逐题变化,且坐标随图片令牌变化,**不存在可复用规律**。
|
||
|
||
---
|
||
|
||
## 四、签名 / 加密参数分析
|
||
|
||
### 4.1 `pow_answer` —— MD5 工作量证明(已完全还原)
|
||
|
||
服务端给 `pow_cfg = {"prefix": "a79f541e7c1ca571#", "md5": "0264f7a4329e2b6f9f9076c53294c277"}`。
|
||
|
||
反推验证:
|
||
|
||
```
|
||
md5("a79f541e7c1ca571#16447") = 0264f7a4329e2b6f9f9076c53294c277 ✅ 命中
|
||
```
|
||
|
||
即 `pow_answer = prefix + str(i)`,其中 `i` 从 0 递增直到 MD5 命中;`pow_calc_time`
|
||
是浏览器自己测量的耗时(抓包 46ms,本机 Python 约 120~190ms)。
|
||
|
||
### 4.2 `collect` —— 自定义 XTEA 分组加密 + 行为轨迹(结构与编码已还原)
|
||
|
||
`collect` 是 URL-encoded 的 Base64,**不是** 4 段分别 base64 后拼接的产物:
|
||
|
||
| 来源 | Base64 长度 | 解码字节数 | 内部 `=` |
|
||
| --- | --- | --- | --- |
|
||
| 点选.har 真实浏览器 | 1784 | 1336 = 48 + **768** + 504 + 16 | 仅末位 `==` |
|
||
| 本仓库 Node 沙箱执行真实 tdc.js | 1654 | 1168 = 48 + **600** + 504 + 16 | 仅末位 `==` |
|
||
| 本仓库纯 Python 还原 | 1712~1822 | 随轨迹长度浮动 | **块内出现 `=`** ❌ |
|
||
|
||
明文结构(按 8 字节分组、32 轮自定义 XTEA 加密,密钥 4 个 32bit word):
|
||
|
||
```
|
||
[0 .. 48) cd 前缀 {"cd":[[],"1920-1080-1055-24-*-*-|-*",0,0,"", (空格补齐)
|
||
[48 .. 816) 鼠标轨迹 [[x0,y0,2],[dx1,dy1,dt1],...] 长度必须是 24 的倍数
|
||
[816 .. 1320) cd 后缀 画布指纹/UA/平台/分辨率/时区/语言/…
|
||
[1320 .. 1336) sd 结尾 "sd":{"od":"C"}}
|
||
```
|
||
|
||
**已修掉的实现缺陷**:原 `generate_trajectory_payload()` 把轨迹补齐到 8 的倍数,
|
||
导致分块 base64 后出现**块内 `=` 填充**(真实样本只有末尾一个 `==`)。已改为补齐到
|
||
**24(= lcm(8,3))的倍数**,与真实样本结构一致。
|
||
|
||
**已定论(2026-10-11 实测修正)**:`DEFAULT_KEY = [1448563793, 1847730015, 1733184827, 1364613714]`
|
||
以及两个轮加盐 `2829068 / 328986`,是**当时逆向的那一份 tdc.js 构建**里的密码学参数。
|
||
实测三份脚本(每份都先跑真实 tdc.js 拿到 collect,再尝试解密):
|
||
|
||
| tdc.js 来源 | 轮加盐(脚本内联值) | 用 DEFAULT_KEY + 该加盐解密 |
|
||
| --- | --- | --- |
|
||
| `tcaptcha/tdc_sample.js`(雨云样本) | `2829068 / 328986` | ✅ 明文 `{"cd":[[],"1920-1080..."` |
|
||
| 雨云 aid=2039519451 **当天实时** | `394753 / 657930` | ❌ 随机字节 |
|
||
| 1ms aid=195672344 当天实时 | `197386 / 1513230` | ❌ 随机字节 |
|
||
|
||
结论:**XTEA 的 4 个密钥字与 2 个轮加盐都是"每次构建随机"的,且不是可 grep 的字面量**
|
||
(它们藏在 JSVMP 的操作数流里,DELTA `2654435769` 与 `DELTA*32 = 84941944608` 是唯一可见的常量)。
|
||
所以「纯 Python 硬编码密钥」本质上不是算法还原,而是**单份脚本的参数固化**。
|
||
|
||
**但关键反证**:服务端**并不按 tdc.js 的会话密钥去解密 `collect`**。实测雨云 aid 用
|
||
纯 Python(硬编码密钥)的 collect 提交正确答案 → `errorCode=0` + 真实 ticket。
|
||
因此参数随机化**不构成阻塞**,纯 Python 交付形态依然成立(默认 `telemetry_mode="python"`)。
|
||
node / browser 后端保留为「目标 aid 风控更严格」时的备选。
|
||
|
||
### 4.3 `eks` —— 会话密钥信封
|
||
|
||
`eks` 由 tdc.js 内部 `TDC.getInfo().info` 产出,形如 Base64 的 236~248 字符
|
||
(= 176~184 字节),推测是用服务端公钥包裹的会话密钥,服务端用它解密 `collect`。
|
||
|
||
* 早期用写死变量名的正则(`window.KQnlRHY...`)提取,得到 236 字符,与真实浏览器的 248 不一致。
|
||
现已改为**通用**匹配(任取最长的 `window.<随机名>='<base64>'`),在 2026-10-11 的实时构建上
|
||
稳定取到 **248 字符**,与真实浏览器观测一致(见 E8 那次运行 `eks_len=248`)。
|
||
`eks` 的来源与 `collect` 必须**同源**(同一份 tdc.js、同一次执行),不可混用。
|
||
|
||
### 4.4 `ans` —— 点击坐标(明文,无需签名)
|
||
|
||
```json
|
||
[{"elem_id":1,"type":"DynAnswerType_POS","data":"162,249"},
|
||
{"elem_id":2,"type":"DynAnswerType_POS","data":"104,37"},
|
||
{"elem_id":3,"type":"DynAnswerType_POS","data":"561,156"}]
|
||
```
|
||
|
||
坐标位于 **672x480 原图坐标系**,顺序即提示语顺序(`mark_style: inc_number`)。
|
||
|
||
### 4.5 无法确定的内容(明确声明,不虚构)
|
||
|
||
1. **XTEA 会话密钥的派生过程**:tdc.js 每次加载都会重新混淆,密钥常量未以字面量出现
|
||
(`1448563793` 等 4 个常量在 tdc.js 中检索不到),需要源码级插桩才能完全还原。
|
||
本实现改为执行真实 tdc.js,绕过该问题。
|
||
2. **`eks` 的加密算法与字段布局**:未逆向,直接复用 tdc.js 产出。
|
||
3. **`click_cfg` 之外的题型**(2408 图标点选、2402/2401 滑块)尚未实现求解器。
|
||
|
||
---
|
||
|
||
## 五、文字点选题型的视觉求解(2404)
|
||
|
||
与雨云那套「图标点选」完全不同,本题型:
|
||
|
||
* **没有提示图**:`img_url` 换成 `img_index=0` 返回 **0 字节**(实测多次)
|
||
* 提示是纯文本:`instruction = "请依次点击:虫 呈 般 "`
|
||
* **背景图里只有这 3 个汉字**(无干扰字),位置随机,部分题目带斜切变形
|
||
* 汉字颜色恒定:BGR≈(58,176,228) → HSV H≈20 S≈185 V≈228
|
||
|
||
因此求解 = `颜色分割定位` → `汉字识别` → `按提示顺序做最优一对一指派`,实现见
|
||
`tcaptcha/glyph_solver.py`:
|
||
|
||
1. HSV 阈值分割(H∈[8,34] S>85 V>145)+ 形态学闭运算 + 13px 膨胀把偏旁并成整字
|
||
2. 连通域得到包围盒,按面积/长宽比过滤,得到候选字形
|
||
3. 掩膜 → 白底黑字 → 3 倍放大,交给 OCR **集成**打分
|
||
4. 得分 = 每个引擎在 CTC 各时间步上目标字概率的**最大值**,取多预处理变体最大值后加权求和
|
||
5. 剔除全部目标字得分都 < 0.02 的伪字形(照片暖色区域会误检)
|
||
6. 3! 全排列求最优指派,并用归一化后验概率作为置信度
|
||
|
||
### OCR 集成为什么必要
|
||
|
||
| 字形 | ddddocr max P(正确字) | PP-OCRv4 |
|
||
| --- | --- | --- |
|
||
| 筹(生僻,带斜切) | **0.0046** ❌ | 正确 ✅ |
|
||
| 雹 | 正确 ✅ | 误识为「电」❌ |
|
||
| 笆 | 误识为「芭」❌ | 正确(or 集成后仍正确指派)✅ |
|
||
|
||
单引擎都不够;概率级集成后 6/6 真实样本全部正确且后验置信度 = 1.000。
|
||
|
||
### 6/6 回归样本
|
||
|
||
| 样本 | 提示 | 求解结果 |
|
||
| --- | --- | --- |
|
||
| `har_round1` | 虫 呈 般 | (149,252) (48,55) (549,351) ✅ |
|
||
| `har_round2` | 佰 衬 稗 | (449,53) (549,353) (349,153) ✅ |
|
||
| `har_round3` | 簿 蚕 沧 | (549,153) (50,255) (48,154) ✅ |
|
||
| `har_round4` | 沉 驰 齿 | (150,154) (550,153) (49,53) ✅ |
|
||
| `live_probe` | 笆 辩 裁 | (549,153) (50,54) (351,152) ✅ |
|
||
| `live_rare` | 筹 炽 雹 | (449,253) (49,353) (250,255) ✅ |
|
||
|
||
用户抓包里失败的 3 次,回看坐标可确认是**人工点错**,不是协议问题:
|
||
|
||
* 第 1 题「虫 呈 般」:`虫`(162,249) ✅、`呈`(104,37) ✅、`般` 点在 (561,**156**),
|
||
而 `般` 实际在 (549,**351**) —— 第 3 击差 195px → 50
|
||
* 第 2 题「佰 衬 稗」:`佰` ✅,但第 2、3 击把 `稗` 和 `衬` 点反了 → 50
|
||
* 第 3 题「簿 蚕 沧」:第 2 击点在 (45,118) 是 `沧` 而非 `蚕`,第 3 击 (252,146) 落空 → 9
|
||
|
||
---
|
||
|
||
## 六、线上验证记录(errorCode 语义)—— 以及一个必须纠正的初判
|
||
|
||
### 6.1 1ms aid=195672344 的返回规律
|
||
|
||
| 提交内容 | errorCode | 复现次数 |
|
||
| --- | --- | --- |
|
||
| 三击全部落在图外的乱坐标 `(10,10)(20,20)(30,30)` | `50` | 4 次 |
|
||
| **正确坐标**(求解器命中 3/3) | `12` | 8 次 |
|
||
| **正确坐标但故意把第 1、2 击顺序点反** | `50` | 1 次 |
|
||
|
||
```
|
||
[REAL ] 请依次点击:安 胺 猜 post=1.000 -> 12
|
||
[BOGUS] 请依次点击:睬 持 层 -> 50
|
||
[REAL ] 请依次点击:半 陈 补 坐标正确但 1<->2 点反 -> 50
|
||
```
|
||
|
||
**`errorCode 12` 只在答案完全正确时出现** → 答题链路(坐标空间、顺序、`elem_id`、
|
||
`pow_answer`、`sess` 状态机)100% 正确,服务端是在答案通过后进入风控评估才拒绝的。
|
||
|
||
### 6.2 三个对照实验(决定了最终结论)
|
||
|
||
**对照 A —— 同一套代码换 aid,直接拿到 ticket:**
|
||
|
||
```
|
||
雨云 aid=2039519451, 图标点选(2408), 纯 Python 硬编码密钥 collect
|
||
→ errorCode=0, ticket=tr03jvqlp225MjNBxjDj_-mnM1VTS8O9, randstr=@wBc ✅
|
||
```
|
||
|
||
这一条同时证明了两件事:
|
||
1. **纯 Python 交付形态完全有效**,服务端不校验 collect 与 eks 的同源性;
|
||
2. 1ms 的 12 **不是**遥测密钥/算法的问题。
|
||
|
||
**对照 B —— 三种遥测后端在 1ms 上结果一致:**
|
||
|
||
| 后端 | 说明 | 1ms 结果 |
|
||
| --- | --- | --- |
|
||
| `python` | 硬编码密钥纯算法还原 | 12 |
|
||
| `node` | Node VM 伪造环境执行真实 tdc.js | 12 |
|
||
| `browser` | **真实 Chrome** + CDP 可信鼠标事件执行真实 tdc.js | 12 |
|
||
|
||
**对照 C —— 换出口 IP(住宅代理),彻底排除 IP 滚动风控:**
|
||
|
||
| 出口 IP | 归属 | 结果 |
|
||
| --- | --- | --- |
|
||
| `118.248.52.65` | 本机(与抓包 IP `118.248.47.92` 同 /16) | 12 |
|
||
| `123.181.235.161` | 河北秦皇岛(住宅代理) | 12 |
|
||
| `114.222.19.181` | 江苏南京(住宅代理) | 12 |
|
||
| `123.181.235.170` | 住宅代理 | 12 |
|
||
|
||
每个新 IP 上都是「后验置信度 1.000 的正确答案 → 12」,其中还夹杂一次
|
||
「后验 0.789 的题 → 50」(说明这道题确实点错了,反向印证了 12/50 的语义)。
|
||
另外还做过「静默 9 分钟 + 单发一次」,同样是 12。
|
||
|
||
### 6.3 结论(诚实版)
|
||
|
||
* ✅ **文字点选求解器**:16 道实时题全部命中,后验置信度均 ≈1.000
|
||
(尝辨长 / 佰簿班 / 半陈补 / 安胺猜 / 侈持钡 / 稗伴瓣 / 乘匙层 / 辰本虫 /
|
||
秤遍稗 / 背岸拔 / 奔薄芭 / 倍玻畅 / 趁骋薄 / 筹辩彻 …)
|
||
* ✅ **协议与答题提交**:正确(服务端只在答案正确时才进入风控评估)
|
||
* ✅ **纯 Python 遥测**:在雨云 aid 上端到端产出真实 ticket,交付形态成立
|
||
* ❌ **1ms aid 的票据发放**:被该 aid 的风控拦在 `errorCode=12`
|
||
|
||
已经排除的原因:算法、XTEA 参数随机化、遥测引擎(python/node/browser)、
|
||
请求参数与 `sess` 状态机、短时频控、**出口 IP 滚动风控**。
|
||
|
||
剩余唯一可能:**该 aid 配置了更严格的风控,要求会话具备"真实验证码页面上下文"**
|
||
(tdc.js 运行在真实验证码 iframe 内:真实 iframe location、真实 Canvas/WebGL 指纹、
|
||
`isTrusted` 鼠标事件、真实 `document.referrer`),而我们的 node/browser 后端
|
||
都是把 tdc.js 注入到"空白页"执行,缺少这层上下文。
|
||
|
||
### 6.4 下一步(能真正拿到 ticket 的路)
|
||
|
||
**完整浏览器内求解**:用 CDP/Playwright 打开真实页面触发验证码,让原生 SDK 自己跑
|
||
tdc.js;复用本仓库已完成的视觉求解器算坐标,用 `Input.dispatchMouseEvent`
|
||
在 iframe 内真实点击,从 SDK 回调取 `ticket` / `randstr`。
|
||
|
||
`tcaptcha/tdc_browser.js` 已经把 CDP 骨架写好了(启动 Chrome / 注入脚本 /
|
||
派发可信事件 / 读回 `TDC.getData()`),只需把「注入 tdc.js 的空白页」换成
|
||
「真实验证码页面」。换 IP 的能力也已经内置(`tcaptcha/proxy.py` +
|
||
`--proxy-api`,遇 12 自动换 IP),届时可直接复用。
|
||
|
||
### 6.5 求解器鲁棒性修复(本次线上测试暴露的真实缺陷)
|
||
|
||
代理测试暴露出 2 例定位失败,根因是 **HSV 下限 `V>=145` 把暖色草地/沙地并进了字形掩膜**:
|
||
|
||
| 样本 | 现象 | 根因 |
|
||
| --- | --- | --- |
|
||
| `长 澄 班` | 只检出 1~2 个字形 | 草地 `H≈25 S≈170 V≈156` 落在阈值内,把 `澄/班` 一起吞进大块背景连通域 |
|
||
| `菜 倍 钵` | 只检出 2 个字形 | `倍/钵` 竖排相邻,13px 膨胀把它们粘成 `332x80` 长条,被长宽比过滤器丢弃 |
|
||
|
||
实测颜色统计:**汉字 `H≈20.5 S≈200 V≈232`;草地 `V≈122~156`** —— 亮度 `V` 才是干净的分隔维度。
|
||
把下限收紧为 `H>=12 S>=145 V>=190` 后,8 张图(含这 2 张失败样本)**全部稳定检出 3 个字形**,
|
||
两张失败样本也已加入 `tcaptcha/tests/fixtures/` 作为回归用例。
|
||
|
||
另外把 `min_posterior` 从 `0.6` 提到 `0.95`:实测全部正确解的后验都是 `1.000`,
|
||
而那次后验 `0.789` 的提交确实被判 50(点错),门限收紧可避免提交不可靠答案。
|
||
|
||
### 6.6 题型覆盖
|
||
|
||
20 次连续抽题统计(aid=195672344):
|
||
|
||
| subcapclass | 题型 | 占比 | 状态 |
|
||
| --- | --- | --- | --- |
|
||
| 2408 | 图标点选(提示为 `img_index=0` 图标图) | 35% | 需按 1ms 的图标类别重训模型 |
|
||
| 2404 | **文字点选**(本次目标题型) | 15% | ✅ 求解器完成并验证 (8/8 回归) |
|
||
| 2402 / 2401 | 滑块拼图 | 50% | 未实现(需模板匹配找缺口) |
|
||
|
||
> 题型在**会话内固定**:`getsig` 不换题型,只有重新 `prehandle` 才能抽到别的题型。
|
||
> `client.solve` 已内置「抽到目标题型再提交」的循环(默认 `max_refreshes` 次)。
|
||
|
||
---
|
||
## 七、errorCode=12 根因分析 —— 证据账本(v2,推倒重写)
|
||
|
||
> v1 版本(已废弃)里我下了多个无证据结论,本节按 §1.3 重写:
|
||
> 只写有证据的"已知"、明确的"未定"、以及被推翻的假设。
|
||
|
||
### 7.1 已确证事实(每条附证据)
|
||
|
||
| # | 事实 | 证据 |
|
||
| --- | --- | --- |
|
||
| E1 | 答案判错→`50`,判对→`12`,稳定复现 | 乱坐标 4 次→50;正确 8+ 次→12;正确但顺序点反→50 |
|
||
| E2 | 同一套纯协议代码在雨云 aid 上 → `0` + 真实 ticket | aid=2039519451 端到端实测 |
|
||
| E3 | 真实签到流程(已登录、真页面、真浏览器、同 IP `118.248.52.65`)→ `0` + 真实 ticket | `签到成功-1.har` 条目 66/67 |
|
||
| E4 | 官方 SDK 注入在 `1ms.run` 首页(未登录)+ 真人点击 → `12` | `solve_in_browser.js` 抓到的 verify 响应 |
|
||
| E5 | **域不在白名单时 SDK 直接短路**:不出题、立刻回 `trerror_1006_...` 错误票据 | `127.0.0.1` 页面上 SDK 加载 1 秒内即回调 `ret:0`,无任何挑战 UI |
|
||
| E6 | 纯 Python 的 collect 各种变体(空/垃圾/截断/真实环境值/真实轨迹 dt)→ 全部 `12` | 变异矩阵 + 多轮实测 |
|
||
| E7 | 成功流程的 captcha 请求带 `qcloud.com` 域 cookie(`uin`/`skey`/`qcmainCSRFToken`/`intl`);我们不带 | HAR 条目 60/61/62 请求头 |
|
||
| E8 | **captcha 域"带 / 不带"腾讯云登录 cookie,结果完全相同**:单变量 A/B 均为 `12` | 2026-10-11 `scripts/experiment_h3_cookie_ab.py --both`:A=12、B=12(同一次运行、同一 tdc.js 构建,题目 2404 求解 posterior=1.000 全对,collect_len=1856) |
|
||
|
||
### 7.2 显式撤回的错误结论(§5.7)
|
||
|
||
| 曾下的结论 | 撤回理由 |
|
||
| --- | --- |
|
||
| ❌ "服务端不看 collect" | E6 里 collect 置空也 12,更可能是"校验失败即 12",不能反推"不看" |
|
||
| ❌ "环境指纹取值是瓶颈" | 填入真实值后仍 12,未证实 |
|
||
| ❌ "CDP/临时 profile 不被检测" | **依据无效**:E5 显示 127.0.0.1 上根本不出题,`ret:0` 是域授权短路,与风控无关 |
|
||
| ❌ "12 是业务账号上下文问题"(腾讯云 cookie) | 仅一次用可能过期的 skey 测试,不能定论 |
|
||
|
||
### 7.3 关键混淆变量(必须承认)
|
||
|
||
E3(成功)与 E4(失败)之间**同时变了三个变量**:
|
||
① 真实签到页 vs 注入首页 ② 已登录 vs 未登录 ③ 无 CDP vs 有 CDP
|
||
|
||
所以目前**无法**把 12 归因于其中任何一个 —— 之前归因"腾讯云 cookie"是过度解读。
|
||
|
||
### 7.4 剩余假设(互相独立,需单变量实验区分)
|
||
|
||
| # | 假设 | 状态 |
|
||
| --- | --- | --- |
|
||
| H1 | 需要**已登录业务会话 / 真实验证码页面上下文**(页面自己初始化的验证码) | 唯一未被单变量区分的剩余假设 |
|
||
| H2 | **CDP / 自动化被检测** | 唯一未被单变量区分的剩余假设 |
|
||
| H3 | 需要 `qcloud.com` 账号 cookie(有效 `uin`/`skey`) | ❌ **已排除**(E8:带/不带 cookie 单变量 A/B 均为 12) |
|
||
|
||
### 7.5 决定性实验(单变量,二选一即可闭合)
|
||
|
||
**在同一浏览器里登录 1ms,走真实签到页,让页面自己的验证码弹出并人工完成**(我方仅监听 verify):
|
||
|
||
- 结果 `0` → **H1 成立**(CDP/自动化无嫌疑),修复方向 = 复刻登录态与业务会话
|
||
- 结果 `12` → **H2 成立**(CDP 被检测),纯算+CDP 均不可行,只能半自动
|
||
|
||
若两者都排除,再做 H3(取有效 `qcloud.com` cookie 复测)。
|
||
|
||
### 7.6 与本题无关但已完成的资产
|
||
|
||
* XTEA 算法逐字节还原 + 每构建密钥提取(`tcaptcha/crypto.py`、`tdc_worker.js` captureKey)
|
||
* 文字点选求解器 8/8 回归(`tcaptcha/tests/test_glyph_solver.py`)
|
||
* 模板标定 + 纯 Python 回填管线(`generate_collect_success`)
|
||
|
||
---
|
||
|
||
## 八、路线修订(2026-10-11 晚)
|
||
|
||
### 8.1 已废弃「补环境」路线
|
||
|
||
曾实现 3 个"在伪造/真实环境里跑 tdc.js 产出 collect"的后端(node VM / browser CDP /
|
||
probe 深度 Proxy),**现已全部从代码移除**。依据:§7.1 E6 与 §6.2 对照 B/C 已证明
|
||
collect 内容、遥测后端、出口 IP、静默时长 **都不是** 12 的自变量。
|
||
|
||
废弃后唯一路径:`python_keyed`(Node 仅提取 XTEA 密钥字)+ 纯 Python 模板回填。
|
||
已删除的模块级符号:`apply_real_environment`、4 段拼接旧结构、`human_dt_ms`、
|
||
`generate_telemetry_node/browser/probe`、`generate_collect_pure`。
|
||
|
||
### 8.2 一处必须留意的置信度缺陷(已修)
|
||
|
||
`glyph_solver` 的"字形一致性强校验"此前用**单引擎 `ocr_text`** 判断,会把正确解误判为
|
||
不匹配(实测 `簿→垂`、`笆→芭`、`筹→努`),导致回归从 8/8 掉到 5/8。
|
||
现改为用**用于指派的集成得分矩阵行 argmax** 做自洽校验(每个字形的行 argmax 必须等于
|
||
其被指派目标),8/8 恢复,且该校验仍能拦住真正的"图字不符"。
|
||
|
||
### 8.3 下一步(唯一有价值的实验)
|
||
|
||
按 §7.5 做**单变量实验**,不要再用环境假说:
|
||
|
||
1. captcha 域带上成功样本的 `uin/skey/qcmainCSRFToken/intl` cookie,其余不变;
|
||
2. 登录态真页面触发 vs 未登录触发。
|
||
|
||
二选一闭合即可判定 H1(页面上下文)还是 H2(自动化检测)。
|