AFL源码部分解析

fuzzy

afl-as

add_instrumentation()

在实际调用as前进行add instrumentation,也就是插桩。

通过识别如下前导标签后,紧接着进行插桩 (只在.text段进行插桩):

  • main: - function entry point (always instrumented)
  • .L0: - GCC branch label
  • .LBB0_0: - clang branch label (only in clang mode)
  • \tjnz foo - conditional branches

如下为x64、x86桩代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const u8* trampoline_fmt_64 =

"\n"
"/* --- AFL TRAMPOLINE (64-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leaq -(128+24)(%%rsp), %%rsp\n"
"movq %%rdx, 0(%%rsp)\n"
"movq %%rcx, 8(%%rsp)\n"
"movq %%rax, 16(%%rsp)\n"
"movq $0x%08x, %%rcx\n"
"call __afl_maybe_log\n"
"movq 16(%%rsp), %%rax\n"
"movq 8(%%rsp), %%rcx\n"
"movq 0(%%rsp), %%rdx\n"
"leaq (128+24)(%%rsp), %%rsp\n"
"\n"
"/* --- END --- */\n"
"\n";
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static const u8* trampoline_fmt_32 =

"\n"
"/* --- AFL TRAMPOLINE (32-BIT) --- */\n"
"\n"
".align 4\n"
"\n"
"leal -16(%%esp), %%esp\n"
"movl %%edi, 0(%%esp)\n"
"movl %%edx, 4(%%esp)\n"
"movl %%ecx, 8(%%esp)\n"
"movl %%eax, 12(%%esp)\n"
"movl $0x%08x, %%ecx\n"
"call __afl_maybe_log\n"
"movl 12(%%esp), %%eax\n"
"movl 8(%%esp), %%ecx\n"
"movl 4(%%esp), %%edx\n"
"movl 0(%%esp), %%edi\n"
"leal 16(%%esp), %%esp\n"
"\n"
"/* --- END --- */\n"
"\n";

最后再插入main_payload_32main_payload_64,实际上就是写入__afl_maybe_log等一系列正式fuzz需要使用的函数和变量的汇编,,之后会详细解读。

main()

设置随机种子值为tv.tv_sec ^ tv.tv_usec ^ getpid(),再设置好真正as的参数。当开启ASANMSAN时,插桩密度inst_ratio设置为%33左右,初始为%100。然后插桩,fork子进程用来实现真正的as,等待子进程结束后,unlink掉插桩完的文件,然后退出。

afl-clang-fast

afl-llvm-pass

llvm是用来进行代码优化,提供了IR,通过对IR操作,进行优化。对IR操作其实也就是插入桩代码,只不过afl-as是对汇编进行操作,而llvm是对IR操作。llvm提供模块化、标准化的一系列IR API,将程序分为Module-Function-BasicBlock-Instruction-Operand&Opcode,层次结构清晰,可以直接遍历每个基本块方便地插桩。

插入的IR Instruction效果如下:

1
2
3
cur_location = <COMPILE_TIME_RANDOM>; 
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;

afl-llvm-rt

LLVM_MODE有额外三种模式分别是deffered instrumentationpersistent modetrace-pc-guard

deferred instrumentation

推迟forkserver的启动,启动时机为程序初始化工作后,但是在main函数之前。这样减少了初始化的开销或者程序解析fuzz data的开销

选好合适的位置,插入下面的代码,即可启用该模式

1
2
3
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif

__AFL_INIT调用__afl_manual_init

1
2
3
4
5
6
7
8
9
10
11
12
13
void __afl_manual_init(void) {

static u8 init_done;

if (!init_done) {

__afl_map_shm();
__afl_start_forkserver();
init_done = 1;

}

}

__afl_map_shm函数读取环境变量 "__AFL_SHM_ID"来获得shm_id,然后调用shmat获取共享内存赋值给_afl_area_ptr

__afl_start_forkserver逻辑如下:

  • 初始化child_stopped为0,通过FORKSRV_FD + 1fd向管道写入4个字节,告知afl fuzz fork server已经准备好开始了。
  • 进入fuzz loop
    • 通过FORKSRV_FDfd向管道读入4个字节赋值给was_killed
    • 如果child_stoppedwas_killed都不为空值,说明子进程停下了并且发出SIGKILL信号,那么通过waitpid函数回收子进程
    • 如果child_stopped为空值,那么fork一个子进程,并且关闭子进程与管道的通信,然后子进程退出__afl_start_forkserver函数,执行程序
    • 如果child_stopped不为空值,通过kill(child_pid, SIGCONT)重启子进程,并设置child_stopped为0。该分支仅针对persistent mode,因为只有该模式不fork完成fuzz,而是复用同一个进程。
    • 再通过FORKSRV_FD + 1fd向管道写入child_pid,告知afl fuzz子进程的pid。
    • 等待子进程结束,回收子进程,状态保存在status。如果为persistent mode,那么程序发出SIGSTOP信号,便结束等待,并且将child_stopped设置为1。
    • 通过FORKSRV_FD + 1fd向管道写入status,告知afl fuzz子进程的状态,并结束此次fuzz

persistent mode

当程序的某些API是无状态的或者读入不同的输入数据时,API的状态会自动重置,那么这个程序就可以长期存活复用,减少了fork的开销。

通过向程序插入如下代码:

1
2
3
4
5
6
7
while (__AFL_LOOP(1000)) {

/* Read input data. */
/* Call library code to be fuzzed. */
/* Reset state. */

}

__AFL_LOOP调用__afl_persistent_loop,逻辑如下:

  • 第一次loop时,如果is_persistent为1,清空__afl_area_pptr,将__afl_area_ptr[0]设置为1,__afl_prev_loc设置为0,cyclic_cnt设置为max_cnt,也就是循环次数。
  • 不是第一次的loop--cyclic_cnt,发出信号SIGSTOP,程序暂停,直到程序被重启,将__afl_area_ptr[0]设置为1,__afl_prev_loc设置为0。
    • cyclic_cnt为0时,也就是结束了所有fuzz时,将__afl_area_ptr指向__afl_area_initial,也就是无用数组,不再记录覆盖率等数据。

