feat(collect): 按 docs/tdc-案例.md 重做主链路 —— 四段加密 + 字符串指纹识别 37/37
做的什么:
1. 判决并修正 collect 结构: 不是「整体一次加密」, 而是
chunk1 / trajectory / chunk2 / sd **四段独立加密**, 前 3 段
空格 pad 到 24 字节对齐、sd 不 pad, 4 段 base64 直接拼接。
2. 按案例 §5.3 实现字符串+结构指纹的模块识别器, 把 37 个采集器
全部识别且跨构建唯一。
3. 修 reorder 的恒等映射 bug, 并补强断言。
4. 在线抓取全新样本作为唯一数据源, 清理探索性脚本。
为什么:
* 旧「整体加密 + \x00 补齐」与真机数据矛盾 —— 导致多轮实验
建立在结构错误的 collect 上(这正是此前反复"乱碰"的根因)。
* 模块 id 每份构建重排, 不能当跨构建锚点; 但模块的**代码**
(字符串集合、结构)不变 —— 这是案例指明的不变量。
* reorder 旧实现用「目标槽下标」切「源数组」, 顺序不同即全盘错位;
原测试只断言"是置换", 恒等函数也满足, 所以 bug 被放过了。
怎么验证:
* 四段结构(3 条独立证据, 其中一条不需密钥):
- 真机明文 3 处连续空格串 [+5,+10,+24], 且 pad == 24-len%24 全中
- 密文长度算术 480+96+416+56 = 1048 == 真实 1048 字符
- '=' 只出现在 1046/1048, 对应 sd 段 33B→补 7→40B
* 指纹: 4 份全新在线构建 37/37 识别、0 unknown、跨构建语义名集合
完全一致、单构建内 0 重名
* 配对正确性: 独立证据 37/37 一致; 负对照(清单下标 / 模块 id /
随机配对)均只有 2-3/37 —— 证明归一化没有过拟合
* 端到端重排 3/3 正确且非恒等
* selftest 全绿; 强断言自查: 旧实现过不了新断言
新增:
tools/tdc_modules.py 字符串+结构指纹识别(案例 §5.3)
scripts/fetch_online_samples.py 在线抓取全新 tdc.js
scripts/verify/*.py 7 个可复现的验证脚本
docs/tdc-案例.md 案例原文(看雪, 作者执着的猫)
修改:
tcaptcha/crypto.py +build_collect / split_collect_segments(四段)
tools/tdc_collectgen.py reorder 改为跨构建锚点对齐 + spec_from_seq
tools/tdc_layout.py +static_fingerprint / opcode_histogram
tools/tdc_program.py 修符号求值插值类型崩溃
tcaptcha/tests/test_collectgen.py 补「非恒等」强断言 + 在线新样本端到端
tcaptcha/client.py debug 目录改到系统临时目录(不再写仓库内 scratch/)
har/1ms_dianxuan/PROGRESS.md 新增已知 #41~#49, 显式修正被推翻的旧结论
.gitignore +scratch/ +har/online/
删除:
scratch/(含 10-11 遗留的一次性脚本, 已确认不可恢复后清理)
注意:
* 账本记录了一次自我纠错: 曾据案例 §5.5 误判 kind=2「不进 cd」,
实测(cd 38 槽、轨迹在第 10 槽)否定后已撤回。
* 仍未打通: 纯离线生成需要每个语义名的取值来源(已知 #38)。
This commit is contained in:
@@ -4,7 +4,31 @@
|
||||
> 全部在 Python 内完成; ~~Node 只保留一个用途: 从当前 tdc.js 提取本次构建的 XTEA 密钥字~~
|
||||
> **2026-10-11 起 Node 连提取都不再需要**(纯 Python 静态还原,见已知 #19~#22;Node 仅作兜底)。
|
||||
> 依据: AGENTS.md §2.3(拒绝补环境) / §2.4(分层交付) / §3.3(字节码 VM) / §4.3(运行时捕获) / §5.8(标定式交付)。
|
||||
> 最后更新: 2026-10-11 (纯 Python XTEA 密钥静态提取落地 + opcode 表按构建推导)
|
||||
> 最后更新: 2026-10-12 (**按 docs/tdc-案例.md 重做**:collect 四段加密判决 + 字符串指纹模块识别 37/37)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 2026-10-12 路线修正(对照 `docs/tdc-案例.md`,全部基于**在线新抓样本**)
|
||||
|
||||
之前多轮「乱碰」的根因,是几条**基础假设错了**:
|
||||
|
||||
| 旧假设(已推翻) | 新结论(已判决) | 证据 |
|
||||
| --- | --- | --- |
|
||||
| collect = **整体 JSON 一次加密**(`\x00` 补齐) | **4 段独立加密**,前 3 段**空格 pad 到 24 字节**,sd 不 pad | 真机明文 3 处连续空格串 `[355:+5, 422:+10, 720:+24]`;密文长度算术 480+96+416+56 = **1048**,与真实 1048 精确相等;`==` 只出现在末尾(sd 段) |
|
||||
| `kind=2` 的项"仍产出 1 槽" | **这条原本就是对的**(真机 cd 38 槽、轨迹在第 10 槽),本轮一度误判为"不进 cd"已撤回。补充一层理解:它**同时**是 chunk1/chunk2 的**分段触发点**(案例 §5.5) | 真机 cd 38 槽 + 轨迹槽位 10 + trajectory 独立成段 |
|
||||
| 模块 id 可当跨构建锚点 | id **每份构建重排**:交集 28 个里 `id→语义` 相同仅 **1/11** | 模块 8 在 A 是 `navigator.platform`,在 B 是 canvas 指纹 |
|
||||
| (自创)指令直方图是够用的锚点 | 字符串+结构指纹语义名 **37/37 唯一**;直方图只有 20/37 | 见已知 #43/#44 |
|
||||
|
||||
现在的代码路径:
|
||||
|
||||
```
|
||||
tdc_modules.analyze() -> 每项的语义名(37/37,跨构建稳定)
|
||||
tdc_collectgen.reorder(s, dst, src) -> 按语义名锚点把源样本对齐到本次构建
|
||||
crypto.build_collect(c1,traj,c2,sd,key) -> 4 段独立加密后拼接
|
||||
```
|
||||
|
||||
**数据纪律:本轮起所有新研究只用 `har/online/` 下在线抓取的样本**
|
||||
(`scripts/fetch_online_samples.py --n 4`),不再依赖仓库里的旧抓包。
|
||||
|
||||
---
|
||||
|
||||
@@ -72,6 +96,20 @@
|
||||
| 40 | **"谁在发 HTTP"是 12 的剩余唯一自变量**(结论,待最后一步坐实)| 证据链:① 槽位序证伪(§三);② 包内容证伪(本会话真实 collect 由 Python 发仍 12);③ 已知 #26:宽松 aid 190758559 上 **httpx + 无 cookie + 无头保真** 也能 0 —— 说明"请求侧细节"对宽松 aid 无所谓,但对 195672344 不够。④ 真机同 aid 只出 50/9/0(#25)。→ 剩余差在**会话↔请求方绑定**。**收口实验**:把同一 sess/eks/tkid + 同一 collect,分别由浏览器发与 Python 发,比较 errorCode | 结论(待收口) |
|
||||
| 38 | **纯离线生成还没打通(诚实标注)** | 当前 `--from-sample` 依赖**一份同机的真机明文**做重排;`--values`(语义名→值)需要调用方提供取值字典。**仍缺**:纯算地为每个语义名算出值(多数是静态环境量可配置,但 canvas/WebGL 指纹只能取自真实环境)。这一步完成前,不能声称"纯离线过风控"。 | 状态说明 |
|
||||
|
||||
### 2026-10-12 新增(对照 `docs/tdc-案例.md`,全部基于 `har/online/` 在线新抓样本)
|
||||
|
||||
| # | 结论 | 证据 | 确信度 |
|
||||
| --- | --- | --- | --- |
|
||||
| 41 | **collect 是「4 段独立加密」拼接,不是整体一次加密**:`chunk1 + trajectory + chunk2 + sd`,前 3 段**空格 pad 到 24 字节对齐**,sd 不 pad;4 段 base64 **直接拼接无分隔符** | ① 真机明文 3 处连续空格串 `[355:+5, 422:+10, 720:+24]`,且 `pad == 24 - len%24` 三段全中;② **无需密钥的纯算术判决**:360/72/312/40 → base64 480+96+416+56 = **1048**,与真实 collect 的 1048 字符精确相等;③ `==` 只出现在位置 1046/1048(正是 sd 段 33B→补 7→40B 的产物);④ 段边界与案例 §5.5 逐字吻合(chunk1 以 `,2560,` 结尾、chunk2 以 `,"UTF-8"],` 结尾) | 确证 |
|
||||
| 42 | **`kind=2`(`type_flag==2`)产出 1 槽(就是轨迹),并同时触发**分段加密**——它是 chunk1/chunk2 的分界** | 案例 §5.5;真机明文 cd **38 槽**、轨迹位于第 **10** 槽,trajectory 段 `[360:432]` 独立成段,结尾 `[1,1,12]`(案例 §6.1 的固定标记)。<br>→ 即「产槽」与「分段」**同时成立**:旧已知 #36 的"仍产出 1 槽"**未被推翻**,本轮早期我一度误判为"不进 cd",已用 cd 槽数(38)+ 轨迹槽位(10)纠正 | 确证(含一次自我纠错) |
|
||||
| 43 | **模块 id 不能当跨构建锚点**(每份构建重排) | 两份构建清单交集 28 个模块里 `id→语义` 完全相同仅 **1/11**(模块 8 在 A 是 `navigator.platform`,在 B 是 canvas 指纹);案例 §1.2 亦明确列出"37 个模块的 ID 随机重新分配" | 确证 |
|
||||
| 44 | **字符串+结构指纹可把 37 个采集器全部识别且唯一**(案例 §5.3 方案) | 4 份全新在线构建:`37/37` 识别、**0 unknown**、跨构建语义名集合**完全一致**、单构建内**0 重名**;端到端重排 3/3 正确且非恒等 | 确证 |
|
||||
| 45 | **指纹必须按 specificity 排序 + 支持前缀匹配** | `?rand=` 在真机里是 `?rand=1519713624347`(带会话随机数),精确匹配永远命中不了 → `template_url` 被误判成 `location_href`;改为 `?rand=*` 前缀匹配 + 给 `location_href` 加禁用串后消歧 | 确证(踩坑修正) |
|
||||
| 46 | **fp_salt 的随机串每构建都变**(`AyqhIdr`/`QtDKYSO`/`ryieWP_`/`mYmmgLx`),**不能**用字符串识别 | 4 份新构建各不相同;改用结构规则 `entry≥120 且 get≥100` 抓,每份构建恰好 1 个 | 确证 |
|
||||
| 47 | **结构完全相同的 2 项可用「闭包最后写入什么」消歧** | 这 2 项 entry 40 条、.get 9 条、指令逐条相同(只差 JUMP 绝对地址);差异在**闭包 target**(`CREATE_CLOSURE.target`,不是 JUMP 目标):`new Array()[0] = ""` vs `new Array()[0] = <随机int32>`。实测每份构建恰好 empty×1 + rand×1 | 确证 |
|
||||
| 48 | **配对正确性经独立证据 + 负对照验证** | 独立证据:同一归一化指令序列(抹掉地址/模块号/字符值)下 37/37 全等(唯一例外 fp_salt 的随机 int32,抹掉后 37/37);负对照:清单下标配对 2-3/37、模块 id 配对 2-3/37、随机配对 2/37 —— 对照组极低,证明归一化**没有过拟合** | 确证 |
|
||||
| 49 | **`reorder` 旧实现是恒等映射(真 bug,已修)** | 旧代码 `sample[slot:slot+n]` 里的 `slot` 是**目标**构建自己的槽下标 → 顺序不同时全盘错位、相同时才碰巧对。原测试只断言"是置换",恒等函数也满足 → **弱断言把 bug 放过了**。已补强断言(== 已知正确答案 且 != 原样),并自查:旧实现过不了新断言 | 确证 |
|
||||
|
||||
---
|
||||
|
||||
## 二、未知(附假设 + 确信度)
|
||||
@@ -111,6 +149,17 @@
|
||||
| **H5:1ms 后端用 `identity_context` 把会话标记为可信** | 拿抓包里**签名过的** `identity_context`(599B `emctx1.*`) 先调 `POST /api/v1/captcha/params`(响应与不带时**完全相同**,无额外 token)再解题,仍 12 → H5 排除 |
|
||||
| **"请求侧保真度不够"这一整类** | 直接反证:aid 190758559 上 **未做任何保真的 httpx + 无 cookie** 也拿到 ticket(0);即请求侧细节本就足够,12 与保真度无关 |
|
||||
|
||||
### 2026-10-12 新增推翻的假设(对照 `docs/tdc-案例.md`)
|
||||
|
||||
| 假设 | 否定证据 |
|
||||
| --- | --- |
|
||||
| **"整体加密"是 collect 的结构**(旧 `crypto.encrypt_string` / `tdc_collectgen.encrypt_plain` 的做法) | 判决书据 3 条,互相独立:<br>① 真机明文有 **3 处连续空格串**(`+5 / +10 / +24`),`\x00` 补齐不会产生空格;<br>② **不需要密钥**的纯算术:4 段长度 360/72/312/40 → base64 480+96+416+56 = **1048**,与真实 collect 的 1048 字符精确相等(差一个字节都不行);<br>③ `==` 只出现在 1046/1048,恰好对应 sd 段 33B→XTEA 补 7→40B。<br>→ 改为 `crypto.build_collect()`(4 段独立加密 + 空格 pad 到 24)。旧函数**仍保留但已不是主路径**。<br>⚠️ 注意被推翻的是**加密结构**,不是槽数模型——cd 仍是 38 槽(见已知 #42)。 |
|
||||
| ~~**`kind=2` 的项"仍产出 1 槽"是错的**~~ ⚠️ **本条是我 2026-10-12 的误判,已撤回** | 我曾据案例 §5.5 断言 `type_flag==2`"不进 cd"、并据此认为旧"37 项→38 槽"推导被推翻。**实测否定了这个断言**:真机明文 cd 就是 **38 槽**,轨迹位于**第 10 槽**。→ 真相是「产 1 槽」与「触发分段」**同时成立**,旧已知 #36 仍然正确。教训:引用外部案例时必须用本项目数据复核,不能拿案例文字直接覆盖已有实测(见已知 #42)。 |
|
||||
| **模块 id 可当跨构建对齐锚点** | 清单交集 28 个模块里 `id→语义` 完全相同仅 **1/11**;案例 §1.2 明确"37 个模块的 ID 随机重新分配"。→ 改用**语义名**做锚点(已知 #44)。 |
|
||||
| **自创的「指令直方图」是够用的锚点** | 直方图跨构建交集 37/37 但单构建内唯一仅 **20/37**;语义名方案 **37/37 唯一**。直方图降级为兜底锚点。 |
|
||||
| **fp_salt 可用固定字符串识别**(旧规则写死了 `AyqhIdr`) | 4 份新构建里分别是 `AyqhIdr` / `QtDKYSO` / `ryieWP_` / `mYmmgLx` —— **每构建都变**。改用结构规则识别(已知 #46)。 |
|
||||
| **"按模块 id 配对就能跨构建对齐"** | 同 #43;且我用"模块 id 相等"做校验时**自己误判过一次**(看到 35/37 不一致就以为配对错了)—— 实际上 id 不一致是**预期行为**,不能用来否定配对。正确校验见已知 #48。 |
|
||||
|
||||
---
|
||||
|
||||
## 四、当前实现(纯算单一路径)
|
||||
@@ -128,15 +177,53 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
|
||||
```
|
||||
|
||||
* 纯 Python 提取器: `tcaptcha/tdc_key.py` (算法) + `tcaptcha/tdc_opcodes.py` (opcode 表推导)
|
||||
* **2026-10-12 新增(按案例重做后的主路径):**
|
||||
* `tools/tdc_modules.py` —— 字符串+结构指纹识别 37 个采集器(**37/37 唯一**,案例 §5.3)
|
||||
* `tcaptcha/crypto.py::build_collect()` —— **4 段独立加密**(前 3 段空格 pad 到 24,sd 不 pad)
|
||||
* `tcaptcha/crypto.py::split_collect_segments()` —— 从已解密明文切回 4 段(对拍/标定用)
|
||||
* `tools/tdc_collectgen.py::reorder(sample, dst, src)` —— 按语义名锚点**跨构建**对齐(旧恒等 bug 已修)
|
||||
* `scripts/fetch_online_samples.py` —— 在线抓取全新 tdc.js(**新研究的数据来源**)
|
||||
* CLI: `tools/tdc_key_pure.py [tdc.js] [-v] [--opcodes]`
|
||||
* 模板: `tcaptcha/models/success_collect_template.b64`(真实成功样本明文)
|
||||
* 加密: `tcaptcha/crypto.py` 中**唯一**的教科书 XTEA
|
||||
* Node 现存用途: 仅 `tdc_worker.js` 兜底提取密钥(主路径已不需要)
|
||||
|
||||
### 2026-10-12 起的新主路径(替代上面 `generate_collect_success` 的模板回填)
|
||||
|
||||
```
|
||||
1. scripts/fetch_online_samples.py -> har/online/<ts>/tdc_*.js(本次会话的构建)
|
||||
2. tdc_key.extract_xtea_key_pure(js) -> 本次构建的 XTEA 密钥(纯 Python,零 Node)
|
||||
3. tdc_modules.analyze(js) -> 37 项的语义名 + require 顺序(37/37 唯一)
|
||||
4. tdc_collectgen.reorder(cd, dst, src) -> 把标定样本对齐到本次构建(跨构建锚点)
|
||||
5. crypto.build_collect(c1,traj,c2,sd,k) -> 4 段独立加密后拼接 = collect
|
||||
```
|
||||
|
||||
⚠️ 第 4 步仍需要一份**同源标定样本**提供取值(已知 #38)。这是当前唯一未打通的环节。
|
||||
|
||||
---
|
||||
|
||||
## 五、下一步(可被下一轮直接执行)
|
||||
|
||||
### 🆕 2026-10-12 优先(按案例重做后的收尾,按成本排序)
|
||||
|
||||
0. **把"纯离线生成"打通(唯一剩余阻塞点)**。当前链路第 4 步仍需一份**同源标定样本**
|
||||
提供每个语义名的取值(已知 #38)。要做的是:为每个语义名写一个**取值生成器**
|
||||
—— 多数是静态环境量(UA / 屏幕 / 时区 / 语言 / 核数 / charset)可直接配置,
|
||||
canvas/WebGL 指纹用 `fingerprint/profiles.json` 画像(案例 §6.3 的做法)。
|
||||
完成标志:不依赖任何真机明文,纯算生成 collect 并拿到 `errorCode=0`。
|
||||
1. **把 4 段结构接进生成链路**(已知 #41 已判决,但 `SlotSpec` 还没产出段边界):
|
||||
`tdc_collectgen` 目前只输出一个扁平 `cd` 列表;需要额外给出
|
||||
`chunk1 / trajectory / chunk2 / sd` 四段(轨迹项所在位置即分界),
|
||||
再交给 `crypto.build_collect()`。改完用真机明文对拍总长度(应 1048 字符量级)。
|
||||
2. **用在线新样本做一次端到端验证**:`fetch_online_samples.py` 抓构建 → 纯算生成
|
||||
collect → 打 verify。先在**宽松 aid 190758559** 上跑(已知 #26 那里 httpx 裸发就能 0),
|
||||
通过后再上 1ms。
|
||||
3. **1ms 的 12 收口实验**(已知 #40):同一 sess/eks/tkid + 同一 collect,
|
||||
分别由浏览器发与 Python 发,比较 errorCode。这一步必须在上面 0~2 完成之后才有意义
|
||||
—— 否则"包内容"这个变量本身还没被正确构造出来,实验没有区分度。
|
||||
|
||||
### 既有项
|
||||
|
||||
0. ~~**静态提取 XTEA 密钥(去掉最后一个 Node 依赖)**~~ —— **已完成**(已知 #19~#22):
|
||||
`tcaptcha/tdc_key.py` 纯 Python 零 Node,6/6 构建与 Node 逐字一致。
|
||||
残留增强项(非阻塞):① 若服务端改了 handler 语义,`tdc_opcodes.derive_opcode_table`
|
||||
@@ -209,3 +296,31 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
|
||||
.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 打印按构建推导的 opcode 表
|
||||
.venv/bin/python scripts/analyze_collect_layout.py --json /tmp/cap.json # collect 槽位序对比 (已知 #31)
|
||||
```
|
||||
|
||||
### 2026-10-12 新增的复验入口(按案例重做后)
|
||||
|
||||
```bash
|
||||
# ① 在线抓取全新样本(新研究的数据来源;同一个 URL 每次返回不同构建,见 #29)
|
||||
.venv/bin/python scripts/fetch_online_samples.py --n 4
|
||||
|
||||
# ② 字符串+结构指纹识别 37 个采集器(应 37/37、0 unknown、0 重名)
|
||||
.venv/bin/python tools/tdc_modules.py har/online/<ts>/tdc_01_*.js
|
||||
|
||||
# ③ 四段结构判决(不需要密钥:密文长度算术应精确等于 1048 / 该份的长度)
|
||||
.venv/bin/python scripts/verify/verify_segment.py <已解密的 collect 明文文件>
|
||||
.venv/bin/python scripts/verify/verify_seglen.py <collect 的 base64>
|
||||
|
||||
# ④ 指纹跨构建一致性(语义名集合应完全一致、单构建内 0 重名)
|
||||
.venv/bin/python scripts/verify/verify_fp_consistency.py
|
||||
|
||||
# ⑤ 配对正确性(实验组≈37/37,三个负对照应 2-3/37 —— 证明没过拟合)
|
||||
.venv/bin/python scripts/verify/verify_pair_correct.py
|
||||
.venv/bin/python scripts/verify/verify_control_negative.py
|
||||
|
||||
# ⑥ 端到端重排(新数据,应 3/3 正确且非恒等)
|
||||
.venv/bin/python scripts/verify/verify_reorder.py
|
||||
|
||||
# ⑦ 回归测试(已并入 selftest,含「reorder 非恒等」强断言)
|
||||
.venv/bin/python -m tcaptcha.tests.test_collectgen
|
||||
.venv/bin/python -m tcaptcha.selftest
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user