AFL forkserver源码阅读
前阵子出于好奇读了部分源码,由于工作原因还没看完,后续有时间把后面的补上
AFL forkserver部分源码阅读
afl-gcc.c(替换as)
主要分为两部分
find_as和edit_params
find_as负责找到afl-as插桩,从三个地方触发,第一个是直接从环境变量中读取u8 *afl_path = getenv("AFL_PATH");
第二个是程序运行时目录寻找dir = ck_strdup(argv0);
如上两种情况都找不到就会从编译时的AFL_PATH中寻找access(AFL_PATH "/as", X_OK)
edit_params则是负责解析命令行参数并进行修改和替换
第一步是确定编译器,目录中存在afl-clang, afl-g++等文件,其实都是个软链接指向了afl-gcc
,afl-gcc会根据命令行文件判断使用哪个编译器,如下
1 |
|
确定完编译器后会进行后续参数的解析和替换,这里会忽略一些参数如-B,这个是默认带的,afl-gcc会使用-B指定afl-as为汇编器对代码进行汇编和插桩,用于统计代码覆盖率。这是整个函数中最关键的一步,其余的基本就是对参数的解析等
1 |
|
afl-as.c(代码插桩)
首先也是edit_params,在afl-gcc中,程序指定了afl-as作为连接器,edit_params用于对afl-gcc调用传入的参数进行解析和修改,后续会调用add_instrumentation来对汇编文件进行插桩。这里比较重要的是设置了修改后的.s文件保存路径
modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),(u32)time(NULL));
最初始的inputfile由afl-gcc传入。参数解析完后会对原始的汇编文件进行插桩,在每个分支都会有如下插桩代码
1 |
|
64位程序如上
其中主要实现位于__afl_maybe_log
,代码类似如下
1 |
|
设计时存在一个右移操作是想维持覆盖率的方向性
cur_location为编译时的随机数,在afl-as.c中体现如下
1 |
|
上述进行了随机数的初始化
每遇到一个分支就会进行代码插入,并把随机数填充
1 |
|
add_instrumentation函数中最主要的插桩代码如下
1 |
|
判断力当前程序架构并插入对应的汇编指令,填充对应的cur_location值。
所有的汇编解析完后,在最后会插入很长的main_payload。里面就包括了上述统计覆盖率的afl_maybe_log 函数和初始化内存的afl_setup函数
afl-as.h(fork server,覆盖率统计实现)
主要讲解main_payload_64
1 |
|
首先判断共享内存区域是否被映射,如果没有被映射则进入__afl_setup函数,否则进入store函数,这里负责使得对应loc的map值加一统计覆盖率。
下面看setup函数
1 |
|
判断之前是否初始化失败,失败直接return,接着判断全局变量值是否为空,非空则直接进行赋值然后跳转至afl_store函数,如果都不满足则进入setup_first函数如下
1 |
|
首先保存环境,push一堆寄存器,随后对rsp进行了16字节对齐操作,作者注释中也写到了,andq $0xfffffffffffffff0, %rsp\n
保证了rsp为16字节对齐,开头的sub rsp也是为了防止覆盖先前压入的寄存器,最原始的rsp被保存在了r12寄存器中。后续就是一些初始化,首先获取环境变量AFL_SHM_ENV然后转为整数后传入shmat将内存映射至当前进程的内存空间然后为afl_area_ptr和afl_global_area_ptr进行赋值。如果中间有失败就会进入afl_setup_abort函数中,可以看到代码如下
1 |
|
对afl_setup_failure值加1然后恢复现场,这也保证了编译出来的程序可以正常运行而不是必须以fuzzer的形式。
最后是forkserver部分如下
1 |
|
首先压栈两次保证16字节对齐,接着向指定的管道FORKSRV_FD + 1
写入四字节magicnumber表示子进程已准备就绪,接着比较返回值是否为4判断是否写入成功,失败则进入afl_fork_resume
函数,否则进入afl_fork_wait_loop
,通过调用read函数来从管道接收消息判断父进程是否返回响应,如果读取失败或者读取字节不足4则进入afl_die
函数。如果成功响应则开始fork。如果fork失败则会进入afl_die,如果为0则代表为子进程进入fork_resume,成功的话会保存子进程pid并写入给父进程,接着调用watpid等待子进程结束。关于afl_resume可以看到是关闭了两个管道并恢复了现场环境然后进入覆盖率统计函数,afl_die则是直接exit。
至此可以大致看到整体的afl框架
1 |
|
这也可以看出来afl比libfuzzer的一个优势,测试用例程序的崩溃并不会影响父进程,都是隔离的,而libfuzzer一旦崩溃就直接退出。