trace-pc-guard

该模式不是通过基本块,而是边来插桩。要使用这个模式需要重新编译LLVM_MODE,通过

1
AFL_TRACE_PC=1 make clean all

从而定义了DUSE_TRACE_PC宏,从而在执行afl-clang-fast传入 -fsanitize-coverage=trace-pc-guard参数

1
2
3
ifdef AFL_TRACE_PC
CFLAGS += -DUSE_TRACE_PC=1
endif

__sanitizer_cov_trace_pc_guard

这个函数在经过每条edge都会调用

1
2
3
void __sanitizer_cov_trace_pc_guard(uint32_t* guard) {
__afl_area_ptr[*guard]++;
}

通过guard指向的值确定共享内存对应的索引

__sanitizer_cov_trace_pc_guard_init

该函数来初始化guard指向的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void __sanitizer_cov_trace_pc_guard_init(uint32_t* start, uint32_t* stop) {

u32 inst_ratio = 100;
u8* x;

if (start == stop || *start) return;

x = getenv("AFL_INST_RATIO");
if (x) inst_ratio = atoi(x);

if (!inst_ratio || inst_ratio > 100) {
fprintf(stderr, "[-] ERROR: Invalid AFL_INST_RATIO (must be 1-100).\n");
abort();
}

/* Make sure that the first element in the range is always set - we use that
to avoid duplicate calls (which can happen as an artifact of the underlying
implementation in LLVM). */

*(start++) = R(MAP_SIZE - 1) + 1;

while (start < stop) {

if (R(100) < inst_ratio) *start = R(MAP_SIZE - 1) + 1;
else *start = 0;

start++;

}

}

afl-fuzz

setup_shm

  • 如果in_bitmap为空,则将virgin_bits初始化每bit都为1。
  • 初始化virgin_tmout、virgin_crashbit为1。
  • 通过shmget开辟一块读写共享内存,返回该内存块对应的共享标识符到shm_id。注册atexit handlerremove_shm,用来当程序结束时,删除共享内存块。
  • 如果dumb_mode为空,则设置SHM_ENV_VARshm_id
  • 通过shmat获取对该共享内存的访问,并将共享内存地址赋值给trace_bits

maybe_add_auto

  • 如果设置MAX_AUTO_EXTRAS或者USE_AUTO_EXTRAS为0,则直接返回

  • for i = 1 to len,如果mem[0]mem[i]不同,则跳出循环。

  • 如果i等于len,也就是mem里的字节值都和第一个字节值相等,则直接返回。

  • 如果len等于2,就和interesting_16数组里的元素比较,有相同就直接返回。

  • 如果len等于4,就和interesting_32数组里的元素比较,有相同就直接返回。

  • for i = 0 to extra_cnt,如果extras[i].lenlen就跳出循环,因为extras数组按照长度排序。

  • 继续遍历i,如果i<extras_cnt并且extras[i].len==len,用memcmp_nocase(extras[i].data, mem, len),如果这两个内容是一样的,则直接返回。

  • auto_changed置为1。

  • for i = 0 to a_extras_cnt

    • 如果a_extras[i].len等于len并且a_extras[i].datamem内容相同
      • a_extras[i].hit_cnt++,跳转到sort_a_extras
  • 如果a_extras_cnt小于MAX_AUTO_EXTRAS,说明a_extras数组没被填满,则用memlen构造新的元素放入这个数组。

  • 否则,从a_extras数组后半段随机挑选个元素,将它的data替换为ck_memdup(mem, len),长度替换为lenhit_cnt置为0。

  • sort_a_extras

    • 先用qsort(a_extras, a_extras_cnt, sizeof(struct extra_data), compare_extras_use_d);,按照hit_cnt递减排序。
    • qsort(a_extras, MIN(USE_AUTO_EXTRAS, a_extras_cnt),sizeof(struct extra_data), compare_extras_len); ,按照len递增排序。

perform_dry_run

执行所有测试用例,检查是否正常工作

  • 打开q->fname,并读取内容到use_mem
  • res = calibrate_case(argv, q, use_mem, 0, 1)校准测试用例
  • 如果rescrash_modeFAULT_NOBITS,则输出SAYF(cGRA " len = %u, map size = %u, execspeed = %llu us\n" cRST, q->len, q->bitmap_size, q->exec_us);
  • 根据res判断错误结果:
    • FAULT_NONE
      • 如果q是第一个测试用例,则调用check_map_coverage,评估map coverage
        • 如果trace_bits路径数小于100,则直接返回
        • trace_bits数组中后半段有值,则直接返回
    • FAULT_TMOUT:
      • 如果指定了-t参数,则timeout_given参数设置为2,则警告Test case results in a timeout (skipping)q->cal_failed设置为CAL_CHANCEScal_failures计数++。
    • FAULT_CRASH:
      • 抛出异常FATAL("Test case '%s' results in a crash", fn);
    • FAULT_ERROR:
      • 抛出异常FATAL("Unable to execute target application ('%s')", argv[0]);
    • FAULT_NOINST
      • 抛出异常FATAL("No instrumentation detected");
    • FAULT_NOBITS:
      • 没有出现路径信息,说明这个测试样例是无用的。useless_at_start++
  • 如果q->var_behavior不为空,则说明该测试用例在相同条件下,多次执行,有不同的路径记录,抛出警告WARNF("Instrumentation output varies across runs.");
  • q = q->next,执行下个测试样例。

