随笔

逆向工程需要更大的模型吗?

作者 N0zoM1z0 · 2026 年 10 月 13 日

要点

  • 模型能力会影响 agent 能提出哪些假设。 后来的工作解决了早期尝试未能解决的问题。
  • 熟悉的编译器为专用模型提供了具体的学习对象。 有些精确匹配依赖细小但可以复用的源码模式。
  • 也要公平评估代码仓库里的知识。 较小的模型配合良好的检索,可能已经掌握了足以推进工作的知识。
  • 训练模型选择下一个有价值的实验,同时检验它使用的验证器。 反馈是否有用,取决于验证器究竟检查了什么。
  • 结合时间和成本,评估通过验证的重建成果。 这是我希望从整个 agent 工作流中获得的结果。

更强的模型改变了什么

从 2026 年 8 月开始,我一直在探索:如果给 agent 访问程序的能力,以及验证自身工作的手段,它能独立完成多少逆向工作?我的经验主要来自游戏重建,尤其是东方系列。随着我更换模型,代码仓库记录了每次工作还原出的内容。

这些工作很大一部分追求精确重建:用固定版本的历史工具链编译我们维护的源码,检查它能否复现目标函数。当后来的模型解决了早期工作卡住的问题,我们可以从源码变化中看出,是什么促成了这次进展。

临时值需要同时保持有效

TH09 中一个攻击函数的大部分逻辑已经还原,但编译器对临时结果的存储安排仍然与原版不同。剩下的问题在于:这些结果的生命周期应该多长,编译器何时可以复用它们的存储空间?

GPT-6-Sol 的一次工作记录表明,尝试几种常见的源码写法后,差异仍未消除。后来,GPT-6.1-Sol 重新研究这个问题,提出了一个有价值的假设:关键可能在于这些临时值的生命周期。

这个函数会产生临时结果,再把它们复制到动画的位置数据中。在 C++ 中,临时结果会一直存在,直到使用它的表达式执行完毕。把实际操作放进同一个表达式,就能让这些结果同时存活,编译器也就不能为它们全部复用同一块存储空间。

其中一个结果仍然与稍后用于碰撞检查的值共享存储。给那个值单独的生命周期后,最后一处重叠也消失了。经过几次有针对性的实验,一次干净的重新构建确认,整个函数都已匹配。

我感兴趣的是,发现与验证如何共同推进工作。生命周期的假设给 agent 指明了新方向;编译器的反馈又帮助它逐步调整源码,直到找到匹配的写法。

换一种控制流结构

TH09 的另一个函数说明了,新方向为什么重要。这次的难点是多个分支如何共享末尾的操作。GPT-6.1-Sol 之前的工作已经让函数的一些部分匹配,但分支汇合的方式仍与原版不同。

GPT-6-Astra 改写了外围的控制流图,让主要路径汇入共同的最终更新与返回。在这种源码结构下,编译器合并分支的方式与原版一致。整个函数实现了匹配,另一次独立的重新构建也确认了这一结果。

换一种表达方式,打开了更有价值的搜索空间。 这两个例子让我认真看待通用模型的能力:agent 可能花很长时间尝试局部变化,才找到一种更好的整体写法。

不过,后来的工作也继承了之前的源码和失败记录。这些积累本身就是优势的一部分。历史记录让我们可以检查发现及其验证过程;要区分模型能力和继承经验各自带来的帮助,还需要对照实验。

这正是我觉得有趣的地方。有些进展来自新的表达方式,剩下的大量工作则是在熟悉的编译器上试验不同写法。其中有多少需要广泛的推理能力?又有多少可以通过反复接触这个编译器和这些代码来学会?

为什么专用模型可能做得很好?

东方系列给了我们一个具体的理由,去期待经验积累能发挥作用。大量游戏代码出自同一个作者 ZUN,因此,把反复出现的编程习惯作为初始假设是合理的。代码仓库也提供了实际例子:相似的循环和辅助函数组织方式,曾为其他游戏的重建提供假设。

把编译器也考虑进去,这些源码模式就更加具体了。在固定版本和选项下,源码的小变化可能稳定地产生不同的机器码。其中一些规律藏得很深。

以 TH095 为例,连局部变量的名字,都可能改变 VC7.1 为它安排的存储位置。只移动声明并不能解决布局问题,但先前重建的一个背景函数揭示了会影响编译器选择的命名模式。这为重建 Supervisor::LoadDat提供了更有希望的假设。

把这个模式用于表示文件大小的变量,不增加额外存储就得到了所需布局。随后,整个函数实现了匹配。

这类细节非常具体,通用模型第一次接触时未必知道。而针对这个编译器训练的专用模型,可以接触许多类似案例,学会哪些源码变化值得优先尝试。

当我们已经理解函数行为,只剩最后的匹配差异时,这个机会尤其明显。此时的问题是:这个编译器会把哪一种普通写法编译成眼前的指令?专用训练可能让模型更高效地提出这些写法,而最终由目标程序决定哪些假设成立。

