chore(git): har/ 下只忽略抓包产物,放行分析文档入库
做了什么 - .gitignore 的 `har/` 改为 `har/**` + `!har/**/` + `!har/**/*.md` —— 仍忽略 har/ 下的 jpg/js/json 抓包产物,但 *.md 分析文档正常入库 - 首次纳入两份文本证据资产: har/1ms_dianxuan/ANALYSIS.md (330+ 行协议分析报告) har/1ms_dianxuan/PROGRESS.md (已知/未知/已排除假设/下一步) 为什么 - 原规则是整目录忽略,把「证据文档」和「大文件抓包」一起挡在版本控制外; 按 AGENTS.md §1.3,进度与证据必须落盘可追溯,文档应随代码版本化 怎么验证的 - git check-ignore 逐项确认: PROGRESS.md/ANALYSIS.md 入库, bg_3.jpg/tdc.js/live/session.json 仍忽略 - git add 预演仅 3 个文本文件, 无二进制混入 - 已检查文档内无 API key 等凭据 (仅几个已过期住宅代理 IP)
This commit is contained in:
+5
-1
@@ -30,7 +30,11 @@ yolov8n.pt
|
||||
.env.*
|
||||
|
||||
# 抓包文件与临时压缩包
|
||||
har/
|
||||
# 注意: har/ 下同时存放「分析文档」(证据资产, 需入库) 与「抓包/图片」(大文件, 不入库),
|
||||
# 因此只忽略非 .md 产物 —— 文档按 AGENTS.md §1.3 随代码一起版本化。
|
||||
har/**
|
||||
!har/**/
|
||||
!har/**/*.md
|
||||
*.har
|
||||
*.zip
|
||||
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
# 毫秒 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\.<随机名>\s*=\s*'<base64>'` 可从 tdc.js 中提取到一份 eks,
|
||||
但**与真实浏览器产出的长度不一致**(236 vs 248),说明沙箱环境不同会让 tdc.js
|
||||
生成不同结构的 eks。
|
||||
* `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` 次)。
|
||||
@@ -0,0 +1,75 @@
|
||||
# 反 VM 密钥还原 & 纯算交付 进度文档
|
||||
|
||||
> 目标: **算法 100% 纯 Python**,Node 仅用于"跑出密钥 + 一次性标定明文模板"。
|
||||
> 依据: AGENTS.md §2.4 分层交付 / §3.3 字节码 VM 还原 / §4.3 运行时捕获 / §5.8 标定式交付。
|
||||
> 最后更新: 2026-10-11
|
||||
|
||||
---
|
||||
|
||||
## 一、已知(附证据 + 确信度)
|
||||
|
||||
| # | 结论 | 证据 | 确信度 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | `collect` 的加密算法 = **教科书 XTEA**(32 轮,`delta=0x9E3779B9`,sum 取 mod 2³²) | 用提取密钥做纯 Python 加密,与真实 tdc.js 产物**前 48 字节逐字节一致**(`f6a24e3e4fb5dd2b...`) | 确证 |
|
||||
| 2 | 原 `crypto.py` 里带加盐的写法与该算法**等价**,只是把两个密钥字折叠了:`K' = [k0, k1+salt1, k2+salt2, k3]` | 标准 XTEA + `K'=[1448563793,1850559083,1733513813,1364613714]` 解密样本 collect 得到 `{"cd":[[],"1920-1080..."` | 确证 |
|
||||
| 3 | **密钥按构建固定,不随运行变化** | 同一 tdc.js 连续 3 次运行,密文首块均为 `f6a24e3e4fb5dd2b`(首段明文是常量) | 确证 |
|
||||
| 4 | 密钥不在脚本字面量里(全脚本只 3 个数 >1e9,无一个是密钥字) | 正则清点全部 ≥7 位数字 | 确证 |
|
||||
| 5 | 密钥由 VM 在运行时通过 ADD 算出:`(sum + K[j])` 的两操作数必有一个等于 `r*DELTA` | 插桩 ADD opcode 后,样本 Top4 候选恰好是 `K'`;实时构建同样只出现 4 个高频道候选 | 确证 |
|
||||
| 6 | opcode 表的**栈变量名每份构建都不同**(样本 `g` / 雨云实时 `j` / 1ms `M`) | 同一正则在三份脚本上分别命中,写死 `g` 时实时脚本匹配数为 0 | 确证 |
|
||||
| 7 | tdc.js 至少两代明文布局,**新版字段序还随构建变化** | 旧版首段 `{"cd":[[],"1920-1080..."`;雨云实时 `{"cd":[1,[],"Apple M1",24,...`;1ms 实时 `{"cd":[1791712995,0,...` | 确证 |
|
||||
| 8 | 服务端**不校验** `collect` 与 `eks` 的同源性(宽松 aid) | 雨云 aid + 硬编码旧密钥 + 旧版明文 → `errorCode=0` + 真实 ticket | 确证 |
|
||||
| 9 | 「Node 跑密钥 + Python 全算法」端到端可用 | 雨云 aid 纯 Python 出包 → `errorCode=0`,`ticket=tr03IsAVmQyixMlGay--3wugC_...` | 确证 |
|
||||
|
||||
---
|
||||
|
||||
## 二、未知(附假设 + 确信度)
|
||||
|
||||
| # | 未知 | 当前假设 | 确信度 |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 该 aid 启用了更严格风控,要求会话具备**真实验证码页面上下文** | 中 |
|
||||
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 `1[6-9]\d{8}` 形态的大整数;轨迹是 `[x,y,t]` 三元组数组 | 中高 |
|
||||
| 3 | 明文里 `[360,360]`、`"1920-1080-1055-24-*-*-|-*"` 的作用 | 后者是屏幕/窗口指纹串,在新版被移到了 `cd` 数组末尾 | 中 |
|
||||
|
||||
---
|
||||
|
||||
## 三、已排除的假设(负结果也是资产,§5.7)
|
||||
|
||||
| 假设 | 否定证据 |
|
||||
| --- | --- |
|
||||
| 密钥每次运行随机 | 同一脚本 3 次运行首块完全一致 |
|
||||
| 密钥可从 `eks` 推导(MD5/SHA/切片) | 34 种推导 + 40 种偏移切片全部解密失败 |
|
||||
| 密钥是脚本里的字面量 | 全脚本 >1e9 的数只有 3 个 |
|
||||
| 用 `Array.prototype.push` 打桩能抓到密钥 | VM 算术 opcode 用 `g[g.length-2]=...` 直接下标赋值,`push` 桩只记录到 5783 个值且无信号 |
|
||||
| 实时构建复用旧版明文布局 | 旧版校验器对两份实时脚本全部失败;解密后首部格式完全不同 |
|
||||
| 换出口 IP 能解决 1ms 的 12 | 4 个不同省份住宅 IP(含代理)全部 12 |
|
||||
| 换遥测引擎能解决 1ms 的 12 | `python` / `python_keyed` / `node` / `browser`(真实 Chrome)全部 12 |
|
||||
| 长静默窗口能解决 1ms 的 12 | 静默 9 分钟 + 单发一次 → 仍 12 |
|
||||
|
||||
---
|
||||
|
||||
## 四、本轮目标(已完成)
|
||||
|
||||
- [x] 识别 `collect` 加密算法为教科书 XTEA 并逐字节验证
|
||||
- [x] 实现版本无关的 opcode 插桩(反向引用自动识别栈变量名)
|
||||
- [x] 从任意 tdc.js 无歧义复原 4 个密钥字(含索引歧义处理)
|
||||
- [x] 实现模板标定 + 纯 Python 回填,端到端拿到真实 ticket
|
||||
- [x] 固化回归测试(`tcaptcha/tests/test_xtea_key.py`,11 项断言)
|
||||
|
||||
## 五、下一步(可被下一轮直接执行)
|
||||
|
||||
1. **拿下 1ms 的 12**:用 CDP 打开真实验证码页面,让原生 SDK 自己跑 tdc.js,
|
||||
复用本仓库视觉求解器算坐标、`Input.dispatchMouseEvent` 在 iframe 内真实点击。
|
||||
骨架在 `tcaptcha/tdc_browser.js`。
|
||||
2. **补齐新版明文字段语义**:对照 10+ 份实时模板做差分,把时间戳/指纹/轨迹位点名。
|
||||
3. **实现 2408 图标点选**:用 1ms 的图标类别重训 `yolo_detector` + `icon_classifier`。
|
||||
4. **实现滑块 (2401/2402)**:模板匹配找缺口 + 生成滑动轨迹。
|
||||
|
||||
---
|
||||
|
||||
## 六、复验入口
|
||||
|
||||
```bash
|
||||
.venv/bin/python -m tcaptcha.tests.test_xtea_key # XTEA + 密钥提取 (11 项)
|
||||
.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解 (8 张真实样本)
|
||||
.venv/bin/python -m tcaptcha.selftest # 全部
|
||||
```
|
||||
Reference in New Issue
Block a user