AFL分析与实战

文章一开始发表在微信公众号

https://mp.weixin.qq.com/s?__biz=MzUyNzc4Mzk3MQ==&mid=2247486292&idx=1&sn=0e2e298881fcb7a67b170af9dc59e803&chksm=fa7b0a18cd0c830edc734f6cdf08ae4cbf4f66011289cb9d7ed7954d12e4fcbca4da7f443341&scene=21#wechat_redirect

AFL

AFL是Coverage Guided Fuzzer的代表,AFL通过在编译时插桩来获取程序执行的覆盖率,AFL可以获取基本块覆盖率和边覆盖率,下图所示是一个函数的流程图

image-20191026191258513

A, B 是两个基本块, A->B 则是一条边表示程序从A基本块执行到了B基本块,边覆盖率比基本块覆盖率更能表示程序的执行状态,所以一般也推荐使用边覆盖率。

本章最开始提到过Fuzz的速度和样本集是Fuzz测试的两个重要因素,而AFL的实现机制很好的改进了这两个问题。具体而言,AFL的forkserver机制大大提升了Fuzz的测试速度,其覆盖率反馈机制则让AFL能够自动化的生成一个质量比较高的样本集。

下面我们先简单地介绍一下AFL中forkserver的实现机制

image-20191030201136331

AFL通过源码插桩的方式在程序的每个基本块前面插入 _afl_maybe_log 函数,当执行第一个基本块时会启动forkserver,afl-fuzz和forkserver之间通过管道通信,每当afl-fuzz生成一个测试用例,就会通知forkserver去fork一个子进程,然后子进程会从forkserver的位置继续往下执行并处理数据,而forkserver则继续等待afl-fuzz的请求,工作示意图如下:

image-20191030202809008

通过插桩,AFL可以在运行时获取到程序处理每个样本的覆盖率,AFL会把能够产生新用例的路径保存到样本队列中,这样随着Fuzzing的进行,AFL会得到一个质量比较高的样本集。

image-20191030203751711

源码插桩模式

下面介绍如何用AFL来测试libredwg, 首先我们需要用 afl-gcc 把库给编译出来

CC=/path/of/afl/afl-gcc ./configure

如果是c++程序则加上

CXX=/path/of/afl/afl-g++ 

用afl来编译软件时会打印出

image-20191030214627405

编译完后会在src/.libs/目录下生成libredwg.so,然后会在 examples/.libs 生成dwg2svg2。dwg2svg2是一个示例程序用于解析一个dwg文件, dwg2svg2依赖libredwg.so,

~/workplace/libredwg-0.9.2425/examples/.libs$ ldd dwg2svg2 
    linux-vdso.so.1 =>  (0x00007ffe0e7eb000)
    libredwg.so.0 => not found
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f84b6739000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f84b636f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f84b6a42000)

为了能够正常运行该程序,我们先需要设置环境变量,添加库路径到系统搜索路径中

export LD_LIBRARY_PATH=/path/of/libredwg/src/.libs/

当程序能够正常运行后,就可以使用afl-fuzz来开始Fuzz了,Fuzz的命令如下

/path/of/afl/afl-fuzz -i in/ -o out -- ./dwg2svg2 @@

其中

in: 存放初始用例
out: 存放afl测试过程中的输出
@@: 文件路径的占位符,afl fuzz时会用样本的路径替换@@, 被测程序会读取文件然后处理数据

一次Fuzzing的结果如下

image-20191030214125539

二进制插桩

AFL还可以通过qemu在Linux下Fuzz二进制程序,使用qemu执行一个程序时,qemu会从被执行程序的入口点开始对基本块翻译并执行,为了提升效率,qemu还会把翻译出来的基本块存放到 cache 中,当 qemu 要执行一个基本块时首先判断基本块是否在 cache 中,如果在 cache 中则直接执行基本块,否则会翻译基本块并执行。AFL 的 qemu 模式的实现机制是在执行基本块的和翻译基本块的前面增加一些代码来获取代码覆盖率以及启动forkserver。

本节将介绍如何利用AFL qemu模式来Fuzz libredwg这个库,通过查看源码发现了一个数据处理的接口

/** dwg_read_file
 * returns 0 on success.
 *
 * everything in dwg is cleared
 * and then either read from dat, or set to a default.
 */
EXPORT int
dwg_read_file (const char *restrict filename, Dwg_Data *restrict dwg)

这个接口会读取一个dwg文件,然后把解析文件的结果存放到dwg里面。为了Fuzz该接口,首先我们要先用dlopen把libredwg.so这个库加载到内存中,然后通过dlsym获取dwg_read_file函数的地址,最后我们传入文件路径调用目标函数。

typedef int (*dwg_read_file)(char *filename, Dwg_Data *dwg);
dwg_read_file p_dwg_read_file = NULL;