但这些模式必须保留适用的上下文。TH08 使用的 Visual C++ 版本早于 TH095 和 TH09。TH04 用的是另一种 DOS 时代的编译器,其玩家逻辑重建需要不同的假设,既涉及源码写法,也涉及编译后的代码如何链接成可执行文件。同一作者为我们提供了先验,具体目标则告诉我们它是否适用。区分这些情况,也是专用模型需要学会的事。

这些经验应该存在哪里?

在决定通过训练让模型学会这些模式之前,我会先看看代码仓库里已有的知识。在上一篇文章中,我写过:仓库正在成为项目的工作记忆。这里保存着对编译器的认识,也保存着相关源码和失败实验,让这些经验有据可查。

检索这些记录后,agent 可以从已知结果出发,看看我们是如何得到它的,也可以避开在相同条件下已经失败的方案。模型的参数没有改变,工作的起点却提高了。

重建 TH09 的敌人生成函数时,GPT-6-Luna-max 的一次工作从 TH08 对应的源码中找到了假设。之前的写法提示了循环该如何组织,以及内部失败该如何处理。TH09 自身的证据补上了游戏特有的细节,干净的重新构建确认了整个函数的匹配。

“模型不知道这个编译器特性”和“模型不会利用它”,是两个不同的问题。 检索可能解决前者,但把知识变成有价值的候选源码,仍然需要能力。如果候选方案失败了,模型还得判断这次结果对下一次尝试意味着什么。知道相关模式,只是这个过程的起点。

所以,在任何对照实验中,我都会加入一个具备良好检索能力的小型通用模型。给它相同的经验库,才能看清训练究竟增加了什么:更好的候选方案、更好的反馈后决策,还是两者都有?

学习选择下一个有价值的实验

反馈之后如何决策,是我最想研究的部分。在 agent 驱动的重建中,模型提出解释,再用程序来验证。即使候选方案失败了,结果也可能缩小剩余的可能性,提示下一步更值得尝试什么。

再看前面的临时结果例子。最终源码展示了成功的写法,但得到它的过程包含更多信息。下面的每次实验,都帮助我们确定了还有什么需要改变。

实验 结果告诉了我们什么
尝试另一种复制结果的方式 复制代码改变了,但函数仍有差异。
直接使用返回值 复制方式与原版一致,但临时存储布局仍有差异。
让临时结果同时保持有效 它们得到了各自的存储空间,但其中一个仍与稍后用于碰撞检查的值重叠。
让碰撞检查的值拥有独立存储 干净的重新构建确认了整个函数的匹配。

选自前面攻击函数的重建实验,按每次得到的认识整理。

保存这个过程的训练记录,比单独保存最终源码能教会模型更多东西。它把每个改动、观察到的结果和下一步决策联系起来。无效的尝试也值得保留:后来的 agent 可以据此知道,在这个上下文中,哪些假设已经检验过。

我希望模型学会如何利用反馈。 结果没有变化,可能意味着该重新考虑这个改动;差异发生变化,则可能指向另一个原因。当局部修改不再提供有效信息时,agent 应该可以重新考虑源码的整体表达,或者回头检查证据。

学会这些选择,需要自主性。有时,更有价值的下一步是检查另一个调用者,而不是再修改一次源码并编译。如果强制按照固定顺序改写,就会剥夺这种选择,也失去一部分我们希望模型学会的行为。

为什么逆向工程值得用来探索强化学习

范围明确的重建任务,为研究这些选择提供了一个实际环境。目标和编译器版本固定之后,许多源码实验都能快速给出可重复的反馈。agent 可以编译候选源码、检查差异,再根据观察选择下一步。

强化学习可以利用这些反馈,训练模型的决策过程。候选源码通过明确定义的检查,就给出了一个具体成果;而尝试的过程,让我们可以看到实现它花了多少功夫。

已有研究支持这个方向。LLM4Decompile研究专门用于反编译的模型。AutoDecompiler利用编译和执行反馈,训练多轮改进能力。这些研究关注的是功能还原。对于我们的工作,我会以它们为起点,研究一个相关任务:找到能复现历史编译器输出的源码。

不过,要得到这样一个定义明确的任务,有时得先确定函数负责哪些资源,或者设计新的方法观察程序运行时的行为。在 TH08 中,一次碰撞差异最终追溯到原版处理浮点计算的方式。agent 必须先弄清楚该测量什么,才能验证有价值的修正。

这提示了能力全面的模型的另一个作用:先把问题变得可验证。验证手段建立起来后,我们才可以研究,学出来的搜索策略能走多远。

当验证器出错时

一旦用某项检查来训练模型,它的覆盖范围就会成为学习环境的一部分。我把这种参考标准或验证器称为 oracle。即使它能给出可重复的答案,我们也必须知道,这个答案覆盖了什么。

TH08 展示了检查范围之外可能藏着多少问题。一些重建函数使用了错误的浮点常量,却仍然通过了精确比较。比较器会先调整重定位地址,再检查指令;但只有明确登记过的常量,才会进一步检查地址指向的值。其他常量引用,即使值没被检查,也可能通过。

