做了什么:
- 新增 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
366 lines
46 KiB
Markdown
366 lines
46 KiB
Markdown
# 反 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 文字点选为止(刷新耗尽则退出、不提交)。
|
||
```
|