做了什么 - 新增 scripts/check_real_tencent_aid.py: 用**可反复测**的真实 aid 回归纯算链路 (默认 aid=190758559 / mliev.login.wang 短信登录; 也可 --aid 195672344 对照)。 - 账本 har/1ms_dianxuan/PROGRESS.md: 已知 #26~#28、已排除新增三行、下一步重写为"已闭合结论 + 可复现靶子"。 为什么 (AGENTS §2.1 第 0 问 / §5.7) - 用户提供可重复的真机登录抓包 har/原始抓包/登录抓包-点选.har, 得以把"12 到底是不是我们的问题" 用一个能成功的目标做单变量反证。 怎么验证 (2026-10-11 实测) - aid 190758559: 本客户端 solve() => errorCode=0 + 真 ticket, 连续 4 次: tr03lxdviLWtn4uXGjWjmrLQ / tr03A5zTSQ9bCog51sDm / tr039KeeQxXX-OdduJoY / tr03fGUOpcJTZWXWmZG7Uioc0Y8GlHPn 其中 **httpx + 无 cookie + 无任何头部保真** 也拿到 0 —— 证明纯算链路本就完好。 - aid 195672344 (1ms): 在 cookie / curl_cffi Chrome 指纹 / 逐 endpoint 头保真 / collect 内容 逐字节同构 / 带签名 identity_context 先行注册 —— 全部仍 12。 - 结论: 1ms 的 12 是该 aid 自身风控不放行"非真机页面"会话; 请求侧已无自变量, 纯协议侧无解, 只能真机桥接或换入口。
16 KiB
16 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-11 (纯 Python XTEA 密钥静态提取落地 + opcode 表按构建推导)
〇、路线变更(重要)
已废弃「补环境」路线。 曾经实现过 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 → 12 是"请求方身份"判定,不是答案/载荷 |
确证 |
| 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 是该 aid 自身的风控策略,不是我们的请求侧差异 | 同一天同一台机器:aid 190758559 用 httpx/无 cookie/无头保真 → 0;aid 195672344 用 curl_cffi(Chrome 指纹)+真机 cookie+逐 endpoint 头保真 → 仍 12。→ 请求侧已无自变量(§2.1 第 0 问:服务端就是不放行这个 aid 来自非真机页面的会话) | 确证 |
| 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}) |
确证 |
二、未知(附假设 + 确信度)
| # | 未知 | 当前假设 | 确信度 |
|---|---|---|---|
| 1 | 1ms aid=195672344 为何在答案正确时返回 errorCode=12 |
需要"真实业务会话/真实验证码页面上下文"(登录态),而非环境指纹 | 中(未做单变量实验) |
| 2 | 新版明文字段语义(哪一位是时间戳/指纹/轨迹) | 时间戳是 1[6-9]\d{8} 大整数;轨迹是 [x,y,t] 三元组 |
中高 |
| 3 | 明文里 [360,360]、`"2560-1440-1345-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)→ 传输层排除 |
| 请求头保真度是 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 |
H5:1ms 后端用 identity_context 把会话标记为可信 |
拿抓包里签名过的 identity_context(599B emctx1.*) 先调 POST /api/v1/captcha/params(响应与不带时完全相同,无额外 token)再解题,仍 12 → H5 排除 |
| "请求侧保真度不够"这一整类 | 直接反证:aid 190758559 上 未做任何保真的 httpx + 无 cookie 也拿到 ticket(0);即请求侧细节本就足够,12 与保真度无关 |
四、当前实现(纯算单一路径)
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 表推导) - CLI:
tools/tdc_key_pure.py [tdc.js] [-v] [--opcodes] - 模板:
tcaptcha/models/success_collect_template.b64(真实成功样本明文) - 加密:
tcaptcha/crypto.py中唯一的教科书 XTEA - Node 现存用途: 仅
tdc_worker.js兜底提取密钥(主路径已不需要)
五、下一步(可被下一轮直接执行)
静态提取 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)
- 证据链:① 同一份代码在 aid 190758559 上
errorCode=0出真 ticket(且 httpx+无 cookie 也 0); ② 1ms aid 195672344 在 cookie / 传输指纹 / 头保真 / collect 内容 /identity_context全部单变量排除后恒为 12;③ 真机同 aid 只出 50/9/0。 - 即 1ms 这个 aid 的风控策略不放行"非真机页面"的会话(§2.1 第 0 问:服务端就是判这个)。
- 因此只剩两条路,且都不在"协议纯算"范围内: (a) 桥接保底(§2.4 ③):在真机页面里跑一次拿 ticket,再走纯算业务提交; (b) 换一个风控更宽的入口/aid(如 190758559 那种),或与 1ms 沟通白名单。
- 证据链:① 同一份代码在 aid 190758559 上
- 可复现靶子(新增资产):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 - 补齐新版明文字段语义:对照 10+ 份实时模板做差分。
- 实现 2408 图标点选:按 1ms 图标类别重训
yolo_detector+icon_classifier。 - 实现滑块 (2401/2402):模板匹配找缺口 + 生成滑动轨迹(1ms 大幅下发滑块,见抓包)。
六、复验入口
.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.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 表