calibrate_case

  • 如果q->exec_cksum == 0说明这个case是第一次运行,设置first_run为1。

  • q->cal-failed++

  • 设置stage_namecalibration,并且如果fast_cal不为0,则设置stage_max为3否则为8。stage_max是最大校验次数。

  • dumb_mode != 1 && !no_forkserver && !forksrv_pid,调用init_forserver(argv)。也就是不处于dumb_mode并且no_forkserver为空,还未启动forkserver,就初始化forkserver

  • 如果这个queue_entry不来自input文件夹,而是评估新case,则此时q->exec_cksum不为0,拷贝trace_bitsfirst_trace,并计算has_new_bits,如果这个值比new_bits大,则更新new_bits为这个值。

  • 执行calibrate stage,共stage_max

    • 如果不是第一次执行,就调用show_status
    • 调用run_target(argv, use_tmout)将结果赋值给fault
    • 如果stop_soon不为空或者fault != crash_mode,就跳到abort_calibration标签处。
    • 如果不处于dumb_mode并且处于第一次循环,并且count_bytes(trace_bits)为0,也就是没有路径记录,则设置faultFAULT_NOINST,跳到abort_calibration标签处。
    • 根据当前的trace_bits通过hash32(trace_bits, MAP_SIZE, HASH_CONST)函数计算赋值给cksum
    • 如果q->exec_cksum != cksum说明这个case第一次执行或者这次执行和上次执行的路径不同,则调用has_new_bits(virgin_bits)计算,计算的值如果大于new_bits,则更新new_bits
      • 如果q->exec_cksum不为0,说明不是第一次执行,说明执行触发的路径可变,则遍历整个shm,如果!var_bytes[i] && first_trace[i] != trace_bits[i],则说明当前索引的路径可变,设置var_bytes[i]为1,将stage_max更新为40。将var_detected设置为1。
      • 否则就是第一次执行,将q->exec_cksum设置为cksum,将trace_bits当前的路径记录拷贝给first_trace
  • 将执行calibrate stage的总时间加到total_cal_us

  • 将执行总次数stage_max加到total_cal_cycles

  • q->exec_us更新为执行一次的平均时间。

  • q->bitmap_size更新为覆盖的路径数。

  • q->cal_failed设置为0。

  • total_bitmap_size加上q->bitmap_size

  • total_bitmap_entries++

  • update_bitmap_score(q)

  • 如果!dumb_mode && first_run && !fault && !new_bits,则设置faultFAULT_NOBITS

  • abort_calibration

    • 如果new_bits == 2 && !q->has_new_cov,说明当前queue发现了新路径,设置q->has_new_cov为1,并且queue_with_cov++
    • 如果var_detected不为0,var_byte_count赋值为count_bytes(var_bytes),如果q->var_behavior为0,调用mark_as_variable(q),将当前的queueout_dir/queue/.state/variable_behavior/filename创建符号链接,并且设置q->var_behavior为1。queued_variable++
  • 返回fault

init_forkserver

  • 通过pipe函数创建st_pipe状态管道、ctl_pipe控制管道。

  • fork进程赋值给forksrv_pid

  • 当前进程为子进程

    • 重定向标准输出、标准错误到dev_null_ld。如果设置了out_file则重定向标准输入到dev_null_fd,否则重定向到out_fd,并且关闭out_fd

    • 重定向FORKSRV_FDctl_pipe[0],即通过FORKSRV_FD来读取控制管道。

    • 重定向FORKSRV_FD + 1st_pipe[1],即通过FORKSRV_FD + 1来写入状态管道。

    • 关闭不必要的文件描述符。

      • close(ctl_pipe[0]);
        close(ctl_pipe[1]);
        close(st_pipe[0]);
        close(st_pipe[1]);
        
        close(out_dir_fd);
        close(dev_null_fd);
        close(dev_urandom_fd);
        close(fileno(plot_file));
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30
        31
        32
        33
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60

        - 如果`LD_BIND_LAZY`环境变量为空,则设置`LD_BIND_NOW`环境变量为1。

        - 设置`ASAN_OPTIONS`环境变量为`abort_on_error=1:` `detect_leaks=0:` `symbolize=0:` `allocator_may_return_null=1`,同理设置`MSAN_OPTIONS`。

        - 调用`execv(target_path, argv)`,替换进程空间,执行`target_path`。

        - 如果调用失败,则将`*(u32 *)tarce_bits`设置为`0xfee1dead`,并退出。

        - 当前进程为父进程

        - 关闭`ctl_pipe[0]` `st_pipe[1]`。
        - 设置`fsrv_ctl_fd`为`ctl_pipe[1]`,设置`fsrv_st_fd`为`st_pipe[0]`。
        - 等待`forkserver`启动,从`fsrv_st_fd`读取4个字节到`status`。
        - 如果读取长度为4个字节,说明启动成功,返回。

        ## has_new_bits

        - 将`trace_bits`的首`u64`元素地址赋值给`current`,`virgin_map`的首`u64`地址赋值给`virgin`。
        - 将`i`赋值为`MAP_SIZE >> 3`,因为**每8个字节遍历`MAP`**。
        - 进入while循环遍历map
        - 如果`*current`不为0且`*current & *virgin`不为0,说明发现覆盖了新路径或者某条路径的执行次数发生了变化
        - 如果`ret`小于2
        - `cur`赋值为`(u8 *)current`,`vir`赋值为`(u8 *)virgin`。
        - 在这8个字节中,如果有一个字节`*cur`不为0且`*vir == 0xff`,即发现覆盖了新路径,设置`ret`为2,否则为1。
        - `*virgin &= ~*current`
        - 如果`ret`不为0且`virgin_map == virgin_bits`,那么`bitmap_changed`设置为1。
        - 返回`ret`。

        ## count_bytes

        - 设置`ptr = (u32 *)mem`。
        - `i == (MAP_SIZE >> 2)`,`ret = 0`。
        - 进入while循环,每四个字节遍历
        - `v = *(ptr)`
        - 如果`v`为0,即当前4个字节没有路径覆盖,直接`continue`。
        - 否则每个字节如果不为0,都会`ret++`。
        - 返回`ret`。

        ## run_target

        - 初始化`trace_bits`,全部置为0。

        - 如果`dumb_mode == 1`或者`no_forkserver`不为0

        - `fork`进程赋值给`child_pid`。

        - 当前进程为子进程:

        - 重定向标准输出、标准错误到`dev_null_fd`。

        - 如果`out_file`不为空,则重定向标准输入到`dev_null_fd`。否则,重定向标准输入到`out_fd`,关闭`out_fd`。

        - 关闭不需要的`fd`:

        - ```c++
        close(dev_null_fd);
        close(out_dir_fd);
        close(dev_urandom_fd);
        close(fileno(plot_file));
      • 设置ASAN_OPTIONSMSAN_OPTIONS

      • execv替换进程空间,执行target_path

      • 替换失败,则*trace_bits = 0xfee1deadexit(0)退出。

  • 否则就是处于有forkserver的模式,且forkserver已经初始化完毕。

  • 通过fsrv_ctl_fd向控制管道写入prev_timed_out4个字节,表示forkserver可以开始fork子进程进行fuzz

  • 通过fsrv_st_fd向状态管道读入4个字节,赋值到child_pid,表示forkserver fork出来的子进程pid

  • 如果dumb_mode == 1或者no_forkserver不为0

    • waitpid等待子进程结束,状态变量保存在status
  • 否则,通过fsrv_st_fd向状态管道读取4个字节,赋值到status,表示子进程执行完最后的状态。

  • 计算exec_ms,即target执行时间,并将total_execs++

  • trace_bits首4个字节的值赋值给tb4

  • 调用classify_counts((u64 *)trace_bits)

  • child_timed_out赋值给prev_timed_out

  • 如果WIFSIGNALED(status)不为0且!stop_soon,也就是target执行完返回的是异常状态

    • 通过WTERMSIG(status)获得异常状态信号值,赋值给kill_signal
    • 如果child_timed_out不为0且kill_signal等于SIGKILL,说明target执行超时了,返回FAULT_TMOUT
    • 否则返回FAULT_CRASH
  • 如果uses_asan不为0且WEXITSTATUS(status) == MSAN_ERROR,也就是target退出状态是MSAN_ERROR,返回FAULT_CRASH

  • 如果(dumb_mode为1或者no_forkserver不为0)且tb4等于0xfee1dead,也就是execv调用失败,返回FAULT_ERROR

  • 如果exec_tmout大于等于timeoutexec_ms大于slowest_exec_ms,将slowest_exec_ms赋值给exec_ms

  • 返回FAULT_NONE

classify_counts

  • i赋值为MAP_SIZE >> 3,即8个字节遍历。

  • 进入while循环遍历

    • 如果*mem不为0:

      • mem16 = (u16 *)mem,每2个字节赋值。

      • 每2个字节通过count_class_lookup16[*mem16]取到对应的值。

      • EXP_ST void init_count_class16(void)
        {
        
          u32 b1, b2;
        
          for (b1 = 0; b1 < 256; b1++)
            for (b2 = 0; b2 < 256; b2++)
              count_class_lookup16[(b1 << 8) + b2] =
                  (count_class_lookup8[b1] << 8) |
                  count_class_lookup8[b2];
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15

        - ```c++
        static const u8 count_class_lookup8[256] = {

        [0] = 0,
        [1] = 1,
        [2] = 2,
        [3] = 4,
        [4 ... 7] = 8,
        [8 ... 15] = 16,
        [16 ... 31] = 32,
        [32 ... 127] = 64,
        [128 ... 255] = 128

        };
      • count_class_lookup16是为加快分类速度,count_class_lookup8是使不同的执行次数更有区分度、稀疏化,也减小了微小执行次数变化的干扰。

