做了什么: - PROGRESS.md §一 已知表新增 4 条: 10. XTEA 密钥字 = 4 个「ASCII 打包的 4 字符串」(大端拼 32bit) 11. 密钥既不是立即数也不是字面字符串(明文/大端字节/base64 都搜不到) 12. 密钥数组经闭包捕获传入: @09463 CREATE_CLOSURE caps=[r7=r8], XTEA 轮以 reg7[(sum>>>11)&3] 取用 13. 轮加盐随构建变化(本构建 723726/855311, 台账另一构建 2829068/328986), 但都是可见立即数 - PROGRESS.md §五 下一步新增第 0 条: 静态提取密钥(逐闭包符号执行) 为什么: - 这是"去掉最后一个 Node 依赖"的关键定位, 避免重复摸索 怎么验证: - 密钥字节解剖: 1818510928 -> 0x6c644650 "ldFP" 等 (core_js/tdc_sample.js) - 反汇编: .venv/bin/python tools/tdc_disasm.py tcaptcha/core_js/tdc_sample.js
115 lines
7.6 KiB
Markdown
115 lines
7.6 KiB
Markdown
# 反 VM 密钥还原 & 纯算交付 进度文档
|
||
|
||
> 目标: **算法 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 的自变量 → 补环境没有收益,只会引入大量手写桩的脆弱面。
|
||
|
||
---
|
||
|
||
## 一、已知(附证据 + 确信度)
|
||
|
||
| # | 结论 | 证据 | 确信度 |
|
||
| --- | --- | --- | --- |
|
||
| 1 | `collect` 的加密算法 = **教科书 XTEA**(32 轮,`delta=0x9E3779B9`,sum 取 mod 2³²) | 用提取密钥做纯 Python 加密,与真实 tdc.js 产物**前 48 字节逐字节一致**(`f6a24e3e4fb5dd2b...`) | 确证 |
|
||
| 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` | 确证 |
|
||
| 10 | **XTEA 密钥字是 4 个「ASCII 打包的 4 字符串」**(每字 = 4 个可打印字符按大端拼成 32bit)| `core_js/tdc_sample.js` 的密钥 `[1818510928,1381395306,1245924197,1768384072]` 大端解码 = `ldFP`/`RVkj`/`JCKe`/`igfH`;root 样本 = `VWTQ`/`nMJk`/`gSRU`/`QVZR` | 确证 |
|
||
| 11 | 密钥字**既不是立即数、也不是字面字符串**(无论明文、大端字节序列、base64 都不在 tdc.js/字节码里)| 立即数 1.3e9~1.9e9 区间为空;`ldFPRVkjJCKeigfH` 及其 base64 均搜不到 | 确证 |
|
||
| 12 | 密钥数组在 XTEA 轮里通过**寄存器间接寻址**读取,且经闭包捕获传入 | 反汇编 `reg7[(sum>>>11)&3]`;`@09463 CREATE_CLOSURE target=@9109 caps=[r7=r8]` | 确证 |
|
||
| 13 | 轮加盐**每次构建都不同**(但都是可见立即数)| 本构建 = `723726`(j==2) / `855311`(j==3);台账另一构建 = `2829068` / `328986` | 确证 |
|
||
|
||
---
|
||
|
||
## 二、未知(附假设 + 确信度)
|
||
|
||
| # | 未知 | 当前假设 | 确信度 |
|
||
| --- | --- | --- | --- |
|
||
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 需要"真实业务会话/真实验证码页面上下文"(登录态),而非环境指纹 | **中(未做单变量实验)** |
|
||
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 `1[6-9]\d{8}` 大整数;轨迹是 `[x,y,t]` 三元组 | 中高 |
|
||
| 3 | 明文里 `[360,360]`、`"2560-1440-1345-24-*-*-| -*"` 的作用 | 屏幕/窗口指纹串 | 中 |
|
||
|
||
---
|
||
|
||
## 三、已排除的假设(负结果也是资产,§5.7)
|
||
|
||
| 假设 | 否定证据 |
|
||
| --- | --- |
|
||
| 密钥每次运行随机 | 同一脚本 3 次运行首块完全一致 |
|
||
| 密钥可从 `eks` 推导 | 34 种推导 + 40 种偏移切片全部失败 |
|
||
| 密钥是脚本里的字面量 | 全脚本 >1e9 的数只有 3 个 |
|
||
| 用 `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** | 同上三条;环境不是自变量 |
|
||
| **captcha 域带腾讯云登录 cookie 能解决 12 (H3)** | 单变量 A/B:带/不带均 12(`scripts/experiment_h3_cookie_ab.py`,2026-10-11)|
|
||
| 轨迹 dt 太小是 12 的"直接原因" | 换成真实 ms 分布后仍 12(E6) |
|
||
|
||
---
|
||
|
||
## 四、当前实现(纯算单一路径)
|
||
|
||
```
|
||
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`
|
||
|
||
---
|
||
|
||
## 五、下一步(可被下一轮直接执行)
|
||
|
||
0. **静态提取 XTEA 密钥(去掉最后一个 Node 依赖)**:密钥字是 ASCII 打包串、且不是字面量,
|
||
需对**正确反汇编**(`tools/tdc_disasm.py`)做一个小的**逐闭包符号执行/数据流**:
|
||
从 `CREATE_CLOSURE caps=[r7=r8]` 反查父闭包 r8 的赋值表达式并常量折叠。
|
||
寄存器是「每闭包一份、经 `PUSH_IMM_ARRAY <idx>` 引用间接访问」,所以需要真正的数据流而非模式匹配。
|
||
1. **拿下 1ms 的 12**:做账本 §7.5 的**单变量实验** ——
|
||
(a) 给 captcha 域带上成功样本里的 `uin/skey/qcmainCSRFToken/intl` cookie(已做,H3 已排除);
|
||
(b) 登录态真页面触发 vs 未登录触发。二选一即可闭合。
|
||
2. **补齐新版明文字段语义**:对照 10+ 份实时模板做差分。
|
||
3. **实现 2408 图标点选**:按 1ms 图标类别重训 `yolo_detector` + `icon_classifier`。
|
||
4. **实现滑块 (2401/2402)**:模板匹配找缺口 + 生成滑动轨迹。
|
||
|
||
---
|
||
|
||
## 六、复验入口
|
||
|
||
```bash
|
||
.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 # 全部
|
||
```
|