0%

AFL源码阅读小记

afl-gcc

find-as

用来寻找as的路径。分别从环境变量AFL_PATHafl-gcc的路径(如果是绝对路径或相对路径调用afl-gcc),/usr/local/lib/afl三个地方寻找。如果找不到则报出相应错误。下列是我调试时打印出来的参数。

1
2
3
afl-gcc: find_as
argv0: afl-gcc
AFL_PATH: /usr/local/lib/afl

edit_params

用来对原生gcc进行一个封装,加上了一些参数并存放到cc_params中。如笔者使用afl-gcc -o test test.c对文件进行编译,则会被封装成如下代码。

1
2
3
4
5
6
7
8
9
10
11
gcc
-o
test
test.c
-B
/usr/local/lib/afl
-g
-O3
-funroll-loops
-D__AFL_COMPILER=1
-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1

afl-as

edit_params

对原生as进行封装,参数存放到as_params中。同时通过传入的参数设置一些变量的值,并且定义好modified_file。原本传入的命令和经过修改的命令如下。

1
2
3
4
5
6
argv                          as_params
/usr/local/lib/afl/as as
--64 --64
-o -o
/tmp/cc6ribB2.o /tmp/cc6ribB2.o
/tmp/ccBeRi60.s /tmp/.afl-165297-1701960501.s

最终主函数中也会运行这个命令。

add_instrumentation

尝试把input_file中的内容,进行插桩并放到modified_file中。打开文件后满足以下规则进行插桩。

  • !pass_thru,不是用来传送数据
  • !skip_intel,不是intel汇编
  • !skip_app,不是__asm__内联汇编
  • !skip_csect,不与当前架构不适配(比如在x64架构中遇到x86汇编)
  • instr_ok,在text段中
  • instrument_next,有时会把instrument_next置零使得不会进行插桩
  • line[0] == ‘\t’,此行开头要为\t
  • isalpha(line[1]),line[1]要是字母

或者遇到跳转指令且不是jmp时,也会插桩。

最后如果存在满足上述条件的地方,并进行了插桩,在最后则会根据架构插入main_payload_64或main_payload_32

总的来说就是遇到如下汇编时进行插桩。

1
2
3
function:   ——  函数入口
.L0 —— GCC分支跳转标签
.jnz fun —— 条件跳转

trampoline

比如x64程序中插入的代码如下,会调用__afl_maybe_log,且rcx的值是一个不超过1<<16的随机数。

1
2
3
4
5
6
7
8
9
10
"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"

__afl_maybe_log

检查共享内存(__afl_area_ptr)是否已经被设置,如果没被设置就调用__afl_setup进行初始化,设置过就调用__afl_store。

1
2
3
4
5
"  movq  __afl_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup\n"
"\n"
"__afl_store:\n"

__afl_setup

会检查__afl_setup_failure是否为0,也即检查之前__afl_setup是否调用失败,如果之前失败则跳转到__afl_return返回。否则再检查__afl_global_area_ptr是否为0,为0则跳转到__afl_setup_first进行初始化,否则把__afl_global_area_ptr赋值给__afl_area_ptr后,跳转到__afl_store。

1
2
3
4
5
6
7
"  cmpb $0, __afl_setup_failure(%rip)\n"
" jne __afl_return\n"
" movq __afl_global_area_ptr(%rip), %rdx\n"
" testq %rdx, %rdx\n"
" je __afl_setup_first\n"
" movq %rdx, __afl_area_ptr(%rip)\n"
" jmp __afl_store\n"

__afl_setup_first

用来保存寄存器并保证栈对齐。之后调用shmat获取共享内存地址存放到全局变量__afl_area_ptr及__afl_global_area_ptr中。执行完毕调用__afl_forkserver。

__afl_forkserver

向管道中写入4字节,来通知fuzz我们fork-server已经准备好。写入失败则会调用__afl_fork_resume。成功则调用__afl_fork_wait_loop。

1
2
3
4
5
6
7
8
9
10
"  pushq %rdx\n"
" pushq %rdx\n"
" movq $4, %rdx /* length */\n"
" leaq __afl_temp(%rip), %rsi /* data */\n"
" movq $" STRINGIFY((FORKSRV_FD + 1)) ", %rdi /* file desc */\n"
CALL_L64("write")
" cmpq $4, %rax\n"
" jne __afl_fork_resume\n"
"\n"
"__afl_fork_wait_loop:\n"