update_bitmap_score

update top_rated[]

当我们碰到新路径时,我们调用update_bitmap_score来看看这个路径是否比其他已有的更favorable,这个favorable目的在于能够有能触发bitmap所有bit的最小路径集合,专注于fuzz这些路径,而忽略其他的。

  • q->exec_us * q->len赋值给fav_factor,即将当前queue的执行时间和queue的长度的乘积作为fav因子,赋值给fav_factor

  • 按字节for循环遍历map

    • 如果当前字节trace_bits不为0
      • 如果当前字节top_rated不为0
        • 如果fav_factor大于当前字节top_rated的fav因子,则直接continue
        • --top_rated[i]->tc_ref,减少当前字节top_rated的引用计数,如果减到0时,则释放掉top_rated[i]->trace_mini,将top_rated[i]->trace_mini置为0,即丢弃它记录的trace_mini
      • top_rated[i]赋值为当前queueq->tc_ref++
      • 如果q->trace_mini为空
        • 则申请MAP >> 3的空间,将地址赋值给q->trace_mini,调用minimize_bits(q->trace_mini, trace_bits)
      • score_changed置为1。

minimize_bits

trace_bits压缩到更小的bitmap,这个bitmap的大小是MAPSIZE >> 3,这两个bitmap都是按照字节操作的,所以要把原来8个字节压缩到1个字节,且抛弃掉计数信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void minimize_bits(u8 *dst, u8 *src)
{

u32 i = 0;

while (i < MAP_SIZE)
{

if (*(src++))
dst[i >> 3] |= 1 << (i & 7);
i++;
}
}

原来map字节保存的索引通过>> 3实现,因为8个字节要映射到1个字节,原来的 路径存在信息 保存在字节里,现在保存在对应的bit上,通过|= 1 << (i & 7)实现

cull_queue

top_rated[]选出favored

  • 如果dumb_mode不为0或者score_changed等于0,则直接返回。
  • score_changed赋值为0。
  • 初始化temp_vbit上都为1,temp_v为压缩bitmap
  • 初始化queued_favoredpending_favored为0,遍历queue中每个entry,将favored都置为0。
  • 按字节for循环遍历map
    • 如果当前字节top_rated不为0且temp_v对应的索引上的值不为0
      • 则遍历top_rated[i]->trace_mini每个字节,通过temp_v[j] &= ~top_rated[i]->trace_mini[j],清除掉temp_v对应的路径信息。
      • top_rated[i]->favored置为1,标记为favored
      • queued_favored++
      • 如果top_rated[i]->was_fuzzed为空,也就是entry还没被fuzz过,pending_favored++
  • 遍历queue的每个entry,调用mark_as_redundant(q, !q->favored),标记是否是redundant

