afl-gcc
find-as
用来寻找as的路径。分别从环境变量AFL_PATH,afl-gcc的路径(如果是绝对路径或相对路径调用afl-gcc),/usr/local/lib/afl三个地方寻找。如果找不到则报出相应错误。下列是我调试时打印出来的参数。
1 | afl-gcc: find_as |
edit_params
用来对原生gcc进行一个封装,加上了一些参数并存放到cc_params中。如笔者使用afl-gcc -o test test.c对文件进行编译,则会被封装成如下代码。
1 | gcc |
afl-as
edit_params
对原生as进行封装,参数存放到as_params中。同时通过传入的参数设置一些变量的值,并且定义好modified_file。原本传入的命令和经过修改的命令如下。
1 | argv as_params |
最终主函数中也会运行这个命令。
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 | function: —— 函数入口 |
trampoline
比如x64程序中插入的代码如下,会调用__afl_maybe_log,且rcx的值是一个不超过1<<16的随机数。
1 | "leaq -(128+24)(%%rsp), %%rsp\n" |
__afl_maybe_log
检查共享内存(__afl_area_ptr)是否已经被设置,如果没被设置就调用__afl_setup进行初始化,设置过就调用__afl_store。
1 | " movq __afl_area_ptr(%rip), %rdx\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 | " cmpb $0, __afl_setup_failure(%rip)\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 | " pushq %rdx\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 | #ifndef COVERAGE_ONLY |
afl-fuzz
setup_signal_handlers
注册必要的信号处理函数
check_asan_opts
读取环境变量ASAN_OPTIONS和MSAN_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_bits,virgin_tmout,virgin_crash。
- 如果in_bitmap为空,就初始化virgin_bits,virgin_tmout,virgin_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_VAR为shm_str。
- trace_bits = shmat(shm_id, NULL, 0); 启动对共享内存的访问。
init_count_class16
初始化count_class_lookup16,目的是提高count_class_lookup8的效率。先看一下count_class_lookup8定义。
1 | static const u8 count_class_lookup8[256] = { |
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_paths和pending_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_bits进first_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
用来检查是否出现新路径以及是否边的命中次数发生变化。
- 初始化current为trace_bits,virgin为virgin_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<2。
- 如果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
4mem16[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++。
- 如果top_rated[i]存在,并且temp_v[i >> 3] & (1 << (i & 7))存在。
- 遍历队列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
4queue_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
5bitflip 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
7static 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