__afl_fork_wait_loop

等待父进程从管道发送来的命令,并存放到__afl_temp中。读取失败则调用__afl_die,成功则调用fork。子进程中调用__afl_fork_resume,当前进程把子进程号记录到__afl_fork_pid后,再写入管道,并且等待子进程返回。等待子进程执行完毕,把子进程状态写入管道告知fuzz。再重新调用__afl_fork_wait_loop进行下一轮测试。

__afl_fork_resume

关闭管道描述符,恢复寄存器的值。调用__afl_store。

__afl_die

调用_exit

__afl_store

用来计算边的命中次数,RCX即当前边的id。首先计算rcx和更新__afl_prev_loc 。

  • rcx = rcx ^ __afl_prev_loc
  • __afl_prev_loc = __afl_prev_loc ^ rcx
  • __afl_prev_loc = __afl_prev_loc >> 1

之后把__afl_area_ptr[rcx]+1

1
2
3
4
5
6
7
8
9
10
11
#ifndef COVERAGE_ONLY
" xorq __afl_prev_loc(%rip), %rcx\n"
" xorq %rcx, __afl_prev_loc(%rip)\n"
" shrq $1, __afl_prev_loc(%rip)\n"
#endif /* ^!COVERAGE_ONLY */
"\n"
#ifdef SKIP_COUNTS
" orb $1, (%rdx, %rcx, 1)\n"
#else
" incb (%rdx, %rcx, 1)\n"
#endif /* ^SKIP_COUNTS */

afl-fuzz

setup_signal_handlers

注册必要的信号处理函数

check_asan_opts

读取环境变量ASAN_OPTIONSMSAN_OPTIONS并进行检查。

fix_up_sync

如果存在-M或-S,更改out_dir和sync_id。

save_cmdline

保存命令行参数到全局变量orig_cmdline中。

fix_up_banner

获取测试文件名

check_if_tty

检查是否在tty中运行,并记录在not_on_tty里。

get_core_count

获取cpu的核心数

bind_to_free_cpu

linux系统,如果存在空闲核心,就绑定到这个cpu上

check_crash_handling

检查crash的处理方式,执行echo core >/proc/sys/kernel/core_pattern,以防存在延迟。

setup_post

如果存在环境变量AFL_POST_LIBRARY,则会从其记录的动态链接库中获取函数afl_postprocess,并记录到全局函数指针post_handler中。这个函数指针会被common_fuzz_stuff调用,用来处理out_buf

setup_shm