mark_as_redundant

  • 如果state等于q->fs_redundant,则直接返回。
  • q->fs_redundant赋值为state
  • 如果state等于1
    • 则创建out_file/queue/.state/redundant_edges/fname
  • 否则
    • 删除out_file/queue/.state/redundant_edges/fname

fuzz执行

主循环

  • 精简队列cull_queue

  • 如果queue_cur为空,即遍历完队列所有的case或者还未进行过fuzz

    • queue_cycle++current_entry置为0,cur_skipped_paths置为0,queue_cur重新置为queue,代表要开始新的一轮fuzz。
    • 如果seek_to不为空,就从seek_to指定的case开始fuzz。
    • show_stats刷新展示界面。
    • 如果queued_paths等于prev_queued,也就是完成一次完整fuzz后,都没发现新的路径。
      • 如果use_splicing不为0,则cycles_wo_finds++,否则设置use_spicing为1。
    • 否则,cycles_wo_finds置为0。
    • 更新prev_queuedqueued_paths
    • 如果sync_id不为0且queue_cycle等于1且环境变量AFL_IMPORT_FIRST不为空,则调用sync_fuzzers(use_argv)
  • 调用fuzz_one()queue_cur进行一次fuzz,将结果返回给skipped_fuzz

  • 如果stop_soon为0且sync_id不为空且skipped_fuzz为0

    • sync_interval_cnt++,当它为5的倍数时,sync_fuzzers(use_argv)同步fuzzer。
  • queue_cur = queue_cur->next; current_entry++,开始fuzz下一个case。

fuzz_one

  • 如果pending_favored不为0
    • 如果queue_cur->was_fuzzed不为0或者queue_cur->favored为0,即当前的case已经被fuzz过或者它不是favored,就有99%的概率直接返回1。
  • 否则,如果不处于dumb_modequeue_cur不是favoredqueued_paths超过10,即队列里的case数超过10
    • 如果queue_cycle大于1且queue_cur没有被fuzz过,就有75%的概率直接返回1。否则被fuzz过,就有95%概率直接返回1。
  • 打开该case对应的文件,将内容通过mmap映射到内存,将地址返回给orig_inin_buf
  • 申请queue_cur->len大小的内存,将地址返回给out_buf
  • cur_depth赋值为queue_cur->depth

CALIBRATION

  • 如果当前queue_cur出现过校验错误
    • 赋值resFAULT_TMOUT
    • 如果queue_cur->cal_failed小于3
      • queue_cur->exec_cksum置为0,调用calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0)返回值赋给res
      • 如果res等于FAULT_ERROR,则抛出错误FATAL("Unable to execute target application")

TRIMMING

  • 如果不处于dumb_modequeue_cur->trim_done为0
    • 调用trim_case(argv, queue_cur, in_buf)返回给res
    • queue_cur->trim_done置为1。
    • 修正len赋值为queue_cur->len
    • in_buf内容拷贝给out_buf

PERFORMANCE SCORE

  • 调用calculate_score(queue_cur)结果返回给perf_scoreorig_pref
  • 如果skip_deterministic不为0或者queue_cur被fuzz过或者queue_cur->passed_det不为0,就跳转到havoc_stage
  • 如果master_max不为0并且queue_cur->exec_cksum % master_max不等于master_id,则跳转到havoc_stage
  • doing_det置为1。

SIMPLE BITFLIP (+dictionary construction)

1
2
3
4
5
6
7
#define FLIP_BIT(_ar, _b)                   \
do \
{ \
u8 *_arf = (u8 *)(_ar); \
u32 _bf = (_b); \
_arf[(_bf) >> 3] ^= (128 >> ((_bf)&7)); \
} while (0)
  • stage_short赋值为flip1stage_max赋值为len << 3stage_name赋值为bitflip 1/1
  • orig_hit_cnt赋值为queued_paths + unique_crashes
  • prev_cksum赋值为queue_cur->exec_cksum
  • for循环stage_cur = 0; stage_cur < stage_max; stage_cur++
    • 总共次数为len << 3,其实就是每次循环对某个字节的某个bit都进行翻转,循环结束刚好对每个字节的每个bit进行翻转。
    • stage_cur_byte赋值为stage_cur >> 3,即当前翻转的字节索引值。
    • FLIP_BIT(out_buf, stage_cur),进行bit翻转。
    • 调用common_fuzz_stuff(argv, out_buf, len),如果返回值不为0,则跳转到abandon_entry
    • FLIP_BIT(out_buf, stage_cur),再翻转回来。
    • 如果不处于dumb_mode并且(stage_cur & 7) == 7,即刚好完成一个字节的翻转
      • 调用hash32(trace_bits, MAP_SIZE, HASH_CONST)将结果赋值给cksum
      • 如果stage_cur等于stage_max - 1cksum等于prev_cksum
        • 如果a_len小于MAX_AUTO_EXTRA,则a_collect[a_len]赋值为out_buf[stage_cur >> 3]
        • a_len++
        • 如果a_lenMIN_AUTO_EXTRAMAX_AUTO_EXTRA之间,就调用maybe_add_auto(a_collect, a_len)
      • 否则,如果cksum不等于prev_cksum
        • 如果a_len大于等于MIN_AUTO_EXTRA且小于等于MAX_AUTO_EXTRA,则调用maybe_add_auto(a_collect, a_len);
        • a_len 置为0。
        • prev_cksum赋值为cksum
      • 如果cksum不等于queue_cur->exec_cksum
      • 如果a_len小于MAX_AUTO_EXTRA
        • a_collect[a_len]赋值为out_buf[stage_cur >> 3]
      • a_len++
  • new_hit_cnt赋值为queued_paths + unique_crashes
  • stage_finds[STAGE_FLIP1]加上new_hit_cnt - orig_hit_cnt,即新发现的路径或者crash。
  • stage_cycles[STAGE_FLIP1]加上stage_max
  • 设置stage_namebitflip 2/1,和上述过程同理,但是现在一次循环翻转2个bit,步长为1个bit
  • 同理bitflip 4/1
  • 生成 effector map
