feat(collect): 纯离线生成打通 1ms 签到文字点选 —— errorCode=0 收口

做了什么:
- 新增 tcaptcha/tdc_values.py: 37 个语义名的取值生成器(静态画像 / 会话量 /
  每次采集量三层)+ build_plain 四段组装 + generate_collect 纯离线出包
- crypto: 新增 generate_telemetry(mode="python_values");轨迹生成统一到
  trajectory_points()(首点绝对坐标 + 增量 + 固定收尾 [1,1,12])
- crypto.split_collect_segments 重写: pad 只有 1 个空格时旧 ` {2,}` 判据会漏段,
  且 UA 串里的普通空格会假阳性 —— 改为「模 24 算术 + 不落在 JSON 字符串内部 + 结构校验」
- tools/tdc_modules: trajectory 指纹原先指向只做 JSON.stringify 的模块,改为
  mouseMoveEvent/touchStartEvent(真轨迹模块),37/37 不变
- client 默认 telemetry_mode 改为 python_values
- 新增 scripts/verify/{align_session,audit_names,verify_values,verify_offline,send_test}.py
  与 tools/{cdp_collect,cdp_dump_scripts,tdc_profile}.py

为什么:
- 账本 #40「谁在发 HTTP 是 12 的剩余自变量」被推翻。同 aid/同客户端/同求解器、
  答案都对,只换 collect 构造方式: 本次会话构建生成 -> 0 (4/4),
  旧模板回填 -> 12 (2/2)。真因是 collect 必须与本次会话构建同源。

