Files
hello_js_reverse_skill/scripts/verify/align_session.py
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

94 lines
3.3 KiB
Python
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.
# -*- coding: utf-8 -*-
"""把「同会话 collect 明文」与「同会话 tdc.js 的语义名」按位置精确对齐。
这是"不手工配平"的正解,依据都是已确证的链路:
* 槽位序 == 清单模块的 require 序(账本 #34/#35,运行时反证过)
* 语义名 == tdc_modules.analyze(同一份 tdc.js)(账本 #44,37/37 唯一)
* collect 4 段加密(账本 #41),解密后按空格 pad 切回 cd
输入(全部来自**同一次会话**,无需任何人工假设):
/tmp/1ms_bridge/session_build.js 运行时 tdc.js(Debugger 取的)
/tmp/1ms_bridge/session_collect.json 同会话 TDC.getData(true) + getInfo
用法:
.venv/bin/python scripts/verify/align_session.py
"""
import base64
import json
import re
import sys
from pathlib import Path
from urllib.parse import unquote
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "tools"))
import tdc_modules as tm # noqa: E402
from tcaptcha.crypto import split_collect_segments, xtea_decrypt_bytes # noqa: E402
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
BUILD = "/tmp/1ms_bridge/session_build.js"
SESS = "/tmp/1ms_bridge/session_collect.json"
js = open(BUILD, encoding="utf-8", errors="replace").read()
print("运行时 tdc.js: %d 字节" % len(js))
key = extract_xtea_key_pure(js)["key"]
print("本会话密钥 :", key)
print()
sess = json.load(open(SESS, encoding="utf-8"))
if "collect" not in sess and isinstance(sess.get("data"), dict):
sess = sess["data"] # cdp_collect.js 落盘的形状是 {target, ok, data:{...}}
collect = unquote(sess["collect"]) # 浏览器侧返回的是 URL-encoded
print("collect: %d 字符" % len(collect))
# 解密(4 段拼接:整体解一次即可,因为各段独立 XTEA-ECB)
raw = base64.b64decode(collect)
dec = xtea_decrypt_bytes(raw, key).decode("latin1")
plain = dec.rstrip("\x00")
print("解密 %d 字节" % len(dec))
runs = [m.span() for m in re.finditer(r" {2,}", plain)]
print("空格 pad 串:", runs)
print()
if len(runs) == 3:
c1, traj, c2, sd = split_collect_segments(plain)
print("段: chunk1=%d traj=%d chunk2=%d sd=%d" % (len(c1), len(traj), len(c2), len(sd)))
else:
c1 = traj = c2 = sd = None
print()
logical = re.sub(r" {2,}", "", plain)
obj = json.loads(logical)
cd = obj["cd"]
print("cd 槽数:", len(cd))
print()
rows = tm.analyze(BUILD)
names = [r["name"] for r in rows]
mods = [r["module"] for r in rows]
print("清单项 %d, 语义名 %d" % (len(mods), len(names)))
print()
# itoken 占 2 槽(^\\d+:\\d+$),其余 1 槽
multi = [i for i, n in enumerate(names) if n.startswith("itoken")]
slots = 0
print("%-2s %-6s %-34s %s" % ("#", "模块", "语义名", "值"))
print("-" * 110)
out = {}
k = 0
for i, (m, nm) in enumerate(zip(mods, names)):
n = 2 if i in multi else 1
if k + n > len(cd):
break
vals = cd[k:k + n]
k += n
val = vals[0] if n == 1 else vals
out[nm] = val
print("%-2d %-6d %-34s %s" % (i, m, nm, json.dumps(val, ensure_ascii=False)[:60]))
print()
print("覆盖槽数 %d / cd %d" % (k, len(cd)))
print()
Path("/tmp/1ms_bridge/name_values.json").write_text(
json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(">>> 语义名 -> 真值 已存 /tmp/1ms_bridge/name_values.json")