在2007年,我为
Freelancer空间模拟器编写了
两个修改
工具 。 游戏资源以“二进制INI”或“ BINI”格式存储。 可能是出于性能考虑选择了二进制格式:此类文件的加载和读取速度比INI格式的任意文本更快。
大多数游戏内容都可以直接从这些文件中进行编辑,更改名称,产品价格,航天器统计数据,甚至添加新飞船。 二进制文件很难直接修改,因此自然的方法是将它们转换为文本INI,在文本编辑器中进行更改,然后转换回BINI格式并替换游戏目录中的文件。
我没有分析BINI格式,也不是第一个学习如何编辑它们的人。 但是我不喜欢现有的工具,而且我对它们应该如何工作有自己的看法。 我喜欢Unix风格的界面,尽管游戏本身可以在Windows上运行。
那时,我才熟悉
yacc (实际上是
Bison )和
lex (实际上是
flex )以及Autoconf工具,因此我完全使用了它们。 在实践中尝试这些实用程序很有趣,尽管我刻苦地模仿了其他开源项目,却不明白为什么用这种方式完成了所有事情。 由于使用了yacc / lex和配置脚本的创建,因此需要一个完整的类Unix系统。 这些在程序的
原始版本中都是可见
的 。
事实证明该项目非常成功:我本人也成功使用了这些工具,它们出现在Freelancer改装的不同系列中。
重构
在2018年中,我回到了这个项目。 您是否曾经想到过您的旧代码:您甚至想到了什么? 我的INI格式变得比原来更严格和严格,二进制文件以可疑的方式记录,并且汇编甚至无法正常工作。
凭借十年的丰富经验,我确信我现在会更好地编写这些工具。 我几天后就做到了,从头开始重写它们。 现在,此新代码位于Github的主线程中。
我喜欢使一切都尽可能简单 ,因此我摆脱了autoconf的支持,转而使用
更简单,更可移植的Makefile 。 没有更多的yacc或lex,但是解析器是手工编写的。 仅使用适当的可移植C语言,其结果是如此简单,以至于我使用
Visual Studio中的一个短命令
来组装项目,因此实际上不需要Makefile。 如果用
typedef
替换
stdint.h
,甚至可以
在DOS下构建和运行binitools 。
新版本更快,更紧凑,更清洁,更容易。 相对于INI输入,它要灵活得多,因此更易于使用。 但这真的正确吗?
模糊测试
多年来,我一直对
模糊测试很感兴趣,尤其是
afl (美国模糊lop)。 但是,尽管他测试了我经常使用的一些工具,但他从未掌握过它。 但是,至少在我放弃之前,模糊测试并没有发现任何值得注意的事情。 我测试了JSON库,由于某种原因也没有找到任何东西。 很明显,我的JSON解析器不太可靠,对吧? 但是模糊测试什么也没有。 (事实证明,我的JSON库非常可靠,这在很大程度上要感谢社区的努力!)
但是现在我有了一个相对较新的INI解析器。 尽管它可以成功分析并正确组装游戏中的原始BINI文件集,但其功能尚未经过
真正的测试。 当然,在这里模糊测试会发现一些东西。 此外,您无需编写任何一行即可在此代码上运行afl。 默认工具与标准输入配合使用,这是理想的选择。
假设您已经安装了必要的工具(make,gcc,afl),这就是binitools模糊启动的方式:
$ make CC=afl-gcc $ mkdir in out $ echo '[x]' > in/empty $ afl-fuzz -i in -o out -- ./bini
bini
实用程序在输入处接受INI并发出BINI,因此检查它比反向
unbini
过程更有趣。 由于
unbini
分析的是相对简单的二进制数据,因此(可能)模糊测试器无需寻找。 但是,以防万一,我还是检查了一下。

在此示例中,我将默认编译器更改为afl(
CC=afl-gcc
)的GCC shell。 这里afl在后台调用GCC,但它向二进制文件添加了自己的工具包。 当进行模糊测试时,
afl-fuzz
使用此工具包来监视程序的执行路径。
afl文档说明了技术细节。
我还通过在输入目录中放置一个最小的工作示例(它为afl设定了起点)来创建输入和输出目录。 启动时,它会改变输入数据队列并监视程序执行期间的更改。 输出目录包含结果,更重要的是,包含导致唯一执行路径的输入数据主体。 换句话说,许多输入在模糊器输出处进行处理,检查许多不同的边界情况。
最有趣和可怕的结果是程序完全崩溃。 当我第一次启动binitools的模糊器时,
bini
出现了
许多此类崩溃。 在几分钟之内,afl在我的程序中发现了许多细微而有趣的错误,这非常有用。 Fazzer甚至发现了一个不太可能
的过时指针错误,它检查了各种内存分配的不同顺序。 这个特定的错误是一个转折点,使我意识到模糊的价值。
并非所有发现的错误都导致失败。 我还研究了输出,查看哪些输入产生了成功的结果,哪些没有取得成功,并观察了程序如何处理各种极端情况。 她拒绝了我认为可以处理的一些意见。 反之亦然,她处理了一些我认为不正确的数据,并以一种意想不到的方式为我解释了一些数据。 因此,即使在修复了程序崩溃的错误之后,我仍然更改了解析器设置以修复这些不愉快的情况。
创建一个测试套件
修复了模糊器检测到的所有错误并在所有边界情况下调整了解析器后,我就从模糊器数据包中进行了一系列测试-尽管不是直接进行的。
首先,我并行运行了模糊器-这个过程在afl文档中进行了说明-因此我得到了很多冗余输入。 冗余是指输入不同但执行路径相同。 幸运的是,afl有一个可以处理此问题的工具:
afl-cmin
,它是使外壳最小化的工具。 它消除了不必要的输入。
其次,这些输入中的许多输入都比调用其唯一执行路径所需的时间更长。
afl-tmin
,一种减少了测试用例的测试用例最小化器,在
afl-tmin
了帮助作用。
我分离了有效输入和无效输入-并在存储库中对其进行了检查。 看看基于单个最小输入
的模糊器发明的所有这些愚蠢的入口:
实际上,这里的解析器被冻结在一个状态中,并且通过一组测试来确保特定的构建以
非常特定的方式运行。 这对于确保其他编译器在其他平台上生成的程序集的输出行为相同时特别有用。 我的测试套件甚至在Dietlibc库中检测到错误,因为binitools链接后未通过测试。 如果必须对解析器进行不重要的更改,那么从本质上讲,您将不得不放弃当前的测试集并重新开始,这样afl将为新的解析器生成一个全新的主体。
当然,模糊已将自身确立为一项强大的技术。 他发现了许多我自己无法发现的错误。 从那时起,我开始更熟练地使用它来测试其他程序-而不仅仅是我的程序-并且发现了许多新的错误。 现在,fuzzer在我的开发套件中的工具中占据了永久的位置。