Files
hello_js_reverse_skill/scripts/verify/verify_control_negative.py
T
yml2213 80a122245a 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)。
2026-10-12 02:28:08 +08:00

95 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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;②③④应明显更低 => 归一化未过拟合,配对有效")