- tcaptcha/web/:FastAPI + SSE + 零构建单页。把同步阻塞的 solve() 包成可观测 后台任务(工作线程 + 全局串行,因 onnxruntime session 非线程安全),用 logging.Handler 零侵入把 client 日志变成事件流;按靶子实测 min_interval 强制节流。 - tcaptcha/render.py:标注图(点击顺序编号 + 质心偏差)与点位量尺,CLI 与 web 共用。 - tcaptcha/targets.py:靶子表提为单一来源(含实测频控间隔 1ms=60s / mliev=3s)。 - fastapi/uvicorn 走 optional extra `web`,核心包零 web 依赖(带回归断言)。 - 修两个真 bug:hook 求解器时 args[0] 是实例自身(真入参从 args[1] 起); client 默认每次新建 solver,预热实例未被复用(每发重载 56MB ONNX)。 - 复验:icon/text/slider 三题型各拿到 errorCode=0;selftest 全绿;依赖隔离通过。 用法:uv run --extra web python -m tcaptcha.web.app
63 KiB
反 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 项目结构重组(下面的历史条目仍使用旧路径,对照本表)
包从扁平改为分层,并删除了已废弃的整条路线(雨云线 / 模板回填 / Node 兜底 /
"补环境"时代的浏览器 JS / 旧 VM 探索工具 / 训练标注脚本)。
回滚点:tag backup/pre-refactor-20261012。
tcaptcha/
protocol/ client.py transport.py proxy.py # 协议交互
telemetry/ crypto.py values.py key.py opcodes.py # collect 纯离线生成
disasm.py program.py layout.py modules.py # + 密钥静态还原
solvers/ text_click.py icon_click.py icon_click_rotate.py slider.py
ocr/ engines.py slicer.py
adapters/ one_ms.py
models/ 权重(onnx)
tests/
tools/ cdp_eval.js cdp_collect.js cdp_dump_scripts.js tdc_key_pure.py tdc_profile.py
scripts/verify/ align_session.py audit_names.py verify_values.py verify_offline.py send_test.py
| 历史条目里的旧路径 | 现在的位置 |
|---|---|
tcaptcha/client.py |
tcaptcha/protocol/client.py |
tcaptcha/http_transport.py / proxy.py |
tcaptcha/protocol/transport.py / proxy.py |
tcaptcha/crypto.py |
tcaptcha/telemetry/crypto.py |
tcaptcha/tdc_values.py |
tcaptcha/telemetry/values.py |
tcaptcha/tdc_key.py / tdc_opcodes.py |
tcaptcha/telemetry/key.py / opcodes.py |
tools/tdc_disasm.py / tdc_program.py / tdc_layout.py / tdc_modules.py |
tcaptcha/telemetry/{disasm,program,layout,modules}.py |
tcaptcha/glyph_solver.py |
tcaptcha/solvers/text_click.py |
tcaptcha/solver.py |
tcaptcha/solvers/icon_click.py |
tcaptcha/icon_click_solver.py |
tcaptcha/solvers/icon_click_rotate.py |
tcaptcha/slider_solver.py |
tcaptcha/solvers/slider.py |
tcaptcha/ocr_engines.py / slicer.py |
tcaptcha/ocr/engines.py / slicer.py |
tcaptcha/one_ms_adapter.py |
tcaptcha/adapters/one_ms.py |
已删除(历史条目仍会提到,但要按"已不存在"来读):
tcaptcha/{chrome_fetch,manual_captcha,probe_captcha,solve_in_browser,tdc_browser}.js、
tcaptcha/tdc_worker.js、models/success_collect_template.b64、tcaptcha/core_js/**、
tcaptcha/TOOLCHAIN_REPORT.md、
tools/{tdc_collectgen,tdc_slotmap,tdc_lift,tdc_vm_run,tdc_query,tdc_probe.js,tdc_trace.js,tdc_keytrace.js,tdc_vm_disasm.js,unpack_webpack.js}、
tools/cdp_{capture,click,net_capture,script_source,shot,module_probe,show_captcha}.js、
dist/rainyun_qinglong/**、rainyun_checkin.py、
scripts/{analyze_collect_layout,rainyun_checkin,check_real_tencent_aid,experiment_layout_verify}.py、
scripts/verify/{verify_segment,verify_seglen,verify_reorder,verify_pair_correct,verify_fp_consistency,verify_control_negative,verify_40_9_disamb,balance_names_values,label_plaintext}.py、
tcaptcha/tests/{test_collectgen,test_xtea_key}.py、
tcaptcha/{train_classifier,train_detector,prepare_yolo,web_labeler,collector,siamese,evaluate,benchmark_verify}.py
⚠️ 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 取到源串;反汇编读出常量:("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:tr03lxdviLWtn4uXGjWjmrLQ / tr03A5zTSQ9bCog51sDm / tr039KeeQxXX-OdduJoY。关键:最原始的 httpx + 无 cookie 也 0 |
确证 |
| 27 | 1ms 的 12 与"请求侧保真度"无关(原结论"12 是该 aid 的服务端策略"已过强) | 成立部分:aid 190758559 上未做任何保真的 httpx + 无 cookie → 0;aid 195672344 上 curl_cffi + 真机 cookie + 逐 endpoint 头保真 + 传输指纹 → 仍 12。→ 请求侧细节不是自变量(§三 反证仍有效)。 不成立部分:把 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,得`{"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=:真机 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 每次返回不同构建),两边串:静态 = 运行时 = [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 项,逐位相同)→ 由此可判定:之前"部分槽语义对不上"是标注启发式的问题,顺序本身正确。工具: scripts/experiment_require_order.py |
确证 |
| 36 | 「清单项 → cd 槽」的精确规则(同一构建端到端校验确立) | 装配循环(线上构建 @4362,370 条)分支树已读通:if(!item) continue; v,w,kind = item.get();· kind===2(@4820):把 r3 清空转存 r4(@4860),r4 在收尾 @4905 二次加工后拼回,因此仍产出 1 槽(实测第 0 项,随机串 vXc29RxhvT34Q…)· v===null/undefined(@4799)→ out += "null"· typeof v==="number"(@4738)→ `out += (v |
|
| 37 | 按构建生成 collect 的工具链已跑通(含回环自测) | tools/tdc_collectgen.py:静态导出槽位序 → 按 #36 规则算「项→槽」→ 重排 → 该构建密钥加密。实测(同一构建):37 项 → 38 槽,明文 686B / collect 920 字符;解密回环逐字节一致,cd 与真机样本逐槽相同。 规则修正(重要): kind=1 的串只按括号外逗号切槽 —— '[]'/'[360,360]'/'["zh-CN","zh"]' 内的逗号不构成新槽;kind=2 的项仍产出 1 槽(转存 r4 后在收尾 @4905 拼回)。回归: tcaptcha/tests/test_collectgen.py(已并入 selftest) |
确证 |
| 39 | 12 只在"答案正确"之后才判(排查方法学上的关键事实) | 答错一律 50/9:本轮滑块题型下(我们按点选坐标答,必错)两组都是 50;换成真解(text/icon)后立刻全是 12。→ 任何用"答错"做的对照实验都没有区分度,这解释了早期多轮对照为何无效。今后做 12 的 A/B 必须 --solve 真解题 |
确证 |
| 40 | 原证据链:① 槽位序证伪(§三);② 包内容证伪(本会话真实 collect 由 Python 发仍 12);③ 已知 #26:宽松 aid 190758559 上 httpx + 无 cookie + 无头保真 也能 0 —— 说明"请求侧细节"对宽松 aid 无所谓,但对 195672344 不够。④ 真机同 aid 只出 50/9/0(#25)。→ 剩余差在会话↔请求方绑定。收口实验:把同一 sess/eks/tkid + 同一 collect,分别由浏览器发与 Python 发,比较 errorCode | 结论(待收口) | |
| 38 | 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 的固定标记)。→ 即「产槽」与「分段」同时成立:旧已知 #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 构造方式:python_values(本次会话构建 → 槽序与取值都对)→ 0(3/3);python_keyed(仓库里那份 2026-10-11 成功样本模板回填:槽序来自另一份构建,屏串还是另一台设备的 2560-1440-1345-24)→ 12(2/2)。→ 推翻已知 #40("谁在发 HTTP");同时把 #31/#32 的"槽序/取值必须与本次会话构建同源"从假设升级为确证 |
确证(含推翻 #40) |
| 60 | 滑块答案格式 = DynAnswerType_POS,坐标用「背景图原始像素」 |
真机课题 JSON:拼图块元素(id=1)的 move_cfg = {track_limit:"x>=50&&x<=552", move_factor:[1,0], data_type:["DynAnswerType_POS"]};init_pos=[50,128] 与提交答案的 data 是同一坐标系(bg_elem_cfg.size_2d = 672x480)。所以解题时用 scale=1.0,直接把 target_x_image 当答案,不要换成页面显示像素 |
确证 |
| 61 | 滑块的拼图块可直接从课题配置裁剪,不必解析 DOM | 拼图块元素自带 sprite_pos=[140,490] + size_2d=[120,120];与早先从 CSS background-position/size 反算出的 sprite_region() == (140,490,120,120) 逐像素相同(测试里已加交叉断言) |
确证 |
| 62 | ddddocr 的滑块距离识别在腾讯滑块上不可用 | fixture(真值 target_x=343 / dx=293):slide_match 给 402/352(偏 59px),simple_target=True 同样 402,slide_comparison 直接报尺寸不匹配;实时样本(真值 236)给 296(偏 60px)。判定手段:把拼图块 alpha 轮廓按候选位置叠到背景图上目视 —— 本项目 ZNCC 的 236 与缺口描边严丝合缝,ddddocr 的 296 整体右偏 |
确证(负结果,别再试) |
| 63 | 滑块端到端已打通(纯离线 collect + ZNCC 定位) | send_test.py --kind slider 3/3 拿到 errorCode=0 + 真 ticket;轨迹为拖动型(~34 点、尾点 [1,1,12])。其中一次 margin=0.024 < 阈值 0.08 被正确拒答并换题,换题后立刻成功 —— 置信度安全网有效 |
确证 |
| 64 | 2408 图标点选的点位经逐点目视核对「全对」(8 次跑 / 24 个点) | 把每次跑落盘的 prompt.jpg 用求解器同款切片器切 3 片,与「实际点击点 ±32px 背景 patch」并排放大逐一比对:run1..run6(0,0,12,12,12,12)+ 修复后 2 次(50,50)24/24 全部落在与提示同序的目标图标上。→ 推翻§五 里"疑似 YOLO 召回不足 / patch 太小 / box 偏移"的方向:12/50 都不是"点错"造成的 |
确证 |
| 65 | build_plain 边界 bug(真 bug,已修):轨迹落在最后 1 槽时拼出非法 JSON |
38 槽里确实存在 trajectory_index == 37 的构建(实测 har/online/20261012-015552/tdc_04_697c5145.js):旧写法 chunk2 = ',' + '' + '],' → 整段 = {"cd":[<37 槽>,<traj>,], —— 非法 JSON。13 份构建扫描命中 1/13。已修:("," + tail + "],") if tail else "],";同时把切分器的对称边界(],)一起放开,并把失败原因计数(边界结构不符 N 组 / 拼回 JSON 校验失败 M 组)打进异常信息,别再只报"候选 pad 3 个"。回归:test_build_plain_trajectory_last(含"旧写法必须 json.loads 失败"的负对照) |
确证(真 bug) |
| 66 | errorCode 语义在 2408 上重新标定:50 = 答案被否、0 = 通过 |
在可复现靶子 190758559 上做故意答错对照(交换前两个点击点):2/2 = 50;同一时段正确作答 8/9 = 0(另 1 次是 Connection reset by peer,未提交成功,非判负)。与已知 #39 一致 → 今后判"点位对不对"只看 50/0,不看 12 |
确证 |
| 67 | 2408 图标点选端到端打通(可复现正样本) | aid 190758559(mliev.login.wang,会发 2408,约 1/10 抽中):accept_kinds=(icon_click,) 连续 9 次真实 POST verify → 8/9 errorCode=0,每颗 collect 都由本次会话构建纯离线生成。→ 图标链路(YOLO + 分类器 + 全局指派 + 4 段 collect)本身没有问题 |
确证 |
| 68 | 1ms aid=195672344 的现状:先 12 后 50,且与"答对没答对"无关 |
同一时段(13:15–13:31)、同一客户端 / 生成器 / 求解器: ① aid 195672344 图标×6 → 0,0,12,12,12,12;② aid 195672344 混合题型×6 → 12 ×6(三题型都 12);③ aid 195672344 图标×2 → 50,50,而这两次的点位经目视逐点正确(#64);④ 同小时 aid 190758559 图标×9 → 0 ×8;⑤ 故意答错在 190758559 上 2/2 = 50(#66)。 → 12 → 50 的迁移本身是信息:50 是"包被接纳、答案被裁判"之后的码,说明 12 那道包级关卡到 13:31 已经放行了;但 1ms 仍把逐点正确的答案判成 50。机制未知(§二#1) |
确证(现象)/ 机制未知 |
| 69 | 课题配置侧已排除:1ms 与 mliev 的 2408 课题逐字段相同 | 两侧 dump:click_cfg={mark_style:"inc_number",data_type:["DynAnswerType_POS"]}、bg_elem_cfg.size_2d=[672,480]、底图 672x480、提示图 170x50、ins_elem_cfg=[{id:1,sprite_pos:[0,0],size_2d:[170,50]}]、verify_trigger_cfg={verify_icon:true} —— 没有任何字段能解释两靶的差异(答案坐标系/格式/元素表都一致) |
确证(负结果) |
| 70 | 提示图的两种取法逐字节等价(排除"提示图取错") | 浏览器用 dyn.sprite_url;本客户端是把 bg_elem_cfg.img_url 的 img_index 改写成 0。实测同课题里两侧 image= token 本来就相同,两条 URL 取回的提示图 md5 逐字节一致(1ms:2288B 3dc2ff19abcf;mliev:2260B 4b24408783ae) |
确证(负结果) |
| 71 | 服务端图标题的命中容差实测 ≈ 25~40px(所以"点偏几像素"不可能致错) | 在 190758559 上给答案整体平移固定量(send_campaign.py --offset,每档 2 次):10,0 → 0 ×2(点离质心 3.325,0 → 0 ×2(6.450,0 → 50 ×2(11.2~34.8px);0,75 → 50。→ 容差落在 25 与 50px 之间。而我们正常作答的点击离图标笔画质心只有 2.2px(1ms)/ 3.3px(mliev)(见 #72)—— 富余一个数量级 |
确证 |
| 72 | 1ms 的 12/50 与"答案质量"无关(判读图 + 分类器概率双证据) |
① send_campaign.py --target 1ms --kind icon --n 8(13:49–13:52):0,0,0,0,None,12,12,12;三次 12 的 solver_diag 里每个目标类都有 top-1 概率 = 1.0 的候选(run6 [2,1,4]→2:1.0/1:1.0/4:1.0;run7 [8,9,11]→8:1.0/9:1.0/11:1.0;run8 [1,4,0]→1:0.998/4:1.0/0:1.0)。② judge_icons.py 判读图:8 次跑 24/24 全部落在与提示同序的目标图标上,12 那三次也一样(偏离 0~5px)。③ 点击精度不区分 0 与 12:0 组的质心偏离均值 ≈2.1px、12 组 ≈2.4px。④ 同一时段该 aid 在混合题型下 12 ×6(三题型都不行)。→ 12 是包级/会话级,不是答案级 |
确证 |
| 73 | 轨迹的 dt 分布曾与真机严重不符(真 bug,已修)—— 用户"位置没问题那就是轨迹被校验"的判断正确 | 把 4 份真机抓包 / 6 条成功轨迹(360 个非首点 dt)全部解出汇总: ① 点数硬不变量:6/6 全是 61 点,而旧实现随机 57 ② **dt 紧密聚集在 16 修复 tcaptcha/telemetry/values.py:_sample_dt() 按 6 条轨迹重拟合(13~20ms 占 78%)、新增 _fit_points() 把点选轨迹钉死 61 点(补齐用 [0,0,dt] 驻留点,不能复制最后位移点——否则净位移被放大,实测 166 vs 139.5)。修复后 A/B:1ms 图标 4/8=0 → 7/8=0(前 7 连 0);文字 2/3=0、滑块 1/3=0(样本少)。回归:selftest 全绿 |
确证(真 bug + A/B 见效) |
| 74 | 61 点不变量只属于点选轨迹,拖动轨迹不适用(差点犯的错) | 修 61 点时曾把 drag_trajectory_points(滑块拖动)也套了 61 点 → 滑块测试立刻红(净位移 166 vs 139.5、点数 61 超范围)。拖动是另一种交互(按下→横移→松手),61 点是从点选轨迹量出来的,不能外推。已回退:拖动按 steps 走(实测 34 点、净位移 140≈139.5) |
确证(含一次自我纠错) |
| 75 | 1ms 的残余 12 = aid+IP 级频控,已标定窗口;冷却足够后三种题型全过 |
2026-10-14 实测(send_campaign.py --kind any --interval N,每次 prehandle 次数都是 1,排除"抽题太多次"这条):① 间隔 4s × 12 发 → 0,0,12,12,12,12,12,12,12,12,12,12(只有前 2 发过,之后 10 连 12;icon/text/slider 三种题型都被打到 12);② 冷却 2 分钟 + 间隔 30s × 6 → 6 发全 12(冷却不够); ③ 冷却 25 分钟 + 间隔 60s × 4 → 4 发全 0(text×2 + slider×2)。→ 频控与题型无关、与 prehandle 次数无关,只与"短时间内的请求量"有关。结论:1ms 必须低频使用(≥60s 一发、连续打不超过 ~2 发),回归测试一律用 190758559。 |
确证(含恢复验证) |
| 76 | Web 观测台上线(形态 A:观测/调试台)+ 顺带抓到两个真 bug | 新增 tcaptcha/web/(FastAPI + SSE + 零构建单页)、tcaptcha/render.py(标注图 + 点位量尺,CLI 与 web 共用)、tcaptcha/targets.py(靶子表,含实测频控间隔)。设计要点:solve() 同步阻塞 → 工作线程 + 全局串行(onnxruntime session 非线程安全);用 logging.Handler 零侵入把 client 的日志变成事件流;按靶子 min_interval 强制节流。bug ①:hook 求解器时把 args[0] 当成了题目图 —— 实例方法调用时 args[0] 是求解器实例自身(真入参从 args[1] 起),于是 'CaptchaSolver' object has no attribute 'convert',标注图与点位全丢。bug ②: TencentCaptchaClient 默认每次新建 solver,预热加载的实例根本没被复用(每发重载 56MB ONNX)→ 改为把缓存实例传进 solver= / text_solver=。另:滑块硬套"离笔画质心"量尺会给出 10~40px 噪声(滑块答案是缺口位置),已对滑块置 None。 复验:icon/text/slider 三题型各拿到 errorCode=0;SSE 事件序列 + 3 张图全部可取;import tcaptcha 不引入 fastapi(依赖隔离) |
确证 |
二、未知(附假设 + 确信度)
| # | 未知 | 当前假设 | 确信度 |
|---|---|---|---|
| 1 | 真因是轨迹遥测的时序指纹:旧实现的 dt 分布(50% 落 012(8 连发里最后 1 次,当时 prehandle 已被风控拖到 14 次)符合"连发触发频控"的形态,低频使用即可 |
已确证 | |
| 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 证伪) |
|
| 两个独立实验都否定它: ① A/B( scripts/experiment_layout_verify.py --solve):按本次构建槽位序生成的 collect vs 仓库旧模板,两组 9/9 全是 12;② 严格对照(决定性):同一批取值、同一长度 920、只把槽位序完全颠倒,两组 6/6 全是 12。 → 槽位序不影响 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 的自变量 | 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 条,互相独立: ① 真机明文有 3 处连续空格串( +5 / +10 / +24),\x00 补齐不会产生空格;② 不需要密钥的纯算术:4 段长度 360/72/312/40 → base64 480+96+416+56 = 1048,与真实 collect 的 1048 字符精确相等(差一个字节都不行); ③ == 只出现在 1046/1048,恰好对应 sd 段 33B→XTEA 补 7→40B。→ 改为 crypto.build_collect()(4 段独立加密 + 空格 pad 到 24)。旧函数仍保留但已不是主路径。⚠️ 注意被推翻的是加密结构,不是槽数模型——cd 仍是 38 槽(见已知 #42)。 |
kind=2 的项"仍产出 1 槽"是错的 |
我曾据案例 §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。 |
2026-10-12 图标点选(2408)新增排除
| 假设 | 否定证据 |
|---|---|
| "2408 的 12/50 是 YOLO 召回不足 / patch 太小 / box 偏移导致点错"(旧 §五 的头号方向) | 目视逐点核对 8 次跑 / 24 个点 24/24 全对(已知 #64):把落盘的提示图切 3 片,与"实际点击点 ±32px 的背景 patch"并排放大,每次每个点都落在与提示同序的目标图标上;连 4 次 12 与 2 次 50 也都是全对。→ 点错不是自变量。 |
| "1ms 与 mliev 的图标课题配置不同(答案坐标系 / 格式 / 元素表)" | 两侧 dump 逐字段相同(已知 #69):data_type=["DynAnswerType_POS"]、size_2d=[672,480]、底图 672x480、提示图 170x50、ins_elem_cfg 只有"提示图元素 id=1"、verify_trigger_cfg={verify_icon:true}。→ 配置侧解释不了两靶差异。 |
"提示图取错了(应当用 dyn.sprite_url,我们改写了底图 URL 的 img_index)" |
同课题里两条 URL 的 image= token 本来就相同,取回的提示图 md5 逐字节一致(已知 #70)。→ 这是个真实但不影响结果的偏差。 |
"轨迹落在末槽时 chunk2 仍要补前导逗号" |
旧写法在该构建上拼出 {"cd":[<37 槽>,<traj>,], —— 非法 JSON(已知 #65)。真机是 JSON.stringify 语义:逗号只在槽与槽之间,两端空区间都不补。已修 + 回归。 |
四、当前实现(纯算单一路径)
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 优先(按案例重做后的收尾,按成本排序)
🔴 2026-10-12 晚:2408 图标点选收尾(下一轮从这里开始)
现状(判读见 §三 新增排除 + 已知 #64~#72 + 专文 har/1ms_dianxuan/ICON2408.md):
图标链路本身已经通了,而且"点得不准"这条怀疑已被定量排除 ——
- 可复现靶子 aid 190758559:本轮累计 正确作答 22 次 →
0× 20(另 2 次Connection reset by peer网络层失败); 而故意扰动 5 次(交换前两点 ×2 / 整体平移 50px ×2 / 75px ×1)→50× 5 —— 靶子对我们非常稳定,是理想回归台; - 点击精度:点击点离「图标暗色笔画质心」仅 2.2px(1ms)/ 3.3px(mliev);
- 服务端容差:给答案整体平移
25px仍0、50px才50(已知 #71)→ 精度富余一个数量级; - 1ms 上三次
12的答案里,每个目标类都有 top-1 概率 = 1.0 的候选;判读图 24/24 全对(已知 #72)。
→ 所以 12 / 50 都不是"点错":50 = 答案被否(#66 已用"故意答错"负对照标定),
12 = 包级/会话级拒答(答案还没被裁判)。1ms 在同一时段、同一代码下会在
0 / 50 / 12 之间漂移,且一次爆发里呈现"前 2~4 次 0、之后 12"。
下一轮只剩两件事(按成本排序):
- 判
12是不是"按 aid+IP 的令牌桶/频控"(唯一还有区分度的自变量)。做法: 静默 ≥20 分钟后只发 1 次看是否回到0;再对"同一轮里成功次数是否有限"做复现。 本轮send_campaign.py已记录每次求解的 prehandle 次数(points.json.prehandles), 可直接把「请求量 ↔ errorCode」画出来。注意:连打会自伤,测 1ms 时务必拉开间隔。 若确认是频控 → 结论就是"1ms 需低频使用,回归一律用 190758559",不必再改代码。 - 只在需要时再追"为什么 1ms 会在正确答案上给
50"(50的语义是答案被否, 但已知 #72 表明答案是对的)。优先级低于 1,因为它已不影响交付能力。
复验入口(仓内工具,2026-10-12 起替代 /tmp 一次性脚本):
# 批量真实发送 + 落图 + 统计(支持 --offset 容差标定 / --swap 故意答错 / --kind icon|text|slider|any)
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 8
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 2 --offset 25,0
.venv/bin/python scripts/verify/send_campaign.py --target mliev --kind icon --n 2 --swap # 负对照,期望 50
# 目视判读图(列 = 提示图第 j 个目标 vs 第 j 个点击点 patch;红框上标了 #1/#2/#3 顺序)
.venv/bin/python scripts/verify/judge_icons.py har/1ms_dianxuan/icon_campaign/<tag>
产物默认写到项目内 har/1ms_dianxuan/icon_campaign/<tag>/(har/** 只跟踪 .md,
所以图片不入库但留在项目树里可复核);本轮的原始产物已就位。
既有优先项
- ✅ 已完成(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()是旧拟合)。 - ✅ 已完成:4 段结构已接进生成链路 ——
tdc_values.build_plain()按 「轨迹槽 = 分界点」产出chunk1 / trajectory / chunk2 / sd,直接喂crypto.build_collect()。 ⚠️ 注意 pad 可能只有 1 个空格(已知 #50),切分/对拍一律用修好的crypto.split_collect_segments(),别再自己写{2,}找 pad。 - ✅ 已完成(2026-10-12):端到端验证直接上 1ms 签到页文字点选,3/3 拿到
errorCode=0+ 真 ticket(见已知 #58)。不需要再走宽松 aid 做前置台阶 —— 直接打真靶即可。 - ✅ 已完成(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…), 所以任何"固定模板"的路线都不可能长期成立,必须按会话构建生成。
既有项
-
静态提取 XTEA 密钥(去掉最后一个 Node 依赖)—— 已完成(已知 #19~#22):tcaptcha/tdc_key.py纯 Python 零 Node,6/6 构建与 Node 逐字一致。 残留增强项(非阻塞):① 若服务端改了 handler 语义,tdc_opcodes.derive_opcode_table会抛"指纹未识别",此时应更新参考指纹而非改定位逻辑;② 抗"多套诱饵数组 >2 层捕获"的 兜底(当前两层已覆盖实测全部 6 份)。 -
拿下 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)——所以任何拿"答错"跑出来的对照都无效。 这解释了为什么早期很多对照实验没有区分度。
-
-
可复现靶子(新增资产):aid 190758559 /
mliev.login.wang短信登录 —— 可反复测, 用来回归"纯算链路是否完好"(1ms 每天只能试一次,不适合做回归):.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 -
补齐新版明文语义 + 槽位表:语义(哪一槽是 UA / 屏幕串 / WebGL / 轨迹 / 时间戳)已从 已知 #30 的真机明文基本对齐;关键是每份构建的槽位序(已知 #31)—— 见上面 1(b)。分析入口:
scripts/analyze_collect_layout.py。 -
✅ 已完成(2026-10-12)—— 2408 图标点选端到端打通:纯 ONNX 三件套 (YOLOv8 召回 + ResNet18 13 类分类器 + 全局非冲突组合搜索)直接吃现成权重, 不需要重训(原计划"按 1ms 图标类别重训"已被实测推翻:现权重在 1ms 与 mliev 上 点位都正确,见已知 #64)。端到端证据:可复现靶子 aid 190758559 8/9 = errorCode=0(#67)。 剩余:1ms aid=195672344 的 12/50(与图标链路无关,见本页最上方"2408 收尾")。
-
✅ 已完成(2026-10-12)—— 滑块 (2401/2402):不用模板匹配,改用拼图块内容与缺口做 ZNCC (缺口 = 同一块内容被压暗,真缺口相关性最高、干扰缺口次之)。答案格式从课题 JSON 的
move_cfg.data_type取(DynAnswerType_POS,坐标=背景图原始像素),轨迹走drag_trajectory_points。 实测 3/3 拿到errorCode=0。ddddocr 的滑块距离识别已验证不可用(偏 59~60px,见已知 #62)。
六、复验入口
# 一键全量
.venv/bin/python -m tcaptcha.selftest
# 单套
.venv/bin/python -m tcaptcha.tests.test_crypto_core # XTEA 往返 + 四段切分回归 + 纯离线闭环
.venv/bin/python -m tcaptcha.tests.test_tdc_key # 纯 Python 密钥提取(5 份构建,零 Node)
.venv/bin/python -m tcaptcha.tests.test_glyph_solver # 文字点选求解
.venv/bin/python -m tcaptcha.tests.test_icon_click_solver # 图标点选求解
.venv/bin/python -m tcaptcha.tests.test_slider_solver # 滑块缺口求解
# 静态分析 CLI(原来的 tools/tdc_*.py 已并入包里)
.venv/bin/python tools/tdc_key_pure.py -v tcaptcha/tdc_sample.js # 取密钥 + 推导过程
.venv/bin/python tools/tdc_key_pure.py --opcodes tcaptcha/tdc_sample.js # 按构建推导的 opcode 表
.venv/bin/python -m tcaptcha.telemetry.modules tcaptcha/tdc_sample.js # 37 个语义名识别
.venv/bin/python -m tcaptcha.telemetry.layout tcaptcha/tdc_sample.js # 清单 require 顺序
# 在线抓全新样本(同一个 URL 每次返回不同构建,见 #29)
.venv/bin/python scripts/fetch_online_samples.py --n 4
标定 / 闭环 / 真实发送(2026-10-12 起的主入口)
# ⑧ 「同会话构建 + 同会话 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 --kind text --mode python_values # 文字点选,期望 0
.venv/bin/python scripts/verify/send_test.py --kind slider --mode python_values # 滑块,期望 0
.venv/bin/python scripts/verify/send_test.py --kind icon --mode python_values # 图标点选
# --kind any 表示三种都接。脚本会 hook generate_telemetry 打印这一颗 collect 的
# 段几何 + 轨迹点数,用来证明发出去的确实是"本次会话构建"生成的包。
# 注意 prehandle 三种题型混发,脚本会按 accept_kinds 自动刷新到目标题型
# (刷新耗尽就退出、不提交 —— 那是没抽到题,不是求解失败)。