feat(collect): 按 docs/tdc-案例.md 重做主链路 —— 四段加密 + 字符串指纹识别 37/37
做的什么:
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)。
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""验证 struct_40_9 的消歧特征是否跨构建稳定。
|
||||
|
||||
判别:
|
||||
A 组: 闭包体最后写入 **空串** (PUSH_EMPTY_STR; SET_PROP)
|
||||
B 组: 闭包体最后写入 **随机 int32** (PUSH_IMM <big>; SET_PROP)
|
||||
"""
|
||||
import glob
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_modules as tm # noqa: E402
|
||||
import tdc_program as tp # noqa: E402
|
||||
|
||||
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
|
||||
|
||||
|
||||
def closure_body(p, entry):
|
||||
"""取模块唯一闭包的 target 体。"""
|
||||
cls = [c for c in p.closures if p.owner(c.pc) == entry]
|
||||
if not cls:
|
||||
return None
|
||||
return p.function_code(cls[0].target)
|
||||
|
||||
|
||||
def disamb(p, entry):
|
||||
"""返回 'empty' | 'rand' | None"""
|
||||
body = closure_body(p, entry)
|
||||
if body is None:
|
||||
return None
|
||||
imms = [x["operands"][0] for x in body
|
||||
if x["name"] == "PUSH_IMM" and x["operands"]]
|
||||
big = [v for v in imms if v > 1_000_000]
|
||||
if big:
|
||||
return "rand"
|
||||
return "empty"
|
||||
|
||||
|
||||
stats = defaultdict(lambda: defaultdict(list))
|
||||
for f in BATCH:
|
||||
p = tp.load(f)
|
||||
reg = dict(p.module_registry(0))
|
||||
rows = tm.analyze(f)
|
||||
for r in rows:
|
||||
if r["name"] != "struct_40_9":
|
||||
continue
|
||||
d = disamb(p, reg[r["module"]])
|
||||
stats[f.split("/")[-1]][d or "?"].append((r["item"], r["module"]))
|
||||
|
||||
print("%-24s %-8s %s" % ("构建", "判别", "项(模块)"))
|
||||
print("-" * 70)
|
||||
ok = True
|
||||
for f, groups in stats.items():
|
||||
for d, members in sorted(groups.items()):
|
||||
print("%-24s %-8s %s" % (f, d, members))
|
||||
# 每份构建必须 empty 1 个 + rand 1 个
|
||||
if len(groups.get("empty", [])) != 1 or len(groups.get("rand", [])) != 1:
|
||||
ok = False
|
||||
print()
|
||||
print(">>> 每份构建都是 empty×1 + rand×1 :", ok)
|
||||
print(">>> 结论:", "可静态消歧(写入空串 vs 写入随机 int32)" if ok else "仍不可分")
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""对照实验:证明「按语义名配对」不是归一化过拟合出来的。
|
||||
|
||||
风险: 归一化若抹掉太多,任何配对都会"全对",校验就失去意义。
|
||||
|
||||
做法(负对照,同一套归一化 + 同一套比对):
|
||||
① 按语义名配对(实验组)
|
||||
② 按清单下标配对(负对照:槽位序随机,必然错)
|
||||
③ 按模块 id 配对(负对照:id 每次重排,必然错)
|
||||
④ 随机配对(负对照基线)
|
||||
|
||||
若①接近 37/37 而 ②③④ 明显更低 => 归一化没有过拟合,配对确实有效。
|
||||
"""
|
||||
import glob
|
||||
import random
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_modules as tm # noqa: E402
|
||||
import tdc_program as tp # noqa: E402
|
||||
|
||||
# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉
|
||||
ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"}
|
||||
|
||||
|
||||
def norm_code(p, entry, maxlen=400):
|
||||
"""归一化指令序列(与 verify_pair_correct2.py 同一套规则,此处内联避免导入副作用)。
|
||||
|
||||
抹掉三类**会重排**的操作数,保留指令骨架与字符串的位置:
|
||||
1. 代码地址(JUMP / JUMP_IF_* / PUSH_TRY)
|
||||
2. require(N) 的模块号 N
|
||||
3. PUSH_STR_CHAR 的具体字符值(fp_salt 随机盐串、混淆变量名每次都变)
|
||||
"""
|
||||
code = p.function_code(entry)
|
||||
out = []
|
||||
for k, inst in enumerate(code):
|
||||
nm, ops = inst["name"], tuple(inst["operands"] or [])
|
||||
if nm in ADDR_OPS:
|
||||
ops = ("ADDR",) * len(ops)
|
||||
elif nm == "PUSH_STR_CHAR":
|
||||
ops = ("CH",)
|
||||
elif nm == "PUSH_IMM" and ops:
|
||||
if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM"
|
||||
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"):
|
||||
ops = ("MOD",) * len(ops)
|
||||
out.append((nm, ops))
|
||||
return tuple(out[:maxlen])
|
||||
|
||||
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
|
||||
|
||||
|
||||
def load(f):
|
||||
p = tp.load(f)
|
||||
rows = tm.analyze(f)
|
||||
reg = dict(p.module_registry(0))
|
||||
sig = {r["name"]: norm_code(p, reg[r["module"]]) for r in rows}
|
||||
return rows, sig
|
||||
|
||||
|
||||
A, fa = load(BATCH[0])
|
||||
print("基准 %s (归一化:抹掉地址/模块号/字符值,保留指令骨架与串的位置)" % BATCH[0].split("/")[-1])
|
||||
print()
|
||||
print("%-22s %-22s %-8s %-8s" % ("对比构建", "配对方式", "一致", "不一致"))
|
||||
print("-" * 66)
|
||||
|
||||
for f in BATCH[1:]:
|
||||
B, fb = load(f)
|
||||
names = sorted(set(fa) & set(fb))
|
||||
|
||||
def score(pairs):
|
||||
return sum(1 for a, b in pairs if a == b)
|
||||
|
||||
# ① 实验组:语义名
|
||||
p1 = [(fa[n], fb[n]) for n in names]
|
||||
# ② 负对照:清单下标
|
||||
p2 = [(fa[A[i]["name"]], fb[B[i]["name"]]) for i in range(min(len(A), len(B)))]
|
||||
# ③ 负对照:模块 id(用 id 当 key 去查另一边同名集合的任意一项)
|
||||
fb_by_id = {r["module"]: fb[r["name"]] for r in B}
|
||||
p3 = [(fa[A[i]["name"]], fb_by_id.get(A[i]["module"], ())) for i in range(len(A))]
|
||||
# ④ 负对照:随机配对
|
||||
rnd = random.Random(7)
|
||||
shuf = names[:]
|
||||
rnd.shuffle(shuf)
|
||||
p4 = [(fa[n], fb[m]) for n, m in zip(names, shuf)]
|
||||
|
||||
for label, pairs in (("①语义名(实验组)", p1), ("②清单下标(负对照)", p2),
|
||||
("③模块id(负对照)", p3), ("④随机配对(负对照)", p4)):
|
||||
s = score(pairs)
|
||||
print("%-22s %-22s %-8d %-8d" % (f.split("/")[-1], label, s, len(pairs) - s))
|
||||
print()
|
||||
|
||||
print(">>> 判读: ①应≈37/37;②③④应明显更低 => 归一化未过拟合,配对有效")
|
||||
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""验证字符串+结构指纹方案的**跨构建一致性** —— 只基于 har/online/ 全新样本。
|
||||
|
||||
案例 §5.3 的验收标准: "3 个 tdc.js 版本、37 个模块、全部正确识别,无一 unknown"。
|
||||
本脚本再进一步:不仅要求"无 unknown",还要求
|
||||
① 每份构建内部 37 项都有名字
|
||||
② 各构建之间的**语义名集合**一致(说明识别的是同一批采集器)
|
||||
③ 语义名 -> 每项在 cd 里的**相对位置**可交叉核对
|
||||
"""
|
||||
import glob
|
||||
import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_modules as tm # noqa: E402
|
||||
|
||||
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
|
||||
print("全新在线样本 %d 份(只使用这批数据)" % len(BATCH))
|
||||
print()
|
||||
|
||||
results = {}
|
||||
for f in BATCH:
|
||||
rows = tm.analyze(f)
|
||||
names = [r["name"] for r in rows]
|
||||
results[f] = rows
|
||||
unk = sum(1 for n in names if n == "unknown")
|
||||
via_struct = sum(1 for r in rows if r.get("via_struct"))
|
||||
via_deleg = sum(1 for r in rows if r.get("via_delegate"))
|
||||
print(" %-24s 项%2d unknown %d | 字符串直判 %2d 委派链 %d 结构兜底 %d"
|
||||
% (f.split("/")[-1], len(rows), unk,
|
||||
len(rows) - via_struct - via_deleg, via_deleg, via_struct))
|
||||
|
||||
print()
|
||||
keys = list(results)
|
||||
base = Counter(r["name"] for r in results[keys[0]])
|
||||
print("以 %s 为基准:%d 个语义名(去重 %d)"
|
||||
% (keys[0].split("/")[-1], len(base), len(set(base))))
|
||||
print()
|
||||
for f in keys[1:]:
|
||||
c = Counter(r["name"] for r in results[f])
|
||||
same = sum((base & c).values())
|
||||
only_base = sum((base - c).values())
|
||||
only_it = sum((c - base).values())
|
||||
print(" vs %-22s 相同 %2d | 仅基准有 %2d | 仅有它有 %2d"
|
||||
% (f.split("/")[-1], same, only_base, only_it))
|
||||
|
||||
print()
|
||||
common = set(base)
|
||||
for f in keys[1:]:
|
||||
common &= set(Counter(r["name"] for r in results[f]))
|
||||
allnames = set()
|
||||
for f in keys:
|
||||
allnames |= set(r["name"] for r in results[f])
|
||||
print(">>> 所有构建**共同**识别出的语义名: %d 个" % len(common))
|
||||
print(">>> 出现过的语义名总数 : %d 个" % len(allnames))
|
||||
print(">>> 差异名:", sorted(allnames - common) if allnames - common else "无(完全一致)")
|
||||
print()
|
||||
|
||||
# 唯一性检查:单构建内是否有重名(重名 = 锚点有歧义)
|
||||
print("单构建内重名检查(重名说明该锚点有歧义):")
|
||||
for f in keys:
|
||||
c = Counter(r["name"] for r in results[f])
|
||||
dup = {k: v for k, v in c.items() if v > 1}
|
||||
print(" %-24s 重名 %d 组 %s"
|
||||
% (f.split("/")[-1], len(dup), dict(sorted(dup.items()))))
|
||||
@@ -0,0 +1,94 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""判决「按语义名配对」是否**正确** —— 用独立于指纹的方法。
|
||||
|
||||
⚠️ 上一轮我用「模块 id 相等」做校验,那个假设是**错的**:
|
||||
案例 §1.2 与账本 #29 都明确说「37 个模块的 ID 每次构建随机重新分配」。
|
||||
registry 全集交集 62/62 只说明编号**范围**相同(都是 0..61),
|
||||
不代表 id -> 语义 的映射相同。id 不一致是**预期行为**,不能用来否定配对。
|
||||
|
||||
本轮改用真正独立的证据:**函数体的归一化指令序列**。
|
||||
指纹规则只用到「字符串集合」和「结构统计(指令数/直方图)」,
|
||||
没有用到「指令的完整顺序 + 操作数」。所以拿它做校验是独立的。
|
||||
|
||||
归一化: 抹掉 JUMP 的绝对地址(每构建不同),保留其余操作数。
|
||||
"""
|
||||
import glob
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_layout # noqa: E402
|
||||
import tdc_modules as tm # noqa: E402
|
||||
import tdc_program as tp # noqa: E402
|
||||
|
||||
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
|
||||
|
||||
|
||||
# 带"代码地址"的操作数:每份构建的布局不同,必须抹掉
|
||||
ADDR_OPS = {"JUMP", "JUMP_IF_TRUE", "JUMP_IF_FALSE", "PUSH_TRY"}
|
||||
|
||||
|
||||
def norm_code(p, entry, maxlen=400):
|
||||
"""归一化指令序列。
|
||||
|
||||
必须抹掉两类**会重排**的操作数,否则校验必然失败:
|
||||
1. 代码地址:JUMP / JUMP_IF_TRUE / JUMP_IF_FALSE / PUSH_TRY
|
||||
2. require(N) 的模块号 N:`LOAD_REG_IMM; PUSH_IMM N; CALL_FUNC`
|
||||
(案例 §1.2:37 个模块的 ID 每次构建随机重新分配)
|
||||
其余操作数(字符串 charCode、小常量、寄存器号)跨构建稳定,保留。
|
||||
"""
|
||||
code = p.function_code(entry)
|
||||
out = []
|
||||
for k, inst in enumerate(code):
|
||||
nm, ops = inst["name"], tuple(inst["operands"] or [])
|
||||
if nm in ADDR_OPS:
|
||||
ops = ("ADDR",) * len(ops)
|
||||
elif nm == "PUSH_STR_CHAR":
|
||||
# 只保留"这是一个字符"这件事,**不看具体是哪个字符**。
|
||||
# 这样 fp_salt 的随机盐串、混淆变量名这些「会变的内容」就不会干扰校验,
|
||||
# 而"字符串的长度/位置"仍然参与比对 —— 足够独立,且仍然很强。
|
||||
ops = ("CH",)
|
||||
elif nm == "PUSH_IMM" and ops:
|
||||
# require(N) 形状:前一个 LOAD_REG_IMM,后一个 CALL_FUNC
|
||||
if (k >= 1 and code[k - 1]["name"] == "LOAD_REG_IMM"
|
||||
and k + 1 < len(code) and code[k + 1]["name"] == "CALL_FUNC"):
|
||||
ops = ("MOD",) * len(ops)
|
||||
out.append((nm, ops))
|
||||
return tuple(out[:maxlen])
|
||||
|
||||
|
||||
print("构建 : 每个模块的函数体归一化指令序列 -> 作为独立指纹")
|
||||
print()
|
||||
|
||||
A = tm.analyze(BATCH[0])
|
||||
pa = tp.load(BATCH[0])
|
||||
ra = dict(pa.module_registry(0))
|
||||
fa = {r["name"]: norm_code(pa, ra[r["module"]]) for r in A}
|
||||
|
||||
print("基准 %s (%d 项)" % (BATCH[0].split("/")[-1], len(A)))
|
||||
print()
|
||||
print("%-24s %-8s %-8s %-8s %s" % ("对比构建", "配对", "序列一致", "不一致", "判决"))
|
||||
print("-" * 66)
|
||||
all_ok = True
|
||||
for f in BATCH[1:]:
|
||||
B = tm.analyze(f)
|
||||
pb = tp.load(f)
|
||||
rb = dict(pb.module_registry(0))
|
||||
fb = {r["name"]: norm_code(pb, rb[r["module"]]) for r in B}
|
||||
common = set(fa) & set(fb)
|
||||
same = [n for n in common if fa[n] == fb[n]]
|
||||
diff = [n for n in common if fa[n] != fb[n]]
|
||||
all_ok &= not diff
|
||||
print("%-24s %-8d %-8d %-8d %s"
|
||||
% (f.split("/")[-1], len(common), len(same), len(diff),
|
||||
"✓" if not diff else "✗"))
|
||||
for n in sorted(diff)[:3]:
|
||||
a, b = fa[n], fb[n]
|
||||
hit = next((k for k in range(min(len(a), len(b))) if a[k] != b[k]), -1)
|
||||
print(" %-26s 首个分歧@%d A=%s B=%s" % (n, hit,
|
||||
a[hit] if hit >= 0 else "-", b[hit] if hit >= 0 else "-"))
|
||||
|
||||
print()
|
||||
print(">>> 全部配对的函数体序列一致:", all_ok)
|
||||
print(">>> 判决:", "语义名配对**正确**(独立证据)" if all_ok else "配对有误,需检查")
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用**全新在线样本**做端到端重排测试(不需要真机明文,自证)。
|
||||
|
||||
做法:
|
||||
给每个语义名编一个**假值** value_i,按构建 A 的清单顺序排成 cd_A。
|
||||
再用锚点把它重排到构建 B 的顺序,得到 cd_B。
|
||||
正确答案 = 按 B 的清单顺序、取各自语义名的假值。
|
||||
|
||||
这个测试能抓住 reorder 的核心 bug(恒等映射):
|
||||
若 B 的顺序 ≠ A 的顺序,恒等映射必然错;只有真对齐才对。
|
||||
"""
|
||||
import glob
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools"))
|
||||
import tdc_collectgen as cg # noqa: E402
|
||||
import tdc_modules as tm # noqa: E402
|
||||
|
||||
BATCH = sorted(glob.glob(str(ROOT / "har" / "online" / "*" / "tdc_*.js")))
|
||||
print("新样本 %d 份" % len(BATCH))
|
||||
print()
|
||||
|
||||
|
||||
A = BATCH[0]
|
||||
rowsA = tm.analyze(A)
|
||||
namesA = [r["name"] for r in rowsA]
|
||||
seqA = [r["module"] for r in rowsA]
|
||||
n = len(namesA)
|
||||
print("基准 %s 项 %d 唯一名 %d" % (A.split("/")[-1], n, len(set(namesA))))
|
||||
|
||||
# 假值:每项一个可区分的值
|
||||
fake = {nm: "V<%s>" % nm for nm in namesA}
|
||||
cd_A = [fake[nm] for nm in namesA]
|
||||
|
||||
src = cg.spec_from_seq(seqA, [None] * n, namesA, sample_cd=cd_A)
|
||||
|
||||
print()
|
||||
for f in BATCH[1:]:
|
||||
rowsB = tm.analyze(f)
|
||||
namesB = [r["name"] for r in rowsB]
|
||||
seqB = [r["module"] for r in rowsB]
|
||||
dst = cg.spec_from_seq(seqB, [None] * len(rowsB), namesB)
|
||||
|
||||
# 正确答案:按 B 的顺序取各自语义名的假值
|
||||
truth = [fake[nm] for nm in namesB]
|
||||
|
||||
try:
|
||||
out = cg.reorder(cd_A, dst, src)
|
||||
ok = out == truth
|
||||
identical = out == cd_A
|
||||
print(" vs %-24s" % f.split("/")[-1])
|
||||
print(" 重排结果 == 正确答案 : %s" % ok)
|
||||
print(" 重排结果 == 原样(说明没重排): %s" % identical)
|
||||
if not ok:
|
||||
bad = [i for i, (a, b) in enumerate(zip(out, truth)) if a != b]
|
||||
print(" 错位 %d 处, 前5: %s" % (len(bad), bad[:5]))
|
||||
except Exception as e:
|
||||
print(" vs %-24s %s: %s" % (f.split("/")[-1], type(e).__name__, str(e)[:110]))
|
||||
print()
|
||||
@@ -0,0 +1,75 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""用真机 collect 密文的**长度与填充**判决「四段加密」结构。
|
||||
|
||||
不需要密钥:段结构在 base64 的长度与 '=' 分布上留下指纹。
|
||||
|
||||
判决依据(docs/tdc-案例.md §5.1):
|
||||
chunk1/trajectory/chunk2 各 pad 到 24 字节 -> 密文长度是 24 的倍数 -> base64 无 '='
|
||||
sd 不 pad,只被 XTEA 补到 8 的倍数 -> 长度%3 != 0 -> base64 带 '='
|
||||
|
||||
用法:
|
||||
.venv/bin/python scripts/verify/verify_seglen.py <collect 的 base64>
|
||||
"""
|
||||
import base64
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: verify_seglen.py <collect 的 base64(文件路径或直接给串)>")
|
||||
raise SystemExit(2)
|
||||
_arg = sys.argv[1]
|
||||
_p = Path(_arg)
|
||||
b64 = (_p.read_text(encoding="ascii") if _p.exists() else _arg).strip()
|
||||
print("真机 collect: %d 字符, 含 '=' : %s" % (len(b64), "=" in b64))
|
||||
print()
|
||||
|
||||
raw = base64.b64decode(b64)
|
||||
print("解码后密文: %d 字节" % len(raw))
|
||||
print()
|
||||
|
||||
# 真机明文(另一份同会话样本)给出的各段长度
|
||||
segs = [("chunk1", 355, 5), ("trajectory", 62, 10), ("chunk2", 288, 24), ("sd", 33, None)]
|
||||
print("%-11s %8s %6s %10s %12s %10s" % ("段", "内容", "pad", "密文字节", "base64字符", "含'='"))
|
||||
print("-" * 66)
|
||||
total = 0
|
||||
total_b64 = 0
|
||||
for name, content, pad in segs:
|
||||
if pad is not None:
|
||||
n = content + pad # 空格 pad 到 24
|
||||
assert n % 24 == 0
|
||||
else:
|
||||
n = content + ((8 - content % 8) % 8) # sd: 只被 XTEA 补到 8 的倍数
|
||||
enc = base64.b64encode(b"\x00" * n).decode()
|
||||
total += n
|
||||
total_b64 += len(enc)
|
||||
print("%-11s %8d %6s %10d %12d %10s"
|
||||
% (name, content, pad if pad is not None else "(xtea)",
|
||||
n, len(enc), "=" in enc))
|
||||
print("-" * 66)
|
||||
print("%-11s %8s %6s %10d %12d" % ("合计", "", "", total, total_b64))
|
||||
print()
|
||||
print(">>> 推算总密文字节 %d == 实际 %d : %s" % (total, len(raw), total == len(raw)))
|
||||
print(">>> 推算 base64 字符 %d == 实际 %d : %s" % (total_b64, len(b64), total_b64 == len(b64)))
|
||||
print()
|
||||
|
||||
# 反过来看:把整串按 24 字节切,前三段应能整除
|
||||
print("[反向校验] 把密文按 24 字节切分:")
|
||||
off = 0
|
||||
for name, content, pad in segs:
|
||||
if pad is None:
|
||||
print(" %-11s 剩余 %3d 字节 (sd, 非 24 倍数)" % (name, len(raw) - off))
|
||||
break
|
||||
n = content + pad
|
||||
print(" %-11s [%4d:%4d] %3d 字节 24倍数=%s"
|
||||
% (name, off, off + n, n, n % 24 == 0))
|
||||
off += n
|
||||
print()
|
||||
|
||||
# 关键指纹: '=' 只应出现在最后(来自 sd 段)
|
||||
idx = b64.find("=")
|
||||
print("'=' 首次出现位置: %d / 总长 %d -> %s"
|
||||
% (idx, len(b64), "在末尾区域(符合 sd 段不 pad)"
|
||||
if idx >= len(b64) - 4 else "⚠️ 在中间(与四段结构矛盾)"))
|
||||
if idx >= 0:
|
||||
print(" 尾部填充: %r" % b64[idx:])
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""按 docs/tdc-案例.md §5.1 验证 collect 的「四段加密」结构(真机明文判决)。
|
||||
|
||||
要判决的问题:
|
||||
项目现有 crypto.encrypt_string / tdc_collectgen.encrypt_plain 都是
|
||||
「整体 JSON -> \x00 补齐到 8 -> 一次加密 -> 一个 base64」。
|
||||
案例说「4 段独立加密,前 3 段空格 pad 到 24 字节对齐,sd 不 pad」。
|
||||
两者只能有一个对 —— 用真机明文里的**空格填充串**来判。
|
||||
|
||||
用法:
|
||||
.venv/bin/python scripts/verify/verify_segment.py <已解密的 collect 明文文件>
|
||||
"""
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: verify_segment.py <已解密的 collect 明文文件>")
|
||||
print(" (明文来源: 用同会话 tdc.js 的密钥解密 TDC.getData(true) 的产出)")
|
||||
raise SystemExit(2)
|
||||
RAW = sys.argv[1]
|
||||
|
||||
data = open(RAW, encoding="latin1").read()
|
||||
print("原始(含尾部 \\x00):", len(data), "字节")
|
||||
stripped = data.rstrip("\x00")
|
||||
print("去掉尾部 \\x00 后 :", len(stripped), "字节 (尾部 \\x00 个数 = %d)" % (len(data) - len(stripped)))
|
||||
print()
|
||||
|
||||
runs = [(m.start(), m.end()) for m in re.finditer(r" {2,}", stripped)]
|
||||
print("[1] 连续空格串(只能来自「空格 pad」):")
|
||||
for a, b in runs:
|
||||
print(" 位置 %3d..%3d 长度 %2d" % (a, b, b - a))
|
||||
print()
|
||||
|
||||
# 段边界 = 每个空格串的结束位置(若空格串在段尾)
|
||||
bounds = [0] + [b for _a, b in runs[:-1]] if len(runs) > 1 else [0]
|
||||
# 更稳: 段边界取每个空格串的"起点所在段"的末尾 -> 直接用空格串终点
|
||||
b1, b2, b3 = runs[0][1], runs[1][1], runs[2][1]
|
||||
segs = [("chunk1", 0, b1), ("trajectory", b1, b2), ("chunk2", b2, b3), ("sd", b3, len(stripped))]
|
||||
print("[2] 按空格串切出的 4 段:")
|
||||
for name, a, b in segs:
|
||||
s = stripped[a:b]
|
||||
print(" %-11s [%3d:%3d] len=%3d 24倍数=%-5s 8倍数=%-5s"
|
||||
% (name, a, b, len(s), len(s) % 24 == 0, len(s) % 8 == 0))
|
||||
print()
|
||||
|
||||
print("[3] 空格 pad 公式校验 (pad_len = 24 - len%24,总是补 1..24 个):")
|
||||
for name, a, b in segs[:3]:
|
||||
s = stripped[a:b]
|
||||
content = s.rstrip(" ")
|
||||
pad = len(s) - len(content)
|
||||
print(" %-11s 内容 %3d + pad %2d = %3d 公式给出 %2d -> %s"
|
||||
% (name, len(content), pad, len(s), 24 - len(content) % 24,
|
||||
"OK" if pad == 24 - len(content) % 24 else "FAIL"))
|
||||
print()
|
||||
|
||||
print("[4] sd 段不 pad(只被 XTEA 的 8 字节块 \x00 补齐):")
|
||||
sd = stripped[b3:]
|
||||
print(" sd = %r" % sd)
|
||||
print(" sd 长度 %d (非 8 的倍数 -> 加密时被补 %d 个 \\x00)"
|
||||
% (len(sd), (8 - len(sd) % 8) % 8))
|
||||
print(" 实际尾部 \\x00 个数 = %d" % (len(data) - len(stripped)))
|
||||
print()
|
||||
|
||||
print("[5] 逻辑整体是否是合法 JSON,以及轨迹在 cd 里的位置:")
|
||||
obj = json.loads(stripped)
|
||||
cd = obj["cd"]
|
||||
traj_idx = [i for i, v in enumerate(cd)
|
||||
if isinstance(v, list) and v and all(isinstance(x, list) and len(x) == 3 for x in v)]
|
||||
print(" cd 槽数 = %d" % len(cd))
|
||||
print(" 轨迹槽下标 = %s" % traj_idx)
|
||||
if traj_idx:
|
||||
print(" 轨迹内容前 3 点 = %s" % cd[traj_idx[0]][:3])
|
||||
print()
|
||||
|
||||
print("[6] 去 pad 后 4 段拼接 == 逻辑明文(应能解析成同一对象):")
|
||||
rebuilt = "".join(stripped[a:b].rstrip(" ") for _name, a, b in segs[:3]) + stripped[b3:]
|
||||
obj2 = json.loads(rebuilt)
|
||||
print(" 可解析为 JSON :", True)
|
||||
print(" 与原对象逐项相同 :", obj2 == obj)
|
||||
print(" 重建长度 %d (原文去尾 \\x00 后 %d, 差 = pad 总量 %d)"
|
||||
% (len(rebuilt), len(stripped), len(stripped) - len(rebuilt)))
|
||||
print()
|
||||
print("[7] 三段之间的连接处(验证 chunk1 以逗号结尾 / chunk2 以 '],' 结尾):")
|
||||
print(" chunk1 内容末尾 : %r" % stripped[0:b1].rstrip(" ")[-24:])
|
||||
print(" trajectory 内容 : %r" % stripped[b1:b2].rstrip(" ")[:40])
|
||||
print(" chunk2 内容开头 : %r" % stripped[b2:b3].rstrip(" ")[:24])
|
||||
print(" chunk2 内容末尾 : %r" % stripped[b2:b3].rstrip(" ")[-24:])
|
||||
print(" sd 内容 : %r" % stripped[b3:])
|
||||
Reference in New Issue
Block a user