拿到一份混淆过的签名 SDK,第一感觉通常是:代码像一团黑箱,每一行都在算某种完全看不懂的东西。
但完整走过一遍逆向流程后,你往往会发现事实恰好相反:其中 90% 的代码都是标准算法。哈希是公开哈希,编码是公开编码,流密码也是公开流密码。这里根本不需要从头“理解”或重写算法;识别出它属于哪一种,直接参考 RFC 或成熟实现复用,字节级结果都不会错。
真正致命的是剩下的 10%:标准算法被悄悄动过一点手脚的地方。原本固定的轮数变成变量,基础常量被替换为相邻数值,某一轮的 word 索引被调换,密钥被藏进密文里。每项改动都很小,但只要漏掉一处,重写结果就无法和目标一致;而更麻烦的是,其他 90% 明明全都对了,你却不知道错在何处。
所以,逆向混淆签名的核心不在于读懂那 90%,而在于找出这 10% 的偏离点。本文讨论的就是如何让大模型替你做这件事,以及为什么在这个环节,模型尤其难以被替代。
(前置步骤——通过常量和结构识别算法家族,例如看出“这是 ChaCha”“这是 FNV”——已在上一篇文章介绍过。这里默认你已经识别出算法骨架,直接进入偏离点排查。)
偏离点,恰好是人最容易看漏的地方
人脑在识别模式时有一种很强的倾向:一旦认出了大概,就不再逐项细看。
看到一段代码,常量符合 ChaCha 的特征,整体结构也对得上,大脑立刻打钩:“这是 ChaCha20。”然后就翻篇了。你不会逐字节核对每一个轮数、每一个索引、每个常量的最低位,因为“已经认出来了”的满足感会让注意力自动关闭。
混淆作者利用的正是这一点。他们通常不会重写整个算法——成本高,也容易出 bug——而是只对标准算法做最小幅度的修改:改一个数字、换一个索引、多加一步。这样的改动小到足以被模式识别忽略,却又足以让按标准实现的复刻彻底失效。
这是一场注意力不对称的战争。作者只要藏住一个偏离点,而你必须找全所有偏离点。人类天生不擅长持续怀疑“看上去已经正确”的东西。
模型在这里反而有一个违反直觉的优势:它没有“我已经认出来了”的满足感,因而不会自动松懈。只要提示词明确要求它寻找偏离,它就会逐项检查,不会做到一半就停下来——前提是提示方式正确,这正是第 5 节要讲的内容。先来看四种常见的偏离形式。
四类最常见的偏离手法
下面每一类都会先用公开标准算法说明“标准版本是什么样”,再说明“偏离版本如何出现”。实际项目中,同一个签名器里经常会叠加多种手法。
1. 固定参数变变量:本该写死的值由运行时决定
标准流密码的轮数是固定的。ChaCha20 就是 20 轮,不可变动;在所有标准实现里,这个数字都是硬编码的。RFC 8439的引言也明确说明,它只描述 20 轮 ChaCha,8 轮和 12 轮的变体定义在其他文档中。轮数本来就是标准中写死的常量。
偏离方式是:把固定轮数改为由密钥动态计算的值。quarter-round 逻辑完全不变,但执行多少轮取决于密钥中的某些字节。密钥一变,轮数也会随之变化。
人为什么容易漏掉:你看到了 quarter-round 结构,也认出了 σ 常量,于是直接判定“ChaCha20”,照着 20 轮去实现。至于控制循环次数的变量究竟是常量还是表达式,你根本没有检查——毕竟你过去见过的所有 ChaCha 实现里,它都是常量。
这类偏离的信号是:本应出现常量的位置,出现了依赖输入的表达式。一次被正确引导的反证检查,会专门验证:“标准实现这里是硬编码值,而当前代码是否改成了计算结果?”
如何用差分测试确认:故意选择两把只相差一个字节的密钥运行。如果输出差异远远超过这个字节本应带来的影响范围,说明该字节可能参与控制某个全局参数,例如轮数,而不只是 XOR 进密钥流。
2. 常量微调:把基础常量换成相邻数值
FNV-1a 哈希有两个公开的魔数:offset basis 和 prime。每个正确的 FNV-1a 实现都会使用这两个精确数值,标准中也有明确记录。共同作者 Landon Curt Noll 维护的FNV 参考页面给出了 32 位 offset basis 为 2166136261、prime 为 16777619,精确到每一位数字。
偏离方式是:把其中一个基础常量替换成与标准只差极小数值的邻近值。哈希的整体结构完全一样:XOR、乘法、循环都没问题,只有初始基础值被做了几乎难以察觉的修改。
人为什么容易漏掉:这是四类中最阴险的一种。你看到了 FNV 的结构,又看到一个长得很像 offset basis 的大常量,就直接认定“标准 FNV-1a”。可一旦觉得已经识别出来,谁还会把这个魔数和标准值逐位比对?
这类偏离几乎只能靠差分测试抓到,因为人眼核对大数极不可靠。做法是:用一个已知输入,分别送入标准实现和目标黑盒,再比较输出。如果整体结构相同但结果不同,问题很可能就在基础常量。随后把可疑常量交给模型,让它与标准值做 diff;机器做这件事远比人可靠。
模型在这里的价值非常直接:它能记住标准 FNV-1a offset basis 的每一位,而你通常记不住。你只需问一句“这个常量是否与标准 FNV-1a offset basis 完全相等”,它就能立刻指出差异。
3. 局部结构改写:标准算法中的某一步被动了
每个 ChaCha double round 由 8 个 quarter round 组成:前 4 个处理列,后 4 个处理对角线。每个 quarter round 操作哪些 word,是标准固定规定的一组索引。RFC 8439 §2.3明确写出了 block function,以及每一轮会触及哪些 state word。
偏离方式是:在某一轮里悄悄替换一两个 word 索引。绝大多数轮次仍符合标准,只有中间某处,本来应参与计算的 word 被换成了另一个。它依旧像 ChaCha,运行也不会报错,但生成的密钥流会和标准 ChaCha 完全不同。
人为什么容易漏掉:quarter-round 索引本身就是一长串数字,例如 8 组 (0,4,8,12)(1,5,9,13)…。扫一眼时,你只会确认“没错,前面是列轮,后面是对角轮”,却不会逐组核对每四个数字是否都处于标准位置。把改动藏在一串本就让人眼花的索引里,效果极佳。
模型也不会只靠一眼看出问题;你必须明确要求它按轮列出索引,并与标准 ChaCha 逐项 diff。这是纯机械核对工作,恰好是模型不容易走神、人却容易走神的任务。让它输出“标准索引 vs. 实际索引”的对照表,偏离点自然会暴露出来。
差分测试如何确认:如果前两类问题已经排除——轮数正确、常量正确——输出仍不一致,问题就很可能在结构上。逐轮导出中间 state;第一个开始偏离标准 ChaCha 的轮次,就是被修改的那一轮。
4. 材料嵌入:把密钥数据藏在输出里
前三类改变的是算法本身,这一类改变的则是数据组织方式。
一种常见做法是:加密密钥不通过独立通道传输,而是拆分后插入密文本身,接收方再按同样规则取回。更隐蔽的版本中,插入位置不是固定的,而是根据数据内容计算出来;密文一变,密钥藏的位置也跟着变。最外层再套上自定义字母表的 Base64 和一个标记前缀字节。
人为什么容易漏掉:你一直在和加密算法死磕,却没有意识到密钥根本不需要破解——它就在手上的密文里,只是不知道藏在哪个片段。初学者经常卡在这里,试图“破解”一个明文其实已经摆在眼前的东西。
这类偏离的信号是:一段数据块内部存在统计特征明显不同于周边的区域。密钥通常是高熵随机字节,插入密文中间后会形成可识别的“异质片段”。模型可以协助分析“输出中哪一段的字节分布与其余部分不同”,从而定位嵌入材料的边界。
如何确认:如果能找到插入规则,例如字节和取模,就用几组已知输入/输出反推插入位置的公式,再正向验证。模型可以根据少量样本协助归纳规则,但规则最终是否正确,仍要由 assert 决定。
反证环节,才是推理层真正的战场
看完这四类偏离,你应该能发现一条共同规律:识别算法家族只是候选判断,容易;找出标准之外的偏离,才是难点。
候选判断几乎任何模型都能完成。ChaCha 的 σ 常量、FNV 的结构,训练数据里见过这些内容的模型都能认出来。真正拉开模型差距的是反证环节:模型是否愿意、也是否有能力继续挑剔一个刚刚识别出的算法,并指出“这里不符合标准”。
较弱的模型往往会在这一步失去锋芒。它已经判断“这是 ChaCha20”,反证部分就退化成换一种说法重复候选结论:“该实现遵循标准 ChaCha20 结构,采用经典 quarter round……”——全是在复述,没有真正检查任何偏离点。这样的反证,无法指导下一步。
强推理模型的反证则完全不同。它会写:“候选算法为 ChaCha20,但实现与标准版本存在三处差异:第一,轮数受依赖密钥的表达式控制,标准 ChaCha20 固定为 20 轮;第二,第 N 轮的 word 索引与标准对角轮不符;第三……”每一项都指向具体、可验证的偏离点。这样的反证,本身就是你在第 3 阶段要写的差分测试清单。
这就是为什么算法家族识别应该交给推理层,也为什么在正式投入前值得亲自测试。不同模型的反证质量,直接决定你会写多少无效测试、走多少弯路。
我在这套流程中使用四个层级:
阶段 | 所需能力 | 选择 | model id |
|---|---|---|---|
拆分后的结构映射 | 长上下文,一次读完整个模块 | Kimi K3 |
|
算法家族识别与反证 | 强推理能力,敢于否定自己的初步判断 | Claude Opus 5 |
|
批量符号重命名 | 成本低、并发高 | Claude Sonnet 5 |
|
差异归因 | 中等推理能力,能针对具体字节解释差异 | GPT-5.6 Sol |
|
第二层是本文的核心。不要只听我的推荐,应该自己测。流程很简单:
从自己的混淆 bundle 中挑出 2–3 个叶子函数,其中至少一个你已经知道答案,作为对照组。
使用上一篇文章中的“候选 / 证据 / 反证”三段式提示词,将同一份输入分别交给
claude-opus-5和gpt-5.6-sol。只看反证环节:它是否真的在逐项检查偏离,还是只是在换句话复述候选结论?对已知答案的对照组而言,每个模型抓到了多少偏离点?
抓到偏离点的数量和质量,就是你的选型标准。
一次实际运行就能看出差别,比任何 benchmark 排行榜都更直接。
真正的门槛是切换成本
三个供应商、四个模型。最朴素的多模型用法,是分别接入三套 SDK、三套认证、三套错误处理;多数人算完这笔账就觉得不值得,最终让一个模型包办所有环节。于是算法家族识别用了反证能力较弱的一层,自己绕了一堆弯路却不知道原因。
AIReiter把这一层抹平了:一把 key、一个兼容 OpenAI 的接口,四个模型都在后面;切换模型只需要改请求体中的 model 字段。
# Algorithm-family ID + counter-evidence: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<three-part prompt + leaf function + constants>"}]
}'
# Bulk symbol renaming: change one field
# "model": "claude-sonnet-5"
# Difference attribution:
# "model": "gpt-5.6-sol"
如果你已经在使用 OpenAI SDK,只需将 base_url 指向 https://aireiter.com/api/v1,其他部分无需修改。使用 Anthropic SDK?用同一把 key 请求 POST /api/v1/messages 即可。
价格方面,Claude 模型按官方定价七折,GPT 模型半价。对这套流程来说,折扣正好落在最花钱的环节:算法家族识别阶段需要反复迭代提示词,对同一个函数进行多轮提问,是整个流程中调用最密集的部分;批量符号重命名则很快会进入数百次调用。这两项占据了大部分成本。
免注册试用——手动跑几轮,亲眼对比两个模型的反证结果,再决定是否使用。
最后总结
逆向混淆签名的真相是:绝大多数代码都是可以原样复用的标准算法,真正的工作在于找出标准算法被悄悄改动的地方。
固定参数变变量、常量微调、局部结构改写、材料嵌入,这四类偏离有一个共同特点:改动小到人类模式识别会跳过,却足以让整个复刻结果失败。人不擅长持续怀疑看似正确的东西,而这恰好是经过正确提示的模型所擅长的。
不过,模型只能提出怀疑,不能完成确认。每一项偏离假设最终都必须变成差分测试——这正是差分测试文章要讨论的主题。模型会给你一份“可能被改动的位置”清单;真正判断哪些位置确实被改过的,仍然是 assert。