用来初始化共享内存,设置SHM_ENV_VAR,和初始化virgin_bitsvirgin_tmoutvirgin_crash

  • 如果in_bitmap为空,就初始化virgin_bitsvirgin_tmoutvirgin_crash\xff
  • shmget(IPC_PRIVATE, MAP_SIZE, IPC_CREAT | IPC_EXCL | 0600); 分配共享内存,并把id存到shm_id里。
  • atexit(remove_shm); 注册处理函数在退出时删除共享内存。(这个atexit也就是ctf里所谓的“exit-hook”的一种。😀
  • setenv(SHM_ENV_VAR, shm_str, 1); 如果不是dumb_mode,设置SHM_ENV_VARshm_str
  • trace_bits = shmat(shm_id, NULL, 0); 启动对共享内存的访问。

init_count_class16

初始化count_class_lookup16,目的是提高count_class_lookup8的效率。先看一下count_class_lookup8定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
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_lookup8用来对路径命中率做规整。比如有些时候A->B,出现了4次,有时候出现5次,这可能并不意味着出现新的路径。同时也为了减少因为命中次数不一样导致的区别。于是就定义了count_class_lookup8用来做一个规整。把4-7次都看作8次,以此类推。

AFL中进行规整的时候一次性读入2字节,于是出现了count_class_lookup16。

setup_dirs_fds

创建输出文件夹以及记录fd。

read_testcases

从输入文件夹里读取所有文件,并将其加入测试队列中。

  • nl_cnt = scandir(in_dir, &nl, NULL, alphasort); 记录文件数量和信息
  • 通过循环遍历所有文件
    • 通过文件属性,大小和名称过滤掉无效文件
    • add_to_queue(fn, st.st_size, passed_det); 通过筛选,则加入测试队列

add_to_queue

增加测试样例到队列中。

  • 新建一个queue_entry结构体以记录test case
  • 更新max_depth
  • 如果queue_top存在,则queue_top->next = q; queue_top = q;,否则q_prev100 = queue = queue_top = q;
  • queued_pathspending_not_fuzzed加1
  • 如果queued_paths % 100 = 0,则q_prev100->next_100 = q; q_prev100 = q;
  • 设置last_path_time = get_cur_time();

load_auto

加载自动生成的提取出来的词典token。从目标文件中读取内容,如果满足要求则调用maybe_add_auto(tmp, len);

maybe_add_auto

  • 遍历mem里的字节,当出现有字节和第一个字节相同时停止遍历。
  • 如果mem[0]==mem[1],则直接返回
  • 如果mem的长度为2,则将mem和interesting_16进行比较,出现相同则直接返回
  • 如果mem的长度为4,则将mem和interesting_32进行比较,出现相同则直接返回
  • mem和extras中现有的数据进行比较,如果出现与mem相同的片段则直接退出。
  • 设置auto_changed = 1;
  • 遍历a_extras,出现相同则a_extras[i].hit_cnt++;,否则把mem加入a_extras

pivot_inputs

in_dir里的testcase在out_dir/queue里创建hard link。遍历输入目录的所有文件名,判断是否符合命名规范,如果不符合则按照规范重新命名,之后创建硬链接。

load_extras

如果指定了extras_dir,那么从extras_dir里读取数据到extras里,并按照大小排序。

find_timeout

没有指定timeout_given时进入这个函数,设置timeout_given

detect_file_args

识别命令行参数中是否含有**@@,如果存在则替换为out_dir/.cur_input**。

setup_stdio_file

如果没有指定out_file,那么删除旧的并创建新的out_dir/.cur_input,并获取其fd存放到out_fd中。

check_binary

检查目标文件是否存在以及确保他不是一个shell script。并检查是否是完整的elf文件以及是否被插桩。

get_cur_time

毫秒级获取时间

perform_dry_run

执行测试用例,检查是否按照预期进行。

  • 获取环境变量AFL_SKIP_CRASHES
  • 遍历queue
    • 打开q->fname,并读取其内容。
    • 调用res = calibrate_case(argv, q, use_mem, 0, 1);
    • 如果stop_soon==1,则直接返回。
    • 如果res为crash_mode或者FAULT_NOBITS,则打印提示。
    • 根据错误类型抛出不同异常。
    • 如果q->var_behavior为真,说明该测试样例多次运行所覆盖的路径不同。

calibrate_case

测试输入样例是否会触发异常,以及检测新发现的路径样例的行为是否可变。

  • 如果q->exec_cksum为0,代表这个测试样例第一次运行,把first_run置为1。
  • 保持原来的stage_cur、stage_max、stage_name。
  • 如果测试样例不是来自queue,则use_tmout增大,且q->cal_failed++;。
  • 设置stage_name = “calibration”;,并且通过fast_cal来判断,stage_max是3还是CAL_CYCLES(默认8),即这个样例测试的次数。
  • 不是dumb mode,并且forkserver没有启动,则调用**init_forkserver(argv);**。
  • 如果不是第一次运行这个测试样例,则把trace_bits拷贝进first_trace
  • 开始calibrate stage
    • 如果不是第一次运行,在第一轮calibrate stage结束时,显示界面,展示执行结果。
    • 调用write_to_testcase(use_mem, q->len);,把测试内容记录到out_file中。
    • 调用run_target(argv, use_tmout);执行测试样例,结果保存在fault中。
    • 如果这是第一轮calibrate case,不是dumb mode,并且trace_bits没有记录到数据,则设置fault = FAULT_NOINST,跳转到abort_calibration。
    • 通过hash32(trace_bits, MAP_SIZE, HASH_CONST);计算出cksum
    • 如果q->exec_cksum != cksum,代表不是第一次运行,或者这个测试样例行为可变。
      • u8 hnb = has_new_bits(virgin_bits);
      • 如果hnb > new_bits,则new_bits = hnb。
      • 如果q->exec_cksum不为0,则代表不是第一次运行。
        • 如果var_bytes为空,并且first_trace[i] != trace_bits[i],则设置var_bytes[i] = 1,stage_max = CAL_CYCLES_LONG。
        • var_detected = 1;
      • 否则,代表是第一次运行。
        • 设置q->exec_cksum = cksum;
        • 拷贝trace_bitsfirst_trace
    • 计算时间和轮次,并计算一些信息
    • 如果new_bits == 2 && !q->has_new_cov,则设置q->has_new_cov = 1,并且queued_with_cov++。
    • 如果var_detected为1,则当前测试用例行为可变。调用mark_as_variable(q),并且queued_variable++。
    • 恢复stage值。
    • 如果不是第一次运行stage则调用show_stats,展示状态。

init_forkserver

初始化fork server

  • 建立状态管道st_pipe和控制管道ctl_pipe
  • fork出子进程
    • FORKSRV_FD重定向到ctl_pipe[0],把FORKSRV_FD+1重定向到**st_pipe[1]**。
    • 关闭一些问价描述符。
    • 调用execv(target_path, argv)。这里会执行我们之前插桩的程序,第一次执行时,会调用__afl_fork_wait_loop,以充当fork server
    • 如果失败,则*(u32*)trace_bits = EXEC_FAIL_SIG。
  • 父进程
    • 关闭ctl_pipe[0]和st_pipe[1]。
    • 等待fork server启动。
      • 能从**st_pipe[0]**读取到4字节即代表启动成功,超时则表示失败。
    • 子进程异常处理

has_new_bits

用来检查是否出现新路径以及是否边的命中次数发生变化。

  • 初始化currenttrace_bitsvirginvirgin_map,ret=0。
  • 8字节一组遍历trace_bits。如果**(*(u32*)current)不为0,并且(*(u32*)current & *(u32*)virgin)**不为0。则代表出现新的路径或者路径命中次数出现变化。
    • 如果ret<2。
      • 如果存在**(cur[i] && vir[i] == 0xff)**,那么设置ret=2,这代表出现新的路径。
    • *virgin &= ~*current;
  • 如果ret存在,并且virgin_map == virgin_bits,则设置bitmap_changed = 1
  • 返回ret

count_bytes

统计非零字节数

  • 每4字节一组遍历mem,u32* ptr = (u32*)mem,ret=0.
  • 将ptr 与0xff,0xff00,0xff0000,0xff000000,分别相与,如果非零,则ret++。
  • 返回ret,表示非零字节数。

run_target

用来fork出子进程进行fuzz测试。

  • 首先清空trace_bits(记录路径的共享内存)。
  • 如果是dumb mode或者no_forkserver非零。
    • fork出一个子进程,在关闭一系列文件描述符后,去执行**execv(target_path, argv)**。
  • 否则正常通过fork server来fork出一个进程进行fuzz。
    • 通过fsrv_ctl_fd写入4字节,来告诉fork server要开始fuzz。
    • 再通过fsrv_st_fd读取4字节,判断fork server是否成功fork出子进程。
  • AFL自己会等待fork出来的程序执行结束,并获取状态保存到status中。total_execs++。
  • 调用classify_counts((u64*)trace_bits)。
  • 根据status的值,返回fuzz执行状态。

classify_counts(u64* mem)

用来对trace_bits进行分类规整。

  • 8字节一组遍历trace_bits

    • 每次2字节进行规整。
    • 代码如下
    1
    2
    3
    4
    mem16[0] = count_class_lookup16[mem16[0]];
    mem16[1] = count_class_lookup16[mem16[1]];
    mem16[2] = count_class_lookup16[mem16[2]];
    mem16[3] = count_class_lookup16[mem16[3]];

update_bitmap_score

当发现新的路径时,判断触发新的路径的样例是否更加favorable,favorable的意思是当前路径是否包含最小的路径集合来遍历到bitmap中的所有位。

  • fav_factor = q->exec_us * q->len。
  • 遍历trace_bits,如果trace_bits[i]存在,则代表这里所指的路径已被覆盖。
    • 如果fav_factor > top_rated[i]->exec_us * top_rated[i]->len,则代表原来的更优,直接continue,去判断下一条路径。
    • 否则就说明这个新样例对于当前边更加favorable,–top_rated[i]->tc_ref,top_rated[i]->trace_mini = 0。
  • 替换top_rated[i],top_rated[i] = q,并且q->tc_ref++。
  • 如果q->trace_mini==0,则调用minimize_bits(q->trace_mini, trace_bits)。
  • score_changed = 1

minimize_bits

将描述边是否被覆盖到的以字节记录的trace_bits,转化为以bit记录的图。

cull_queue

用来精简队列

  • 如果是dumb mode,或者score_changed==0,那么就直接返回。
  • 设置score_changed=0,初始化temp_v[MAP_SIZE >> 3]为0xff。
  • 遍历queue队列,使得q->favored = 0。
  • 遍历top_rated次。
    • 如果top_rated[i]存在,并且temp_v[i >> 3] & (1 << (i & 7))存在。
      • 从temp_v中清除掉top_rated[i]所有覆盖到的边。
      • top_rated[i]->favored = 1,queued_favored++。
      • 如果top_rated[i]->was_fuzzed==0,则pending_favored++。
  • 遍历队列queue,调用mark_as_redundant(q, !q->favored)。

mark_as_redundant

标记冗余队列。

  • 如果state == q->fs_redundant,则直接返回。
  • 设置q->fs_redundant = state。
  • 构造路径fn=”out_dir/queue/.state/redundant_edges/q->fname”。
  • 对state进行判断。
    • state存在,则说明该队列冗余,创建文件fn。
    • state为空,则删除文件。

show_init_stats

初始化之后,显示一些信息,以及设置一些值。

find_start_position

用来查询开始队列的位置。

  • 如果resuming_fuzz==0,则直接退出。
  • 否则就找到fuzzer_stats文件,打开并找到cur_path,并设置为ret。
  • 如果ret >= queued_paths,则ret=0,返回ret。

write_stats_file

用来更新fuzzer_stats文件。

save_auto

用来自动保存生成的extras。

  • 如果auto_changed==0,则直接退出。
  • 打开”out_dir/queue/.state/auto_extras/auto_%06u”文件,并写入a_extras[i].data。

fuzz 主循环

fuzz的过程

  • 调用**cull_queue()**,精简队列。

  • 如果queue_cur==0,代表所有queue已被执行过。

    • 有以下设置

      1
      2
      3
      4
      queue_cycle++;
      current_entry = 0;
      cur_skipped_paths = 0;
      queue_cur = queue;
    • 调用show_stats,刷新界面。

  • 执行**skipped_fuzz = fuzz_one(use_argv)**。

fuzz_one

从queue中取出entry进行fuzz,成功返回0,否则返回1。

  • 如果pending_favored非0,并且当前queue已经被fuzz过,并且queue_cur->favored==0,则有99%的概率直接返回1。
  • 如果pending_favored为0,并且不是dumb mode,并且queue_cur->favored==0,queued_paths > 10。
    • 如果queue_cycle > 1,并且queue没有被fuzz过,则有75%的概率直接返回1。
    • 如果queue已经被fuzz过,则有95%的概率直接返回1。
  • 打开测试样例文件,并且设置len = queue_cur->len,将文件映射到内存中,地址复制给orig_in,in_buf。

CALIBRATION阶段

  • 如果queue_cur->cal_failed存在,并且queue_cur->cal_failed<3,则再次调用calibrate_case(argv, queue_cur, in_buf, queue_cycle - 1, 0)。

TRIMMING阶段

  • 如果不是dumb mode,并且queue没有被修剪过,则调用**trim_case(argv, queue_cur, in_buf)**。

  • len = queue_cur->len,调用memcpy(out_buf, in_buf, len)。

PERFORMANCE SCORE阶段

  • perf_score = calculate_score(queue_cur)。
  • 如果skip_deterministic存在,或者queue已经被fuzz过,或者queue_cur->passed_det存在,则跳转去havoc_stage。

SIMPLE BITFLIP阶段

bit翻转策略。

  • 定义了如下宏,这个宏的作用是,翻转_ar的(_bf) >> 3偏移处字节的,第7-((_bf) & 7)比特。

    1
    2
    3
    4
    5
    #define FLIP_BIT(_ar, _b) do { \
    u8* _arf = (u8*)(_ar); \
    u32 _bf = (_b); \
    _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); \
    } while (0)
  • bitflip 1/1阶段,逐步翻转每一个字节的每一比特,并且调用common_fuzz_stuff(argv, out_buf, len),之后会恢复这个比特。

  • 并且如果在bitflip 1/1阶段,连续翻转几个字节时,路径覆盖与原路径不一样,并且这几个新覆盖的路径一样,则AFL猜测这几个字节为token,并调用maybe_add_auto(a_collect, a_len)。

  • bitflip 2/1阶段,连续翻转2bit。

  • bitflip 4/1阶段,连续翻转4bit。

  • 生成Effector map。这个结构的作用是使得AFL对文件格式有一定的判断,从而提高效率。如果将某个字节完全反转,也不能导致路径发生变化,那么这个字节可能就是data,对fuzz来说意义不大,在Effector map中标记为0,表示该字节无效,在以后变异中跳过这些字节,以节省资源。

  • bitflip 8/8阶段,以字节为单位进行翻转。在满足不是dumb mode和len >= 128的条件下,计算cksum,如果cksum != queue_cur->exec_cksum,那么标记eff_map[EFF_APOS(stage_cur)] = 1,即代表这个字节值得变异。

  • bitflip 16/8阶段,以2字节为单位进行翻转,但是会跳过eff_map[EFF_APOS(stage_cur)]为0的字节。

  • bitflip 32/8阶段,以2字节为单位进行翻转,同样会跳过eff_map[EFF_APOS(stage_cur)]为0的字节。

