介绍
模糊测试是自动软件测试中最常用的一种方法。通过模糊测试,我们可以根据一组规则来生成大量的输入,然后将这些非预期的输入注入到程序中来观察程序的异常行为。在安全领域,模糊测试是一种发现漏洞的有效方法。
市场上现在有很多模糊测试框架,包括开源的框架和商业框架。模糊测试技术主要分为两类:
基于演化的模糊测试
它们使用遗传算法来增加代码覆盖率。它们会对提供的测试用例进行修改,目的是进一步的分析应用程序。不过坦白说,这需要进行一些代码插桩来给变化引擎提供反馈。基于演化的模糊测试其实就是忽视规定的输入格式,输入各种各样的格式来查看是否有异常并研究这个异常。这种技术在开源社区中有很好的支持和维护。有几个框架非常优秀,比如American Fuzzy Lop(AFL),libfuzzer和honggfuzz等。
基于世代的模糊测试
与基于演化的模糊测试相反,它们会基于与上下文环境相关的规则和格式来构建输入。这类技术中比较优秀的工具包括商业的Defensics和PeachFuzzer,开源的包括Peach,Spike和Sulley。
这两类技术并不是对立的,也不是相互排斥的。它们只是在设计模式上有所区别。有的工具同时包含了这两种技术,比如PeachFuzzer。
在应用和威胁情报(ATI)研究中心,我们的目标之一就是识别应用程序中的漏洞,并在漏洞被发现之前协助开发者修复漏洞。我们是通过将不同的应用程连接到我们的模糊测试框架来实现的。本文我们将会向大家展示我们在安全研究中是如何使用fuzzing技术的,主要是着重讲解我们在研究开源库过程中发现的一些漏洞来展示。
对SDL库进行模糊测试
SDL库是一个跨平台的库,提供了实现多媒体软件如游戏和模拟器的API。该库是由C语言编写的,由社区积极维护和使用。
选择模糊测试框架
本文我们将使用著名的AFL测试框架(lcamtuf编写)来对SDL库进行模糊测试。AFL使用运行时引导技术,编译时代码插桩和遗传算法来构造大量的非预期输入来测试应用程序。这个框架曾经因为发现重大漏洞而获奖,这也是它被公认为是最好的模糊测试框架之一的原因。于是一些研究员就开始仔细研究AFL框架,并且编写了修改某些组件行为的扩展,例如异常策略和不同代码分支的重要性。这些扩展又产生了其他的项目,比如FairFuzz,AFL-GO,afl-unicorn,AFLSmart和python-AFL。
我们这里将使用AFLFast,这个项目实现了一些模糊测试策略,不仅针对高频代码路径,也针对低频代码路径,“在相同的时间内,更加强调程序的行为”。简而言之,通过研究我们发现,对于某种模糊测试对比,跟vanilla AFL相比,这种优化方法大约提高了2倍的速度和更好的代码覆盖率。
模糊测试准备
要使用AFL,我们必须要用AFL的编译器包装器来编译库的源代码。
$ ./configure CC=afl-clang-fast \
CFLAGS ="-O2 -D_FORTIFY_SOURCE=0 -fsanitize=address" \
LDFLAGS="-O2 -D_FORTIFY_SOURCE=0 -fsanitize=address"
$ make; sudo make install
如上所示,我们会同时使用AFL插桩和ASAN(地址净化器)编译工具,ASAN编译工具是用来识别内存相关的报错的。GitHub上关于ASAN的项目中提到,ASAN为已测试过程序的执行速度添加了2倍的减速,但是效果却更好,让我们可以检测到与内存相关的问题,如:
· UAF漏洞(悬空指针取消引用)
· 堆缓冲区溢出
· 栈缓冲区溢出
· 全局缓冲区溢出
· Use After Return(返回后使用)
· Use After Scope(范围后使用)
· 初始化命令错误
· 内存泄露
此外,为了优化模糊测试过程,我们使用下面这些参数编译了源代码:
-D_FORTIFY_SOURCE=0
因为ASAN不支持源代码强化,所以我们禁用它来避免错误警告
-O2
使用-O参数,开启所有的优化标志;对于LLVM3.6而言,默认设置是-O1
$ checksec /usr/local/lib/libSDL-1.2.so.0
[*] '/usr/local/lib/libSDL-1.2.so.0'
Arch: amd64-64-little
RELRO: No RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
ASAN: Enabled
Checksec是一个不错的工具,我们可以用它来检测二进制文件的安全选项,比如二进制文件是使用非可执行堆栈(NX)构建的,还是使用重定位表作为只读(RELRO)构建的。它还可以检测二进制文件是否使用ASAN插桩构建,这也是我们需要的。这个工具是pwntools Python包的一部分。如上所示,二进制文件是启用了ASAN插桩进行编译的,现在我们来继续编译!
编写测试工具
AFL模糊测试操作包括三个主要步骤:
· 第一步:fork一个新的进程
· 第二步:输入由变化引擎修改过的内容
· 第三步:通过跟踪使用此输入最终达到了哪些路径来监控代码覆盖率,这样我们就能得知程序是否崩溃或者挂起了。
这是由AFL自动完成的,对于那些接受输入作为参数并进行解析的二进制文件,AFL就是一款非常理想的工具。但对于测试库文件而言,我们需要先编写一款测试工具并进行编译。在这里,我们的测试工具就是一个简单的C程序,可以使用库中的某些方法,让你可以对它进行间接的模糊测试。
#include <stdlib.h>
#include "SDL_config.h"
#include "SDL.h"
struct {
SDL_AudioSpec spec;
Uint8 *sound; /* Pointer to wave data */
Uint32 soundlen; /* Length of wave data */
int soundpos; /* Current play position */
} wave;
/* Call this instead of exit(), to clean up SDL. */
static void quit(int rc){
SDL_Quit();
exit(rc);
}
int main(int argc, char *argv[]){
/* Load the SDL library */
if ( SDL_Init(SDL_INIT_AUDIO) < 0 ) {
fprintf(stderr, "[-] Couldn't initialize SDL: %s\n",SDL_GetError());
return(1);
}
if ( argv[1] == NULL ) {
fprintf(stderr, "[-] No input supplied.\n");
}
/* Load the wave file */
if ( SDL_LoadWAV(argv[1], &wave.spec, &wave.sound, &wave.soundlen) == NULL ) {
fprintf(stderr, "Couldn't load %s: %s\n",
argv[1], SDL_GetError());
quit(1);
}
/* Free up the memory */
SDL_FreeWAV(wave.sound);
SDL_Quit();
return(0);
}
这里我们的目的是初始化SDL环境,然后对与SDL音频模块相关的SDL_LoadWAV方法进行模糊测试。我们需要提供一个WAV示例文件,AFL将使用变化引擎来尽可能的篡改库代码。引入一些新的模糊测试术语,该文件代表着我们的初始化种子,将会放在corpus_wav文件夹中。
现在我们来进行编译:
$ afl-clang-fast -o harness_sdl harness_sdl.c -g -O2 \
-D_FORTIFY_SOURCE=0 -fsanitize=address \
-I/usr/local/include/SDL -D_GNU_SOURCE=1 -D_REENTRANT \
-L/usr/local/lib -Wl,-rpath,/usr/local/lib -lSDL -lX11 -lpthread
然后开始模糊测试:
$ afl-fuzz -i corpus_wave/ -o output_wave -m none -M fuzzer_1_SDL_sound \
-- /home/radu/apps/sdl_player_lib/harness_sdl @@
可以看到,启动模糊测试工作十分简单,只需要使用下列参数来执行afl-fuzz:
· 初始语料库(-i corpus_wave)
· 模糊测试输出(-o output_wave)
· 编译工具路径
· 提示AFL如何发送测试样例到模糊测试程序中(提供它来作为参数)
· 子进程内存限制(-m none 因为在x86_64系统架构中,ASAN需要接近20TB的内存)
你还可以使用其他有用的参数,例如指定字典,包含与特定文件格式相关的字符串,理论上可以帮助变化引擎更快的到达某些路径。不过这里,我们就暂时先使用这些参数,来看看是如何执行的:
我们这里是在一台RAM为32G的机器上进行研究测试,该机器有2个AMD Opteron CPU,每个CPU每个插槽有4个内核,每个内核有2个线程,共有16个线程。我们可以看到,我们的模糊测试速度是每秒获得170个评估样本。我们是否能做得更好?
优化配置来提升模糊测试速度
有一些地方我们可以进行一些调增:
默认情况下,AFL在每次测试不同的输入时都会fork出一个进程,我们可以控制AFL在单个程序实例中运行多次模糊测试,不用为了每个测试样本,都将程序恢复到原始状态。这将减少内核空间中所花费的时间,从而提升模糊测试速度。这被称为AFL_PERSISTENT模式。我们可以使用测试工具加_AFL_LOOP(1000) macro参数来实现。根据这一点,指定macro,将强制AFL运行1000次,使用1000个不同的输入提供给库。之后,AFL重新启动该进程,这确保我们在定期更换过程中避免出现内存泄漏。
指定为初始语料库的测试用例大小为119KB,这个太大了,也许我们能找到一个更小的测试用例,或者提供更多测试用例,以提高初始代码覆盖率。
我们现在是在硬盘中运行模糊测试,如果我们切换到ramdisk,强制模糊测试器直接从RAM中获取测试用例,这样也能提升模糊测试速度。
最后一点,我们可以并行运行多个实例,强制AFL为每个测试实例使用1个CPU。
做了这些修改之后,来看看我们的模糊测试器如何执行,如图:
对于一个实例,速度提升了2.4倍,而且已经崩溃了。运行一个主实例和4个从属实例,状态如下:
$ afl-whatsup -s output_wave/
status check tool for afl-fuzz by <lcamtuf@google.com>
Summary stats
=============
Fuzzers alive : 5
Total run time : 0 days, 0 hours
Total execs : 0 million
Cumulative speed : 1587 execs/sec
Pending paths : 6 faves, 35 total
Pending per fuzzer : 1 faves, 7 total (on average)
Crashes found : 22 locally unique
同时运行5个fuzzers,每秒有超过1500次的执行,这个速度已经相当不错了,我们来看下它们是如何运行的,如图:
总结
经过一天的模糊测试后,我们总共发生了60次特殊的崩溃事件。对它们进行分类,我们总结出了值得注意的12种崩溃事件,并且已经提交到SDL社区和MITRE。实际上,这些提交的漏洞也获得了很多CVE编号,比如CVE-2019-7572,CVE-2019-7573,CVE-2019-7574,CVE-2019-7575,CVE-2019-7576,CVE-2019-7577,CVE-2019-7578,CVE-2019-7635,CVE-2019-7636,CVE-2019-7637,CVE-2019-7638。库的维护者也承认在最新的2.0.9版本中确实存在上述这些漏洞。只是为了强调有的漏洞能够隐藏很多年,这次提交的漏洞中,有几个是因为2006年所提交的一个漏洞所引入的,一直都没人发现,直到现在。
本文翻译自:https://www.ixiacom.com/company/blog/how-use-fuzzing-security-research
翻译作者:SoftNight 原文地址:https://www.4hou.com/web/16392.html