int main(int argc, char** argv)
{
    void *handle;
    Dwg_Data dwg;

    handle = dlopen("./libredwg.so", RTLD_LAZY);
    printf("loader main address:%p\n", main);
    struct link_map *lm = (struct link_map*)handle;
    printf("base:%p\n", lm->l_addr);
    p_dwg_read_file = dlsym(handle, "dwg_read_file");
    printf("p_dwg_read_file:%p\n", p_dwg_read_file);
    
    p_dwg_read_file(argv[1], &dwg);
    
    dlclose(handle);
    return EXIT_SUCCESS;
}

然后我们把程序编出来

gcc parse.c -lpng -L .libs/ -o parse

AFL qemu模式下默认会在可执行程序的入口点出初始化fork server并开始插桩基本块,我们可以通过环境变量来控制AFL的fork server的初始化位置以及基本块插桩的范围。首先我们直接运行AFL来fuzz看看, 执行的命令如下

~/workplace/AFLplusplus/afl-fuzz -Q -i in/ -o output -- ./loader @@

Fuzz一段时间后发现AFL一条新路径也没有发现,这是很不正常的

image-20191101205512162

通过分析AFL的源码,发现是AFL在记录程序的执行路径时,最多只记录MAP_SIZE条边。

unsigned int afl_inst_rms = MAP_SIZE; 

当执行到被测库libredwg.so时,记录的执行边的数量已经达到了最大值,导致libredwg.so里面的执行路径没有被记录,所以程序的每次执行都会得到一样的覆盖率记录,这也就是AFL一直没有发现新路径的原因。为了解决这个问题,我们可以通过环境变量来让AFL只记录libredwg.so里面的执行路径。

export AFL_CODE_START=0x400105a7b0
export AFL_CODE_END=0x400142c38d

AFL_CODE_START和AFL_CODE_END分别表示要插桩的起始位置和结束位置,在这里分别表示libredwg.so模块在内存中代码段的起始地址。为了定位这个地址,首先我们使用afl-qemu-trace来看一下libredwg.so模块和loader可执行文件的加载基地址。

$ ~/workplace/AFLplusplus/afl-qemu-trace ./loader
loader main address:0x400736
base:0x4001019000
p_dwg_read_file:0x40010db5b0

可以看到libredwg.so的加载基地址为0x4001019000,loader可执行文件的加载基地址为0x400000,然后用IDA就可以定位代码段的起始地址了。设置环境变量后再次运行afl-fuzz,可以发现AFL发现很多新路径

image-20191101210900504

不过目前的测试速度很慢,速度慢的主要原因是默认情况下AFL会在程序执行的第一条指令前启动forkserver,这会导致每次Fuzz时程序都需要用dlopen加载libredwg.so,这个过程的开销比较大。为了提升测试速度,我们可以使用AFL_ENTRYPOINT来指定程序初始化forkserver的位置,我们可以设置在dlopen之后才启动forkserver,这样在之后的Fuzz过程中不需要执行dlopen了。一般而言forkserver初始化的位置越后越好,不过forkserver初始化的位置必须要在打开输入文件之前。

.text:00000000004007E0                 call    _printf
.text:00000000004007E5                 mov     rax, cs:p_dwg_read_file
.text:00000000004007EC                 mov     rdx, [rbp+var_FA0]
.text:00000000004007F3                 add     rdx, 8
.text:00000000004007F7                 mov     rdx, [rdx]
.text:00000000004007FA                 lea     rcx, [rbp+var_F80]
.text:0000000000400801                 mov     rsi, rcx
.text:0000000000400804                 mov     rdi, rdx
.text:0000000000400807                 call    rax ; 调用 dwg_read_file 

值得注意的是在qemu中是按控制转移指令来切分基本块,比如call, jmp指令。在IDA中查看汇编代码可以找到0x4007E5是最靠近文件打开函数(dwg_read_file)的基本块,当我们设置AFL_ENTRYPOINT为这个基本块的地址时,在Fuzz开始时会在AFL_ENTRYPOINT启动forkserver,在之后的每次Fuzz时,程序都会从AFL_ENTRYPOINT处开始往下执行,这样AFL_ENTRYPOINT前的dlopen代码就只会执行一次,大大节省了时间,设置环境变量如下

export AFL_ENTRYPOINT=0x4007E5

执行截图如下,可以看到执行速度得到了很大的提升,达到了149.9 次每秒

image-20191113202002668

使用内存磁盘也可以提升速度,Linux下创建内存磁盘的命令如下

sudo mkdir /mnt/ramdisk
sudo mount  -t tmpfs -o size=500m  tmpfs /mnt/ramdisk

执行命令后会在/mnt/ramdisk挂载内存磁盘,把初始数据拷贝到/mnt/ramdisk/in/中后启动AFL即可

~/workplace/AFLplusplus/afl-fuzz -t 200  -Q -i /mnt/ramdisk/in/ -o /mnt/ramdisk/out  -- ./loader @@

image-20191113202906956

posted @ 2021-01-14 19:38  hac425  阅读(1307)  评论(1编辑  收藏  举报