refactor(纯算): 移除全部补环境后端,收敛为纯算单一路径 + 修复两处红灯

做了什么:
- 删除 generate_telemetry_node/browser/probe 三个伪造环境后端, 以及 apply_real_environment、
  4 段拼接旧结构、human_dt_ms、generate_collect_pure (crypto.py 955 -> 389 行)
- 遥测只剩纯算: python_keyed = Node 仅提取 XTEA 密钥字 + Python 模板回填 + 教科书 XTEA
- 修复 crypto.py 重复的 xtea_encrypt_block (内置自测从失败到通过), 并并入 selftest
- 修复 glyph_solver 强校验: 改用集成得分矩阵行 argmax 自洽校验, 回归 5/8 -> 8/8
- 移除注释里无证据的 errorCode=12 归因; 移除 12 自动换 IP 重试
- extract_eks 不再写死变量名; UA 常量统一为 Chrome/155
- 文档对齐: ANALYSIS §8 修订 / PROGRESS 重写 / TOOLCHAIN_REPORT §三 更正

为什么:
- 本项目证据账本已证: 12 与遥测后端/出口 IP/静默时长均无关, 补环境无收益且引入脆弱面
- 强校验用单引擎 ocr_text 会误杀正确解 (实测 簿->垂 / 笆->芭 / 筹->努)

