fuzz初试(AFL++)
简介:
模糊测试(Fuzzing/Fuzz)是一种自动化软件测试技术,它基于为程序提供随机或变异的输入值并监视它的异常和崩溃。
AFL 是一个覆盖引导的模糊器(coverage-guided fuzzer),这意味着它收集每个变异输入的覆盖信息,来发现新的执行路径和潜在的错误。当源代码可用时,AFL 可以使用插桩(instrumentation),在每个基本块(函数、循环等)的开头插入函数调用。
AFL++使用qemu用户模式模拟仿真来运行二进制文件,其使用的qemu是进行修改的版本,在程序执行时检测基本块,根据收集的信息生成测试用例,通过生成的大量测试用例触发不同的代码路径,从而提高代码的覆盖率,提高触发Crash的概率。
AFL的一些基础
AFL对开源代码进行fuzzing的过程可以用以下五步描述:
从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage)
#AFL的覆盖率,计算在一次运行中,相应边执行的次数,次数单位为2的幂(以缓解路径爆炸)。如果一个用例输入发现了至少一条边,也就是创建一个新的桶来装入新的边的次数,那么这个用例就是interesting用例,并放入队列。AFL用一个bitmap把这些装有边次数的桶整合起来,一个byte代表一条边
选择一些输入文件,作为初始测试集加入输入队列(queue)
将队列中的文件按一定的策略进行 “突变”
如果经过变异文件更新了覆盖范围,则将其保留添加到队列中
上述过程会一直循环进行,期间触发了crash的文件会被记录下来
其大致思路是,对输入的seed文件不断地变化,并将这些mutated input喂给target执行,检查是否会造成崩溃。因此,fuzzing涉及到大量的fork和执行target的过程。也就是说,父进程fork出一个子进程执行输入用例,父进程等待结果。这样可以避免频繁调用execve()函数。
但是,fork也会有性能瓶颈的问题,AFL提出persistent mode,该模式下不会为每个测试用例fork。 取而代之的是,可以将循环的方式添加到目标程序中,也就是每次迭代执行一个测试用例。
AFL++
变异器(对测试输入进行一些操作
自定义变异器允许相关的模糊测试的研究在 AFL++ 之上构建新的调度算法、变异算法等等,而无需像当前许多工具那样fork 和修补 AFL。插件可以用 C ABI 兼容的语言编写,甚至可以用 Python 进行原型设计。例如,使用当前的 API,AFLSMART 可以作为 AFL++ 插件完全重写十次。
目前AFL++实现了以下功能:
- afl_custom_(de)init :初始化AFL++的伪随机数种子生成器
- afl_custom_queue_get:其是一个回调函数,用于确定自定义的FUZZer是否应该FUZZ当前队列的用例。
- afl_custom_fuzz:对给定的输入执行自定义变异。
- afl_custom_havoc_mutation:对给定的输入执行单个自定义变异。 这种突变与AFL的havoc阶段的其他变异策略叠加在一起。
- afl_custom_post_process:在某些情况下,从自定义 mutator 返回的变异数据的格式不适合作为输入到目标程序执行。例如,当使用 libprotobuf-mutator 时,返回的数据是对应于给定语法的 protobuf 格式,首先需要将其转换为目标的纯文本格式。 在这种情况下,或者要修复校验和以及大小,用户可以定义 afl_custom_post_process 函数。
- afl_custom_queue_new_entry:在将新测试用例添加到队列后调用,这是一个存储元数据的API。
支持用例修剪的API
修剪用例的目的是减少因为大量产生用例导致格式过于复杂,以至于不符合协议格式。
- afl_custom_init_trim:该API在每次修剪操作开始时被调用并接收初始缓冲区。它返回此次输入上可能的迭代次数(例如,如果输入有 n 个元素,其中一个应该被删除,则返回 n-1)。 如果实现的修剪算法不允许确定(剩余)步骤的数量,那么它可以返回 1 表示可以执行进一步的修剪,这将在 afl_custom_post_trim 返回 0 时执行。
- afl_custom_trim:每次修剪操作都会调用 afl_custom_trim。 它可以记住当前状态,因此可以保存每次迭代的重新分析的步骤。该API返回修剪后的输入缓冲区,其返回的数据长度不得超过初始输入数据。
- afl_custom_post_trim:该API在每次修剪操作后调用以通知修剪步骤是否成功。
插桩
1 LLVM
LLVM主要包含以下两种插桩方式:
上下文敏感的边缘覆盖:edge覆盖是将每个block被分配的ID与被调用者的唯一ID进行异或运算。
Ngram:在记录edge时不考虑前一个块和目标块,而是考虑目标块和前 N-1 个块,其中 N 是 2 到 16 之间的数字。
2 GCC
除了包含旧的 afl-gcc 包装器,AFL++ 还附带了一个 GCC 插件。 它包括对延迟初始化和persistent 模式的支持,例如 AFL LLVM 模式。
3 QEMU
该模式针对二进制程序进行模糊测试。
CVE-2019-13288 in XPDF 3.02
实验环境配置
虚拟机Ubuntu 20.04.2 LTS 镜像。用户名为 fuzz
/ fuzz
。
下载并构建
首先为Fuzz目标创建一个新目录:
1 | cd $HOME |
可能需要安装一些额外的工具(即 make 和 gcc)
1 | sudo apt install build-essential |
下载 Xpdf 3.02:
1 | wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz |
构建 Xpdf:
1 | cd xpdf-3.02 |
现在可以开始测试Xpdf。首先,需要下载一些 PDF 示例:
1 | cd $HOME/fuzzing_xpdf |
可以使用以下命令测试 pdfinfo 二进制文件:
1 | $HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf |
安装AFLplusplus
安装依赖:
1 | sudo apt-get update |
构建 AFL++:
1 | cd $HOME |
输入afl-fuzz如下即为成功
使用AFL++开始Fuzz
当源代码可用时,AFL 可以使用检测,在每个基本块(函数、循环等)的开头插入函数调用。为了为目标应用程序启用检测,所以需要使用 AFL 的编译器编译代码。简单来说就是需要使用afl编译器来编译目标。
首先,清理所有以前编译的目标文件和可执行文件:
1 | rm -r $HOME/fuzzing_xpdf/install |
现在将使用afl-clang-fast编译器构建 xpdf
1 | export LLVM_CONFIG="llvm-config-11" |
现在可以使用以下命令运行Fuzz:
1 | afl-fuzz -i $HOME/fuzzing_xpdf/pdf_examples/ -o $HOME/fuzzing_xpdf/out/ -s 123 -- $HOME/fuzzing_xpdf/install/bin/pdftotext @@ $HOME/fuzzing_xpdf/output |
执行参数说明:
- -i:表示输入文件目录
- -o:表示 AFL++ 将存储变异文件的目录
- -s:表示要使用的静态随机种子(AFL 使用非确定性测试算法,因此两个Fuzz会话永远不会相同。这就是为什么设置固定种子 -s 123的原因。用以保证Fuzz结果和示例相同。)
- @@:是占位符目标的命令行,AFL 将用每个输入文件名替换它
如果收到「Hmm, your system is configured to send core dump notifications to an external utility…」类似的命令,执行以下操作关闭核心转储:
1 | sudo su |
可以看到红色的uniq crashes值,显示找到的唯一崩溃的数量。可以在$HOME/fuzzing_xpdf/out/default/crashes
目录中找到这些崩溃文件。一旦发现第一个崩溃,就可以使用control+c
停止 fuzzer。
什么时候可以停止fuzzer?其中一个指标可以参考cycles done
的数字颜色,依次会出现洋葱红色,黄色,蓝色,绿色,变成绿色时就很难产生新的crash文件了。
将此文件作为输入传递给 pdftotext 二进制文件(如果提示无法打开文件,将文件名称改为xxx.pdf再尝试)
1 | HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/crash.pdf $HOME/fuzzing_xpdf/output |
使用 gdb 找出程序因该输入而崩溃的原因
首先,需要使用调试信息重建 Xpdf 以获得符号堆栈跟踪:
1 | rm -r $HOME/fuzzing_xpdf/install |
现在,可以运行 GDB:
1 | gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/crash.pdf $HOME/fuzzing_xpdf/output |
然后,在 GDB 中输入:
1 | run |
如果一切顺利,应该会看到以下输出:
然后输入bt
以获取栈回溯:bt
- 报错信息
Program received signal SIGSEGV, Segmentation fault
,存在内存泄漏 - 报错位置
_int_malloc (av=av@entry=0x7ffff7c63b80 <main_arena>, bytes=bytes@entry=7) at malloc.c:1210
,glibc报了个错,显然是堆内存出了问题 - 执行流信息,分析一下可以看出调用过程是循环的,判断为无限循环漏洞
- 根据函数调用找到漏洞位置
漏洞修复
下个xpdf4.02源码对比一下就好,修复方式比较简单,加了个变量,记录循环次数,超过一定次数就结束进程。
参考:
[原创]AFL速通——流程及afl-fuzz.c源码简析-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com