AFL运行的整体流程:

  1. 加载用户提供的初始测试用例到队列中
  2. 从队列中取出1个文件
    1. 对文件进行修剪,在不改变其执行轨迹的前提下将其修剪为最小size
    2. 使用多种变异的方法对当前文件进行变异
    3. 若变异文件产生新的路径了,那么就把它也加入队列中
  3. 重复第2步骤

总结起来:变异生成新输入、运行监控覆盖率

一. 运行监控覆盖率

运行:主要涉及的函数是initforkserver()、run_target()

覆盖率监控:主要涉及trace_bits数组(用于记录一次执行时的运行轨迹)

init_forkserver()

当开启了插桩模式时,会先调用init_forkserver()

说明:当以-n选项启动afl-fuzz时,即不开启插桩模式,一般情况下dumb_mode会被设为1

故当dumbmode!=1(也就是开启了插桩模式的fuzz)时,会去调用init_forkserver()来fork并初始化一个子进程,之后每次运行一个新的输入时,都会去调用这个子进程来运行目标程序。

init_forkserver()函数中大体流程是:

1. 创建2个管道,分别用于 "父进程写 -> 子进程读 (ctl_pipe)"  和  "父进程读 <- 子进程写 (st_pipe)"
2. 调用fork()
3. 子进程:
        将ctl_pipe[0]与某个固定的文件描述符(FORKSRV_FD)绑定(用于读),将st_pipe[1]与FORKSRV_FD+1绑定(用于写)(以后子进程即通过这两个描述符来与父进程读写通信了);
        execv(target_path, argv) 通过execv来创建新进程,覆盖当前子进程,即此时“子进程”执行target_path这个程序:
                在Qemu插桩模式下(即以-Q选项启动afl-fuzz), target_path 为 afl-qemu-trace(利用qemu对二进制程序进行运行时插桩)
                在利用AFL对源码插桩的模式下, target_path 为 插桩后的目标程序
        因此:子进程的主要功能即对单个输入运行目标程序,通过插桩获取执行信息,提供反馈
4. 父进程:主要做些错误处理相关的工作,通过读取子进程返回的状态信息,来确定server是否正常运行起来了

上面第4步说到了父进程通过读取子进程返回的状态信息,从而判断是否正常启动。那么子进程是在什么时候返回信息的呢? ps:这里只以qemu插桩的模式来分析,因为这一模式下是通过对qemu打补丁的形式来实现这些功能的(详见qemu_mode/patches/afl-qemu-cpu-inl.h),所以直接是C程序;而利用对于源码直接插桩的情况,是用汇编实现的(详见afl-as.h文件),不那么容易理解,但两者原理是相同的。

在afl-qemu-cpu-inl.h中:

当程序执行到start处会调用afl_setup()和afl_forkserver()做一些预处理操作,其中afl_forkserver()即上述分析相关:

(1) 先是往FORKSRV_FD+1中写入一个4字节的任意数,以告诉父进程自己正常启动了(上面第4步中父进程即是通过这一信息判断的);

(2) 然后当前server就处于循环等待状态,一旦读取到父进程运行目标程序的通知(在run_target()中发出通知),则会再fork出一个子进程去运行目标程序;

此时server会将子进程的pid发送会其父进程,并当子进程执行完后将子进程的执行状态也返回给父进程。

上面提到了,父进程每次需要运行目标程序时,会调用run_target()函数,在该函数中通知forkserver开始工作。

run_target()

(1) run_target()中执行的操作与上述分析是对应的,先给forkserver发送通知,然后forkserver开始工作(fork出子进程来运行目标程序),最后读取forkserver返回的子进程pid和运行状态status。

至此,运行过程大体讲完了,其中主要的就是上文介绍的父子进程的通信部分。

trace_bits

trace_bits保存一次运行时的执行轨迹,它直接影响了输出目录中的fuzz_bitmap文件

results matching ""

    No results matching ""