做了什么:
- 包改为分层: tcaptcha/{protocol,telemetry,solvers,ocr,adapters,models,tests}
* protocol/ client.py transport.py proxy.py
* telemetry/ crypto.py values.py key.py opcodes.py
disasm.py program.py layout.py modules.py
* solvers/ text_click.py icon_click.py icon_click_rotate.py slider.py
* ocr/ engines.py slicer.py
* adapters/ one_ms.py
- tools/ 里原本是**包运行时依赖**的 tdc_{disasm,program,layout,modules}.py 并入 telemetry/
(放在 tools/ 属于分层倒置),tools/ 只留 CLI: tdc_key_pure.py / tdc_profile.py / cdp_*.js
- crypto.py 529 -> 340 行: 切除「成功样本模板回填」与「Node 插桩兜底」两条老路;
generate_telemetry 只剩 python_values 一种模式,传别的会直接报错并说明原因
- 求解器改名分清: solver.py -> solvers/icon_click.py(client 实际在用的 YOLO+ResNet18 ONNX),
icon_click_solver.py -> solvers/icon_click_rotate.py(仅测试在用,留作对照)
- pyproject: 去掉 ultralytics/onnx(训练栈已删),补上真正在用的 onnxruntime
删除了什么:
- 雨云整条线 + dist/rainyun_qinglong(104M 旧快照)
- 模板回填: models/success_collect_template.b64、tdc_worker.js、analyze_collect_layout.py
- 「补环境」时代浏览器 JS: chrome_fetch/manual_captcha/probe_captcha/solve_in_browser/tdc_browser
- 旧 VM 探索工具 10 个(tdc_collectgen/tdc_slotmap/tdc_lift/tdc_vm_run/tdc_query/tdc_probe 等)
+ 8 个基于它们的旧 verify 脚本
- 训练/标注/评估: train_classifier/train_detector/prepare_yolo/web_labeler/collector/
siamese/evaluate/benchmark_verify
- tcaptcha/core_js/(60 个分析产物)、TOOLCHAIN_REPORT.md(整篇在描述已删工具)
- 3 个已无任何引用的 PyTorch 权重:icon_classifier.pt 43M / yolo_detector.pt 5.9M /
siamese_matcher.pt 1.6M —— 线上只跑 onnx,tcaptcha/ 117M -> 66M
为什么:
- 项目后面要专门承载「滑块 / 文字点选 / 图标点选」三种求解器;扁平包 + 运行时依赖混在
tools/ + 两条已废弃的 collect 生成路线,会让后续扩展持续踩坑
怎么验证:
- .venv/bin/python -m tcaptcha.selftest 全绿
- scripts/verify/verify_offline.py 逐槽全等 + 分段几何一致
- scripts/verify/verify_values.py 4 份真机明文 152/152 逐槽全等
- scripts/verify/send_test.py --mode python_values 1ms 签到页文字点选 errorCode=0
- 回滚点: tag backup/pre-refactor-20261012
442 lines
17 KiB
Python
442 lines
17 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
腾讯天御 TDC 混沌 VM (Chaos VM) —— XTEA 密钥的**纯 Python** 静态还原器。
|
||
|
||
为什么需要它
|
||
------------
|
||
此前要拿到某一版 tdc.js 的 XTEA 密钥, 只能把 tdc.js 丢进 Node 插桩跑一遍
|
||
(ADDU opcode 吐密钥字)。密钥**不是字面量**, 而是 VM 运行时由字节码算出来的,
|
||
所以必须"读指令流"而不是"搜字符串"。
|
||
|
||
本模块完全离线(零 Node、零 JS 运行时), 基于**正确反汇编**(tools/tdc_disasm.py)做
|
||
**模式 + 数据流**定位, 复刻密钥构造算法。全部结论都有运行时神谕(真浏览器 TDC.getData)
|
||
逐条对照过, 见 har/1ms_dianxuan/PROGRESS.md。
|
||
|
||
算法 (已证: 纯 Python 结果 == Node extract_xtea_key 逐字一致)
|
||
------------------------------------------------------------
|
||
1. **4 个密钥字**: 每个字由一个专门的闭包算出:
|
||
word_i = Σ_{k=0..3} ( (ord(src_i[k]) + off_i) & 0xFF ) << (8*k)
|
||
源串 `src_i` 是紧跟在该闭包 `CREATE_CLOSURE` 之后的 4 个 `PUSH_STR_CHAR`
|
||
字面量; 偏移 `off_i` 是打包点的立即数; 存入下标 `i` 由闭包尾部的
|
||
`PUSH_IMM_ARRAY / PUSH_IMM [i] / ... / SET_PROP` 给出。
|
||
|
||
2. **哪些闭包是真密钥**: 密钥数组会被 **XTEA 闭包捕获**。
|
||
XTEA 轮里出现 `delta = 0x9E3779B9`; 含 delta 的那个闭包体所属的
|
||
`CREATE_CLOSURE` 即 XTEA 闭包, 其 `caps=[r7=r<K>]` 的 `<K>` 就是密钥数组所在的
|
||
父帧寄存器。**只有 caps 右侧等于该寄存器的密钥字闭包**才是真密钥
|
||
(同一份脚本往往还并行算了第二个"诱饵"数组, 用不同寄存器, 必须排除)。
|
||
|
||
3. **轮加盐**: XTEA 轮内 `key[(sum>>>11)&3]` 查表之后会**条件性**加上一个立即数
|
||
(salt): `GET_PROP / PUSH_IMM <salt> / ADD / ADD / XOR`。加给哪个下标由守卫条件
|
||
`idx == ((sum>>>11)&3)` / `idx == (sum & 3)` 里的 `idx` 常量决定。
|
||
有效密钥 = base 字按盐的下标位置相加。
|
||
|
||
所有定位都是**模式 + 立即数**, 不写死任何 pc (每份构建都会重排)。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import re
|
||
import sys
|
||
from typing import Dict, List, Optional, Tuple
|
||
|
||
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
if _ROOT not in sys.path: # 允许在任意 cwd 下 import tools.*
|
||
sys.path.insert(0, _ROOT)
|
||
|
||
from tcaptcha.telemetry.disasm import extract_bytecode, disassemble_cfg # noqa: E402
|
||
|
||
from tcaptcha.telemetry.opcodes import derive_opcode_table # noqa: E402
|
||
|
||
M32 = 0xFFFFFFFF
|
||
XTEA_DELTA = 0x9E3779B9 # 2654435769, XTEA 黄金比常数
|
||
KEY_LEN = 4
|
||
|
||
# 走"下标表达式"时要跳过的算子 (它们构成 `(sum & 3)` / `(sum>>>11)&3`)
|
||
_SKIP_OPS = {
|
||
"BITWISE_AND", "SHR_UNSIGNED", "LOAD_REG_IMM", "MAKE_REG_PROP_REF",
|
||
"GET_PROP", "SWAP", "STORE_REG", "ADD", "DUP", "LOAD_REG_INDIRECT",
|
||
"PUSH_IMM_ARRAY", "POP",
|
||
}
|
||
# 这些算子紧跟在"掩码 PUSH_IMM 3"之前, 说明那个 3 是掩码而不是下标
|
||
_MASK_NEIGHBOURS = {"BITWISE_AND", "LOAD_REG_IMM", "SHR_UNSIGNED"}
|
||
|
||
|
||
class _Dis:
|
||
"""对反汇编结果做少量索引封装, 便于按 pc 前后游走。"""
|
||
|
||
def __init__(self, js_text: str) -> None:
|
||
self.z = extract_bytecode(js_text)
|
||
# opcode 编号每份构建都重排 -> 必须先按构建把语义表推导出来
|
||
names, arity, dynamic, diag = derive_opcode_table(js_text)
|
||
self.names, self.arity, self.dynamic, self.diag = names, arity, dynamic, diag
|
||
self.insts = disassemble_cfg(self.z, names=names, arity=arity, dynamic=dynamic)
|
||
self.by_pc: Dict[int, dict] = {i["pc"]: i for i in self.insts}
|
||
self.pcs: List[int] = sorted(self.by_pc)
|
||
self._idx: Dict[int, int] = {pc: n for n, pc in enumerate(self.pcs)}
|
||
|
||
def op(self, pc: Optional[int]) -> Optional[str]:
|
||
i = self.by_pc.get(pc) if pc is not None else None
|
||
return i["name"] if i else None
|
||
|
||
def ops(self, pc: Optional[int]) -> List[int]:
|
||
i = self.by_pc.get(pc) if pc is not None else None
|
||
return list(i["operands"]) if i else []
|
||
|
||
def note(self, pc: int) -> str:
|
||
return " ".join(self.by_pc[pc].get("note") or [])
|
||
|
||
def nxt(self, pc: Optional[int]) -> Optional[int]:
|
||
n = self._idx.get(pc) if pc is not None else None
|
||
return self.pcs[n + 1] if n is not None and n + 1 < len(self.pcs) else None
|
||
|
||
def prev(self, pc: Optional[int]) -> Optional[int]:
|
||
n = self._idx.get(pc) if pc is not None else None
|
||
return self.pcs[n - 1] if n is not None and n > 0 else None
|
||
|
||
def in_range(self, lo: int, hi: int) -> List[int]:
|
||
import bisect
|
||
a = bisect.bisect_left(self.pcs, lo)
|
||
b = bisect.bisect_right(self.pcs, hi)
|
||
return self.pcs[a:b]
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 1. 闭包清单 / 密钥字打包点
|
||
# --------------------------------------------------------------------------- #
|
||
def _closures(d: _Dis) -> List[dict]:
|
||
"""所有 CREATE_CLOSURE -> {pc, target, caps:{dst:src}, str_after}。"""
|
||
out: List[dict] = []
|
||
for i in d.insts:
|
||
if i["name"] != "CREATE_CLOSURE":
|
||
continue
|
||
pc = i["pc"]
|
||
note = " ".join(i["note"] or [])
|
||
m = re.search(r"target=@(\d+)", note)
|
||
if not m:
|
||
continue
|
||
caps: Dict[int, int] = {}
|
||
caps_part = re.search(r"caps=\[([^\]]*)\]", note)
|
||
if caps_part:
|
||
for dst, src in re.findall(r"r(\d+)=r(\d+)", caps_part.group(1)):
|
||
caps[int(dst)] = int(src)
|
||
out.append({"pc": pc, "target": int(m.group(1)), "caps": caps,
|
||
"str_after": _string_after(d, pc)})
|
||
return out
|
||
|
||
|
||
def _string_after(d: _Dis, pc: int) -> Optional[str]:
|
||
"""CREATE_CLOSURE 紧随其后的 `PUSH_EMPTY_STR + PUSH_STR_CHAR*` 字面量。"""
|
||
p = d.nxt(pc)
|
||
if p is None or d.op(p) != "PUSH_EMPTY_STR":
|
||
return None
|
||
chars: List[str] = []
|
||
q = d.nxt(p)
|
||
while q is not None and d.op(q) == "PUSH_STR_CHAR":
|
||
chars.append(chr(d.ops(q)[0]))
|
||
q = d.nxt(q)
|
||
return "".join(chars)
|
||
|
||
|
||
def _pack_sites(d: _Dis, lo: int, hi: int) -> List[dict]:
|
||
"""
|
||
在 [lo,hi] 里找"密钥字打包点":
|
||
... CALL_METHOD[1] / PUSH_IMM c / SUB (off = -c)
|
||
或 ... CALL_METHOD[1] / PUSH_IMM 0 / PUSH_IMM c / SUB / SUB (off = +c)
|
||
/ PUSH_IMM 8 / LOAD_REG_IMM / MUL / SHL / BITWISE_OR
|
||
返回 [{or_pc, off}]。
|
||
"""
|
||
res: List[dict] = []
|
||
for pc in d.in_range(lo, hi):
|
||
if d.op(pc) != "PUSH_IMM" or d.ops(pc) != [8]:
|
||
continue
|
||
a, b, c, e = d.nxt(pc), None, None, None
|
||
b = d.nxt(a)
|
||
c = d.nxt(b)
|
||
e = d.nxt(c)
|
||
if not (d.op(a) == "LOAD_REG_IMM" and d.op(b) == "MUL"
|
||
and d.op(c) == "SHL" and d.op(e) == "BITWISE_OR"):
|
||
continue
|
||
off = _decode_offset(d, pc)
|
||
if off is not None:
|
||
res.append({"or_pc": e, "imm8_pc": pc, "off": off})
|
||
return res
|
||
|
||
|
||
def _decode_offset(d: _Dis, imm8_pc: int) -> Optional[int]:
|
||
"""解出紧跟 CALL_METHOD 之后的偏移立即数 (含两种编码)。"""
|
||
p1 = d.prev(imm8_pc) # 最后一个 SUB
|
||
if d.op(p1) != "SUB":
|
||
return None
|
||
p2 = d.prev(p1)
|
||
if d.op(p2) == "PUSH_IMM": # X - c => off = -c
|
||
return -d.ops(p2)[0]
|
||
if d.op(p2) == "SUB": # X - (0 - c) => off = +c
|
||
p3 = d.prev(p2)
|
||
p4 = d.prev(p3)
|
||
if d.op(p3) == "PUSH_IMM" and d.op(p4) == "PUSH_IMM" and d.ops(p4) == [0]:
|
||
return d.ops(p3)[0]
|
||
return None
|
||
|
||
|
||
def _store_index(d: _Dis, lo: int, hi: int) -> Optional[int]:
|
||
"""闭包尾部的 `PUSH_IMM_ARRAY / PUSH_IMM [i] / MAKE_REG_PROP_REF / LOAD_REG_IMM / SET_PROP`。"""
|
||
hits: List[int] = []
|
||
for pc in d.in_range(lo, hi):
|
||
if d.op(pc) != "PUSH_IMM_ARRAY":
|
||
continue
|
||
a, b, c, e = d.nxt(pc), None, None, None
|
||
b = d.nxt(a)
|
||
c = d.nxt(b)
|
||
e = d.nxt(c)
|
||
if (d.op(a) == "PUSH_IMM" and d.op(b) == "MAKE_REG_PROP_REF"
|
||
and d.op(c) == "LOAD_REG_IMM" and d.op(e) == "SET_PROP"):
|
||
hits.append(d.ops(a)[0])
|
||
return hits[-1] if hits else None
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 2. XTEA 闭包定位 + 轮加盐
|
||
# --------------------------------------------------------------------------- #
|
||
def _find_xtea_closure(d: _Dis, closures: List[dict]) -> Optional[dict]:
|
||
"""含 XTEA 常数(delta)的那个闭包体所属的 CREATE_CLOSURE。"""
|
||
delta_pcs = [pc for pc in d.pcs
|
||
if d.op(pc) == "PUSH_IMM" and d.ops(pc)[0] in (XTEA_DELTA, XTEA_DELTA * 32)]
|
||
# 取"最内层"(区间最小)的闭包体 —— 外层 init 函数的体也会横跨 delta,
|
||
# 但它不捕获密钥数组。
|
||
best: Optional[dict] = None
|
||
best_end: Optional[int] = None
|
||
for cl in closures:
|
||
end = _body_end(d, cl["pc"])
|
||
if end is None or end < cl["target"]:
|
||
continue
|
||
if any(cl["target"] <= pc <= end for pc in delta_pcs):
|
||
if best is None or best_end is None or (end - cl["target"]) < (best_end - best["target"]):
|
||
best, best_end = cl, end
|
||
return best
|
||
|
||
|
||
def _largest_reg(regs: Dict[int, int]) -> Optional[int]:
|
||
return max(regs.values()) if regs else None
|
||
|
||
|
||
def _caps_single(caps: Dict[int, int]) -> Optional[Tuple[int, int]]:
|
||
"""字/XTEA 闭包只捕获一个数组: 返回 (dst, src)。"""
|
||
if len(caps) != 1:
|
||
return None
|
||
(dst, src), = caps.items()
|
||
return dst, src
|
||
|
||
|
||
def _defining_closure(d: _Dis, closures: List[dict], pc: int) -> Optional[dict]:
|
||
"""含给定 pc 的**最内层**闭包(即创建 pc 处那条 CREATE_CLOSURE 的那个函数)。"""
|
||
best: Optional[dict] = None
|
||
best_span: Optional[int] = None
|
||
for c in closures:
|
||
e = _body_end(d, c["pc"])
|
||
if e is None or e < c["target"]:
|
||
continue
|
||
if c["target"] <= pc <= e:
|
||
span = e - c["target"]
|
||
if best_span is None or span < best_span:
|
||
best, best_span = c, span
|
||
return best
|
||
|
||
|
||
def _body_end(d: _Dis, cl_pc: int) -> Optional[int]:
|
||
"""
|
||
闭包体结束 pc (该体的 RET_TRUE)。约定: 闭包被创建后要么紧跟其体尾的 RET_TRUE,
|
||
要么中间夹一条 `PUSH_IMM_ARRAY`(闭包被塞进数组时)。
|
||
"""
|
||
e = d.prev(cl_pc)
|
||
if e is not None and d.op(e) == "PUSH_IMM_ARRAY":
|
||
e = d.prev(e)
|
||
return e if (e is not None and d.op(e) == "RET_TRUE") else None
|
||
|
||
|
||
def _guard_index(d: _Dis, eq_pc: int) -> Optional[int]:
|
||
"""
|
||
守卫 `idx == ((sum & 3) | ((sum>>>11)&3))` 里的常量 idx。
|
||
从 EQ_LOOSE 反向游走, 跳过下标表达式自身的算子, 第一个落在 0..3 的立即数即 idx。
|
||
掩码 3 靠"它的后继是 BITWISE_AND / LOAD_REG_IMM / SHR_UNSIGNED"识别并跳过。
|
||
"""
|
||
pc = d.prev(eq_pc)
|
||
last: Optional[str] = None
|
||
for _ in range(24):
|
||
if pc is None:
|
||
return None
|
||
nm, ops = d.op(pc), d.ops(pc)
|
||
if nm in ("PUSH_IMM", "REPLACE_TOP_IMM") and ops:
|
||
v = ops[0]
|
||
if 0 <= v <= 3:
|
||
if not (nm == "PUSH_IMM" and v == 3 and last in _MASK_NEIGHBOURS):
|
||
return v
|
||
elif nm not in _SKIP_OPS:
|
||
# 出现意料之外的算子: 继续向前找, 但记录上下文
|
||
pass
|
||
last = nm
|
||
pc = d.prev(pc)
|
||
return None
|
||
|
||
|
||
def _salts(d: _Dis, lo: int, hi: int) -> List[Tuple[int, int]]:
|
||
"""
|
||
XTEA 轮内的条件加盐: `GET_PROP / PUSH_IMM <salt> / ADD / ADD / XOR`。
|
||
返回 [(idx, salt)] (按 (idx,salt) 去重)。
|
||
"""
|
||
salt_pcs: List[int] = []
|
||
for pc in d.in_range(lo, hi):
|
||
if d.op(pc) != "PUSH_IMM":
|
||
continue
|
||
if d.op(d.prev(pc)) != "GET_PROP" or d.op(d.nxt(pc)) != "ADD":
|
||
continue
|
||
salt_pcs.append(pc)
|
||
if not salt_pcs:
|
||
return []
|
||
|
||
# 收集所有 (JUMP_IF_TRUE, EQ_LOOSE) 守卫; block(T) 到 T 之后第一条无条件 JUMP
|
||
guards: List[Tuple[int, int, Optional[int]]] = [] # (target, jump_pc, idx)
|
||
for pc in d.in_range(lo, hi):
|
||
if d.op(pc) != "JUMP_IF_TRUE":
|
||
continue
|
||
tgt_ops = d.ops(pc)
|
||
if not tgt_ops:
|
||
continue
|
||
tgt = tgt_ops[0]
|
||
eq = d.prev(pc)
|
||
if d.op(eq) != "EQ_LOOSE":
|
||
continue
|
||
idx = _guard_index(d, eq)
|
||
if idx is not None:
|
||
guards.append((tgt, pc, idx))
|
||
|
||
def block_end(tgt: int) -> int:
|
||
p = tgt
|
||
while p is not None and p <= hi:
|
||
if d.op(p) == "JUMP":
|
||
return p
|
||
p = d.nxt(p)
|
||
return hi
|
||
|
||
out: List[Tuple[int, int]] = []
|
||
for spc in salt_pcs:
|
||
salt = d.ops(spc)[0]
|
||
best: Optional[Tuple[int, int]] = None # (target, idx)
|
||
for tgt, _j, idx in guards:
|
||
if tgt <= spc < block_end(tgt):
|
||
if best is None or tgt > best[0]:
|
||
best = (tgt, idx)
|
||
if best is not None:
|
||
out.append((best[1], salt))
|
||
# 去重保序
|
||
seen = set()
|
||
uniq: List[Tuple[int, int]] = []
|
||
for x in out:
|
||
if x not in seen:
|
||
seen.add(x)
|
||
uniq.append(x)
|
||
return uniq
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 3. 主入口
|
||
# --------------------------------------------------------------------------- #
|
||
def word_from_chars(src: str, off: int) -> int:
|
||
"""word = Σ ((ord(c) + off) & 0xFF) << (8*k) (小端打包)"""
|
||
w = 0
|
||
for k, ch in enumerate(src):
|
||
w |= ((ord(ch) + off) & 0xFF) << (8 * k)
|
||
return w & M32
|
||
|
||
|
||
def extract_xtea_key_pure(js_text: str, verbose: bool = False):
|
||
"""
|
||
纯 Python 从 tdc.js 里还原本次构建的 XTEA 有效密钥 (4 个 32 位字)。
|
||
|
||
返回 dict:
|
||
base : 4 个 base 字 (未折盐)
|
||
salts : [(idx, salt), ...]
|
||
key : 有效密钥
|
||
words : [(idx, word, src, off), ...] (便于诊断)
|
||
"""
|
||
d = _Dis(js_text)
|
||
closures = _closures(d)
|
||
xtea = _find_xtea_closure(d, closures)
|
||
if xtea is None:
|
||
raise ValueError("未找到 XTEA 闭包 (字节码里没有 delta 常数?)")
|
||
xs = _caps_single(xtea["caps"])
|
||
if xs is None:
|
||
raise ValueError("XTEA 闭包没有唯一捕获的数组, 无法确定密钥数组")
|
||
xtea_src = xs[1] # XTEA 所在帧里, 密钥数组所在的寄存器
|
||
|
||
# 密钥数组是"被 XTEA 闭包捕获"的那一个。字闭包与 XTEA 闭包往往不在同一帧:
|
||
# 父帧 G: r<R> = 密钥数组
|
||
# ├─ 字闭包 W: caps=[r7 = 本帧 r<Wsrc>] (Wsrc ∈ {R, 诱饵寄存器})
|
||
# └─ XTEA 闭包 : caps=[r7 = 本帧 r<xtea_src>]
|
||
# 所以要把 W 的捕获穿一层: B = 定义 W 的那个闭包(B 帧 -> G 帧), 检查
|
||
# B.caps[Wsrc] == xtea_src -> 该字闭包写的就是真密钥数组。
|
||
words: Dict[int, Tuple[int, str, int]] = {}
|
||
picked: List[str] = []
|
||
for cl in closures:
|
||
if cl["str_after"] is None:
|
||
continue
|
||
cs = _caps_single(cl["caps"])
|
||
if cs is None:
|
||
continue
|
||
w_src = cs[1] # 该字闭包捕获的、定义帧里的寄存器
|
||
definer = _defining_closure(d, closures, cl["pc"])
|
||
if definer is not None and definer["caps"]:
|
||
real = definer["caps"].get(w_src) == xtea_src
|
||
else: # 同帧: 直接比
|
||
real = (w_src == xtea_src)
|
||
if not real:
|
||
continue
|
||
end = _body_end(d, cl["pc"])
|
||
if end is None or end < cl["target"]:
|
||
continue
|
||
packs = _pack_sites(d, cl["target"], end)
|
||
if not packs:
|
||
continue
|
||
idx = _store_index(d, cl["target"], end)
|
||
if idx is None or not (0 <= idx < KEY_LEN):
|
||
continue
|
||
src, off = cl["str_after"], packs[-1]["off"]
|
||
words[idx] = (word_from_chars(src, off), src, off)
|
||
picked.append("%s(idx=%d,off=%+d)" % (src, idx, off))
|
||
|
||
if len(words) != KEY_LEN:
|
||
raise ValueError(
|
||
"密钥字不完整: 只定位到 %s (期望 0..3); XTEA 密钥帧寄存器 r%s"
|
||
% (sorted(words), xtea_src))
|
||
|
||
base = [words[i][0] for i in range(KEY_LEN)]
|
||
salts = _salts(d, xtea["target"], d.prev(xtea["pc"]) or xtea["target"])
|
||
|
||
key = list(base)
|
||
for idx, salt in salts:
|
||
key[idx] = (key[idx] + salt) & M32
|
||
|
||
report = {
|
||
"base": base,
|
||
"salts": salts,
|
||
"key": key,
|
||
"words": [(i, words[i][0], words[i][1], words[i][2]) for i in range(KEY_LEN)],
|
||
"xtea_closure_pc": xtea["pc"],
|
||
"key_reg": xtea_src,
|
||
}
|
||
if verbose:
|
||
print("[tdc_key_pure] XTEA closure @%d key array = r%s" % (xtea["pc"], xtea_src))
|
||
for i, w, s, o in report["words"]:
|
||
print(" word[%d] <- %-6r off=%+d = %d" % (i, s, o, w))
|
||
for idx, salt in salts:
|
||
print(" salt +%d -> key[%d]" % (salt, idx))
|
||
print(" base = %s" % base)
|
||
print(" key = %s" % key)
|
||
return report
|
||
|
||
|
||
def extract_xtea_key(js_text: str) -> List[int]:
|
||
"""便捷入口: 只取有效密钥 (与 crypto.extract_xtea_key 同口径)。"""
|
||
return list(extract_xtea_key_pure(js_text)["key"])
|