1
2
3
4
#define EFF_APOS(_p) ((_p) >> EFF_MAP_SCALE2)
#define EFF_REM(_x) ((_x) & ((1 << EFF_MAP_SCALE2) - 1))
#define EFF_ALEN(_l) (EFF_APOS(_l) + !!EFF_REM(_l))
#define EFF_SPAN_ALEN(_p, _l) (EFF_APOS((_p) + (_l)-1) - EFF_APOS(_p) + 1)
  • 申请EFF_ALEN(len)大小的内存,将地址赋值给eff_mapeff_map也是压缩存储的bitmap,总是标记第一字节和最后一字节的值为1。
1
2
3
4
5
6
7
eff_map[0] = 1;

if (EFF_APOS(len - 1) != 0)
{
eff_map[EFF_APOS(len - 1)] = 1;
eff_cnt++;
}
  • bitflip 8/8阶段,通过每个字节异或0xFF进行翻转,调用common_fuzz_stuff(argv, out_buf, len)

    • 如果eff_map[EFF_APOS(stage_cur)]为0
      • 如果不处于dumb_mode或者len大于等于EFF_MIN_LEN
        • cksum通过hash32(trace_bits, MAP_SIZE, HASH_CONST)计算,否则,直接按照~queue_cur->exec_cksum计算。
        • 如果cksum不等于queue_cur->exec_cksum,则将eff_map[EFF_APOS(stage_cur)]置为1,并eff_cnt++,即当case的长度够长时,才会通过翻转字节后会不会改变覆盖的路径,来判断需不需标记这个字节是meatdata,将这个字节对应的位置的eff_map标记为1。也就是这个字节是关键字时,翻转它才会改变路径,否则它是plain data的话,翻转它不会造成路径变化。在后续的bitflip中,会跳过eff_map对应位置为0的字节。但是当case的长度太短了,则会直接标记为1。
        • 再异或0xFF翻转回来。
  • 如果eff_cnt不等于EFF_ALEN(len)并且eff_cnt * 100 / EFF_ALEN(len)大于EFF_MAX_PERC

    • 则直接把eff_map所有字节标记为1,即认为所有字节都是有必要进行之后的fuzzblock_eff_select加上EFF_ALEN(len)
  • 否则block_eff_select加上eff_cnt

  • block_eff_total加上EFF_ALEN(len)

  • bitflip 16/8同理,但是eff_map[EFF_APOS(i)]eff_map[EFF_APOS(i + 1)]都为0,就会stage_max--并跳过此次字节翻转。

  • bitfilp 32/8同理,但是是四个字节对应的eff_map值都为0,跳过此次字节翻转。

ARITHMETIC INC/DEC

  • arith 8/8arith 16/8arith 32/8总过这三个小阶段,和bitflip原理类似,只不过变成加上或减去值,由于ARITH_MAX的值是35,所以变化的范围是[-35, 35],同时也会考虑大小端序。
  • 通过eff_map和加上或减去某些值判断是不是和bitflip一样,来跳过某些mutate

INTERESTING VALUES

  • interest 8/8interest 16/8interest 32/8,对对应长度的值替换成interesting value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#define INTERESTING_8 \
-128, /* Overflow signed 8-bit when decremented */ \
-1, /* */ \
0, /* */ \
1, /* */ \
16, /* One-off with common buffer size */ \
32, /* One-off with common buffer size */ \
64, /* One-off with common buffer size */ \
100, /* One-off with common buffer size */ \
127 /* Overflow signed 8-bit when incremented */

#define INTERESTING_16 \
-32768, /* Overflow signed 16-bit when decremented */ \
-129, /* Overflow signed 8-bit */ \
128, /* Overflow signed 8-bit */ \
255, /* Overflow unsig 8-bit when incremented */ \
256, /* Overflow unsig 8-bit */ \
512, /* One-off with common buffer size */ \
1000, /* One-off with common buffer size */ \
1024, /* One-off with common buffer size */ \
4096, /* One-off with common buffer size */ \
32767 /* Overflow signed 16-bit when incremented */

#define INTERESTING_32 \
-2147483648LL, /* Overflow signed 32-bit when decremented */ \
-100663046, /* Large negative number (endian-agnostic) */ \
-32769, /* Overflow signed 16-bit */ \
32768, /* Overflow signed 16-bit */ \
65535, /* Overflow unsig 16-bit when incremented */ \
65536, /* Overflow unsig 16 bit */ \
100663045, /* Large positive number (endian-agnostic) */ \
2147483647 /* Overflow signed 32-bit when incremented */

DICTIONARY STUFF

  • user extras (over):在对应的偏移处覆盖成extras

    • 如果extras_cnt > MAX_DET_EXTRAS并且UR(extras_cnt) >= MAX_DET_EXTRAS并且剩下的长度小于extras[j].len或者原来的值和要插入的extras一样或者eff_map对应位置值为0,跳过此次mutate
  • user extras (insert):在对应偏移处插入extras

  • auto extras (over):和第一小阶段类似,但是使用的是自动生成的extras

  • 此时完成了所有的deterministic mutate,如果queue_cur->passed_det为空,则调用mark_as_det_done(queue_cur)标记。

RANDOM HAVOC

  • 总的来说,就是在随机次数下采取随机种mutate组合,并且mutate的执行也会是随机的。如果在这个阶段发现了新路径,执行次数也会增加。

SPLICING

  • 只有在遍历整个queue都没有新发现的情况下才会启用splicing
  • 随机选取一个queue_entry,寻找合适的位置和当前case拼接,然后跳转到havoc stage

