FuzzingLab
前言
该文章是我还未完成分析的.c文件的,我会对fuzz的文件进行分析,在分析的途中,由于我还未对fuzz有过多的进行实质性的应用和简单的测试,所以我有很多源码的地方可能会写得不好,或者不知道,但我都会在文章中做好记号,来方便未来我对自己的分析进行补坑。
分析的函数顺序便是程序执行流程的顺序,所以我会简单的说出每个部分的作用。
编译
直接编译可能会遇到以下错误
[-] PROGRAM ABORT : Suboptimal CPU scaling governor
Location : check_cpu_governor(), afl-fuzz.c:7408
输入echo core > /proc/sys/kernel/core_pattern
然后又遇到如下错误
[-] PROGRAM ABORT : Suboptimal CPU scaling governor
Location : check_cpu_governor(), afl-fuzz.c:7408
输入export AFL_SKIP_CPUFREQ=1
AFL-Fuzz.c
fuzz初始化
对参数进行解析
while ((opt = getopt(argc, argv, "+i:o:f:m:b:t:T:dnCB:S:M:x:QV")) > 0)
switch (opt) {
....
}
判断是否有输入目录或者输出目录是否为空,为空则直接退出
if (optind == argc || !in_dir || !out_dir) usage(argv[0]);
setup_signal_handlers
注册一些信号处理函数。
handle_stop_sig
将stop_soon
设置为1
如果child_pid
存在,则给child_pid
发送SIGKILL
信号
如果forksrv_pid
存在,则给forksrv_pid
发送SIGKILL
信号
SIGHUP/SIGINT/SIGTERM
挂起/终端/终止信号
SIGALRM
alarm发出的信号
处理超时
如果child_pid
存在,child_timed_out=1
,并向child_pid
发送信号SIGKILL
如果child_pid=-1
并且forksrv_pid
大于0,则设hichild_timed_out=1
,并向forksrv_pid
发送SIGKILL
信号
SIGWINCH
窗口大小改变
处理窗口大小
clear_screen=1
SIGUSR1
用户信号1
处理跳过请求
设置skip_requested为1
SIGTSTP
进程停止
忽略信号
SIGPIPE
管道破损,没有读端的管道写数据
忽略信号
check_asan_opts
获取环境变量ASAN_OPTIONS和MSAN_OPTIONS的值然后检查其设置的参数值是否满足,否则报错。
fix_up_sync
如果sync_id存在才会调用该函数
使用 -S时,验证并修复out_idr和sync_dir(这里不太理解sync_dir和sync_id是干嘛的,到时候需要动调确认一下)
看了Sakura师傅的解释是
如果通过-M或者-S指定了sync_id,则更新out_dir和sync_dir的值
设置sync_dir的值为out_dir
设置out_dir的值为out_dir/sync_id
save_cmdline
调用该函数前,会对一些环境变量和参数进行检查。
该函数时保存当前命令行到变量orig_comline中
fix_up_banner
修建并且创建一个运行横幅(这里不太懂横幅是什么意思)
Check_if_tty
检查是否在tty终端设备上运行的
读取环境变量AFL_NO_UI的值,如果存在,则设置not_on_tty=1标识不在tty设备上。
否则调用Ioctl(1,TIOCGWINSZ,&ws)通过ioctl来读取Window size,如果报错为ENOTTY,则代表当前不在一个tty终端设备运行,设置not_on_tty
get_core_count
获取cpu核心数量
bind_to_free_cpu
绑定空闲的cpu
check_crash_handl
检查核心存储不会进入程序中
check_cpu_governor
检查cpu调节器
setup_post
setup_shm
获取共享内存并且初始化virgin_bits,这个将会在程序调用时被使用
init_count_class16
初始化count_class_lookup16
,这里的count_class_lookup8
用一个字节是为了记录该路径的次数,4-7都算8次
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
};
至于这里使用双字节来初始化,我是看了sakura师傅的分析,是为了效率,不过效果还是跟一个字节是差不多的,只是范围变大了
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];
setup_dirs_fds
如果sync_id存在,则打开sync_dir,权限为rwx
if (sync_id && mkdir(sync_dir, 0700) && errno != EEXIST)
PFATAL("Unable to create '%s'", sync_dir);
创建ouput,权限为rwx,如果报错不是EEXIST则报错
if (mkdir(out_dir, 0700)) {
if (errno != EEXIST) PFATAL("Unable to create '%s'", out_dir);
maybe_delete_out_dir();
}
否则如果设置了in_place_resume,则抛出异常
if (in_place_resume)
FATAL("Resume attempted but old output directory not found");
否则以只读形式打开out_dir
out_dir_fd = open(out_dir, O_RDONLY);
接着创建目录
out_dir/queue/ 权限0700
out_dir/queue/.state/ 用于保存queue metadata、session resume、related task
out_dir/queue/.state/deterministic_done/ 标记过去经历过deterministic fuzzing的queue entries
out_dir/queue/.state/auto_extras/ auto-selected dictionary entries目录
out_dir/queue/.state/redundant_edges/ 保存多余路径
out_dir/queue/.state/variable_behavior/ 显示可变行为的路径集
如果sync_id存在创建out_dir/.synced/权限为0700,该目录主要用于用于跟踪cooperating fuzzers
创建目录out_dir/crashes 权限为0700,保存recorded crash
创建目录out_dir/hangs 权限为0700,所有recorded hangs
通常有用的文件描述符
dev_null_fd = open("/dev/null", O_RDWR);以读写模式打开/dev/null
dev_urandom_fd = open("/dev/urandom", O_RDONLY);,以只读模式打开/dev/urandom
创建Gnuplot输出文件
fd = open(tmp, O_WRONLY | O_CREAT | O_EXCL, 0600);//以只写方式打开out_dir/plot_data文件,如果文件不存在,就创建,并获取句柄
plot_file = fdopen(fd, "w");//根据句柄得到FILE* plot_file
向其中写入# unix_time, cycles_done, cur_path, paths_total, pending_total, pending_favs, map_size, unique_crashes, unique_hangs, max_depth, execs_per_sec\n
read_testcases
读取所有input目录中的测试样例,并把它加入到queue中
打开fn=input/queue目录,如果有则让in_dir=fn,否则释放fn
利用scandir扫描input目录
遍历in_dir下的所有文件或目录和in_dir/.state/determinsitic_done/下的所有文件或目录
过滤目录下的.和..
通过access来判断文件是否存在这个检查是用来判断是否这个entry已完成deterministic fuzzing。在恢复异常终止的扫描时,我们不想重复deterministic fuzzing,因为这将毫无意义,而且可能非常耗时
利用add_to_queue
将其加入队列中
判断queued_paths是否存在,设置queue_at_start 为 queued_paths
load_auto
加载自动生成的提取出来的词典token
以权限0600,只读形式打开`in_dir/.state/atuo_extras/auto_i`
读取文件的内容,并判断其大小是否在`MIN_AUTO_EXTRA`和`MAX_AUTO_EXTRA`中,在则判断其是否可以加入自动生成字典中maybe_add_auto
privot_inputs
为input_dir在out_dir创建硬连接
load_extras
如果定义了extras_dir,则从extras_dir读取extras到extras数组里,并按size排序。
find_timeout
超时时间
detect_file_args
解析@@参数
setup_stdio_file
如果out_file不存在,则删除out_dir/.cur_input,再重新创建
check_binary
查看目标文件是否存在,并且目标文件不是shell脚本文件
get_cur_time
获取当前时间
perform_dry_run
对所有测试用例进行试运行,以确认应用程序按预期工作。这只对初始输入执行一次。
获取变量AFL_SKIP_CRASHES
遍历q(在前面已经把测试样例添加了进去)
打开测试用例,读取数据
调用res = calibrate_case(argv, q, use_mem, 0, 1);校准测试样例
如果stop_soon(用户自己中断的)为1,则直接返回
如果res的结果为crash_mode或者FAULT_NOBITS
打印SAYF("len = %u, map size = %u, exec speed = %llu us\n", q->len, q->bitmap_size, q->exec_us);
FAULT_NONE:
check_map_coverage检查map coverage范围,为第一个测试用例调用
计算trace_bits发现的路径数是否大于100,小于100则直接返回
查看trace_bits后半段是否有发现的路径,如果有则直接返回
否则抛出异常
如果是crash_mode模式,则抛出异常,该文件不崩溃
FAULT_TMOUT:
如果指定了-t参数,则timeout_given值为2
FAULT_CRASH:
如果是crash_mode模式,则直接跳出
如果skip_crashes为1,则抛出异常,并设置q->cal_failed=CAL_CHANCES,cal_failures++
FAULT_ERROR:
抛出异常,不能执行目标文件
FAULT_NOINST:
抛出异常,这个样例运行没有出现任何路径信息
FAULT_NOBITS:
如果这个样例出现路径信息,但是没有任何新路径,则抛出警告,认为这是无用路径,useless_at_start计数器加一
如果q->var_behavior为真,则代表它多次运行,同样的输入条件下,却输出不同的覆盖信息
抛出警告,这个样例的路径输出可变
如果cal_failures错误次数等于queued_paths路径总数,抛出异常:所有测试时间超时或者crash,。
如果错误次数是总输入样例的1/5则发出警告
calibrate_case(char* argv,struct queue_entry q,u8* use_mem,u32 handicap,u8 from_queue)
校准新的测试用例,这是在处理输入目录时完成的,以在早期警告不可靠或有问题的测试用例;以及在发现新的路径时,评估这个新发现的testcase的行为是否可变
如果first_run=q->exec_cksum==0就代表是第一次运行
设置old_sc=stage_cur,old_sm=stage_max,use_tmout=exec_tmout,old_sn=stage_name
如果from_queue是0或者resuming_fuzz是1,即代表不来自于queue或resuming sessions的时候,则use_tmout的值被设置的更大一些
q->failed加一,stage->name="calibration",stage_max=fast_cal?3:CAL_CYCLES,stage_max含义是每个测试用例的校准周期数,也就是说这个stage要执行几次
不是dumb模式,no_forserver(禁用forserver)并且forkserver没有启动,则调用init_forserver(),来启动for server
如果queue->exec_cksum不是0,如果这个queue不是来自queue文件夹,而是评估新的case,则此时q->exec_cksum不为空,拷贝trace_bits到first_trace中,然后计算has_new_bits的值,赋给new_bits
if(q->exec_cksum){
memcpy(first_trace,trace_bits,MAP_SIZE);
hnb=has_new_bits(virgin_bits);
if(hnb>new_bits) new_bits=hnb;
}
如果queue->exec_sum不是0,即代表这是第一次运行
进入循环,循环stage_max次
查看是否是第一次运行,并且`!(stage_cur%stats_update_freq)`代表着运行几次更新一次,然后执行`show_stats()`来更新当前的状态
利用`write_to_testcase`将`q->frame`里的文件写入`out_dir/.cur_input`
`run_target(char **argv,u32 timeout);`运行目标文件,返回状态信息并保存在falut,调用程序更新trace_bits[]
如果(Ctrl+C)stop_soon为1或者fault不等于crash_mode则直接goto abort_calibration
如果不是dumb模式,并且是第一次运行,并且没有共享内存里没有任何路径,则设置fault为`FAULT_NOINST`然后goto abort_calibration
`cksum=hash32(trace_bits,MAP_SIZE,HASH_CONST);`计算当前路径的hash值保存在cksum里
如果当前q->exec不等于cksum,代表发现了新的路径或者这是第一次运行
hnb=has_new_bits(virgin_bits);
如果hnb的值大于new_bits则设置new_bits=hnb
如果q->exec_cksum如果不是第一次运行
遍历并判断var_bytes[i]是否为0(还未经过),first_trace[i]是否不等于trace_bits[i](新的路径)
如果是,则设置var_byts[i]等于1,stage_max=CAL_CYCLES_LONG,则直接将当前循环次数修改为40次
设置var_detected=1,发现路径可变
否则设置q->exec_cksum等于cksum
并拷贝trace_bits到first_trace
循环结束后,计算出总的时间`total_cal_us+=stop_us-start_us`和总的循环次数`total_cal_cycles+=stage_max`
更新q
`q->exec_us=(stop_us-start_us)/stage_max`
`q->bitmap_size=count_byts(trace_bits)`
`q->handicap=handicap`
`q->cal_failed=0`
加上这个q的总的覆盖数,`total_bitmap_size+=q->bitmap_size`并将total_bitmap_entries加一
`update_bitmap_score(q);`
如果fault为FAULT_NONE,不是dumb模式,且是当前该queue第一次执行,new_bits为0,代表在这个样例所有执行里,没有发现新的路径和异常出现,设置fault为FAULT_NOBITS
如果new_bits为2并且q没有被覆盖过,则设置q->has_new_cov来设置被覆盖过,并设置总覆盖数queued_with_cov加1
如果发现了可变路径,则计算var_bytes里被置位的个数赋给var_byte_count代表这些位是可变行为,并检查q->behavior是否为0
如果为0,mark_as_variable(q)
创建符号链接out_dir/queue/.state/variable_behavior/q->frame
设置queue的var_behavior为1
queued_variable加一
恢复stage_name为old_sn,stage_cur=old_sc,stage_max=old_sm
如果不是第一次运行,则展示当前状态
返回fault
init_forkserver
建立管道st_pipe和管道ctl_pipe,一个用于传递状态,一个用于传递命令
fork一个子进程
将文件描述符1和2重定向到dev_null_fd
如果指定了out_file,则将文件描述符0重定向导dev_null_fd
否则将文件描述符重定向到out_file并关闭out_file
重定向FORKSRV_FD到ctl_pipe[0],重定向FORKSRV_FD+1到st_pipe[1]
子进程只能读取命令
子进程只能发送("写出")状态
关闭子进程里的一些文件描述符
获取环境变量LD_BIND_LAZY,如果没有设置,则设置环境变量LD_BIND_NOW为1,后续也是设置了一些环境变量
执行目标进程`execv(target_path,argv);`这个函数除非出错,否则不会返回,execv会替换掉原有的进程空间为target_path代表殴打程序,所以这里就相当于直接去执行了target_path。
而在这里非常特殊,第一个target会进入__afl_maybe_log里的__afl_fork_wait_loop,并充当fork server,在整个fuzz过程中,它都不会结束,每次要Fuzz一次target,都会从这个fork server fork出来一个子进程(摘自Sakura大佬的话)
将trave_bits=EXEC_FAIL_SIG,来告诉父进程执行失败,并结束子进程
关闭不需要的指针
fsrv_ctl_fd=ctl_pipe[1];
fsrv_st_fd=st_pipe[0];
等待fork server启动,但不能等待太久,
读取fork进程中的状态,判断是否启动成功
has_new_bits(u8*virgin_map)'
检查有没有新路径或者某个路径的执行次数不同,注意virgin_bits在前面被设置为了0xff
初始化设置了ret为0
进入循环,8个字节一组,每次从trace_bits取出8个字节
如果current的首字节地址为cur,virgin的首字节地址为vir
i的范围是0-7,比较cur[i]&&vir[i]==0xff,如果为真,则设置ret=2
这代表发现了之前没有出现过的路径
否则ret为1
这代表仅仅只是改变了某个路径的hit-count(命中次数)
current和virgin移动到下一组8个字节中,直到MAPSIZE全被遍历完
如果传入给has_new_bits的参数virgin_map等于virgin_bits并且ret不为0,则将Bitmap_changed设置为1
virgin_bits保存还没有被fuzz覆盖到的byte,其初始值每位都被置位1,然后每次按字节置位
返回ret的值
count_bytes
计算按4个字节进行查询mem不为0,进行加一
update_bitmap_score
更新最优路径,每当我们发现一个新的路径,找出所有路径集合中最小的路径集合。
首先算出fav_factor=q->exec_us(执行时间)*q->len;
循环迭代
如果当前trace_bits[i]存在
如果top_rated[i]存在
如果当前路径的fav_factor大于top_rated的fac_factor则直接进入下一次循环
否则
top_rated[i]的引用减1,如果为0,则释放
top_rated[i]=q,q->tc_ref加一
如果q->trace_mini不存在,则为其分配空间,并调用minimize_bits将其进行压缩放入trace_mini字段
设置score_changed为1(说明修改过最优路径)
minimize_bits(u8 dst,u8 src)
这个先鸽了,等精神缓过来,再看压缩算法
cull_queue
精简队列
如果是dumb模式或者score_changed未被置1,则直接返回
遍历q并设置q->favored为0
遍历top_rated[],这个迭代主要是为了筛选出能够快速遍历整个路径的最快最小的一组queue,这是贪心算法,不是最优的,具体分析先鸽了
mark_as_redunant(strucrt queue_entry*q,u8 state)
鸽
show_init_stats
鸽
find_start_position
鸽
write_stats_file
鸽
save_auto
鸽
Fuzz执行
该部分主要是数据变异阶段,每一次编译都会fuzz一次
首先会进入
trim阶段
该阶段会将输入样例进行裁剪,会根据输入的串长度大小进行裁减,进行fuzz
performance score阶段
orig_perf = perf_score = calculate_score(queue_cur);
如果skip_deterministic或passed_det为1或者当前q已经被fuzz过则跳到havoc_stage阶段
如果exec path checksum将其超出此主实例的范围,则跳过确定性模糊。
flip
flip1/1:循环len<<3次,每次对位翻转一次,就调用common_fuzz_stuff一次,如果遇到SIGUSR1则抛弃这次,再翻转回来,如果这次的路径与上次的路径不一样就当作一个token,并利用maybe_add_auto将这段token字节序列添加进去
flip2/1:循环len<<3次,每次翻转2个bit,就调用一次comm_fuzz_stuff,调用完后恢复
flip4/1:循环len<<3次,每次翻转2个bit,就调用一次comm_fuzz_stuff,调用完后恢复,
#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)
flip8/8:循环len<<3次,每次翻转8个bit,就调用一次comm_fuzz_stuff,调用完后恢复,该部分调用完,会生成一个eff_map,如果eff_map[stage_cur>>3]为0,则检查路径是否与上一次一样,如果不一样,则设置eff_map[stage_cur>>3]为1,并设置eff_cnt++
然后做了一些检查
flip16/8:循环len<<3次,每次翻转16个bit,就调用一次comm_fuzz_stuff,调用完后恢复,但在每次调用之前会检查eff_map[i]如果为0则直接跳过
flip32/8:循环len<<3次,每次翻转32个bit,就调用一次comm_fuzz_stuff,调用完后恢复,但在每次调用之前会检查eff_map[i]如果为0则直接跳过
ARITHMETIC INC/DEC
arith8/8:先利用eff_map判断是否跳过,不跳过利用输入样例与j进行相加在异或自身,则判断是否不是flip的倍数,如果不是则更新out_buf进行fuzz,在将r重新赋值,并判断是否为flip的倍数,不是也再fuzz一遍,最后再恢复orig的值
for (i = 0; i < len; i++) {
u8 orig = out_buf[i];
/* Let's consult the effector map... */
if (!eff_map[EFF_APOS(i)]) {
stage_max -= 2 * ARITH_MAX;
continue;
}
stage_cur_byte = i;
for (j = 1; j <= ARITH_MAX; j++) {
u8 r = orig ^ (orig + j);
/* Do arithmetic operations only if the result couldn't be a product
of a bitflip. */
if (!could_be_bitflip(r)) {
stage_cur_val = j;
out_buf[i] = orig + j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
r = orig ^ (orig - j);
if (!could_be_bitflip(r)) {
stage_cur_val = -j;
out_buf[i] = orig - j;
if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
stage_cur++;
} else stage_max--;
out_buf[i] = orig;
}
}
arith16/8:前两次都是根8/8操作一样的,但条件不一样,首先在第二次中orig要小于j的时候彩绘区判断其flip是否为倍数,第三次则orig>>8+j是否大于0xff,然后将orig+j高8字节与低8字节相交换,第四次orig>>8是否小于j,然后与第三次相反,在进行fuzz
arith32/8:根上面的差不多不做过多阐述,只是从word到dword了
INTERESTING VALUES
对数据进行替换,然后在某些阶段中会对替换的数据进行换位
DICTIONARY STUFF
拿extras的数据进行覆盖或者插入,还有auto_extras数据进行覆盖
RANDOM HAVOC
emm,这里参考sakura师傅的吧(偷个懒)
到此afl-fuzz.c基本上分析完了
afl-fuzz源码会反复观看的,第一次逆完的时候,有几个变量不知道是干啥的,但到后面突然明白作用了,但忘记其的产出方式,不知道分类是怎么样的了,目前计划3天复习一次吧