Files
hello_js_reverse_skill/har/1ms_dianxuan/PROGRESS.md
T
yml2213 51dff870cf 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
2026-10-12 03:38:18 +08:00

366 lines
46 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 反 VM 密钥还原 & 纯算交付 进度文档
> 目标: **算法 100% 纯 Python** —— collect 的明文组装 / 轨迹 / 时间戳 / XTEA 加密 / 编码
> 全部在 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-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`),不再依赖仓库里的旧抓包。
---
## 〇、路线变更(重要)
**已废弃「补环境」路线。** 曾经实现过 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` | 确证 |
| 14 | **密钥字的构造算法(已完全还原)**:`word = Σ ((charCodeAt(源串,k) + off_i) & 0xFF) << (8k)`,4 个源串各自一个**独立偏移常量**,常量是打包点的立即数 | 神谕二分 + DUMP_PC 取到源串;反汇编读出常量:<br>`("IA_t", -8)` `("9:%!", +49)` `("nTLS", -9)` `("7XYf", +3)` | 确证 |
| 15 | **有效密钥 = base 字 + 两个轮加盐** | `real[0]-base[0]=855311`、`real[3]-base[3]=723726`(XTEA 轮里的立即数) | 确证 |
| 16 | **纯 Python 可完整复算密钥,与 `extract_xtea_key` 逐字一致** | `base=[1817655617,1381395306,1245924197,1767660346]` → 折盐 → `[1818510928,1381395306,1245924197,1768384072]` == Node 结果 | 确证 |
| 17 | 密钥链路的运行时位置(神谕实测) | 逐字首次出现 PC:7740/7526/7895/8852(均 reg9);数组组装 @8886(reg7);进 XTEA 闭包 @9030 `CALL_FUNC[2]`;入口 @9109 | 确证 |
| 18 | `extract_xtea_key`(Node)本身正确 | 其密钥把 probe 的真实 collect 解成 `{"cd":[0,10,"top",[360,360],"MacIntel",1,"undefined?rand…` | 确证 |
| 19 | **opcode 编号每份构建整体重排**(不是只有变量名/加盐在变) | 同一源码的 pc0 `SET_STACK_LEN` 在 6 份构建里 = 53/19/32/57/55/1;`JUMP` = 60/17/42/58/21/2;core 表去解其它构建会在 pc0 就 `RET_TRUE` 假象 | 确证 |
| 20 | **opcode 表可从该构建解释器源码推导**:主循环 `X[Y[Z++]]()` 的 `X` 就是 handler 数组,下标即 opcode;handler 规范化指纹可跨构建精确匹配 | 6/6 份构建 **58/58 handler 精确匹配**、0 未识别(`tcaptcha/tdc_opcodes.py`)| 确证 |
| 21 | **密钥字闭包只写"被 XTEA 闭包捕获的那个数组"**;脚本同时算了两套 4 字(真 + 诱饵),靠**两层捕获链**区分:字闭包捕获的 B 帧寄存器 `w_src`,经"定义 B 的那个闭包"的 caps 映射到 G 帧后须 == XTEA 闭包捕获的寄存器 | core/root/1ms×3/实时新抓 全部选对;诱饵组按同规则被排除 | 确证 |
| 22 | **纯 Python 静态提取 XTEA 密钥成功,零 Node** | `tcaptcha/tdc_key.py`:6/6 份构建与 Node 插桩 `extract_xtea_key` **逐字一致**;含 2026-10-11 现场新抓的 `tdc.js`(`har/1ms_dianxuan/live/tdc_fresh.js`,77967B,密钥 `[1397244740,1799507557,1399740484,1516463189]`)| 确证 |
| 23 | **纯 Python 密钥能解密 1ms 真实成功样本的 collect** | 用 `har/签到成功-1.har` 里那次 ec=0 之前的 tdc.js 提密钥 → 解出该次 collect 明文 `{"cd":[2560,"MacIntel",...` 1353B;且与 `models/success_collect_template.b64` **逐字节同构**(仅轨迹/时间戳/盐被回填)| 确证 |
| 24 | **1ms 真实成功链路已完整解析** | prehandle(带腾讯云登录 cookie) → tdc.js → getcapbysig → verify **ec=0**(ticket `tr03…`) → `POST /api/v1/mall/checkin`,body = `{captchaTicket:{provider,data:{ticket,randstr}}, sessionUuid, identity_context}`;`/api/v1/captcha/params` 的 body 带 `identity_context`(`emctx1.<b64>.<sig>`) | 确证 |
| 25 | **真实浏览器对 verify 从不吃 12** | `点选.har`(同一 cookie/同一 aid) 三次 verify = 50/50/9;`签到成功-1.har` = 0。而**当时的**客户端形态都是 12。⚠️ 2026-10-11 晚修复:该结论把"载荷"只理解成"答案",漏掉了 **`cd` 槽位序**(见已知 #31/#32),表述已过强 | 证据成立 / 归因已修正 |
| 26 | **纯算链路在真实 aid 上端到端成功出 ticket**(2026-10-11 实测)| 用新抓包 `har/原始抓包/登录抓包-点选.har` 的 aid=**190758559**(mliev.login.wang 短信登录),本客户端 `solve()` 返回 `errorCode=0` + 真 ticket:<br>`tr03lxdviLWtn4uXGjWjmrLQ` / `tr03A5zTSQ9bCog51sDm` / `tr039KeeQxXX-OdduJoY`。**关键:最原始的 httpx + 无 cookie 也 0** | 确证 |
| 27 | **1ms 的 12 与"请求侧保真度"无关**(原结论"12 是该 aid 的服务端策略"已过强) | 成立部分:aid 190758559 上未做任何保真的 httpx + 无 cookie → 0;aid 195672344 上 curl_cffi + 真机 cookie + 逐 endpoint 头保真 + 传输指纹 → 仍 12。→ **请求侧细节不是自变量**(§三 反证仍有效)。<br>不成立部分:把 12 归因为"服务端不放行该 aid"缺少机制;现在有**更强且可证伪**的解释 → 见 #31/#32(collect 槽位序与本次会话构建不匹配)| 部分成立 / 归因待 #31 验证 |
| 28 | 新抓包暴露另一套业务接入(可复现靶子)| `har/原始抓包/登录抓包-点选.har`:`POST /api/captcha/params`(scene=sendSms + fingerprint/userAgent/platform/language/timezone) → 腾讯点选 → `POST /api/oidc/send-register-verification-code`(`{type,receiver,captchaTicket:{provider,data:{ticket,randstr,fingerprint}},sessionUuid}`) | 确证 |
| 29 | **同一个 `tdc.js` URL 每次返回的构建都不同**(不是"按构建固定",而是**按请求随机化**)| 连抓 4 次 `tdc.js?app_data=…&t=…`:md5 = `b6db2555…` / `032afea5…` / `1b8b7d4d…` / `b1ad4bdd…`,四份长度 77917~78013B、**XTEA 密钥 4 个全不同**。→ 结论 #3"密钥按构建固定"要限定为"**同一份已下载的脚本**内固定",不能外推到"重新请求还是同一份" | 确证 |
| 30 | **真机某次成功会话的 collect 明文已完整解出** | 抓包同时拿到**同会话**的 `tdc.js`(`app_data=1793575418729725952`)与 `cap_union_new_verify` 的 collect,用该构建密钥解密:784B 密文 → 可打印率 0.991,得<br>`{"cd":["",[360,360],0,"Mozilla/5.0 (Macintosh…) Chrome/155…","https://turing.captcha.gtimg.com/1/template/drag_ele.html?rand=…",["zh-CN","zh"],641547489,1791732001,"GgoAAA…png…",2560,[[34,96,3284],[154,50,370],…],1023,1440,"+08",1,0,"ANGLE (Apple, ANGLE Metal Renderer: Apple M4, …)",10,"",1,1986013704,"iframe","MacIntel",0,1791733986,"1440-2560-2530-24-*-*-|-*",1791733982,"Google Inc. (Apple)",24,"",[],0,"https://1ms.run/console/personal/benefits/check-in","98k",2,0,0,"UTF-8"],"sd":{"od":"C","ft":"qf_7Pf__H"}}`<br>(`tdc.js?app_data=…&appid=<aid>`:真机 URL 带 `appid`,我们客户端不带)| 确证 |
| 31 | **`cd` 的槽位顺序按会话/构建随机化(不是按版本固定)** | 5 份同日样本(真机 cap2 / `签到成功-1` / `点选` / `login_checkin` / 仓库模板)全部 `len(cd)=38`、**同一批取值**、**同一个 `sd`**,但**槽位顺序 5/5 互不相同**(两两不同槽位 26~29 个)。复现:`.venv/bin/python scripts/analyze_collect_layout.py --json <cap>.json` | 确证 |
| 32 | **仓库在用的 collect 模板 = `签到成功-1.har` 那一次的槽位序** | 模板指纹与 `签到成功-1#0` **逐槽一致**;与晚些时候的真机会话 29 个槽不同。→ `generate_collect_success()` 虽然回填了新轨迹/时间戳,但**槽位序是那一次的** | 确证 |
| 33 | **VM 程序结构层已吃透:顶层就是 62 个模块工厂的注册表** | 顶层 `@0` 做 `registry[id] = function(module, exports, require){...}`(`DUP; PUSH_IMM [id]; REVERSE_TOP2; JUMP -> CREATE_CLOSURE; SET_PROP`)。`tools/tdc_program.py --modules` 直接导出。修了两个真 bug:① 反汇编器漏了 `RET_TRUE`/`THROW` 是终结符(控制流穿过函数结尾 → 子程序归属全错);② 提升器终结符判定写死 opcode 数值 | 确证 |
| 34 | **`cd` 槽位序的来源已定位并可静态导出** | 装配链:清单模块 `arr[arr.length] = require(N)`(**追加,所以 require 顺序就是数组顺序**,已核)→ 分发模块按 `.on`/`.reset`/`.get` 分三数组 → 装配模块 `r3='{"cd":['` + `for(...) r3 += arr[i].get()`。线上构建实测 **37 个 require + 1 个字面量槽 = 38**,与真机 `cd` 槽数完全吻合;两份构建的 require 串不同(与 #31 一致)。工具:`tools/tdc_layout.py` | 确证(顺序)/ 语义标签部分 |
| 35 | **运行时反证通过:静态导出的 require 序 == 运行时真实 require 序(逐项一致)** | 在真机 iframe(`tdc.js` 实际执行的上下文,`window.TDC.getData` 就在这里)里包一层 `Function.prototype.apply`,模块工厂调用形状 = **4 参 `[object,object,object,function]`**(require 是最后一个),按工厂实例分组记账。触发一次刷新后取运行时串,用 `tools/cdp_script_source.js`(Debugger.scriptParsed + getScriptSource)取到**运行时真正执行的那份 tdc.js**(exactly-once,不能用重新 fetch 代替——#29 每次返回不同构建),两边串:<br>静态 = 运行时 = `[8,9,10,13,14,15,16,17,18,19,20,21,23,24,25,30,31,32,34,36,37,1,38,39,40,41,42,43,44,47,48,49,50,51,56,57,58]`(37 项,逐位相同)<br>→ 由此可判定:之前"部分槽语义对不上"是**标注启发式**的问题,**顺序本身正确**。工具:`scripts/experiment_require_order.py` | 确证 |
| 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 的剩余唯一自变量**~~ **已推翻(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/` 在线新抓样本)
| # | 结论 | 证据 | 确信度 |
| --- | --- | --- | --- |
| 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 放过了**。已补强断言(== 已知正确答案 且 != 原样),并自查:旧实现过不了新断言 | 确证 |
| 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) |
---
## 二、未知(附假设 + 确信度)
| # | 未知 | 当前假设 | 确信度 |
| --- | --- | --- | --- |
| 1 | 1ms aid=195672344 为何在答案正确时返回 `errorCode=12` | 需要"真实业务会话/真实验证码页面上下文"(登录态),而非环境指纹 | **中(未做单变量实验)** |
| 2 | ~~新版明文字段语义(哪一位是时间戳/指纹/轨迹)~~ **已解决** | 见已知 #54:37 个语义名 → 38 槽真值全部对齐,且被生成器逐槽复现 | 已确证 |
| 3 | ~~明文里 `[360,360]`、`"1440-2560-2530-24-*-*-| -*"` 的作用~~ **已解决** | `[360,360]` = 验证码 iframe 可视区(`viewport_size`);屏串字段序 = **宽-高-可用高-色深-\\*-\\*-\\|-\\***,与浏览器真值 1440/2560/2530/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) |
| **opcode 数值是跨构建固定的 VM 常量** | 6 份构建实测 pc0 的 `SET_STACK_LEN`/`JUMP` 数值全不同(见已知 #19);`tools/tdc_disasm.py` 里固化的表**只对 core_js 有效**,已改为"可注入按构建推导的表" |
| **"密钥字必须靠 Node 插桩才能拿到"** | `tcaptcha/tdc_key.py` 纯 Python 零 Node 复算,6/6 构建与 Node 逐字一致(已知 #22)→ Node 降级为兜底 |
| **H3(captcha 域腾讯云登录 cookie)能解决 12** | 用**抓包里的真实 cookie 值**(uin/skey/qcmainCSRFToken/intl)重跑,仍 12(`scripts/experiment_h3_cookie_ab.py --only b`,2026-10-11)→ 与早期结论一致,H3 确凿排除 |
| **HTTP/TLS 传输指纹是 12 的自变量** | `curl_cffi`(impersonate=chrome: JA3/JA4 + HTTP/2 + Chrome 头序) + 真实 cookie,仍 12(`scripts/experiment_transport_ab.py --only b`)→ 传输层排除 |
| **collect 的"内容/取值"是 12 的自变量**(2026-10-12 **证伪**)| 用**本会话真机真实产物** (`TDC.getData(true)`,1012 字符,取值/顺序/密钥全对)原样交给 Python 发:**4/4 全是 12**。→ 包内容**不是**自变量;只剩 **"谁在发这个 HTTP"**(会话↔请求方绑定)|
| **H-LAYOUT:collect 的 `cd` 槽位序是 12 的自变量**(2026-10-11 晚提出,2026-10-12 **证伪**)|
两个独立实验都**否定**它:<br>① A/B(`scripts/experiment_layout_verify.py --solve`):按本次构建槽位序生成的 collect vs 仓库旧模板,**两组 9/9 全是 12**;<br>② **严格对照**(决定性):同一批取值、**同一长度 920**、只把槽位序**完全颠倒**,两组 **6/6 全是 12**。<br>→ **槽位序不影响 verify 结果**。已知 #31(槽位序按构建随机化)与 #34/#35/#36/#37(可静态导出)**仍然成立且是有价值的资产**,但它们**不是 12 的原因**。|
| **请求头保真度是 12 的自变量** | 按真实抓包逐 endpoint 保真:资源 GET 用 `Sec-Fetch-Dest=script/image` + `Mode=no-cors` + `Storage-Access=active` + cookie,POST 用 `empty/cors` + Origin 且**不带** cookie;仍 12 |
| **collect 明文结构/取值是 12 的自变量** | ~~我们的 collect 明文与 1ms 真实成功样本逐字节同构(同 1353B、同字段序、同取值,仅回填轨迹/时间戳/盐),仍 12~~ ⚠️ **该否定实验本身有缺陷(2026-10-11 晚推翻)**:所谓"同字段序"只是与**模板来源的那一次**样本比,而 `cd` 槽位序**按会话随机**(已知 #31),所以它没检验到真正会变的维度。修正后重开为:**"cd 槽位序必须与本次会话构建一致"是 12 的头号可疑自变量**(已知 #32)|
| **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。 |
---
## 四、当前实现(纯算单一路径)
```
TencentCaptchaClient(telemetry_mode="python_keyed") # 默认
├─ extract_xtea_key(tdc.js) **纯 Python 静态还原** (tcaptcha/tdc_key.py, 零 Node)
│ ├─ tdc_opcodes.py 按构建从解释器源码推导 opcode 表 (编号每份都重排)
│ ├─ tools/tdc_disasm.py 可达性 CFG 反汇编 (支持注入按构建的表)
│ ├─ 定位 XTEA 闭包 (delta 常数) -> 两层捕获链选出真密钥数组
│ └─ 读出 4 个字 (源串+偏移+下标) + 折入轮加盐
│ (Node 插桩版本保留为**兜底**: 纯 Python 失败时自动回落)
├─ generate_collect_success() 模板回填: 轨迹 / 当前时间戳 / 随机盐 → XTEA → Base64 (单层 URL 编码)
└─ extract_eks(tdc.js) 通用正则取最长 Base64 候选 (不再写死变量名)
```
* 纯 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 步已可**纯离线**完成:`tcaptcha/tdc_values.py` 为 37 个语义名提供取值生成器(已知 #54/#57)。
---
## 五、下一步(可被下一轮直接执行)
### 🆕 2026-10-12 优先(按案例重做后的收尾,按成本排序)
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…),
所以**任何"固定模板"的路线都不可能长期成立**,必须按会话构建生成。
### 既有项
0. ~~**静态提取 XTEA 密钥(去掉最后一个 Node 依赖)**~~ —— **已完成**(已知 #19~#22):
`tcaptcha/tdc_key.py` 纯 Python 零 Node,6/6 构建与 Node 逐字一致。
残留增强项(非阻塞):① 若服务端改了 handler 语义,`tdc_opcodes.derive_opcode_table`
会抛"指纹未识别",此时应更新参考指纹而非改定位逻辑;② 抗"多套诱饵数组 >2 层捕获"的
兜底(当前两层已覆盖实测全部 6 份)。
1. **拿下 1ms 的 12 —— 归因已改写(2026-10-11 晚,见已知 #29~#32)**
旧归因"1ms 就是不放行非真机页面的会话"**证据不足**,已被下面这条更可证伪的链取代:
* 硬事实:
① 同一个 tdc.js URL **每次请求都返回不同构建**(4/4 md5 与密钥全不同,已知 #29);
② 真机某次成功会话的 collect 明文已**完整解出**(已知 #30);
③ 5 份同日样本的 `cd` 都是 38 槽、同一批值、同一个 `sd`,但**槽位序 5/5 互不相同**(已知 #31);
④ 仓库在用的模板槽位序 == `签到成功-1.har` 那一次(已知 #32),而 `generate_collect_success()`
只回填轨迹/时间戳/entry_url —— **槽位序仍是那一次的**。
* ~~**H-LAYOUT(头号假设)**:槽位序错导致 12~~ → **已证伪(2026-10-12,见 §三)**:
按构建槽位序 vs 旧模板 **9/9 都 12**;同一批值只颠倒顺序(同长度 920)**6/6 都 12**。
**槽位序不是 12 的自变量。**
* **当前最可能的自变量(按成本排序)**:
(a) **collect 的内容/取值本身**(不是顺序):真机那份 38 槽里含 canvas/WebGL/时间戳等
**当次会话真实产物**;我们用"旧模板的值"无论怎么排都不是**本会话**的值。
判定实验:**在真机 iframe 里直接 `TDC.getData(true)` 拿到本会话的 collect,
原样交给 Python 发**(不重排、不改值)——若 Python 也 0,则是"值"的问题;
若仍 12,则排除包内容,只剩"请求方/会话绑定"。
(b) **会话↔请求方绑定**:sess/eks/tkid 与"发出这个 HTTP 的一端"存在绑定
(同包浏览器发 0、Python 发 12)。判定实验 = 上面 (a) 的同一发即可区分。
建议下一步(一条命令,能同时判定 a 与 b):
真机 iframe: `TDC.getData(true)` -> collect (本会话真实产物)
同一 sess/eks/tkid, 分别用浏览器发 / 用 Python 发, 比较 errorCode。
* 附带事实(重要,改变了排查方向):**12 只在"答案正确"之后才判**。
答错一律 50/9(本轮两组在滑块题型下都是 50)——所以任何拿"答错"跑出来的对照都无效。
这解释了为什么早期很多对照实验没有区分度。
2. **可复现靶子(新增资产)**:aid 190758559 / `mliev.login.wang` 短信登录 —— **可反复测**,
用来回归"纯算链路是否完好"(1ms 每天只能试一次,不适合做回归):
```bash
.venv/bin/python - <<'PY'
from tcaptcha.client import TencentCaptchaClient, KIND_TEXT_CLICK
c=TencentCaptchaClient(aid="190758559",
entry_url="https://mliev.login.wang/auth/#/oidc/authorize",
referer="https://mliev.login.wang/", wb="1", accept_kinds=(KIND_TEXT_CLICK,))
print(c.solve()["raw_response"].get("errorCode")) # 期望 0
c.close()
PY
```
3. **补齐新版明文语义 + 槽位表**:语义(哪一槽是 UA / 屏幕串 / WebGL / 轨迹 / 时间戳)已从
已知 #30 的真机明文基本对齐;**关键是每份构建的槽位序**(已知 #31)——
见上面 1(b)。分析入口:`scripts/analyze_collect_layout.py`。
4. **实现 2408 图标点选**:按 1ms 图标类别重训 `yolo_detector` + `icon_classifier`。
5. **实现滑块 (2401/2402)**:模板匹配找缺口 + 生成滑动轨迹(1ms 大幅下发滑块,见抓包)。
---
## 六、复验入口
```bash
.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + encrypt_string 一致性
.venv/bin/python -m tcaptcha.tests.test_xtea_key # XTEA + 密钥提取 (逐字节对拍, Node 参与)
.venv/bin/python -m tcaptcha.tests.test_tdc_key # **纯 Python** 密钥提取 (6 份构建, 零 Node)
.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解 (8 张真实样本)
.venv/bin/python -m tcaptcha.tests.test_icon_click_solver # 图文点选求解 (真机样本)
.venv/bin/python -m tcaptcha.tests.test_slider_solver # 滑块缺口求解 (真机样本)
.venv/bin/python -m tcaptcha.selftest # 全部
.venv/bin/python tools/tdc_key_pure.py -v har/1ms_dianxuan/live/tdc_fresh.js # 单份构建 + 推导过程
.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
# ⑧ 「同会话构建 + 同会话 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 文字点选为止(刷新耗尽则退出、不提交)。
```