afl-fuzz.c细节分析之perform_dry_run函数分析
afl-fuzz.c细节分析之perform_dry_run函数分析
Ivoripuionafl-fuzz.c细节分析之perform_dry_run函数分析
1 | /* |
取队列,并设置cal_failures
为0:
1 | struct queue_entry* q = queue; |
读取环境变量AFL_SKIP_CRASHES
为skip_crashes
:
1 | u8* skip_crashes = getenv("AFL_SKIP_CRASHES"); |
然后是进入队列的循环:
1 | while(q){ |
循环内容如下:
打开当前testcase,并判断是否可读:
1 | fd = open(q->fname, O_RDONLY); //testcase 文件 |
校准testcase:
1 | res = calibrate_case(argv, q, use_mem, 0, 1); |
检查存放q->len
的内存空间use_mem
没有double free以及堆污染等情况:
1 | ck_free(use_mem); |
Ctrl+C就结束:
1 | if (stop_soon) return; |
若校验结果res
为crash_mode
或者FAULT_NOBITS
就打印相关信息:
1 | if (res == crash_mode || res == FAULT_NOBITS) |
根据res
的值对错误进行判断:
1 | switch(res){ |
针对以下几种异常类型进行操作:
- FAULT_NONE:
1 | case FAULT_NONE: |
判断q是不是queue的头结点,是则进行check_map_coverage()
,检查map覆盖率;然后判断是否处于crash_mode,是则打印相关信息。
- FAULT_TMOUT:
若定义了timeout_given
,则进行判断并打印相关信息。
- FAULT_CRASH:
若是crash_mode
,则退出。
若设置了AFL_SKIP_CRASHES
环境变量,则打印警告讯息。
若是mem_limit
,则抛出异常,打出相关讯息。
FAULT_ERROR:打印异常讯息。
FAULT_NOINST:打印异常讯息。
FAULT_NOBITS:
1 | case FAULT_NOBITS:// |
无用路径计数器useless_at_start
加1,并输出警告。
根据计数器cal_failures
来判断dry run到底是否根据预期情况运行:
1 | if (cal_failures) { |
输出dry run成功讯息:
1 | OKF("All test cases processed."); |
calibrate_case函数分析
这个函数是AFL的重点函数之一,用于测试一个样例,在perform_dry_run
,save_if_interesting
,fuzz_one
,pilot_fuzzing
,core_fuzzing
函数中均有调用。
通过检查校验和是否为0检测是否第一次运行testcase:
1 | first_run = (q->exec_cksum == 0); |
根据from_queue
以及resuming_fuzz
判断样例是否为队列中的以及是否刚恢复fuzz,若不是队列中的或者刚恢复fuzz,则给出一个宽限的timeout
:
1 | if (!from_queue || resuming_fuzz) |
设置阶段名称,并根据fast_cal
来设置阶段数目stage_max
,这里的fast_cal
由环境变量AFL_FAST_CAL
给出:
1 | stage_name = "calibration"; |
确保forkserver启动,拷贝trace_bits到first_trace:
1 | if (dumb_mode != 1 && !no_forkserver && !forksrv_pid) |
获取开始时间:
1 | start_us = get_cur_time_us(); |
然后是循环stage_max
次的执行testcase环节。
若不是第一次执行就根据状态刷新频率显示fuzz状态:
1 | if (!first_run && !(stage_cur % stats_update_freq)) show_stats(); |
写入模块化的数据:
1 | write_to_testcase(use_mem, q->len); |
运行testcase,并将结果赋予fault
:
1 | fault = run_target(argv, use_tmout); |
Ctrl+C或者fault
不是crash_mode
则进入abort_calibration
:
1 | if (stop_soon || fault != crash_mode) goto abort_calibration; |
若不处于dumb_mode
且为阶段1且共享内存没有任何路径,则赋failt
为FAULT_NOINST
并进入abort_calibration
:
1 | if (!dumb_mode && !stage_cur && !count_bytes(trace_bits)) { |
根据cksum
来判断stage_max
的循环中每次执行testcase是否会产生新的路径:
1 | cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST); |
if内部结构如下。
首先根据共享内存判断是否有新的路径:
1 | u8 hnb = has_new_bits(virgin_bits); |
若返回值hnb
大于0,则将new_bits
设置为hnb
:
1 | if (hnb > new_bits) new_bits = hnb; |
若当前q->exec_cksum
不为0,即该queue不是第一次执行,则进入判断当前queue的路径是否为可变的循环:
1 | if (q->exec_cksum) { |
这里的判断是否可变的依据就是first_trace[i] != trace_bits[i]
,若可变就将stage_max
增加到CAL_CYCLES_LONG
。
设置路径可变的标记var_detected
设置为1:
1 | var_detected = 1; |
若queue的路径不可变则设置当前queue的校验和为总的cksum
,并拷贝first_trace
至trace_bits
:
1 | q->exec_cksum = cksum; |
计算运行时间和总的阶段数:
1 | stop_us = get_cur_time_us(); |
收集阶段信息:
1 | q->exec_us = (stop_us - start_us) / stage_max; |
后面就是告诉用户检测中没有新bits的情况。
init_forkserver()粗析
通过forkserver执行目标程序,具体原理。
创建两个管道,若创建失败提示信息:
1 | if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed"); |
创建fuzzer的子进程forkserver:
1 | forksrv_pid = fork(); |
然后就是forkserver执行的内容:
1 | if (!forksrv_pid) {//forkserver |
针对OpenBSD更改进程能操控的资源:
能同时打开的文件数量:
1 | if (!getrlimit(RLIMIT_NOFILE, &r) && r.rlim_cur < FORKSRV_FD + 2) { |
其他一些针对OpenBSD的调整:
1 | if (mem_limit) { |
有关异常核心的处理:
1 | r.rlim_max = r.rlim_cur = 0; |
守护forkserver进程:
1 | setsid(); |
重定向/dev/null
到1和2中:
1 | dup2(dev_null_fd, 1); |
如果指定了out_file
则重定向/dev/null
到0,否则重定向out_file
到0:
1 | if (out_file) { |
重定向ctl_pipe[0]
到forkserver,st_pipe[1]
到forkserver子进程:
1 | if (dup2(ctl_pipe[0], FORKSRV_FD) < 0) PFATAL("dup2() failed"); |
关闭相关文件描述符:
1 | close(ctl_pipe[0]); |
有关环境变量LD_BIND_LAZY
以及LD_BIND_NOW
的设置:
1 | if (!getenv("LD_BIND_LAZY")) setenv("LD_BIND_NOW", "1", 0); |
有关ASAN的设置:
1 | setenv("ASAN_OPTIONS", "abort_on_error=1:" |
有关MSAN的设置:
1 | setenv("MSAN_OPTIONS", "exit_code=" STRINGIFY(MSAN_ERROR) ":" |
执行目标程序(forkserver),除非失败不然不会返回:
1 | execv(target_path, argv); |
使用一个标记0xfee1dead
告诉父进程子进程运行失败:
1 | *(u32*)trace_bits = EXEC_FAIL_SIG; |
父进程fuzzer执行的内容:
关闭一些描述符:
1 | close(ctl_pipe[0]); |
等待forkserver启动以及打印相关信息:
1 | /* Wait for the fork server to come up, but don't wait too long. */ |
异常处理:
1 | ...... |
write_to_testcase函数分析
首先判断是否存在了文件,若存在用作fuzz的文件则将其unlink掉,并创建一个新的文件:
1 | if (out_file) { |
将数据写入用于fuzz的文件中:
1 | ck_write(fd, mem, len, out_file); |
修剪文件:
1 | if (!out_file) { |
run_target函数分析
函数会运行目标程序,监控其运行时间,并返回状态信息。调用该函数的程序将更新当前的tuple。
清零共享内存并防止任何其他行为读写该内存:
1 | memset(trace_bits, 0, MAP_SIZE); |
dumb模式下没有forkserver,进行类似init_forkserver
中的处理:
1 | ...... |
若不是dumb模式,并且forkserver开启了,向控制管道写入控制的运行时间:
1 | if ((res = write(fsrv_ctl_fd, &prev_timed_out, 4)) != 4) { |
读取child_pid
:
1 | if ((res = read(fsrv_st_fd, &child_pid, 4)) != 4) { |
forkserver创建失败显示信息:
1 | if (child_pid <= 0) FATAL("Fork server is misbehaving (OOM?)"); |
设置timeout
:
1 | it.it_value.tv_sec = (timeout / 1000); |
分情况保存status
:
1 | if (dumb_mode == 1 || no_forkserver) { |
检查子进程是否正常退出:
1 | if (!WIFSTOPPED(status)) child_pid = 0; |
设置timeout
:
1 | it.it_value.tv_sec = 0; |
区分x86和x86-64设置共享内存:
1 |
|
有关状态的信息设置:
1 | if (WIFSIGNALED(status) && !stop_soon) { |
正常run返回FAULT_NONE
,有异常返回FAULT_CRASH
,dumb mode下有错误返回FAULT_ERROR
。
count_bytes函数分析
用于计算字节的数目。
每次读取mem中的四个字节,通过每个字节与0xff
进行与运算来判断当前字节是否为0x00
,不是则统计数目的变量ret
加1:
1 |
|
has_new_bits函数分析
用于检查当前的运行路径是否为位图带来了新的内容(区分了x86和x86-64的情况)。
若current
不为0且*current & *virgin
不为0则说明发现了新路径或者某条路径的执行次数和之前有所不同:
1 | if (unlikely(*current) && unlikely(*current & *virgin)) |
若继续比较发现ret
小于2,比较当前取出的4个字节,若有不为0则将ret
设置为2:
1 | if (likely(ret < 2)) { |
若virgin_map == virgin_bits
且ret
不为0则设置bitmap_changed
为1,说明bitmap
更新了:
1 | if (ret && virgin_map == virgin_bits) bitmap_changed = 1; |
update_bitmap_score函数分析
通过维护一个top_rated[]
数组来判断当前的路径是否是”favorable”,这个”favorable”的含义就是当前测试用例覆盖了足够多的分支信息,且长度相对较小,运行时间相对较短,这样的测试用例是需要着重fuzz的。
计算测试用例的fav_factor
的值:
1 | u64 fav_factor = q->exec_us * q->len;//执行时间和大小的乘积 |
遍历分支信息记录的数组:
1 | for (i = 0; i < MAP_SIZE; i++){ |
若该分支信息被记录了,就说明这个测试样例覆盖了相同的分支信息,那么就检查该分支信息的top_rate
信息是否记录过,来判断该样例是否为favorable
:
1 | if (trace_bits[i]) { |
若该样例对这个分支信息来说不是更优的就继续判断下一个分支信息:
1 | if (fav_factor > top_rated[i]->exec_us * top_rated[i]->len) continue; |
若当前样例对这条分支信息来说更优,则将被替换的掉的样例的tc_ref
减一,将其trace_mini字段置为空:
1 | if (!--top_rated[i]->tc_ref) { |
将当前样例放入top_rated
数组,并让其tc_ref
加1:
1 | top_rated[i] = q; |
如果当前样例的trace_mini
为空,则调用minimize_bits
将当前样例中记录的累计数据丢弃:
1 | if (!q->trace_mini) { |
trace_mini
的组织方式:trace_mini
的大小为MAP_SIZE / 8
,即每个bit对应了bit_map中的一个byte;如果这个queue访问了bit_map中的一个byte(即访问了一个edge),trace_mini
中对应的bit位就置一。
设置score_changed
为1:
1 | score_changed = 1; |
classify_counts函数分析
fuzzer对分支信息保存方式使用classify_counts
处理,这里仅分析x86的情况。
fuzzer将分支信息根据分支执行次数保存在下面的bucket中:
1 | static const u8 count_class_lookup8[256] = { |
某分支执行了1次,那么落入第2个bucket,其计数byte仍为1;如果某分支执行了4次,那么落入第5个bucket,其计数byte将变为8,等等。
这样就可以相对避免一些因为循环次数的微小区别,而误判为不同执行结果的情况。
参考文章:
http://rk700.github.io/2017/12/28/afl-internals/