AFL-forkserver
forkserver
-
__afl_maybe_log 插装代码
-
__afl_setup
如果没有设置共享内存:如果
__afl_setup_failure
的值不为0(0为正常,非零异常),通过getenv($SHM_ENV_VAR)
环境变量来获得共享内存的ID,如果不为空就调用atoi
以及shmat
,最终将这个地址存储在__afl_area_ptr
中方便之后使用(不必再初始化),下面启动fork_server
。-
__afl_forkserver
-
首先,通过写入状态管道,fork server会通知fuzzer,其已经准备完毕,可以开始fork了
-
接下来,fork server进入等待状态
__afl_fork_wait_loop
,读取命令管道,直到fuzzer通知其开始fork -
一旦fork server接收到fuzzer的信息,便调用
fork()
,得到父进程和子进程:" call fork\n" "\n" " cmpl $0, %eax\n" " jl __afl_die\n" " je __afl_fork_resume\n"
- 子进程是实际执行target的进程,其跳转到
__afl_fork_resume
。在这里会关闭不再需要的管道,并继续执行 - 父进程则仍然作为fork server运行,其会将子进程的pid通过状态管道发送给fuzzer,并等待子进程执行完毕;一旦子进程执行完毕,则再通过状态管道,将其结束状态发送给fuzzer;之后再次进入等待状态
__afl_fork_wait_loop
- 子进程是实际执行target的进程,其跳转到
-
-
-
__afl_store
这里首先把
__afl_prev_loc(之前的位置)
同当前位置的key
异或,保存在edi
寄存器,之后当前的key右移一位,作为下一个的__afl_prev_loc
,这个右移是一个很巧妙的设计,如果代码块的跳转为A->B
或B->A
,直接对两个Key异或结果是一样的,因此右移可以区分出一些特殊情况。下面那个incb代码中edx
为map,edi
为索引,即map表中对应的索引加一,表明一次hit。
Fuzz执行流程总结
第一次的fuzz,核心函数为perform_dry_run
,其功能是将给的所有测试用例跑一遍,确保软件如期工作,故只会跑一遍。需要说明的是这里的文件在初始的时候被抽象到一个自定义的结构体中,且组成了队列。在这个函数中核心的调用为res = calibrate_case(argv, q, use_mem, 0, 1);
,之后对res判断确定程序的运行情况(crash or sth),拿翻译看下,这函数的意思是校准用例,看注释的话意思应该是在输入的时候进行测试,前期发现一些有问题的测试用例。应该一共跑个3次或者8次(取决于是否是快速模式),如果没启动forkserver那么在这用init_forkserver启动。
然后直接就fork出子进程,为其设置一些资源,拷贝文件描述符,设置环境变量等,最终调用execv(target_path, argv);
替换进程去执行Binary,由于execv正常是不会返回的,所以出错后后面会在共享内存那里设置一个EXEC_FAIL_SIG
,通过对于管道文件的读取可以判断forkserver是否正常启动,这块就和之前插桩的代码联系了起来,判断方式是说如果正常操作会有一个4字节的Hello信息,确保启动正常就进入waitpid(forksrv_pid, &status, 0)
的等待,后面对接收到的status进行判断确定子进程的运行状况。
等启动forkserver完毕之后,又一个核心函数出现了,就是这里的run_target,这个函数在之后每次调用新的二进制程序的时候都会使用,其先检查有无启动forkserver,没有的话就先启动一个,否则我们只注重与forkserver的交互即可(使用ctl_fd写,st_fd读),因为 binary那边会一直返回子进程的进程号,所以一直等到结果为-1即可中止,最后拿classify_counts((u32*)trace_bits);将结果进行分类。 has_new_bits判断之前的测试有没有增加新的路径(二元的tuple),逻辑应该就是拿virgin_map这个map同trace_bits进行比较. update_bitmap_score这个函数很有意思,注释里说每当我们发现一个新的路径,都会调用这个函数来判断其是不是更加 favorable,这个favorable的意思是说是否包含最小的路径集合来遍历到所有bitmap中的位,我们专注于这些集合而忽略其他的。核心的比较方式是fav_factor = q->exec_us * q->len;即测试用例执行的时间以及输入长度的乘积,注释说希望找到更快或者规模更小的用例,一旦当前的fav_factor比top_rated[i]要小就会更新这个表,将原来的winner的tc_ref--,当前的插入表中。
-
perform_dry_run
- calibrate_case(三个caller:fuzz_one、perform_dry_run、save_if_interesting)
- init_forkserver
- 调用fork,子进程执行 execv(target_path, argv);
- init_forkserver
- calibrate_case(三个caller:fuzz_one、perform_dry_run、save_if_interesting)
-
cull_queue
下面会调用cull_queue
函数,函数前的注释说我们已经进入了第二个阶段,即用routine
来遍历top_rated entry
,不断寻找之前没见到过的bytes并且将它们标为favored
。函数首先判断sore_changed是不是为真,之后拿贪心算法找能够遍历到所有节点的最小测试集合,比如有三个节点n0,n1,n2,n3和3个测试用例s1,s2,s3。top_rated[0]=s0,top_rated[s2]=s2
且s0覆盖n0,n1;s1覆盖n2
其中初始化temp_v=[1,1,1,1]
,1就表示对应节点没有访问到,初始化为都没访问到。开始先判断temp_v[0]=1
,说明没访问到,之后就去看top_rated[0]
,发现为1,说明存在一个用例能访问到这个n0
,因此进一步查看这个用例,得到其覆盖范围trace_mini=[1,1,0]
故据此更新temp_v=[0,0,1]
,往下看n1,访问过了,再看n2仍未访问到,再去看top_rated得到s2,再看s2的覆盖范围更新temp_v,就这样标注s0和s1为favored,如果他俩还没有被fuzz,还要pending_favored++
。完成上述操作之后将无用的用例标为冗余。
再往后就是一个大的循环,也是AFL最最核心的部分,循环开始依然是用cull_queue
对队列进行筛选,如果一个cycle
都没有新发现尝试更换策略,最终调用skipped_fuzz = fuzz_one(use_argv);
这个函数里对测试用例做了变异,
网友评论