做了什么 - 新增 tcaptcha/tdc_opcodes.py: 按构建从解释器源码推导 opcode 表。 实测 opcode 编号**每份构建整体重排**(同一源码 pc0 的 SET_STACK_LEN 在 6 份构建里 = 53/19/32/57/55/1, JUMP = 60/17/42/58/21/2); 旧固化表只对 core_js 有效 —— 用它反汇编其它构建会在 pc0 撞上 RET_TRUE, 得到"空反汇编"的假象。 做法: 定位主循环 X[Y[Z++]]() 的 handler 数组 -> JS 感知切分 -> handler 源码规范化 (去空白 + 局部变量按首次出现重命名) 取指纹 -> 与内置 58 条 core_js 参考指纹匹配; 数组下标即该构建的 opcode, argc 由语义名取 (与 handler 体内 x[y++] 次数实测一致)。 - 新增 tcaptcha/tdc_key.py: 纯 Python XTEA 密钥静态还原器 (零 Node / 零 pc 硬编码)。 反汇编 -> 定位含 delta 的 XTEA 闭包 -> 用"两层捕获链"选出真密钥数组: 字闭包捕获的 B 帧寄存器, 经"定义 B 的那个闭包"的 caps 映射到 G 帧后须 == XTEA 闭包 捕获的寄存器; 借此排除脚本里并行计算的第二套诱饵字 -> 读 4 个 (源串, 偏移, 下标) -> 折入轮加盐。 - tools/tdc_disasm.py: disassemble_cfg/_decode_one 支持注入按构建推导的 opcode 表 (names/arity/dynamic), 默认仍回落 core_js 表; 控制流 opcode 改为按语义名定位。 - tools/tdc_key_pure.py: CLI (-v 打印推导过程 / --opcodes 打印 opcode 表 / --json)。 - tcaptcha/crypto.py: extract_xtea_key 改为"纯 Python 主路径 + Node 插桩兜底"。 - 新增 tcaptcha/tests/test_tdc_key.py 并接入 selftest; 新增入库 fixture tcaptcha/tests/fixtures/tdc_fresh_2026-10-11.js (现场新抓的 tdc.js)。 为什么 - AGENTS.md §2.4 分层交付 / §1.1: 去掉最后一个 Node 依赖, 提高并发与抗发版能力; - §5.7 负结果也是资产: 显式撤回"opcode 数值跨构建固定"与"密钥只能靠 Node 插桩拿"两条旧结论。 怎么验证 - 6 份构建纯 Python 结果 == Node 插桩 extract_xtea_key, **逐字一致**: core_js / root / 1ms tdc.js / tdc_live / tdc_latest / 2026-10-11 现场新抓 tdc_fresh。 - opcode 表 6/6 构建 58/58 handler 精确匹配, 0 未识别。 - .venv/bin/python -m tcaptcha.selftest 全绿 (含既有逐字节对拍 test_xtea_key)。 - 把 node_bin 指向不存在的程序时 generate_telemetry 仍产出 collect (证明已零 Node)。
109 lines
4.5 KiB
Python
109 lines
4.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
纯 Python XTEA 密钥提取 回归测试 (AGENTS.md §5.9)
|
|
|
|
固化的性质:
|
|
1. **跨构建通用**: opcode 编号每份构建整体重排, 但仍能按构建推导出正确语义表;
|
|
2. **纯 Python == Node 神谕**: 6 份构建(含 1 份新抓的实时 tdc.js)的期望密钥
|
|
全部由 Node `extract_xtea_key` 逐字对照确认过, 此处固化为断言 (不再依赖 Node 跑测试);
|
|
3. **定位不写死 pc**: 断言覆盖 core_js / root / 1ms(旧+两份实时) / 实时新抓 共 6 份
|
|
不同构建, 任一份 opcode 重排或寄存器分配变化都会立刻暴露。
|
|
|
|
运行:
|
|
.venv/bin/python -m tcaptcha.tests.test_tdc_key
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
|
|
from tcaptcha.tdc_opcodes import derive_opcode_table # noqa: E402
|
|
|
|
# (相对路径, 期望有效密钥, 是否必须存在)
|
|
# 期望值来自 Node 插桩 extract_xtea_key, 逐字一致。
|
|
# 入库样本 (core_js / root / 实时新抓 fixture) 为必需门禁; har/** 被 .gitignore 忽略,
|
|
# 本地有这些样本时会一并回归, 缺少时自动跳过。
|
|
FIXTURES = [
|
|
("tcaptcha/core_js/tdc_sample.js", [1818510928, 1381395306, 1245924197, 1768384072], True),
|
|
("tcaptcha/tdc_sample.js", [1448563793, 1850559083, 1733513813, 1364613714], True),
|
|
# 2026-10-11 现场从 turing.captcha.qcloud.com 下发的 tdc.js (入库固化, 防"仅本地可验")
|
|
("tcaptcha/tests/fixtures/tdc_fresh_2026-10-11.js",
|
|
[1397244740, 1799507557, 1399740484, 1516463189], True),
|
|
("har/1ms_dianxuan/tdc.js", [1448106085, 1766670948, 1819173482, 1298358636], False),
|
|
("har/1ms_dianxuan/live/tdc_live.js", [1314475846, 1500397933, 1751410275, 1716217423], False),
|
|
("har/1ms_dianxuan/live/tdc_latest.js", [1800883800, 1850828643, 1197042522, 1800823125], False),
|
|
]
|
|
|
|
_fails: list = []
|
|
|
|
|
|
def check(name: str, cond: bool, detail: str = "") -> None:
|
|
print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" {detail}" if detail else ""))
|
|
if not cond:
|
|
_fails.append(name)
|
|
|
|
|
|
def main() -> int:
|
|
print("1) 按构建推导 opcode 表 (58 个 handler, 无未识别)")
|
|
tables = {}
|
|
skipped = []
|
|
for rel, _expect, required in FIXTURES:
|
|
p = ROOT / rel
|
|
if not p.exists():
|
|
if required:
|
|
check(f"fixture {rel}", False, "缺失")
|
|
else:
|
|
skipped.append(rel)
|
|
continue
|
|
names, arity, dynamic, diag = derive_opcode_table(p.read_text(encoding="utf-8", errors="replace"))
|
|
tables[rel] = names
|
|
check(f"opcode 表 {Path(rel).name}",
|
|
diag["handler_count"] == 58 and not diag["unknown_slots"],
|
|
f"handlers={diag['handler_count']} unknown={diag['unknown_slots']}")
|
|
if skipped:
|
|
print(f" (跳过本地未提供的样本 {len(skipped)} 份: {[Path(s).name for s in skipped]})")
|
|
|
|
print("\n2) opcode 编号确实每份构建都不同 (证明没有依赖写死的数值)")
|
|
jump_ops = {rel: next(o for o, n in tabs.items() if n == "JUMP") for rel, tabs in tables.items()}
|
|
if len(jump_ops) >= 2:
|
|
check("JUMP 的 opcode 跨构建不统一", len(set(jump_ops.values())) >= 2, str(jump_ops))
|
|
|
|
print("\n3) 纯 Python 提取 == Node 神谕 (逐字)")
|
|
for rel, expect, required in FIXTURES:
|
|
p = ROOT / rel
|
|
if not p.exists():
|
|
continue
|
|
js = p.read_text(encoding="utf-8", errors="replace")
|
|
try:
|
|
rep = extract_xtea_key_pure(js)
|
|
check(Path(rel).name, rep["key"] == expect, f"{rep['key']}")
|
|
except Exception as exc: # noqa: BLE001
|
|
check(Path(rel).name, False, f"{type(exc).__name__}: {exc}")
|
|
|
|
print("\n4) 密钥构造自洽: base + 折盐 == key")
|
|
for rel, expect, required in FIXTURES:
|
|
p = ROOT / rel
|
|
if not p.exists():
|
|
continue
|
|
rep = extract_xtea_key_pure(p.read_text(encoding="utf-8", errors="replace"))
|
|
folded = list(rep["base"])
|
|
for idx, salt in rep["salts"]:
|
|
folded[idx] = (folded[idx] + salt) & 0xFFFFFFFF
|
|
check(f"折盐 {Path(rel).name}", folded == rep["key"] == expect)
|
|
|
|
print()
|
|
if _fails:
|
|
print(f"❌ test_tdc_key 失败 {len(_fails)} 项: {_fails}")
|
|
return 1
|
|
print("✅ test_tdc_key 全部通过")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|