怎么验证:
- .venv/bin/python -m tcaptcha.selftest  全绿(含新增四段切分与纯离线闭环回归)
- scripts/verify/verify_values.py        4 份真机明文 152/152 逐槽全等
- scripts/verify/verify_offline.py       逐槽全等 + 分段几何一致
- scripts/verify/send_test.py --mode python_values  1ms 签到页文字点选 errorCode=0 x4
This commit is contained in:
yml2213
2026-10-12 03:38:18 +08:00
parent 80a122245a
commit 51dff870cf
18 changed files with 2102 additions and 91 deletions
+59 -20
View File
@@ -93,8 +93,8 @@ crypto.build_collect(c1,traj,c2,sd,key) -> 4 段独立加密后拼接
| 36 | **「清单项 → cd 槽」的精确规则(同一构建端到端校验确立)** | 装配循环(线上构建 `@4362`,370 条)分支树已读通:<br>`if(!item) continue;` `v,w,kind = item.get();`<br>· **`kind===2`(`@4820`)**:把 `r3` 清空转存 `r4`(`@4860`),`r4` 在收尾 `@4905` 二次加工后**拼回**,因此**仍产出 1 槽**(实测第 0 项,随机串 `vXc29RxhvT34Q…`)<br>· `v===null/undefined`(`@4799`)→ `out += "null"`<br>· `typeof v==="number"`(`@4738`)→ `out += (v||0)`<br>· **`kind===1`(`@4724`)→ `out += v` 原样拼**:返回值是 JSON 碎片串;**只按括号外的逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽,而 `"641547489,1791732001"` → 2 槽<br>· 其余(`@4703`)→ `out += '"' + v + '"'`<br>· 收尾 `@4753`:`typeof w === "number"` 才再 +1 槽(实测 kind=2 的第二元素是 null,不触发;此路径本次未命中)<br>→ **37 项 → 38 槽**,仅第 33 项(模块 1)占 2 槽。工具:`tools/tdc_slotmap.py`(`--items` 运行时取样 + `--plain` 明文对拍) | 确证 |
| 37 | **按构建生成 collect 的工具链已跑通(含回环自测)** | `tools/tdc_collectgen.py`:静态导出槽位序 → 按 #36 规则算「项→槽」→ 重排 → 该构建密钥加密。<br>实测(同一构建):37 项 → **38 槽**,明文 686B / collect 920 字符;**解密回环逐字节一致**,cd 与真机样本逐槽相同。<br>规则修正(重要):`kind=1` 的串**只按括号外逗号**切槽 —— `'[]'`/`'[360,360]'`/`'["zh-CN","zh"]'` 内的逗号不构成新槽;`kind=2` 的项**仍产出 1 槽**(转存 r4 后在收尾 @4905 拼回)。<br>回归:`tcaptcha/tests/test_collectgen.py`(已并入 selftest) | 确证 |
| 39 | **12 只在"答案正确"之后才判**(排查方法学上的关键事实)| 答错一律 50/9:本轮滑块题型下(我们按点选坐标答,必错)两组都是 50;换成真解(text/icon)后立刻全是 12。→ **任何用"答错"做的对照实验都没有区分度**,这解释了早期多轮对照为何无效。今后做 12 的 A/B 必须 `--solve` 真解题 | 确证 |
| 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 指纹只能取自真实环境)。这一步完成前,不能声称"纯离线过风控"。 | 状态说明 |
| 40 | ~~**"谁在发 HTTP"是 12 的剩余唯一自变量**~~ **已推翻(2026-10-12)→ 见已知 #58/#59**:自变量是「collect 有没有用本次会话的构建生成」,与谁发 HTTP 无关(Python + httpx 直接发本次构建生成的包 → 0) | 原证据链:① 槽位序证伪(§三);② 包内容证伪(本会话真实 collect 由 Python 发仍 12);③ 已知 #26:宽松 aid 190758559 上 **httpx + 无 cookie + 无头保真** 也能 0 —— 说明"请求侧细节"对宽松 aid 无所谓,但对 195672344 不够。④ 真机同 aid 只出 50/9/0(#25)。→ 剩余差在**会话↔请求方绑定**。**收口实验**:把同一 sess/eks/tkid + 同一 collect,分别由浏览器发与 Python 发,比较 errorCode | 结论(待收口) |
| 38 | ~~**纯离线生成还没打通**~~ **已解决(2026-10-12)→ 见 #57/#58/#59**:`tcaptcha/tdc_values.py` 已为 37 个语义名全部提供取值,并在 1ms aid 上 3/3 拿到 `errorCode=0`。当时状态:当前 `--from-sample` 依赖**一份同机的真机明文**做重排;`--values`(语义名→值)需要调用方提供取值字典。**仍缺**:纯算地为每个语义名算出值(多数是静态环境量可配置,但 canvas/WebGL 指纹只能取自真实环境)。这一步完成前,不能声称"纯离线过风控"。 | 状态说明 |
### 2026-10-12 新增(对照 `docs/tdc-案例.md`,全部基于 `har/online/` 在线新抓样本)
@@ -109,6 +109,16 @@ crypto.build_collect(c1,traj,c2,sd,key) -> 4 段独立加密后拼接
| 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 放过了**。已补强断言(== 已知正确答案 且 != 原样),并自查:旧实现过不了新断言 | 确证 |
| 50 | **空格 pad 可以只有 1 个字节** → 用 ` {2,}` 找 pad 会漏段,旧 `split_collect_segments` 在这种样本上直接抛异常 | 真机同会话明文:chunk1 = **287** 字节,`287 % 24 == 23` → pad = **1**,它就夹在 `...,0,0,0, [[1,1,12]]...` 之间;旧函数只找到 2 个空格串 → `ValueError: 期望 3 个空格填充串` | 确证(踩坑修正) |
| 51 | **pad 判据必须再加一条「不落在 JSON 字符串内部」** | 只用 `b%24==0 且 (b-a)==24-a%24` 会把 UA 串 `Mozilla/5.0 (Macintosh…` 里偏移 47 的**普通空格**判成 pad(47%24==23);也不能只靠「去掉 pad 后能否 json.loads」排除(少一个空格仍是合法 JSON 串)。用**引号奇偶**判定后,3 份样本候选恰好各 3 个 | 确证 |
| 52 | **轨迹是独立的一段,轨迹槽就是分段点**(3 份样本逐字节一致) | 真机成功样本 441 / **616** / 217 / 33;本会话两次 287 / **10**(`[[1,1,12]]`)/ 356 / 16 与 287 / **516**(61 点)/ 356 / 16。分段写法:chunk1 以 `,` 结尾、traj 是**一个**完整 JSON 值、chunk2 以 `,` 开头且以 `],` 结尾 | 确证 |
| 53 | **`trajectory` 语义名此前认错了模块** | 旧规则 `{"stringify"}` 抓到的是另一个只做 JSON.stringify 的模块(值恒为 `[]`);真轨迹模块的字符串集是 `mouseMoveEvent / mousemove / touchStartEvent / touchmove / tCaptchaDyContent`(entry 279 条)。改规则后仍 37/37、0 unknown,且该模块**正好落在明文分段点上** —— 两条独立证据互证 | 确证(踩坑修正) |
| 54 | **37 个语义名 → 真值全部拿到(38/38 槽全覆盖)** | 用真 Chrome 取「同会话构建 + 同会话 `TDC.getData(true)`」,按「require 序 == 槽位序」对齐(`scripts/verify/align_session.py`);再用取值生成器对 **4 次连续采集**逐槽复算,**152/152 全等**(`scripts/verify/verify_values.py`) | 确证 |
| 55 | **必须区分「会话量」与「每次采集量」** | 4 次 `getData` 实测:`itoken` / 会话随机盐(`struct_40_9#…_rand` = 1591912482)/ 页面加载时刻(`window_time#e48` = 1791745224)**四次完全不变**;而当前时刻(`window_time#e40`)、采集计数(`struct_46_9` = 1,2,3,4)、轨迹**每次都变**。另:`getData` 会把轨迹**清空**(61 点 → 下一次回到 `[[1,1,12]]`) | 确证 |
| 56 | **屏幕宽高方向纠正**:`screen_width = 1440`、`screen_height = 2560` | 浏览器真值 `screen.width/height = 1440/2560`(`availHeight` 2530、`colorDepth` 24、`dpr` 1、`hardwareConcurrency` 10、`getTimezoneOffset` -480),与明文槽位及 `1440-2560-2530-24-*-*-| -*` 串**顺序完全一致**;旧结论「串的 parts[0] → screen_height」是**反的** | 确证(含一次纠正) |
| 57 | **纯离线闭环已打通**(本任务的核心阻塞点) | `scripts/verify/verify_offline.py`:tdc.js → 37 语义名 → 38 槽取值 → 4 段明文 → 4 段加密拼接 → 解密/切段/解析 → **逐槽全等 + 分段几何一致**;产物末尾 `==` 只来自未 pad 的 sd 段 | 确证 |
| 58 | **纯离线生成的 collect 在 1ms aid=195672344 上端到端拿到 `errorCode=0` + 真 ticket**(3/3) | 2026-10-12 实测 `scripts/verify/send_test.py --mode python_values`(aid 195672344 / 签到页 / 2404 文字点选 / 零 Node)。三次分别拿到 `tr03tgI-J732…`、`tr03RLpt4OTMy…`、`tr03n_V7CXAFI…`。每颗 collect 都由**本次会话同一份 tdc.js** 纯离线生成:段几何 461/625/147/16、460/640/148/16、81/708/529/16(每份构建清单序不同 → 分界点不同),轨迹 61/63/71 点、尾点一律 `[1,1,12]` | 确证 |
| 59 | **A/B 隔离出真正自变量:collect 必须「用本次会话的构建」生成** | 同 aid / 同一天 / 同客户端 / 同求解器、答案都正确,只换 collect 构造方式:<br>`python_values`(本次会话构建 → 槽序与取值都对)→ **0**(3/3);<br>`python_keyed`(仓库里那份 **2026-10-11** 成功样本模板回填:槽序来自**另一份构建**,屏串还是**另一台设备**的 `2560-1440-1345-24`)→ **12**(2/2)。<br>→ **推翻已知 #40**("谁在发 HTTP");同时把 #31/#32 的"槽序/取值必须与本次会话构建同源"从假设升级为确证 | 确证(含推翻 #40) |
---
@@ -117,8 +127,8 @@ crypto.build_collect(c1,traj,c2,sd,key) -> 4 段独立加密后拼接
| # | 未知 | 当前假设 | 确信度 |
| --- | --- | --- | --- |
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 需要"真实业务会话/真实验证码页面上下文"(登录态),而非环境指纹 | **中(未做单变量实验)** |
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 `1[6-9]\d{8}` 大整数;轨迹是 `[x,y,t]` 三元组 | 中高 |
| 3 | 明文里 `[360,360]`、`"2560-1440-1345-24-*-*-| -*"` 的作用 | 屏幕/窗口指纹串 | 中 |
| 2 | ~~新版明文字段语义(哪一位是时间戳/指纹/轨迹)~~ **已解决** | 见已知 #54:37 个语义名 → 38 槽真值全部对齐,且被生成器逐槽复现 | 已确证 |
| 3 | ~~明文里 `[360,360]`、`"1440-2560-2530-24-*-*-| -*"` 的作用~~ **已解决** | `[360,360]` = 验证码 iframe 可视区(`viewport_size`);屏串字段序 = **宽-高-可用高-色深-\\*-\\*-\\|-\\***,与浏览器真值 1440/2560/2530/24 一一对应 | 已确证 |
---
@@ -198,7 +208,7 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
5. crypto.build_collect(c1,traj,c2,sd,k) -> 4 段独立加密后拼接 = collect
```
⚠️ 第 4 步仍需要一份**同源标定样本**提供取值(已知 #38)。这是当前唯一未打通的环节。
✅ 第 4 步已可**纯离线**完成:`tcaptcha/tdc_values.py` 为 37 个语义名提供取值生成器(已知 #54/#57)。
---
@@ -206,21 +216,27 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
### 🆕 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. ✅ **已完成(2026-10-12)—— 纯离线生成已打通**。做法:真 Chrome 取「同会话构建 +
同会话 `TDC.getData(true)`」按位置对齐,拿到 37 个语义名 -> 真值(38/38 槽,已知 #54),
再固化成 `tcaptcha/tdc_values.py`(分**静态画像 / 会话量 / 每次采集量**三层)。
复验:`verify_values.py` 对 4 份真机明文 **152/152 逐槽全等**;
`verify_offline.py` 纯离线闭环**逐槽全等 + 分段几何一致**。
仍未标定:**真实拖拽轨迹的采样分布**(本会话的 61 点轨迹是 CDP 注入 mousemove 得到的,
真机人手拖拽的 dt / 位移分布还没采过 —— 现有的 `_success_style_trajectory()` 是旧拟合)。
1. ✅ **已完成**:4 段结构已接进生成链路 —— `tdc_values.build_plain()` 按
「轨迹槽 = 分界点」产出 `chunk1 / trajectory / chunk2 / sd`,直接喂 `crypto.build_collect()`。
⚠️ 注意 pad **可能只有 1 个空格**(已知 #50),切分/对拍一律用修好的
`crypto.split_collect_segments()`,别再自己写 ` {2,}` 找 pad。
2. ✅ **已完成(2026-10-12)**:端到端验证直接上 1ms 签到页文字点选,**3/3 拿到 `errorCode=0` +
真 ticket**(见已知 #58)。不需要再走宽松 aid 做前置台阶 —— 直接打真靶即可。
3. ✅ **已完成(2026-10-12)—— 12 的归因收口**。结论与当时的预期**相反**:#40 假设的
"谁在发 HTTP" 被推翻。真正的自变量是**collect 有没有用本次会话的构建生成**:
同 aid / 同客户端 / 同求解器、答案都对,`python_values`(本次构建生成)**3/3 = 0**,
`python_keyed`(旧模板回填,槽序来自另一份构建、屏串是另一台设备的)**2/2 = 12**。
复现入口:`.venv/bin/python scripts/verify/send_test.py --mode python_values`
(对照加 `--mode python_keyed`)。
新暴露的可复现特性:每次 prehandle 的构建都不同 → 段几何随构建变(461/625/147/16、81/708/529/16…),
所以**任何"固定模板"的路线都不可能长期成立**,必须按会话构建生成。
### 既有项
@@ -323,4 +339,27 @@ TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
# ⑦ 回归测试(已并入 selftest,含「reorder 非恒等」强断言)
.venv/bin/python -m tcaptcha.tests.test_collectgen
.venv/bin/python -m tcaptcha.selftest
# ⑧ 「同会话构建 + 同会话 collect」的取值标定(这一步打通了纯离线生成的最后阻塞点)
# 先备好同源配对(真 Chrome,9222):
# node tools/cdp_dump_scripts.js --match drag_ele --outdir /tmp/1ms_bridge/scripts
# cp /tmp/1ms_bridge/scripts/<含 VM 的那个>.js /tmp/1ms_bridge/session_build.js
# node tools/cdp_collect.js --match drag_ele --out /tmp/1ms_bridge/session_collect.json
.venv/bin/python scripts/verify/align_session.py # 按位置对齐 -> name_values.json
.venv/bin/python scripts/verify/audit_names.py # 语义名 / 模块结构 / 槽值 联合审计
.venv/bin/python scripts/verify/verify_values.py \
--collects /tmp/1ms_bridge/session_collect.json /tmp/1ms_bridge/session_collect2.json \
/tmp/1ms_bridge/session_collect3.json /tmp/1ms_bridge/session_collect4.json
# 期望: 逐槽全等 152/152(4 份 x 38 槽)
# ⑨ 纯离线闭环(tdc.js -> 37 语义名 -> 38 槽取值 -> 4 段明文 -> 4 段加密 -> 解回对拍)
.venv/bin/python scripts/verify/verify_offline.py /tmp/1ms_bridge/session_build.js
# 期望: 逐槽全等=是、分段几何=一致
# ⑩ 真实发送测试(**只打 1ms 签到页的文字点选**;会真的 POST verify)
.venv/bin/python scripts/verify/send_test.py --mode python_values # 期望 errorCode=0
.venv/bin/python scripts/verify/send_test.py --mode python_keyed # 对照:期望 12
# 脚本会 hook generate_telemetry,打印这一颗 collect 的段几何 + 轨迹点数,
# 用来证明发出去的确实是"本次会话构建"生成的包。注意 prehandle 会混发滑块,
# 脚本按 accept_kinds 自动刷新到 2404 文字点选为止(刷新耗尽则退出、不提交)。
```