怎么验证:
- .venv/bin/python -m tcaptcha.selftest 全绿 (crypto_core + glyph 8/8 + xtea_key)
- 端到端纯算: python / python_keyed 均可解回 '{"cd":[' 开头明文, probe 模式被拒绝
This commit is contained in:
yml2213
2026-10-11 21:30:26 +08:00
parent 60ae14d54a
commit 47ce74b160
11 changed files with 297 additions and 753 deletions
+32 -3
View File
@@ -428,6 +428,35 @@ E3(成功)与 E4(失败)之间**同时变了三个变量**:
### 7.6 与本题无关但已完成的资产
* XTEA 算法逐字节还原 + 每构建密钥提取(`tcaptcha/crypto.py`、`tdc_worker.js` captureKey)
* 文字点选求解器 8/8 回归
* 轨迹 dt 修正为真实毫秒分布(`human_dt_ms`)
* 代理提取/换 IP、模板标定回填管线
* 文字点选求解器 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(自动化检测)。
+62 -32
View File
@@ -1,8 +1,28 @@
# 反 VM 密钥还原 & 纯算交付 进度文档
> 目标: **算法 100% 纯 Python**,Node 仅用于"跑出密钥 + 一次性标定明文模板"。
> 依据: AGENTS.md §2.4 分层交付 / §3.3 字节码 VM 还原 / §4.3 运行时捕获 / §5.8 标定式交付。
> 最后更新: 2026-10-11
> 目标: **算法 100% 纯 Python** —— collect 的明文组装 / 轨迹 / 时间戳 / XTEA 加密 / 编码
> 全部在 Python 内完成; Node 只保留**一个**用途: 从当前 tdc.js 提取本次构建的 XTEA 密钥字。
> 依据: AGENTS.md §2.3(拒绝补环境) / §2.4(分层交付) / §3.3(字节码 VM) / §4.3(运行时捕获) / §5.8(标定式交付)。
> 最后更新: 2026-10-11 (纯算收敛: 移除全部补环境后端)
---
## 〇、路线变更(重要)
**已废弃「补环境」路线。** 曾经实现过 3 个"在伪造/真实环境里跑 tdc.js 产出 collect"的后端
(`node` VM 沙箱 / `browser` CDP Dispatch / `probe` 深度 Proxy),现已从代码中**全部移除**,
只保留纯算单一路径 (`python_keyed` / `python`)。
废弃依据(本项目自己的实验,见 `ANALYSIS.md` §7):
| 变量 | 结果 |
| --- | --- |
| collect 变体(空 / 垃圾 / 截断 / 真实环境值 / 真实轨迹 dt)| 全部 `errorCode=12` (E6) |
| 遥测后端 python / python_keyed / node / browser | 全部 12 |
| 出口 IP(4 个不同省份住宅代理)| 全部 12 |
| 静默 9 分钟 + 单发一次 | 仍 12 |
即"环境 / 指纹 / IP / 时序"都**不是** 12 的自变量 → 补环境没有收益,只会引入大量手写桩的脆弱面。
---
@@ -11,14 +31,14 @@
| # | 结论 | 证据 | 确信度 |
| --- | --- | --- | --- |
| 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_...` | 确证 |
| 2 | 早期带加盐的写法与该算法**等价**,只是把两个密钥字折叠了:`K' = [k0, k1+salt1, k2+salt2, k3]` | 标准 XTEA + `K'=[1448563793,1850559083,1733513813,1364613714]` 解密样本 collect 得到 `{"cd":[[],"1920-1080..."` | 确证 |
| 3 | **密钥按构建固定,不随运行变化** | 同一 tdc.js 连续 3 次运行,密文首块均为 `f6a24e3e4fb5dd2b` | 确证 |
| 4 | 密钥不在脚本字面量里 | 正则清点全脚本 ≥7 位数字,无一个是密钥字 | 确证 |
| 5 | 密钥由 VM 运行时通过 ADD 算出:`(sum + K[j])` 有一操作数等于 `r*DELTA` | 插桩 ADD opcode 后 Top4 候选即 `K'` | 确证 |
| 6 | opcode 表的**栈变量名每份构建都不同**(样本 `g` / 雨云 `j` / 1ms `M`) | 同一正则在三份脚本分别命中;写死 `g` 时实时脚本匹配 0 | 确证 |
| 7 | tdc.js 至少两代明文布局,**新版字段序随构建变化** | 旧版首段 `{"cd":[[],...`;雨云实时 `{"cd":[1,[],...`;1ms 实时 `{"cd":[1791712995,0,...` | 确证 |
| 8 | 宽松 aid 下服务端**不校验** `collect` 与 `eks` 的同源性 | 雨云 aid + 硬编码旧密钥 + 旧版明文 → `errorCode=0` + 真实 ticket | 确证(仅宽松 aid,勿外推) |
| 9 | 「Node 跑密钥 + Python 全算法」端到端可用 | 雨云 aid 纯 Python 出包 → `errorCode=0` | 确证 |
---
@@ -26,9 +46,9 @@
| # | 未知 | 当前假设 | 确信度 |
| --- | --- | --- | --- |
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 该 aid 启用了更严格风控,要求会话具备**真实验证码页面上下文** | 中 |
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 `1[6-9]\d{8}` 形态的大整数;轨迹是 `[x,y,t]` 三元组数组 | 中高 |
| 3 | 明文里 `[360,360]`、`"1920-1080-1055-24-*-*-|-*"` 的作用 | 后者是屏幕/窗口指纹串,在新版被移到了 `cd` 数组末尾 | 中 |
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 需要"真实业务会话/真实验证码页面上下文"(登录态),而非环境指纹 | **中(未做单变量实验)** |
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 `1[6-9]\d{8}` 大整数;轨迹是 `[x,y,t]` 三元组 | 中高 |
| 3 | 明文里 `[360,360]`、`"2560-1440-1345-24-*-*-| -*"` 的作用 | 屏幕/窗口指纹串 | 中 |
---
@@ -37,31 +57,40 @@
| 假设 | 否定证据 |
| --- | --- |
| 密钥每次运行随机 | 同一脚本 3 次运行首块完全一致 |
| 密钥可从 `eks` 推导(MD5/SHA/切片) | 34 种推导 + 40 种偏移切片全部解密失败 |
| 密钥可从 `eks` 推导 | 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 |
| 用 `Array.prototype.push` 打桩能抓密钥 | 算术 opcode 用 `g[g.length-2]=...` 直接下标赋值 |
| 实时构建复用旧版明文布局 | 旧版校验器对两份实时脚本全部失败 |
| **换出口 IP 能解决 1ms 的 12** | 4 个不同省份住宅 IP 全部 12 |
| **换遥测引擎能解决 1ms 的 12** | python / python_keyed / node / browser 全部 12 |
| **长静默窗口能解决 1ms 的 12** | 静默 9 分钟 + 单发 → 仍 12 |
| **"补环境"能解决 1ms 的 12** | 同上三条;环境不是自变量 |
| 轨迹 dt 太小是 12 的"直接原因" | 换成真实 ms 分布后仍 12(E6) |
---
## 四、本轮目标(已完成)
## 四、当前实现(纯算单一路径)
- [x] 识别 `collect` 加密算法为教科书 XTEA 并逐字节验证
- [x] 实现版本无关的 opcode 插桩(反向引用自动识别栈变量名)
- [x] 从任意 tdc.js 无歧义复原 4 个密钥字(含索引歧义处理)
- [x] 实现模板标定 + 纯 Python 回填,端到端拿到真实 ticket
- [x] 固化回归测试(`tcaptcha/tests/test_xtea_key.py`,11 项断言)
```
TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
├─ extract_xtea_key(tdc.js) Node 插桩 ADD opcode, 仅提取 4 个密钥字 (不可用时退回 DEFAULT_KEY)
├─ generate_collect_success() 模板回填: 轨迹 / 当前时间戳 / 随机盐 → XTEA → Base64 (单层 URL 编码)
└─ extract_eks(tdc.js) 通用正则取最长 Base64 候选 (不再写死变量名)
```
* 模板: `tcaptcha/models/success_collect_template.b64`(真实成功样本明文)
* 加密: `tcaptcha/crypto.py` 中**唯一**的教科书 XTEA
* 已从代码移除: `apply_real_environment`、4 段拼接旧结构、node/browser/probe 后端、`human_dt_ms`
---
## 五、下一步(可被下一轮直接执行)
1. **拿下 1ms 的 12**:用 CDP 打开真实验证码页面,让原生 SDK 自己跑 tdc.js,
复用本仓库视觉求解器算坐标、`Input.dispatchMouseEvent` 在 iframe 内真实点击。
骨架在 `tcaptcha/tdc_browser.js`。
2. **补齐新版明文字段语义**:对照 10+ 份实时模板做差分,把时间戳/指纹/轨迹位点名。
3. **实现 2408 图标点选**:用 1ms 的图标类别重训 `yolo_detector` + `icon_classifier`。
1. **拿下 1ms 的 12**:做账本 §7.5 的**单变量实验** ——
(a) 给 captcha 域带上成功样本里的 `uin/skey/qcmainCSRFToken/intl` cookie;
(b) 登录态真页面触发 vs 未登录触发。二选一即可闭合。
2. **补齐新版明文字段语义**:对照 10+ 份实时模板做差分。
3. **实现 2408 图标点选**:按 1ms 图标类别重训 `yolo_detector` + `icon_classifier`。
4. **实现滑块 (2401/2402)**:模板匹配找缺口 + 生成滑动轨迹。
---
@@ -69,7 +98,8 @@
## 六、复验入口
```bash
.venv/bin/python -m tcaptcha.tests.test_xtea_key # XTEA + 密钥提取 (11 项)
.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + encrypt_string 一致性
.venv/bin/python -m tcaptcha.tests.test_xtea_key # XTEA + 密钥提取 (逐字节对拍)
.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解 (8 张真实样本)
.venv/bin/python -m tcaptcha.selftest # 全部
```