trim_case

  • 如果q->len小于5,则直接返回 0。
  • bytes_trim_in加上q->len
  • len_p2赋值为next_p2(q->len),也就是赋值为第一个大于等于len的2的幂数。
  • remove_len赋值为MAX(len_p2 / TRIM_START_STEPS, TRIM_MIN_BYTES)
  • while(remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES))
    • remove_pos赋值为remove_len
    • stage_cur置为0,stage_max赋值为q->len / remove_len
    • while(remove_pos < q->len)
      • trim_avail赋值为MIN(remove_len, q->len - remove_pos)
      • 调用write_with_gap(in_buf, q->len, remove_pos, trim_avail)out_fileremove_pos处剪掉trim_avail的长度。
      • 调用run_target(argv, exec_tmout),将结果赋值给fault
      • trim_execs++
      • 如果stop_soon不为0或者fault等于FAULT_ERROR。跳转到abort_trimming
      • cksum赋值为hash32(trace_bits, MAP_SIZE, HASH_CONST)
      • 如果cksum等于q->exec_cksum,即剪掉这部分对case没有影响
        • move_tail赋值为q->len - remove_pos - trim_avail
        • q->len减掉trim_avail
        • len_p2赋值为next_p2(q->len)
        • memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail, move_tail);,实现trim
        • 如果needs_write为0
          • needs_write置为1。
          • trace_bits拷贝到clean_trace
      • 否则,remove_pos加上remove_len
      • show_status(),并且stage_cur++
    • remove右移一位。
  • 如果needs_write不为0
    • unlink(q->fname),打开新的q->fname,将文件描述符赋值给fd
    • trimmed in_buf写进q->fname
    • clean_trace拷贝给trace_bits
    • 调用update_bitmap_score(q)
  • abort_trimming标签
  • bytes_trim_out加上q->len
  • 返回fault

sync_fuzzers

读取其他fuzzer的queue,保存到自己的queue

  • 通过opendir(sync_dir)打开sync_dir,将返回值给sd
  • while循环读取该目录下的文件(sd_ent = readdir(sd))
    • 跳过.开头的文件和自己的输出文件夹。
    • 通过opendir("sync_dir/sd_ent->d_name/queue")打开queue目录,将返回值给qd,跳过没有queue子目录的目录。
    • 打开out_dir/.synced/sd_ent->d_name将返回值给id_fd
    • id_fd读取四个字节赋值给min_acceptminaccept赋值给next_min_acceptmin_accept是上次最后一个读取的case的下一个case的id
    • while循环读取queue下的case(qd_ent = readdir(qd))
      • 跳过.目录和idmin_accept小的case,将读取的id赋值给syncing_case
      • 如果syncing_case大于等于next_min_accept,则next_min_accept等于syncing_case + 1
      • 打开dq_path/qd_ent->d_name,即打开这个case,将返回值给fd
      • 通过fd将内容映射到mem
      • 通过write_to_testcase(mem, st.st_size),将case的内容写入到out_file
      • run_target(argv, exec_tmout)将返回值赋给fault
      • queued_imported加上save_if_interesting(argv, mem, st.st_size, fault)
    • 通过id_fd写入next_min_accept

common_fuzz_stuff

写入mutated caserun_target()执行,通过save_if_interesting()保存interesting case

  • write_to_case(out_buf, len),将mutate过的case写入out_file
  • run_target(out_buf, len),将返回值给fault
  • 如果fault等于FAULT_TMOUT
    • 如果subseq_tmouts++大于TMOUT_LIMIT
      • cur_skipped_paths++
      • 返回1。
  • 否则subseq_tmouts等于0。
  • queued_discovered加上save_if_interesting(argv, out_buf, len, fault)
  • show_stats()
  • 返回0。

save_if_interesting

  • 如果fault等于crash_mode
    • 调用has_new_bits(virgin_bits)返回值给hnb,如果为0
      • 如果crash_mode不为0
        • total_crashes++
        • 返回0。
    • fn赋值为alloc_printf("%s/queue/id:%06u,%s", out_dir, queued_paths,describe_op(hnb));
    • add_to_queue(fn, len, 0);添加到queue
    • 如果hnb等于2,即覆盖了新路径
      • queue_top->has_new_cov赋值为1。
      • queued_with_cov++
    • queue_top->exec_cksum赋值为hash32(trace_bits, MAP_SIZE, HASH_CONST)
    • 调用calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0),将结果给res
    • 如果res等于FAULT_ERROR抛出异常退出。
    • 打开caseopen(fn),将结果赋值给fd
    • 通过fd写入memck_write(fd, mem, len, fn);
    • keeping赋值为1。
  • Switch(fault)
    • FAULT_TMOUT:
      • total_tmouts++
      • 如果unique_hangs大于等于KEEP_UNIQUE_HANG,返回keeping
      • 如果不处于dumb_mode
        • 调用simplify_trace((u64 *)trace_bits)
        • 如果has_new_bits(virgin_tmout)为0,返回keeping
      • unique_tmouts++
      • 如果exec_tmout小于hang_tmout
        • write_to_testcase写入到out_file
        • 再次调用run_target(argv, hang_tmout),将结果赋值给new_fault
        • 如果stop_soon为0且new_fault等于FAULT_CRASH,跳转到keep_as_crash
        • 如果stop_soon不为0或者new_fault不等于FAULT_TMOUT,返回keeping
        • fn赋值为alloc_printf("%s/hangs/id:%06llu,%s", out_dir,unique_hangs, describe_op(0));
        • unique_hangs++
        • last_hang_time等于get_cur_time
    • FAULT_CRASH(keep_as_crash标签)
      • total_crashes++
      • 如果unique_crashes大于等于KEEP_UNIQUE_CRASH,则直接返回keeping
      • 如果不处于dumb_mode
        • simplify_trace((u64 *)trace_bits)
        • 如果has_new_bits(virgin_crash)为0,则直接返回keeping
        • fn赋值为fn = alloc_printf("%s/crashes/id:%06llu,sig:%02u,%s", out_dir,unique_crashes, kill_signal, describe_op(0));
        • unique_crashes++
        • last_crash_time赋值为get_cur_time()
        • last_crash_execs赋值为total_execs
    • FAULT_ERROR:
      • 抛出异常退出。
    • default:
      • 返回keeping
  • open(fn),将mem写入。
  • 返回keeping

simplify_trace

1
2
3
4
5
6
static const u8 simplify_lookup[256] = {

[0] = 1,
[1 ... 255] = 128

};
  • i赋值为MAP_SIZE >> 3
  • while(i--),每8个字节遍历
    • 如果mem不为0
      • u8 *mem8 = (u8 *)mem,遍历每个字节,只要不为0,就会置为128,否则置为1
    • 否则,直接置为0x0101010101010101ULL,也就是字节值为1代表没有路径覆盖,值为128代表有。

