feat(密钥): 纯 Python 静态提取 XTEA 密钥 —— 去掉最后一个 Node 依赖 (6/6 构建与神谕逐字一致)
做了什么 - 新增 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)。
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,108 @@
|
||||
# -*- 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())
|
||||
Reference in New Issue
Block a user