以使用afl-gcc进行插桩后的AFL运行为例。我们来学习AFL的运行。main函数中首先识别参数opt = getopt(argc, argv,"+i: o: f: m: t: T: d: n: C: B: S:M: x: Q")
。然后在setup_shm()
这个函数中初始化MAP_SIZE(2^16)大小的map用来父进程子进程间的信息共享。然后perform_dry_run()
这个函数把初始的种子跑一遍,观察是否有问题。
先补充一些知识:
fd文件(File descriptor)
所有都可以抽象成文件,比如普通的文件、目录、块设备、字符设备、socket、管道等等。当通过一些系统调用(如open/socket等),会返回一个fd(就是一个数字)给你,然后根据这个fd对应的文件进行操作,比如读、写。linux默认对每个进程最大能打开的fd的个数是1024(软限制是1024,硬限制4096)。
管道PIPE
管道初始化函数 int pipe(int pipefd[2]); 成功:0;失败:-1
fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。在后面AFL的forkserer中父进程和子进程交互就使用了st_pipe[2],ctl_pipe[2]。
这里介绍几个很重要的数据结构和函数:
cull_queue()
种子的数据结构为:
struct queue_entry {
u8* fname; /* File name for the test case */
u32 len; /* Input length */
u8 cal_failed, /* Calibration failed? */
trim_done, /* Trimmed? */
was_fuzzed, /* Had any fuzzing done yet? */
passed_det, /* Deterministic stages passed? */
has_new_cov, /* Triggers new coverage? */
var_behavior, /* Variable behavior? */
favored, /* Currently favored? */
fs_redundant; /* Marked as redundant in the fs? */
u32 bitmap_size, /* Number of bits set in bitmap */
exec_cksum; /* Checksum of the execution trace */
u64 exec_us, /* Execution time (us) */
handicap, /* Number of queue cycles behind */
depth; /* Path depth */
u8* trace_mini; /* Trace bytes, if kept */
u32 tc_ref; /* Trace bytes ref count */
struct queue_entry *next, /* Next element, if any */
*next_100; /* 100 elements ahead */
};
我们知道种子是一系列的,每次通过程序运行该种子的时间、是否产生新路径等信息对种子进行打分排序,对一些没有贡献的种子,pass_det这一项就置为表示循环fuzz的时候将跳过该种子。
run__target()
不管是在一开始的perform_dry_run()还是后面测试过程中一直循环的fuzz_one(),这个是运行插桩程序的函数。在里面先是查询有没有forkserver这么一个子进程,如果没有就建立一个forkserver,同时建立好管道。
在init_forkserver这个环节,会生成两个管道ctl_pipe, st_pipe,分别绑定fd 198,fd 199.然后开始运行程序并且进程间通信。一旦init了child进程就不会退出,每次有新的测试样例,就直接运行,运行的时候child进程fork一个grandchild进程,这个进程一直运行_afl_store()这个函数对shm的bitmap进行修改。
如果是第一次运行(一般在perform_dry_run()这个函数中),就会创建一个子进程。子进程会一直用testcase这个缓冲区中的种子运行程序,通过管道把执行路径交互给父进程。交互过程如下:
每次运行run_target()函数都会在ctl_pipe写入值,唤醒卡死在read等待的子进程,子进程就可以fork一个孙进程,孙进程执行程序,把所有程序块中插桩的部分执行一遍,把结果写入共享的内存区,就实现了执行路径的记录。
cur_location = ;
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;
这里的cur_location和prev_location都是程序块的id,这里使用[cur_location ^ prev_location]意为记录边,因为如果一个程序有a、b、c三个基本块,a->b->c 和a->c->b是不同的执行路径,但是只用基本块记录的话都是a、b、c,用边记录的话可以跟好比对运行路径。
关于种子变异,这个是AFL的确定性变异(位翻转、拼接等)和非确定性变异组合而成,在afl-fuzz.c中可以更好学习,就不在这里赘述。
就先写到这里,大家对AFL还有什么疑问,可以留言。