AFL可以对有源码和无源码的程序进行fuzz。对有源码的程序Fuzz的原理,简单来说即是在程序编译时,向汇编代码中插入自己的指令,从而在程序运行时,计算覆盖率。当把样本喂给程序来Fuzz时,如果AFL发现程序执行了新的路径,就把当前的样本保存在Queue中,基于这个新的样本来继续Fuzz。
下载和安装AFL:
安装完成后,默认在/usr/local/bin/目录下,修改.bashrc加入环境变量export PATH=/usr/local/bin:$PATH
后即可使用。
这里以一个demo实践一下afl的用法,首先编写一个需要fuzz的程序,让AFL去fuzz,查看fuzz的效果。源码:
编译代码:
首先验证一下程序的功能完好:打开一个内容为afl!
的文件时,程序退出了。
接下来我们创建in和out目录,在in目录中随机生成一些数据,这些样本用作AFL的输入。
使用AFL开始fuzz,指定输入目录in和输出目录out,@@用于替换输入的参数。
运行后,AFL的fuzz界面如下:
我们可以看到得到了两个crash,内容见下面的图片。
第一个afl!明显符合我们的预期,第二个经过调试得知是由于溢出导致的。(为了不影响行为流畅,分析过程请参考文末栈溢出分析。)
了解了基本用法之后,选择一些实际的,易出crash的软件来fuzz。这里以upx为例。
upx是一个不同可执行文件的打包软件,最新版本截至26 Aug 2018是3.95。
ubuntu源中的upx通过apt-cache showpkg upx
看到版本是3.9.1的,我们直接下载git中的最新版本来安装。
在ubuntu16.04的docker中下载最新版本的和编译安装,步骤如下:
编译和安装的脚本如下:
编译完成后,查看upx的版本如下:
在fuzz之前,为了实现更多的的代码覆盖率,我们需要收集一些不同的binary来放到afl的输入目录。AFL的样本选择对于Fuzz的效果至关重要,如果样本太大会导致AFLfuzz时速度很慢,最好小于1KB。
fuzz之前设置相关的参数:
开始fuzz:
不到半小时不到出了5个crash(后续还出了很多crash,但是经过分析后发现大部分类型相同):
用upx打开发现可以稳定复现:
剩下的几个crash gdb挂上去后,发现都是同一类型的crash。因此挑一个来分析一下。运行后发现存在OOB read:
栈回溯一下看一下调用链:
RSI
的值是哪里来的呢?回溯分析一下上层的函数,发现RSI = R9+R14*4
:
继续看汇编可能有点枯燥了,好在是有源码的,调用链都有了,直接上源码分析吧。
源码在upx/src/p_lx_elf.c
文件中,crash时的调用链如下:PackLinuxElf32help1->elf_lookup->get_te32->getle32,深入的看一下PackLinuxElf32help1和elf_lookup部分的代码来看一下OOB read的原因。
upx程序由于需要将ELF文件压缩,因此程序会将ELF文件映射到内存空间后再解析。在PackLinuxElf32help1函数中获取了dynstr,dynsym,gashtab,hashtab的地址后,调用elf_lookup来查找JNI_OnLoad的符号地址:
在elf_lookup函数源码如下,根据栈回溯信息可以知道crash的点在unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);
。在第二个if判断分支中,n_bitmask是从gashtab中获取的,bitmask是&gashtab+0x10,因此严重怀疑n_bitmask这个从ELF的gashtab中获取的值导致的OOB read,具体调试来看一下。
在jni_onload_sym = elf_lookup("JNI_OnLoad");设置一下断点:
跟进elf_lookup函数,gashtab的地址保存在rdi,是0x7ffff7fd8114。获取bitmask的值,保存在r9中,执行后r9的值是0x7ffff7fd8124:
继续执行我们可以看到获取buckets时,n_bitmask的值为0xe9cd4b0c:
n_bitmask的值之前猜测是从ELF文件中获得的,也就可能是AFL随机生成的。我们在ELF文件中搜索相应的值,可以发现映射到内存空间中的gashtab,验证了我们之前的想法。
继续跟进,执行到源码中计算crash时的参数的地方unsigned const w = get_te32(&bitmask[(n_bitmask -1) & (h>>5)]);
:
计算后,RSI的值就已经OOB的的了(如下图所示),如果继续执行就会发生复现crash时的越界读了,这就不重复粘图片了。
分析出了crash的原因后,发现越界读似乎并不能达到什么效果。首先由于ELF中的gashtab可控导致bitmask可控,以当前程序为例,任意地址读的范围如下:
程序的虚拟地址空间如下。ELF文件被映射到图中0x7ffff7fc9000的区域,那么后面的内容,构造一下gashtab都是可以越界读的。
在源码中,越界读之后的数据还是保存在局部变量中的,读了之后也并不能输出,更不用说越界写和控制PC寄存器了。所以只算是一个DoS吧。
后续我开了几十个AFL来对upx进行fuzz,收获了几百个crash,编写相关的脚本后对crash进行精简后,人工分析得到的crash大多还是不能利用,因此对于upx的fuzz暂时告一段落了。
补充一下第二部分中的栈溢出分析分析过程:
运行程序后,crash在这里:
通过栈回溯信息可以看到,程序发生了栈溢出:
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2019-4-5 21:35
被心许雪编辑
,原因: