FormatFuzzer 是一个用于高效、高质量生成和解析二进制输入的框架。
它接收一个描述二进制输入格式的二进制模板,并生成一个可执行程序,用于生成和解析给定的二进制格式。
例如,从GIF的二进制模板出发,FormatFuzzer 会生成一个GIF生成器——也被称为GIF模糊测试器。
由 FormatFuzzer 生成的生成器效率极高,每秒可生成数千个有效测试输入——这与基于变异的模糊测试器形成鲜明对比,后者绝大多数输入都是无效的。由 FormatFuzzer 生成的输入与被测程序(实际上是任何程序)无关,因此您也可以在黑盒场景中使用它们。不过,FormatFuzzer 也集成了AFL++,以生成旨在实现最大覆盖率的有效输入。在我们的实验中,这种“两全其美”的方法超越了所有其他设置;详情请参阅我们的论文。
FormatFuzzer 使用的二进制模板来自 010 editor。
有超过 170 个二进制模板,可以直接用于 FormatFuzzer 或进行改编后使用。开箱即用地,FormatFuzzer 可以生成诸如 AVI、BMP、GIF、JPG、MIDI、MP3、MP4、PCAP、PNG、WAV 和 ZIP 等格式;并且我们每周都在扩展这个列表。
欢迎贡献者!请访问 FormatFuzzer 项目页面 提交想法和问题,或添加拉取请求。有关 FormatFuzzer 的工作原理和比较详情,请阅读我们的论文以获取更多信息。
FormatFuzzer 可从 FormatFuzzer 项目页面 获取。您可以从发布页面下载并解压最新版本。
如需获取最新版本,您也可以克隆其 Git 仓库:
git clone https://github.com/uds-se/FormatFuzzer.git
所有后续操作都在其主文件夹下进行:
cd FormatFuzzer
要运行 FormatFuzzer,您需要以下内容:
getopt_long())的 C++ 编译器,例如 clang 或 gccpy010parser、six 和 intervaltreezlib 库(用于压缩函数)boost 库(用于校验和函数)如果您打算编辑构建和配置脚本(.ac 和 .am 文件),您还需要:
sudo apt install git g++ make automake python3-full zlib1g-dev libboost-dev
python3 -m venv ~/fuzz
source ~/fuzz/bin/activate
pip3 install py010parser six intervaltree
xcode-select --install
brew install python3 automake boost
pip3 install py010parser six intervaltree
在所有系统上,使用 pip:
pip install py010parser
pip install six
pip install intervaltree
注意:所有构建命令都要求您与 README 文件位于同一文件夹中。目前不支持在此文件夹之外构建模糊测试器。
有一个 build.sh 脚本可以自动化所有构建步骤。
只需运行:
./build.sh gif
即可创建 GIF 模糊测试器。
这适用于 templates/ 中提供的所有文件格式;如果存在文件 templates/FOO.bt,那么 ./build.sh FOO 将构建一个模糊测试器。
有一个 Makefile(源文件为 Makefile.am)可以自动化所有构建步骤。
(需要 GNU make。)
首先执行:
touch configure Makefile.in
然后:
./configure
再然后:
make gif-fuzzer
即可创建 GIF 模糊测试器。
这适用于 templates/ 中提供的所有文件格式;如果存在文件 templates/FOO.bt,那么 make FOO-fuzzer 将构建一个模糊测试器。
如果上述 make 方法不起作用,或者您想要更多控制权,您可能需要手动操作。
运行 ffcompile 编译器,将二进制模板编译为 C++ 代码。它需要两个参数:.bt 二进制模板文件,以及要生成的 .cpp C++ 文件。
./ffcompile templates/gif.bt gif.cpp
使用以下命令创建模糊测试器 gif-fuzzer。
首先,编译通用命令行驱动程序:
g++ -c -I . -std=c++17 -g -O3 -Wall fuzzer.cpp
(-I . 表示 bt.h 文件的位置;-std=c++17 设置 C++ 标准。)
然后,编译二进制解析器/生成器:
g++ -c -I . -std=c++17 -g -O3 -Wall gif.cpp
最后,将二进制解析器/生成器与命令行驱动程序链接,以获得可执行文件。如果您使用了任何额外的库(例如 -lz),请务必在此处指定它们。
g++ -O3 gif.o fuzzer.o -o gif-fuzzer -lz
FormatFuzzer 可以作为特定格式的独立解析器、生成器或变异器运行。 此外,它还可以被通用模糊测试器(如 AFL++)调用,以将这些特定格式的能力集成到模糊测试过程中(请参阅下面关于 AFL++ 集成的部分)。
生成的模糊测试器将一条命令作为第一个参数,后跟该命令的选项和参数。
最重要的命令是 fuzz,用于生成输出。其参数是要以适当格式生成的文件。
运行生成器:
./gif-fuzzer fuzz output.gif
创建一个随机的二进制文件 output.gif,或:
./gif-fuzzer fuzz out1.gif out2.gif out3.gif
创建三个 GIF 文件 out1.gif、out2.gif 和 out3.gif。
请注意,我们提供的 gif.bt 模板已增加了特殊函数,以使有效文件的生成更轻松。如果您使用原始的 .bt 模板文件而未进行改编,您可能会在生成过程中收到警告并创建无效文件。
您也可以使用 parse 命令将模糊测试器作为二进制文件的解析器运行。这对于测试二进制模板的准确性,或者如果您想变异输入(请参阅下面的“决策文件”)很有用。
要运行解析器,请使用:
./gif-fuzzer parse input.gif
如果 input.gif 无法成功解析,您将看到错误消息。
在解析过程中,您还可以将所有解析决策(即采用了哪些解析替代方案)存储在一个决策文件中。这是一个字节序列,列举了所采用的决策。
每个字节代表一个单一的解析决策。字节值为 0 表示采用了第一个替代方案,字节值为 1 表示采用了第二个替代方案,依此类推。
您可以在解析输入时生成这样一个决策文件:
./gif-fuzzer parse --decisions input.dec input.gif
这里,input.dec 存储了解析 input.gif 时所做的决策。
您也可以在生成输入时使用这样的决策文件。然后模糊测试器将采用与解析期间完全相同的决策。以下命令使用解析 input.gif 时确定的决策生成一个新的 GIF 文件:
./gif-fuzzer fuzz --decisions input.dec input2.gif
如果一切正常,这两个文件应该是相同的:
cmp input.gif input2.gif
通过变异一个决策文件(例如替换单个字节),您可以创建与原始解析文件相似的输入。这对于与特定的测试策略和模糊测试器(如 AFL)进行接口交互非常有用,您可以使用 gif-fuzzer 等作为从决策文件到二进制文件以及反向的转换器:AFL 将变异决策文件,而被测程序将在转换后的二进制文件上运行。与直接变异二进制文件(AFL 通常会这样做)相比,这样做的好处是可以始终拥有有效输入——从而更快地提高覆盖率。
除了特定格式的模糊测试器(如 gif-fuzzer)之外,FormatFuzzer 还可以编译为特定格式的共享库,例如 gif.so(为此,只需运行 ./build.sh gif 或 make gif.so)。
这些共享库可以由通用模糊测试器加载,例如 AFL++。
要使用 FormatFuzzer 运行 AFL++,只需按照我们修改版的 AFL++ 上的说明进行操作。 我们支持不同的模糊测试策略,包括:
AFL+FFMut:运行 AFL++ 并使用 FormatFuzzer 提供特定格式的智能变异。
AFL+FFGen:使用 FormatFuzzer 作为特定格式的生成器,而 AFL++ 则变异其决策种子。
要编写您自己的 .bt 二进制模板(从而为此格式创建高效的模糊测试器/解析器),请阅读 010 Editor 手册 中的 模板和脚本简介 部分。
在许多情况下,您要找的格式(或类似格式)的模板可能已经存在。请查看 010 editor 的二进制模板集合,看看是否有可以使用的或者作为您格式基础的模板。
请注意,仓库中提供的 .bt 文件通常是针对解析文件设计的。它们也可以用于生成文件,但通常缺乏关于输入哪些部分是必需的精确信息。
在本节中,我们将讨论一些自定义 .bt 文件以使其与 FormatFuzzer 良好配合的方法。
例如,对于 GIF 格式,文件 templates/gif-orig.bt 展示了原始的二进制模板,它仅用于解析,而文件 templates/gif.bt 是修改后的版本,能够生成有效的 GIF。比较这两个文件,我们看到只需要少量更改即可实现这一点。
如果您已经创建了 gif-fuzzer,无论是通过运行 make gif-fuzzer 还是使用 ffcompile 工具,您已经得到了一个 C++ 文件 gif.cpp,其中包含了 GIF 生成器和解析器的实现。这对于了解您对二进制模板所做的更改如何转化为可执行代码非常有用。关于 C++ 代码的更多详细信息将在下一节中介绍。
GIF 二进制模板使用了向前看函数 ReadUByte() 和 ReadUShort(),以便在实际将字节解析为结构字段之前预先查看文件中后续字节的值。在生成时,我们允许这些函数接收一个额外的参数,该参数指定一组好的已知值,用于选取我们向前看的字节。此外,我们还允许指定一个全局的好已知值集,以便在调用特定的向前看函数(如 ReadUByte())时始终使用。这些值存储在 ReadUByteInitValues 向量中。
默认情况下,我们的翻译过程 ffcompile 尝试挖掘在与向前看字节进行比较时使用的有趣值,并将它们用作全局已知值集。当运行:
./ffcompile templates/gif.bt gif.cpp
时,会打印一条消息,显示已识别的向前看函数以及挖掘到的有趣值:
Finished creating cpp generator.
Lookahead functions found:
ReadUByte
ReadUShort
Mined interesting values:
GlobalColorTableFlag: ['1']
LocalColorTableFlag: ['1']
ReadUByte: ['0x3B', '0x2C']
ReadUShort: ['0xF921', '0xFE21', '0x0121', '0xFF21']
Signature: ['"GIF"']
然而,对于 GIF 生成,更好的做法是在每次调用 ReadUByte() 时单独指定一组好的已知值。因此,我们定义了一个空数组(大小为 0):
const local UBYTE ReadUByteInitValues[0];
以覆盖全局 ReadUByteInitValues 集,并且对于每次对 ReadUByte() 的调用,我们使用一个额外的参数来指定在该特定位置要使用的一组好的值。
二进制模板语言也足够强大,允许根据运行时条件进行这种选择。例如,在以下代码中,我们展示了如何根据当前生成的 GIF 版本来选择 ReadUByte() 调用的适当值。GIF 版本 89a 允许该字节有一个额外的可能值(0x21)。
if(GifHeader.Version == "89a")
local UBYTE values[] = { 0x3B, 0x2C, 0x21 };
else
local UBYTE values[] = { 0x3B, 0x2C };
while (ReadUByte(FTell(), values) != 0x3B) {
...
}
GIF 二进制模板所需的其余编辑类似。例如,对于每个结构字段,也可以指定一组好的已知值。例如,这为 Version 字段指定了正确的值:87a 和 89a。
char Version[3] = { {"87a"}, {"89a"} };
出于调试目的,以及了解如何对生成器和解析器进行适当的更改以提高其性能,了解生成的 C++ 代码的一些内部工作原理可能会很有用。 理想情况下,您应该能够编辑二进制模板文件,直到它们能够以高概率生成有效文件,这样您就不必编辑生成的 C++ 代码。
C++ 代码为二进制模板中定义的每个 struct 和 union 以及原生类型(如 int)创建一个类。
在构造时,当初始化一个变量时,我们可以定义一组该变量可以取的好已知值。例如,构造函数调用:
char_array_class cname(cname_element, { "IHDR", "tEXt", "PLTE", "cHRM", "sRGB", "iEXt", "zEXt", "tIME", "pHYs", "bKGD", "sBIT", "sPLT", "acTL", "fcTL", "fdAT", "IHDR", "IEND" });
将为变量 cname 指定 17 个好值。但这通常还不够,因为合适的块类型的选择是上下文相关的。
因此,我们也允许在生成新块时,在生成时指定一组好的值。
例如,这个调用可用于为第一个块生成一个 chunk 实例,该实例的类型必须是 IHDR。
GENERATE(chunk, ::g->chunk.generate({ "IHDR" }, false));
在生成第二个块时,我们可能会使用这个长列表,列出可以出现在 IHDR 块和 PLTE 块之间的可能块:
GENERATE(chunk, ::g->chunk.generate({ "iCCP", "sRGB", "sBIT", "gAMA", "cHRM", "pHYs", "sPLT", "tIME", "zTXt", "tEXt", "iTXt", "eXIf", "oFFs", "pCAL", "sCAL", "acTL", "fcTL", "fdAT", "fRAc", "gIFg", "gIFt", "gIFx", "sTER" }, true));
生成器将均匀地从好已知值中选取一个用于新实例。我们也允许以小的概率(1/128)选择一个不属于好已知值的“邪恶”值。
此功能可以通过 set_evil_bit 方法随时启用或禁用。
生成器所做的所有随机选择都是通过调用 rand_int() 方法完成的。
long long rand_int(unsigned long long x, std::function<long long (unsigned char*)> parse);
当程序作为生成器运行时,此方法通过从随机缓冲区读取字节来采样一个从 0 到 x-1 的整数。
当程序作为解析器运行时,此方法使用 parse() 函数找出随机缓冲区中必须存在哪些随机字节才能生成目标文件,然后将这些字节写入随机缓冲区。
parse 函数接收文件当前位置的缓冲区作为参数,然后必须返回当前对 rand_int() 的调用应返回的值,以便生成这个确切的文件配置。
FormatFuzzer 由 Rafael Dutra <[email protected]> 设计和编写。
模糊测试编译器 (fuzzer compiler) 的概念由 Rahul Gopinath <[email protected]> 和 Andreas Zeller <[email protected]> 提出。
FormatFuzzer 版权所有 © 2020, 2021 归 CISPA 亥姆霍兹信息安全中心 所有。适用以下许可证:
FormatFuzzer 代码(特别是所有 C++ 代码及其生成相关代码)受 GNU GENERAL PUBLIC LICENSE 许可,详见 COPYING。
作为上述内容的例外情况,由 FormatFuzzer 生成的 C++ 代码(即特定格式的模糊测试器和解析器)属于公共领域。
FormatFuzzer 所基于的_原始_ pfp 代码 受 MIT 许可证许可,详见 LICENSE-pfp。