ARITHMETIC INC/DEC阶段

byte加减法策略。

  • arith 8/8阶段,每8个bit,进行加减运算。
  • arith 16/8阶段,每16个bit,进行加减运算。
  • arith 32/8阶段,每32个bit,进行加减运算。
  • 会跳过被eff_map认为无效的字段,以及跳过被变异过的测试样例。

INTERESTING VALUES阶段

byte替换策略。

  • interest 8/8,每8个bit进行替换。
  • interest 16/8,每16个bit进行替换。
  • interest 32/8,每32个bit进行替换。
  • 会跳过被eff_map认为无效的字段,以及跳过被变异过的测试样例。

DICTIONARY STUFF阶段

替换或插入tokens。

RANDOM HAVOC阶段

dumb fuzz,完全随机的变异。

sync_fuzzers

读取其他fuzzer的测试样例,并进行测试,如果出现新路径,就保存到自己的queue文件夹里。

trim_case

修剪测试样例。

  • 如果测试样例小于5,则直接返回。

  • 计算len_p2,len_p2 = next_p2(q->len),remove_len=1/16 * len_p2 。

    1
    2
    3
    4
    5
    6
    7
    static u32 next_p2(u32 val) {

    u32 ret = 1;
    while (val > ret) ret <<= 1;
    return ret;

    }
  • 进入循环,循环跳出条件是remove_len< 1/1024 * len_p2。

    • 跳过in_buf从remove_pos偏移处的trim_avail字节,写入到.cur_input中。
    • 调用**fault = run_target(argv, exec_tmout)**,cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST)。
    • 如果cksum == q->exec_cksum,就把原本测试样例中少去的这段给删除,实现了测试样例的简化。
    • 否则remove_pos += remove_len。
    • show_stats()
  • 如果needs_write存在,则删除原测试样例,将简化后的测试样例写到同名文件中。

  • 返回fault。

