做的什么:
1. 判决并修正 collect 结构: 不是「整体一次加密」, 而是
chunk1 / trajectory / chunk2 / sd **四段独立加密**, 前 3 段
空格 pad 到 24 字节对齐、sd 不 pad, 4 段 base64 直接拼接。
2. 按案例 §5.3 实现字符串+结构指纹的模块识别器, 把 37 个采集器
全部识别且跨构建唯一。
3. 修 reorder 的恒等映射 bug, 并补强断言。
4. 在线抓取全新样本作为唯一数据源, 清理探索性脚本。
为什么:
* 旧「整体加密 + \x00 补齐」与真机数据矛盾 —— 导致多轮实验
建立在结构错误的 collect 上(这正是此前反复"乱碰"的根因)。
* 模块 id 每份构建重排, 不能当跨构建锚点; 但模块的**代码**
(字符串集合、结构)不变 —— 这是案例指明的不变量。
* reorder 旧实现用「目标槽下标」切「源数组」, 顺序不同即全盘错位;
原测试只断言"是置换", 恒等函数也满足, 所以 bug 被放过了。
怎么验证:
* 四段结构(3 条独立证据, 其中一条不需密钥):
- 真机明文 3 处连续空格串 [+5,+10,+24], 且 pad == 24-len%24 全中
- 密文长度算术 480+96+416+56 = 1048 == 真实 1048 字符
- '=' 只出现在 1046/1048, 对应 sd 段 33B→补 7→40B
* 指纹: 4 份全新在线构建 37/37 识别、0 unknown、跨构建语义名集合
完全一致、单构建内 0 重名
* 配对正确性: 独立证据 37/37 一致; 负对照(清单下标 / 模块 id /
随机配对)均只有 2-3/37 —— 证明归一化没有过拟合
* 端到端重排 3/3 正确且非恒等
* selftest 全绿; 强断言自查: 旧实现过不了新断言
新增:
tools/tdc_modules.py 字符串+结构指纹识别(案例 §5.3)
scripts/fetch_online_samples.py 在线抓取全新 tdc.js
scripts/verify/*.py 7 个可复现的验证脚本
docs/tdc-案例.md 案例原文(看雪, 作者执着的猫)
修改:
tcaptcha/crypto.py +build_collect / split_collect_segments(四段)
tools/tdc_collectgen.py reorder 改为跨构建锚点对齐 + spec_from_seq
tools/tdc_layout.py +static_fingerprint / opcode_histogram
tools/tdc_program.py 修符号求值插值类型崩溃
tcaptcha/tests/test_collectgen.py 补「非恒等」强断言 + 在线新样本端到端
tcaptcha/client.py debug 目录改到系统临时目录(不再写仓库内 scratch/)
har/1ms_dianxuan/PROGRESS.md 新增已知 #41~#49, 显式修正被推翻的旧结论
.gitignore +scratch/ +har/online/
删除:
scratch/(含 10-11 遗留的一次性脚本, 已确认不可恢复后清理)
注意:
* 账本记录了一次自我纠错: 曾据案例 §5.5 误判 kind=2「不进 cd」,
实测(cd 38 槽、轨迹在第 10 槽)否定后已撤回。
* 仍未打通: 纯离线生成需要每个语义名的取值来源(已知 #38)。
468 lines
22 KiB
Python
468 lines
22 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
tdc_collectgen.py —— **按本次构建**生成 collect 明文(并加密)。
|
||
|
||
承 har/1ms_dianxuan/PROGRESS.md #29~#36:
|
||
* #29/#31 槽位序**按构建随机化**,不能用缓存/旧模板;
|
||
* #34/#35 槽位序 = 清单模块的 require 顺序,**可纯静态导出**;
|
||
* #36 「清单项 → cd 槽」不是一一对应:`kind===2` 的项不进 cd,
|
||
`kind===1` 的返回值是 JSON 碎片串、串内逗号会切出额外槽。
|
||
|
||
本模块做两件事:
|
||
1. `reorder(template_cd, mapping, values)` —— 把"语义值"按**本次构建的槽位序**排成 cd;
|
||
2. `build(...)` —— 组装成 `{"cd":[...],"sd":{...}}` 明文,并用该构建的 XTEA 密钥加密。
|
||
|
||
⚠️ 关于「语义值」从哪来(诚实说明,别把它当成已经解决的事):
|
||
目前有两种取值来源,都可用,但**完备性不同**:
|
||
* `--values <json>`:由调用方提供「语义名 -> 值」字典(最稳,推荐);
|
||
* `--from-sample <cd json>`:用**另一份已解密的真机明文**做"同源重排"。
|
||
这要求两份样本跑在**同一台机器/同一环境**(取值集合相同,只有顺序不同,见 #31),
|
||
适用于"先拿一份真机明文当素材、再按本次构建重排"的验证场景。
|
||
**仍需补齐**:纯算地把每个语义名对应的值算出来(多数是静态环境量,可配置;
|
||
canvas/WebGL 指纹这类必须取自真实环境)。这一步未完成前,纯离线生成还不能保证过风控。
|
||
|
||
用法:
|
||
# 用一份同机真机明文做重排, 按本次构建输出 collect
|
||
.venv/bin/python tools/tdc_collectgen.py <tdc.js> \\
|
||
--from-sample /tmp/real_cd.json --out /tmp/collect.b64
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import base64
|
||
import json
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional, Sequence, Tuple
|
||
|
||
ROOT = Path(__file__).resolve().parents[1]
|
||
if str(ROOT) not in sys.path:
|
||
sys.path.insert(0, str(ROOT))
|
||
if str(ROOT / "tools") not in sys.path:
|
||
sys.path.insert(0, str(ROOT / "tools"))
|
||
|
||
from tcaptcha.crypto import xtea_encrypt_bytes # noqa: E402
|
||
from tcaptcha.tdc_key import extract_xtea_key_pure # noqa: E402
|
||
import tdc_layout # noqa: E402
|
||
import tdc_program as tp # noqa: E402
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 槽位映射 #
|
||
# --------------------------------------------------------------------------- #
|
||
def value_sig(v: Any) -> str:
|
||
"""值的**结构签名**(用于在没有环境指纹时当次级锚点)。
|
||
|
||
只描述"形状 + 常量",抹掉随时间/会话变化的具体数值:
|
||
1791736322 -> "int:epochish" [[34,96,3284],...] -> "list:pts"
|
||
"MacIntel" -> "str:MacIntel" [360,360] -> "list:2"
|
||
易变槽(时间戳/轨迹/随机串)的签名在两份样本里**类型一致**,数值不同 —— 正好
|
||
用来把它们彼此对应起来,再由调用方决定"沿用样本值"还是"重新生成"。
|
||
"""
|
||
if v is None:
|
||
return "null"
|
||
if isinstance(v, bool):
|
||
return f"bool:{v}"
|
||
if isinstance(v, int):
|
||
return "int:epochish" if 1_700_000_000 <= v <= 2_000_000_000 else f"int:{v}"
|
||
if isinstance(v, float):
|
||
return "float"
|
||
if isinstance(v, str):
|
||
if len(v) > 200:
|
||
return "str:big"
|
||
if _RANDSTR_RE.fullmatch(v):
|
||
return "str:rand"
|
||
return f"str:{v[:24]}"
|
||
if isinstance(v, list):
|
||
if v and all(isinstance(x, list) and len(x) == 3 for x in v):
|
||
return "list:pts"
|
||
return f"list:{len(v)}"
|
||
return type(v).__name__
|
||
|
||
|
||
_RANDSTR_RE = re.compile(r"[A-Za-z0-9+/=]{20,}")
|
||
|
||
|
||
class _Unmapped:
|
||
"""哨兵:标记"这个目标槽没找到源槽"。
|
||
|
||
为什么不能用 None:真机 cd 里本身就有 null 值(`out += "null"` 分支,
|
||
见 PROGRESS.md #36),用 None 当占位符会把合法的 null 槽误判成"没对齐"。
|
||
"""
|
||
|
||
__slots__ = ()
|
||
|
||
def __repr__(self) -> str: # pragma: no cover - 仅用于调试输出
|
||
return "<unmapped>"
|
||
|
||
|
||
_UNMAPPED = _Unmapped()
|
||
|
||
|
||
def build_anchor_map(src: "SlotSpec", dst: "SlotSpec") -> List[Tuple[int, int]]:
|
||
"""把「源构建的槽」对齐到「目标构建的槽」,返回 [(src_slot, dst_slot), ...]。
|
||
|
||
这是重排的核心。三层锚点,逐层降级(都必须**双向唯一**,否则宁可不对齐):
|
||
|
||
① **环境语义指纹**(主锚点,跨构建稳定,实测多重集交集 23/24)
|
||
源项 i 与目标项 j 的 env_fingerprint 相同 -> 对齐。
|
||
② **结构签名**(次锚点,用于常量槽 / 读不到环境的项)
|
||
value_sig 相同 -> 对齐。
|
||
③ 剩余未对齐的 -> 由调用方显式处理(不猜)。
|
||
|
||
为什么不能按项下标或模块 id 对齐(两个都已实测证伪):
|
||
* 项下标:两次构建的清单顺序**就是**不同的东西,按下标对齐 = 恒等映射;
|
||
* 模块 id:编译器每次重新分配,两份构建交集 28 个模块里 id->语义 相同的
|
||
只有 1/11(2026-10-12 实测)。
|
||
"""
|
||
|
||
def bipartite(keys_src: List[Any], keys_dst: List[Any]) -> Dict[int, int]:
|
||
"""只在**双向唯一**时配对,避免多对一的歧义错位。"""
|
||
from collections import defaultdict
|
||
|
||
by_dst: Dict[Any, List[int]] = defaultdict(list)
|
||
by_src: Dict[Any, List[int]] = defaultdict(list)
|
||
for i, k in enumerate(keys_src):
|
||
if k:
|
||
by_src[k].append(i)
|
||
for j, k in enumerate(keys_dst):
|
||
if k:
|
||
by_dst[k].append(j)
|
||
pair: Dict[int, int] = {}
|
||
for k, srcs in by_src.items():
|
||
dsts = by_dst.get(k) or []
|
||
if len(srcs) == 1 and len(dsts) == 1:
|
||
pair[srcs[0]] = dsts[0]
|
||
return pair
|
||
|
||
pairs: Dict[int, int] = {}
|
||
# 锚点优先级(2026-10-12 实测,4 份全新在线构建):
|
||
# 锚点 跨构建唯一可配对
|
||
# ① 语义名(案例§5.3 指纹) 35/37 <- 主锚点
|
||
# ② 静态语义指纹 fp 29/37
|
||
# ③ 指令直方图 hist 20/37
|
||
# ④ 结构签名 value_sig (仅双方都有实测值时)
|
||
# 所以:语义名先用,再逐级降级。每一级都要求**双向唯一**才配对。
|
||
|
||
# ① 语义名(案例 §5.3:字符串指纹 + 结构兜底 + 重名消歧)
|
||
pairs.update(bipartite([n or "" for n in src.names],
|
||
[n or "" for n in dst.names]))
|
||
# ②③ 降级:静态语义指纹 -> 指令直方图
|
||
for key in (lambda s: [f or "" for f in s.fps],
|
||
lambda s: [h or "" for h in s.hists]):
|
||
for i, j in bipartite(key(src), key(dst)).items():
|
||
pairs.setdefault(i, j)
|
||
# ④ 末级:结构签名(仅在双方都有实测值时可用)
|
||
sig_pair = bipartite([value_sig(v) for v in src.sample_values],
|
||
[value_sig(v) for v in dst.sample_values])
|
||
for i, j in sig_pair.items():
|
||
pairs.setdefault(i, j)
|
||
|
||
out: List[Tuple[int, int]] = []
|
||
for i, (_i, _m, sslot, n) in enumerate(src.assign):
|
||
j = pairs.get(i)
|
||
if j is None or j >= len(dst.assign):
|
||
continue
|
||
_dj, _dm, dslot, dn = dst.assign[j]
|
||
if dn != n:
|
||
continue # 槽数形状不一致 -> 拒绝猜,跳过
|
||
for k in range(n):
|
||
out.append((sslot + k, dslot + k))
|
||
return out
|
||
|
||
|
||
def reorder(sample: Sequence[Any], dst: "SlotSpec",
|
||
src: Optional["SlotSpec"] = None,
|
||
fill: Optional[Dict[int, Any]] = None) -> List[Any]:
|
||
"""把「源样本」的 cd 值重排成**目标构建**的槽位序。
|
||
|
||
⚠️ 旧实现的 bug(2026-10-12 修复):
|
||
旧版签名是 `reorder(sample, spec)`,其中 `spec` 只是**目标构建**的规格,
|
||
而循环里 `sample[slot:slot+n]` 用的 `slot` 恰恰是**目标构建自己的槽下标**。
|
||
两次构建的槽位序不同(#31),所以这等价于"按目标下标切源数组" —— 顺序不同
|
||
时全盘错位,顺序相同时才碰巧对。**必须同时给出源构建的规格**才能对齐。
|
||
|
||
参数:
|
||
sample - 源构建产出的 cd 值数组(真机明文)
|
||
dst - 目标构建的槽位规格(本次要发包的那个构建)
|
||
src - 源构建的槽位规格。**不传则退化为旧行为**(仅当两份构建同序时才正确,
|
||
此时本函数会先校验再放行,不再静默错位)。
|
||
fill - {目标槽号: 值},用于补齐锚点匹配不上的槽。
|
||
|
||
锚点能力上限(2026-12 实测,见 tdc_layout.static_fingerprint / opcode_histogram):
|
||
静态锚点组合 `fp+hist` 单构建内唯一 **29/37**,跨构建交集 35。
|
||
剩下 8 项(分成 2 组)**静态不可区分**:它们的函数体只有一处 JUMP 目标不同,
|
||
语义藏在跳转目标里,符号求值取不到。这些槽必须由调用方用 `fill` 显式给值。
|
||
|
||
未给 fill 且仍对不齐的槽 -> 抛 `UnmappedSlotsError`(宁可暴露"不知道",
|
||
也不要静默填错值 —— 旧实现正是静默填错,导致多轮实验建立在坏数据上)。
|
||
"""
|
||
if src is None:
|
||
# 契约:不提供源构建规格 = 调用方**承诺**样本与目标构建同序。
|
||
# 此时重排只能是恒等映射;先把槽数校验掉,再直接返回副本。
|
||
if len(sample) != dst.n_slots:
|
||
raise ValueError(
|
||
f"样本槽数 {len(sample)} != 目标构建槽数 {dst.n_slots};"
|
||
f"未提供源构建规格时不跨构建重排(见 PROGRESS.md #31)")
|
||
out: List[Any] = [_UNMAPPED] * dst.n_slots
|
||
for d in range(dst.n_slots):
|
||
out[d] = sample[d]
|
||
for d, v in (fill or {}).items():
|
||
if 0 <= d < dst.n_slots:
|
||
out[d] = v
|
||
return out
|
||
|
||
if len(sample) != src.n_slots:
|
||
raise ValueError(
|
||
f"样本槽数 {len(sample)} != 源构建槽数 {src.n_slots};"
|
||
f"样本必须来自 `src` 描述的那个构建")
|
||
|
||
pairs = build_anchor_map(src, dst)
|
||
# 用哨兵而不是 None 标记"未映射":真机 cd 里本身就有 null 值,
|
||
# 拿 None 当占位符会把合法的 null 槽误判成"没对齐"。
|
||
out: List[Any] = [_UNMAPPED] * dst.n_slots
|
||
for s_slot, d_slot in pairs:
|
||
out[d_slot] = sample[s_slot]
|
||
unmapped = [d for d, v in enumerate(out) if v is _UNMAPPED]
|
||
for d, v in (fill or {}).items():
|
||
if 0 <= d < dst.n_slots:
|
||
out[d] = v
|
||
if d in unmapped:
|
||
unmapped.remove(d)
|
||
if unmapped:
|
||
raise UnmappedSlotsError(unmapped, dst, src)
|
||
return out
|
||
|
||
|
||
class UnmappedSlotsError(RuntimeError):
|
||
"""有目标槽找不到源槽与之对应(锚点匹配失败)。"""
|
||
|
||
def __init__(self, slots: List[int], dst: "SlotSpec", src: "SlotSpec") -> None:
|
||
self.slots = slots
|
||
super().__init__(
|
||
f"目标构建有 {len(slots)} 个槽无法与源样本对齐(槽号 {slots[:12]}"
|
||
f"{'...' if len(slots) > 12 else ''})。"
|
||
f"锚点不足时宁可报错也不要静默填错值 —— 请用 `--values` 显式提供这些槽的取值,"
|
||
f"或检查两份构建是否来自同一采集器集合。")
|
||
|
||
|
||
def outer_commas(s: str) -> int:
|
||
"""数**不在**任何 `[]` / `{}` 内的逗号个数(方括号内的逗号不构成顶层分隔)。"""
|
||
depth = 0
|
||
n = 0
|
||
for ch in s:
|
||
if ch in "[{":
|
||
depth += 1
|
||
elif ch in "]}":
|
||
depth = max(0, depth - 1)
|
||
elif ch == "," and depth == 0:
|
||
n += 1
|
||
return n
|
||
|
||
|
||
class SlotSpec:
|
||
"""一次构建下的槽位规格(规则见 PROGRESS.md #36)。"""
|
||
|
||
def __init__(self, seq: Sequence[int],
|
||
kinds: Optional[Sequence[Any]] = None,
|
||
v0s: Optional[Sequence[str]] = None,
|
||
fps: Optional[Sequence[Optional[str]]] = None,
|
||
hists: Optional[Sequence[str]] = None,
|
||
names: Optional[Sequence[Optional[str]]] = None) -> None:
|
||
self.seq = list(seq)
|
||
self.kinds = list(kinds) if kinds is not None else [None] * len(seq)
|
||
# v0s[i] = 该项 get()[0] 的字符串化结果;kind=1 时按它的逗号数切出额外槽
|
||
self.v0s = list(v0s) if v0s is not None else [""] * len(seq)
|
||
# fps[i] = 该项的**环境语义指纹**(跨构建稳定, 见 tdc_layout.env_fingerprint);
|
||
# None/空串 表示该项读不到环境路径(常量槽 / 轨迹槽),需退到值签名或直接填值
|
||
self.fps: List[Optional[str]] = list(fps) if fps is not None else [None] * len(seq)
|
||
# hists[i] = 该项的**指令直方图**(兜底锚点,覆盖 37/37,见 tdc_layout.opcode_histogram)
|
||
self.hists: List[str] = list(hists) if hists is not None else [""] * len(seq)
|
||
# names[i] = 该项的**语义名**——案例 §5.3 的字符串+结构指纹(主锚点)。
|
||
# 实测 4 份全新在线构建:37/37 识别、0 unknown、跨构建完全一致、**35/37 唯一可配对**。
|
||
# 明显优于本项目早期自创的 fp/hist 锚点(29/37),所以放在最前面用。
|
||
self.names: List[Optional[str]] = (
|
||
list(names) if names is not None else [None] * len(seq))
|
||
# 该构建下的一份**实测 cd 值**(源侧重排时要用它算结构签名)。
|
||
# 目标侧可以留空;只有当成"源规格"参与 build_anchor_map 时才必须给。
|
||
self.sample_values: List[Any] = []
|
||
self.assign: List[Tuple[int, int, int, int]] = [] # (item, module, first_slot, n_slots)
|
||
self._build()
|
||
|
||
def _build(self) -> None:
|
||
slot = 0
|
||
for i, m in enumerate(self.seq):
|
||
k = self.kinds[i]
|
||
n = 1
|
||
if k == 1 and isinstance(self.v0s[i], str):
|
||
# 原样拼 -> 该串会被 JSON 解析器按逗号切成多个值。
|
||
# 但**数组/对象形态**的串('[]' / '[360,360]' / '["zh-CN","zh"]')里的逗号
|
||
# 处在方括号内, 拼回去仍是一个值, 不产生新槽 —— 只数"括号外"的逗号。
|
||
n += outer_commas(self.v0s[i])
|
||
if k == 2:
|
||
# kind=2 把 r3 清空转存 r4(不接在 cd 后面),但 r4 会在收尾 @4905
|
||
# 被二次加工后重新拼进 r3 —— 实测它**仍然产出一个槽**。
|
||
n = 1
|
||
self.assign.append((i, m, slot, n))
|
||
slot += n
|
||
|
||
@property
|
||
def n_slots(self) -> int:
|
||
return sum(a[3] for a in self.assign)
|
||
|
||
def describe(self) -> str:
|
||
multi = [(i, m, n) for (i, m, _s, n) in self.assign if n > 1]
|
||
return (f"项 {len(self.seq)} -> 槽 {self.n_slots} "
|
||
f"(kind=2 跳过 {sum(1 for k in self.kinds if k == 2)} 项"
|
||
+ (f"; 多槽项 {multi}" if multi else "") + ")")
|
||
|
||
|
||
def load_shape(items_path: str, seq: Sequence[int]) -> Tuple[List[Any], List[str]]:
|
||
"""从运行时取样(items json)里读出每项的 kind 与 get()[0] 的字符串化值。"""
|
||
d = json.load(open(items_path, encoding="utf-8"))
|
||
res = d.get("result", d)
|
||
rows = res.get("out") or res.get("items") or []
|
||
kinds: List[Any] = []
|
||
v0s: List[str] = []
|
||
for r in rows:
|
||
if not isinstance(r, dict):
|
||
kinds.append(None); v0s.append(""); continue
|
||
kinds.append(r.get("kind"))
|
||
v = r.get("v0")
|
||
v0s.append(str(v) if v is not None else "")
|
||
if len(kinds) < len(seq):
|
||
pad = len(seq) - len(kinds)
|
||
kinds += [None] * pad
|
||
v0s += [""] * pad
|
||
return kinds[: len(seq)], v0s[: len(seq)]
|
||
|
||
|
||
def load_sample_cd(path: str) -> List[Any]:
|
||
raw = json.load(open(path, encoding="utf-8"))
|
||
return raw["cd"] if isinstance(raw, dict) else raw
|
||
|
||
|
||
def spec_from_js(path: str, kinds_path: Optional[str] = None,
|
||
sample_cd: Optional[Sequence[Any]] = None) -> "SlotSpec":
|
||
"""从一份 tdc.js 直接构造该构建的槽位规格(含环境语义指纹)。"""
|
||
prog = tp.load(path)
|
||
_mid, _entry, seq = tdc_layout.find_manifest_module(prog)
|
||
kinds, v0s = load_shape(kinds_path, seq) if kinds_path else (None, None)
|
||
fps = tdc_layout.item_fingerprints(prog, seq)
|
||
hists = tdc_layout.item_histograms(prog, seq)
|
||
# 主锚点:案例 §5.3 的字符串+结构指纹语义名(实测 35/37 唯一可配对)
|
||
# 复用已加载的 prog(analyze_prog),避免把同一份 tdc.js 再解析一遍。
|
||
# 不做静默降级(AGENTS.md 附录 A#8):锚点算不出来就抛错,
|
||
# 让调用方明确知道"这次没有主锚点",而不是悄悄退化到较弱的锚点上。
|
||
import tdc_modules
|
||
names = [r["name"] for r in tdc_modules.analyze_prog(prog)]
|
||
spec = SlotSpec(seq, kinds, v0s, fps, hists, names)
|
||
if sample_cd is not None:
|
||
spec.sample_values = list(sample_cd)
|
||
return spec
|
||
|
||
|
||
def spec_from_seq(seq: Sequence[int],
|
||
kinds: Optional[Sequence[Any]] = None,
|
||
names: Optional[Sequence[Optional[str]]] = None,
|
||
sample_cd: Optional[Sequence[Any]] = None,
|
||
v0s: Optional[Sequence[str]] = None) -> "SlotSpec":
|
||
"""只用清单序列构造规格(**不需要 tdc.js**,供测试/合成用例使用)。
|
||
|
||
纯算重排只需要「每一项占几槽」+「每项的锚点名」;
|
||
tdc.js 只用来**导出**这两样东西,本身不是重排的输入。
|
||
"""
|
||
spec = SlotSpec(seq, kinds, v0s, names=names)
|
||
if sample_cd is not None:
|
||
spec.sample_values = list(sample_cd)
|
||
return spec
|
||
|
||
|
||
# --------------------------------------------------------------------------- #
|
||
# 明文组装 + 加密 #
|
||
# --------------------------------------------------------------------------- #
|
||
def build_plain(cd: Sequence[Any], sd: Optional[dict] = None) -> str:
|
||
sd = sd or {"od": "C", "ft": "qf_7Pf__H"}
|
||
return json.dumps({"cd": list(cd), "sd": sd}, separators=(",", ":"))
|
||
|
||
|
||
def encrypt_plain(plain: str, key: Sequence[int]) -> str:
|
||
data = plain.encode("latin1")
|
||
data += b"\x00" * ((8 - len(data) % 8) % 8) # 与真实样本一致: \x00 补齐
|
||
return base64.b64encode(xtea_encrypt_bytes(data, list(key))).decode("ascii")
|
||
|
||
|
||
def main() -> int:
|
||
ap = argparse.ArgumentParser(description="按本次构建生成 collect")
|
||
ap.add_argument("path", help="本次会话的 tdc.js(**目标构建**)")
|
||
ap.add_argument("--src-build", default=None,
|
||
help="源样本所属的那份 tdc.js。**跨构建重排必须给**;"
|
||
"不给则假定源样本与目标构建同序(并会校验槽数)")
|
||
ap.add_argument("--from-sample", default=None, help="一份已解密的 cd 数组(json)")
|
||
ap.add_argument("--values", default=None, help="语义名->值的 json 字典(实验性)")
|
||
ap.add_argument("--out", default=None, help="输出 collect(base64) 的文件")
|
||
ap.add_argument("--plain-out", default=None, help="同时输出明文")
|
||
ap.add_argument("--kinds", default=None,
|
||
help="运行时取样的 items json(提供每项的 kind,见 #36)")
|
||
args = ap.parse_args()
|
||
|
||
prog = tp.load(args.path)
|
||
_mid, _entry, seq = tdc_layout.find_manifest_module(prog)
|
||
key = extract_xtea_key_pure(open(args.path, encoding="utf-8", errors="replace").read())["key"]
|
||
|
||
kinds, v0s = load_shape(args.kinds, seq) if args.kinds else (None, None)
|
||
fps = tdc_layout.item_fingerprints(prog, seq)
|
||
hists = tdc_layout.item_histograms(prog, seq)
|
||
names = [r["name"] for r in tdc_modules.analyze_prog(prog)]
|
||
dst = SlotSpec(seq, kinds, v0s, fps, hists, names)
|
||
anchored = sum(1 for f in fps if f)
|
||
print(f"文件 : {args.path}")
|
||
print(f"清单模块 : registry[{_mid}] 项数 {len(seq)}")
|
||
print(f"槽位规格 : {dst.describe()}")
|
||
print(f"锚点覆盖 : 语义名 {sum(1 for n in names if n and n != 'unknown')}/{len(seq)}"
|
||
f" 静态指纹 {anchored}/{len(seq)}")
|
||
print(f"XTEA 密钥 : {key}")
|
||
|
||
if args.from_sample:
|
||
sample = load_sample_cd(args.from_sample)
|
||
if args.src_build:
|
||
src = spec_from_js(args.src_build, args.kinds, sample)
|
||
else:
|
||
src = None
|
||
try:
|
||
cd = reorder(sample, dst, src)
|
||
except (ValueError, UnmappedSlotsError) as exc:
|
||
print(f"❌ {exc}")
|
||
return 1
|
||
print(f"样本槽数 : {len(sample)} -> 重排后 {len(cd)}")
|
||
if args.src_build:
|
||
# 自证:锚点对齐后,值集合应当守恒(易变槽数值允许不同)
|
||
print(f"对齐方式 : 环境语义指纹 + 结构签名(源={args.src_build})")
|
||
elif args.values:
|
||
vals = json.load(open(args.values, encoding="utf-8"))
|
||
cd = []
|
||
for (_i, m, _slot, n) in dst.assign:
|
||
v = vals[str(m)]
|
||
cd.extend(v[:n] if isinstance(v, list) else [v])
|
||
print(f"按语义字典 : {len(cd)} 槽")
|
||
else:
|
||
print("需要 --from-sample 或 --values")
|
||
return 1
|
||
|
||
plain = build_plain(cd)
|
||
b64 = encrypt_plain(plain, key)
|
||
print(f"明文长度 : {len(plain)}")
|
||
print(f"collect : {len(b64)} 字符 (tlg={len(b64)})")
|
||
if args.plain_out:
|
||
Path(args.plain_out).write_text(plain, encoding="latin1")
|
||
print(f"明文 -> {args.plain_out}")
|
||
if args.out:
|
||
Path(args.out).write_text(b64, encoding="ascii")
|
||
print(f"collect -> {args.out}")
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|