做了什么 - 新增 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)。
57 lines
2.1 KiB
Python
57 lines
2.1 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
腾讯天御 TDC 混沌 VM —— **纯 Python** XTEA 密钥提取 CLI (零 Node / 零 JS 运行时)。
|
|
|
|
用法:
|
|
.venv/bin/python tools/tdc_key_pure.py [tdc.js] # 默认 core_js 样本
|
|
.venv/bin/python tools/tdc_key_pure.py -v har/.../tdc.js # 打印推导过程
|
|
.venv/bin/python tools/tdc_key_pure.py --opcodes tdc.js # 顺带打印按构建推导的 opcode 表
|
|
|
|
实现见 `tcaptcha/tdc_key.py` (定位算法) 与 `tcaptcha/tdc_opcodes.py` (按构建推导操作码表)。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
if ROOT not in sys.path:
|
|
sys.path.insert(0, ROOT)
|
|
|
|
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="纯 Python 从 tdc.js 提取 XTEA 密钥")
|
|
ap.add_argument("path", nargs="?", default="tcaptcha/core_js/tdc_sample.js")
|
|
ap.add_argument("-v", "--verbose", action="store_true", help="打印密钥构造过程")
|
|
ap.add_argument("--opcodes", action="store_true", help="打印按构建推导的 opcode 表")
|
|
ap.add_argument("--json", action="store_true", help="以 JSON 输出")
|
|
args = ap.parse_args()
|
|
|
|
js = open(args.path, encoding="utf-8", errors="replace").read()
|
|
|
|
if args.opcodes:
|
|
from tcaptcha.tdc_opcodes import derive_opcode_table
|
|
names, arity, dynamic, diag = derive_opcode_table(js)
|
|
print("handlers=%d dynamic=%s" % (diag["handler_count"], sorted(dynamic)))
|
|
for op in sorted(names):
|
|
print(" %3d %-18s arity=%s" % (op, names[op], "dyn" if op in dynamic else arity.get(op, 0)))
|
|
print()
|
|
|
|
report = extract_xtea_key_pure(js, verbose=args.verbose)
|
|
if args.json:
|
|
print(json.dumps(report, ensure_ascii=False))
|
|
else:
|
|
print("base = %s" % report["base"])
|
|
print("salts = %s" % report["salts"])
|
|
print("key = %s" % report["key"])
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|