为了补上这个漏洞,一次更全面的审计在已经通过检查的函数中找到了错误常量。有一个计算中,重建源码加的是 1.0f,而原版加的是 32.0f。

如果用旧检查的结果作为奖励,模型可能学会生成错误的重建源码。 我们没有用这个验证器训练 RL 模型;实际观察到的是检查本身的漏洞。但如果把它用作奖励,同一个盲点也会进入学习环境。

功能测试也可能存在类似盲点。针对 LLM 反编译器的差分模糊测试研究发现,在已有测试之外,程序行为仍可能不同。因此,理解“测试通过”时,必须看它实际覆盖了哪些情况;理解“指令匹配”时,也必须看它检查了哪些字节和数据。

在把任何一种检查用作奖励之前,我都会用刻意构造的错误来测试它。我还会把最终验收机制放在候选源码可修改的范围之外,让模型通过正确重建来满足检查。否则,直接复制目标程序的字节数组,也可能骗过简单的评分,却没有还原出有用的源码。

改进模型和改进它的验证器,属于同一个研究问题。 模型越擅长优化我们给它的反馈,我们就越需要理解反馈本身。

值得做的对照实验

具备这些检查之后,我会从一组固定的 VC7.1 任务开始比较。先为强通用模型和小型通用模型建立基线,再给小模型加入仓库知识,观察检索带来的变化。接着比较监督训练的专用模型,以及在此基础上进行 RL 训练的版本,看看训练增加了什么。

为了让比较有意义,每个模型都应获得相同的目标证据和工具。专门比较训练效果时,还要固定基础模型和可用知识。重复实验有助于区分可靠的搜索策略与偶然成功的候选方案。

我还会区分两种起点:从指令开始重建函数,以及在已有源码中消除最后几处差异。专用模型可能很擅长后者,而通用模型承担了到达这个阶段的大部分工作。分别评估,才能看清这种分工。

两类任务都需要真正未见过的测试案例。东方系列的相关游戏会共享引擎代码,只留出某个游戏,仍可能让几乎相同的函数进入训练集。我会改为按相关源码家族留出测试集,并把它们通过验证的答案同时排除在训练数据和检索库之外。

在这些案例上,要统计的是完整、独立验证过的匹配成果。给每个模型相同的候选数量或编译次数预算,可以比较搜索质量。记录总耗时和总成本,包括编译,才能知道这种质量是否转化成了高效的工作流。训练成本也要单独计算。

两种角度都重要。一个便宜的提议,如果导致大量无效编译,整个工作流仍可能很贵。反过来,较慢的模型如果更早找到有价值的实验,也可能更划算。对照实验应当反映这些成本。

只统计通过验收的结果,也能帮助我们合理看待中间进展。TH09 的一个录像保存候选方案看起来很有希望,因为编译后函数的大小符合预期,但完整比较仍发现大量差异。编译器改变指令排列,也可能让候选源码看起来更接近,却没有解决根本问题。这些诊断可以指导下一步,但若要奖励“变得更接近”,我会先对照完整验收检查,确认这种奖励有效。

我想看这些结果呈现出什么规律。如果专用模型在熟悉的编译器上表现很好,换了工具链后,这种优势还在吗?当双方都拥有良好的检索能力时,训练还能增加多少价值?这些答案能帮助我们决定,每类模型最适合用在哪里。

这意味着怎样的 agent?

如果不同模型在不同阶段各有所长,一种有价值的安排可能是:让能力全面的 agent 可以调用专用模型。当任务变成一种熟悉的编译器搜索时,它可以请专用模型提出候选方案。如果结果开始说不通,通用 agent 可以回头检查目标证据,或者调查测量本身。

在这样的安排中,专用模型成为 agent 随着调查进展可以选择的工具。agent 仍然拥有改变方向的自由,而这正是前面那些突破发生的重要条件。

这自然延续了上一篇文章中谈到的工业化转变。能够开展更多实验,会改变我们组织工作的方式,也让我们有理由追问:哪些决策需要全面的判断,哪些可以通过反复积累某项具体技艺来改善?

目前,最强的通用模型解决了我过去没能解决的问题,因此在我的工作流中占据了一席之地。正是这些成功,让我想更仔细地研究过程:有多少依赖模型能力的广度?又有多少可以交给专门学习这类重建的模型?

这些代码仓库让我们有机会验证这个问题。 我想利用这些记录建立对照实验,看看我们能学到什么。

证据与模型归属

详细证据记录包含历史比较结果与实验背景。

这些例子来自链接提交中记录的历史函数级比较。整款游戏的运行时检查属于另一个里程碑。相关源码与匹配记录核查于 2026 年 10 月 13 日。

TH09 中的标签 gpt-dots 对应 GPT-6-Astra。gpt-web 会随当时使用的模型变化,因此仅凭这个标签无法确定某次运行的具体模型。其他引用提交中的明确模型名,提供了对应的归属记录;这些历史记录并没有统一的单次运行成本或投入统计。

文中提出的专用模型与 RL 对照,属于未来实验。项目历史为这些想法提供了依据;本文尚未评估经过训练的专用模型。

顶部