calculate_score

根据速度,覆盖路径数,路径深度计算出一个得分,havoc变异阶段用。

common_fuzz_stuff

写测试样例进文件并进行测试。

  • 如果post_handler存在,则先执行out_buf = post_handler(out_buf, &len),对测试样例进行处理。
  • 调用write_to_testcase(out_buf, len),写入.cur_input。
  • 调用fault = run_target(argv, exec_tmout)。
  • 如果fault == FAULT_TMOUT,返回1。
  • 调用queued_discovered += save_if_interesting(argv, out_buf, len, fault)。
  • show_stats()。
  • 返回0。

write_to_testcase

从mem写len到.cur_input中。

save_if_interesting

如果这个测试样例很有趣就保存下来,保存就返回1,否则返回0.

  • 如果fault == crash_mode,但是没有出现新路径并且路径命中次数相同,则直接返回0.
  • 调用add_to_queue(fn, len, 0),添加新测试样例到队列中。
  • 调用queue_top->exec_cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST)。
  • 调用calibrate_case(argv, queue_top, mem, queue_cycle - 1, 0)评估queue。
  • 根据fault的值进行不同处理。

simplify_trace

  • 8字节一组,遍历trace_bits。

    • 如果mem不为空,mem8[i] = simplify_lookup[mem8[i]]。

    • 否则*mem = 0x0101010101010101ULL,即代表这8个路径都没有命中。

参考链接

https://eternalsakura13.com/2020/08/23/afl/

https://www.z1r0.top/2023/03/23/AFL-fuzz%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/#simplify-trace

https://forum.butian.net/share/2092