calculate_score

计算perf_score用来判断后续havoc持续的时间

  • total_cal_us / total_cal_cycles计算平均执行时间avg_exec_us
  • total_bitmap_size / total_bitmap_entries计算平均bitmap大小avg_bitmap_size
  • handicap值越大,说明这个case对应的新路径越难发现,越有mutate的价值
  • depth值越大,和handicap类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
if (q->exec_us * 0.1 > avg_exec_us)
perf_score = 10;
else if (q->exec_us * 0.25 > avg_exec_us)
perf_score = 25;
else if (q->exec_us * 0.5 > avg_exec_us)
perf_score = 50;
else if (q->exec_us * 0.75 > avg_exec_us)
perf_score = 75;
else if (q->exec_us * 4 < avg_exec_us)
perf_score = 300;
else if (q->exec_us * 3 < avg_exec_us)
perf_score = 200;
else if (q->exec_us * 2 < avg_exec_us)
perf_score = 150;

if (q->bitmap_size * 0.3 > avg_bitmap_size)
perf_score *= 3;
else if (q->bitmap_size * 0.5 > avg_bitmap_size)
perf_score *= 2;
else if (q->bitmap_size * 0.75 > avg_bitmap_size)
perf_score *= 1.5;
else if (q->bitmap_size * 3 < avg_bitmap_size)
perf_score *= 0.25;
else if (q->bitmap_size * 2 < avg_bitmap_size)
perf_score *= 0.5;
else if (q->bitmap_size * 1.5 < avg_bitmap_size)
perf_score *= 0.75;

if (q->handicap >= 4)
{

perf_score *= 4;
q->handicap -= 4;
}
else if (q->handicap)
{

perf_score *= 2;
q->handicap--;
}

switch (q->depth)
{

case 0 ... 3:
break;
case 4 ... 7:
perf_score *= 2;
break;
case 8 ... 13:
perf_score *= 3;
break;
case 14 ... 25:
perf_score *= 4;
break;
default:
perf_score *= 5;
}

插桩代码

关键变量

  • __afl_area_ptr:存储共享内存的地址。
  • __afl_prev_loc:上一个基本块的id,即上一次R(MAP_SIZE)生成的随机数。
  • __afl_fork_pidfork进程的pid
  • __afl_temp:存储临时变量。

__afl_maybe_log

如果__afl_area_ptr为0

  • 如果__afl_setup_failure不为0,则直接返回。
  • 如果_afl_global_area_ptr不为0,则直接__afl_area_ptr赋值为__afl_global_area_ptr
  • 否则读取环境变量__AFL_SHM_ID,再通过shmat获取共享内存的地址,如果获取失败则__afl_setup_failure++,直接返回。
  • 将共享内存地址给__afl_area_ptr__afl_global_area_ptr
  • 向状态管道写入4个字节,表示forkserver启动成功。如果写入4个字节成功,进入while循环
    • 从控制管道读取4个字节到__afl_temp,如果读取失败,则退出。
    • fork进程,如果为子进程则跳转到__afl_fork_resume
    • 如果为父进程,则将子进程pid保存到__afl_fork_pid,再通过状态管道写入__afl_fork_pid
    • waitpid等待子进程结束,将子进程结束状态保存到__afl_temp
  • __afl_fork_resume标签:
  • 关闭状态管道的写入和控制管道的读取
  • 如下代码为信息记录
1
2
3
cur_location = <COMPILE_TIME_RANDOM>; 
shared_mem[cur_location ^ prev_location]++;
prev_location = cur_location >> 1;

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
char __fastcall _afl_maybe_log(__int64 a1, __int64 a2, __int64 a3, __int64 rnd)
{
char v4; // of
char v5; // al
__int64 v6; // rdx
__int64 v7; // rcx
char *v9; // rax
int v10; // eax
void *shm; // rax
int v12; // edi
__int64 pid; // rax
__int64 v14; // rax
__int64 v15; // [rsp-10h] [rbp-180h]
char v16; // [rsp+10h] [rbp-160h]
__int64 v17; // [rsp+18h] [rbp-158h]

v5 = v4;
v6 = _afl_area_ptr;
if ( !_afl_area_ptr )
{
if ( _afl_setup_failure )
return v5 + 127;
v6 = _afl_global_area_ptr;
if ( _afl_global_area_ptr )
{
_afl_area_ptr = _afl_global_area_ptr;
}
else
{
v16 = v4;
v17 = rnd;
v9 = getenv("__AFL_SHM_ID");
if ( !v9 || (v10 = atoi(v9), shm = shmat(v10, 0LL, 0), shm == (void *)-1LL) )
{
++_afl_setup_failure;
v5 = v16;
return v5 + 127;
}
_afl_area_ptr = (__int64)shm;
_afl_global_area_ptr = shm;
v15 = (__int64)shm;
if ( write(199, &_afl_temp, 4uLL) == 4 )
{
while ( 1 )
{
v12 = 198;
if ( read(198, &_afl_temp, 4uLL) != 4 )
break;
LODWORD(pid) = fork();
if ( pid < 0 )
break;
if ( !pid )
goto __afl_fork_resume;
_afl_fork_pid = pid;
write(199, &_afl_fork_pid, 4uLL);
v12 = _afl_fork_pid;
LODWORD(v14) = waitpid(_afl_fork_pid, &_afl_temp, 0);
if ( v14 <= 0 )
break;
write(199, &_afl_temp, 4uLL);
}
_exit(v12);
}
__afl_fork_resume:
close(198);
close(199);
v6 = v15;
v5 = v16;
rnd = v17;
}
}
v7 = _afl_prev_loc ^ rnd;
_afl_prev_loc ^= v7;
_afl_prev_loc = (unsigned __int64)_afl_prev_loc >> 1;
++*(_BYTE *)(v6 + v7);
return v5 + 127;
}

Reference

sakura