AFL afl_fuzz.c 详细分析
检查是否为AFL_PYTHON_ONLY, 设置参数python_only,skip_deterministic用以跳过确定性步骤和不继续havoc/splice。
将新的测试用例插入队列,并初始化fname文件名称,增加cur_depth深度++ queued_paths测试用例数量++,pending_not_fuzzed没被fuzzed测试用例数量++,更新last_path_time = get_cur_time()。
u8* fname; /* File name for the test case 文件名 */
u32 len; /* Input length testcase大小 */
u8 cal_failed, /* Calibration failed? 校准失败? */
trim_done, /* Trimmed? 该testcase是否被修剪过 */
was_fuzzed, /* historical, but needed for MOpt */
passed_det, /* Deterministic stages passed? */
has_new_cov, /* Triggers new coverage? */
var_behavior, /* Variable behavior? */
favored, /* Currently favored? 当前是否被标记位favored(更多的fuzz机会)*/
fs_redundant; /* Marked as redundant in the fs? */
u32 bitmap_size, /* Number of bits set in bitmap bitmap中bit的数量*/
fuzz_level, /* Number of fuzzing iterations */
exec_cksum; /* Checksum of the execution trace trace_bits的checksum*/
u64 exec_us, /* Execution time (us) 执行时间延迟*/
handicap, /* Number of queue cycles behind */
n_fuzz, /* Number of fuzz, does not overflow */
depth; /* Path depth 路径深度*/
u8* trace_mini; /* Trace bytes, if kept 1个bit存一个byte的trace_mini */
u32 tc_ref; /* Trace bytes ref count top_rate[]中该testcase入选的次数 */
struct queue_entry *next, /* Next element, if any */
*next_100; /* 100 elements ahead */
};
u8* data; /* Dictionary token data */
u32 len; /* Dictionary token length */
u32 hit_cnt; /* Use count in the corpus */
};
/* 00 */ FAULT_NONE,
/* 01 */ FAULT_TMOUT,
/* 02 */ FAULT_CRASH,
/* 03 */ FAULT_ERROR,
/* 04 */ FAULT_NOINST,
/* 05 */ FAULT_NOBITS };
static void init_forkserver(char **argv)启动APP和它的forkserver。
if (pipe(st_pipe) || pipe(ctl_pipe)) PFATAL("pipe() failed");
//pid=0的话就是fork出来的子进程;!=0的话就是父进程 ,<0就是fork失败了
forksrv_pid = fork();
//fork失败就打印关键词弹出失败并退出 (这里是在干什么?)
if (forksrv_pid < 0) PFATAL("fork() failed");
//如果是子进程的话,就执行下面的:
if (!forksrv_pid) {
struct rlimit r;
//dup2函数:复制一个文件的描述符。它们经常用来重定向进程的stdin、stdout和stderr
if (dup2(use_stdin ? prog_in_fd : dev_null_fd, 0) < 0 ||
dup2(dev_null_fd, 1) < 0 ||
dup2(dev_null_fd, 2) < 0) {
*(u32*)trace_bits = EXEC_FAIL_SIG;
PFATAL("dup2() failed");
}
close(dev_null_fd);
close(prog_in_fd);
//该函数用于创建一个守护进程。在这个程序里面意思就是让该进程成为这个进程组的组长。
setsid();
....
//如果成功,执行execv用以开始执行程序,这个函数除非出错不然不会返回。
execv(target_path, argv);
//此处使用一个EXEC_FAIL_SIG 来告诉父进程执行失败。
*(u32*)trace_bits = EXEC_FAIL_SIG;
exit(0);
}
....
//fuzzer从server中读取了四个字节的hello ,那么forkserver程序就设置成功了,如果没有,接下来的代码就是检查错误。
if (rlen == 4) {//判断读取是否成功
OKF("All right - fork server is up.");
return;}
解答:我本来是学Windows的,以为execv()函数类似于windows中的createprocess,但实际上完全不一样。execv()函数执行之后永远不会返回(除非出错),且运行程序会替换当前进程,pid不变。这样fuzzer先fork出来,然后再调用execv()函数,也就是说子进程成为了targetApp。这样下面的一系列代码就能说的通了。
实际流程:fuzzer(调用init_forkserver)->fork fueezer--targetApp(forkserver) call fork ->targetAppfork(实际fuzz的程序)。fuzzer还是实际fuzzer程序的祖父进程。这样接下来的代码就很容易理解了。
static u8 run_target(char** argv, u32 timeout)执行目标应用程序,监控超时。返回状态信息。被调用的程序将更新trace_bits[]。该函数将在每次运行targetBinary的时候调用,次数非常多。
这个函数是在相当大的缓冲区上的每个exec()之后调用的,因此它需要非常快。我们以32位和64位的方式做这件事。因此它需要非常快。我们以32位和64位的方式做这件事。
在给入口可以覆盖的变迁内,不妨设ID为i,比较现在种子的执行时间x种子长度是否小于原来top_rated[i],如果小,则更新之。
static void cull_queue(void) 精简队列,上面第二个被讨论的机制是:检查toprated[]类目,以此前未见过的byte依次争夺优胜者,然后把他们标记为favored在下次开始跑之前。根据top_rated设置queue中的favored标志。在fuzz的过程中favored 条目将会给与更多的时间。
cull_queue()遍历top_rated[]中的queue,然后提取出发现新的edge的entry,并标记为favored,使得在下次遍历queue时,这些entry能获得更多执行fuzz的机会。
这里本质上采用了贪婪算法,如果top_rated[i]存在,且对应temp_v[]中对应bit位还没抹去,即这一轮选出的queue还没覆盖bit_map[i]对应的边,则取出这个top_rated[i]。抹去temp_v中top_rated[i]能访问到的位。最后将这个top_rated[i]标记为favored,如果这个queue还没fuzzed,pending_favored++。
//判断每个byte的top_rated是否存在 该byte对应的temp_v是否被置为1。
if (top_rated[i] && (temp_v[i >> 3] & (1 << (i & 7))))
{
u32 j = MAP_SIZE >> 3;
/* 从temp_v中,移除所有属于当前current-entry的byte,也就是这个testcase触发了多少path就给tempv标记上*/
while (j--)
if (top_rated[i]->trace_mini[j])
temp_v[j] &= ~top_rated[i]->trace_mini[j];
top_rated[i]->favored = 1;
queued_favored++;
if (top_rated[i]->fuzz_level == 0 || !top_rated[i]->was_fuzzed) pending_favored++;
}
s1可覆盖t2,t3 | s2覆盖t0,t1,t4,并且top_rated[0]=s2,top_rated[2]=s1
开始后判断temp_v[0]=1,说明t0没有被访问
top_rated[0]存在(s2) -> 判断s2可以覆盖的范围 -> trace_mini=[1,1,0,0,1]
更新temp_v=[0,0,1,1,0]
标记s2为favored
继续判断temp_v[1]=0,说明t1此时已经被访问过了,跳过
继续判断temp_v[2]=1,说明t2没有被访问
top_rated[2]存在(s1) -> 判断s1可以覆盖的范围 -> trace_mini=[0,0,1,1,0]
更新temp_v=[0,0,0,0,0]
标记s1为favored
此时所有tuple都被覆盖,favored为s1,s2
a. static void show_init_stats(void)在处理输入目录的末尾显示快速统计信息,并添加一系列警告。一些校准的东西也在这里结束了,还有一些硬编码的常量。也许最终会清理干净。
判断queue_cur是否为空,如果是,则表示已经完成对队列的遍历,初始化相关参数,重新开始遍历队列 找到queue入口的testcase,seek_to = find_start_position();直接跳到该testcase 如果一整个队列循环都没新发现,尝试重组策略。 调用关键函数fuzz_one()对该testcase进行fuzz。fuzz_one()函数参见3.4。 上面的变异完成后,AFL会对文件队列的下一个进行变异处理。当队列中的全部文件都变异测试后,就完成了一个”cycle”,这个就是AFL状态栏右上角的”cycles done”。而正如cycle的意思所说,整个队列又会从第一个文件开始,再次进行变异,不过与第一次变异不同的是,这一次就不需要再进行deterministic fuzzing了。如果用户不停止AFL,那么seed文件将会一遍遍的变异下去。
static u8 fuzz_one_original(char** argv)从队列中取出当前testcase并模糊。这个函数太长了…如果fuzzed成功,返回0;如果跳过或退出,返回1。
步骤:
根据是否有pending_favored和queue_cur的情况按照概率进行跳过;有pending_favored, 对于fuzz过的或者non-favored的以概率99%跳过;无pending_favored,95%跳过fuzzed&non-favored,75%跳过not fuzzed&non-favored,不跳过favored。 假如当前项有校准错误,并且校准错误次数小于3次,那么就用calibrate_case进行测试。 如果测试用例没有修剪过,那么调用函数trim_case对测试用例进行修剪。详见3.5 修剪完毕之后,使用calculate_score对每个测试用例进行打分。函数详见3.6 如果该queue已经完成deterministic阶段,则直接跳到havoc阶段 deterministic阶段变异4个stage,变异过程中会多次调用函数common_fuzz_stuff函数见3.8,保存interesting 的种子:
bitflip,按位翻转,1变为0,0变为1
arithmetic,整数加/减算术运算
interest,把一些特殊内容替换到原文件中
dictionary,把自动生成或用户提供的token替换/插入到原文件中
havoc,中文意思是“大破坏”,此阶段会对原文件进行大量变异。
splice,中文意思是“绞接”,此阶段会将两个文件拼接起来得到一个新的文件。详细变异策略见3.7。
该 testcase完成。
首先取testcase长度2的指数倍
第一个while循环,从文件大小1/16的步长开始,慢慢到文件大小的1/1024倍步长。
第二个while循环,嵌套在第一个内,从文件头开始按步长cut testcase,然后target_run();如果删除之后对文件执行路径没有影响那么就将这个删除保存至实际文件中。再删除之前会将trace_bits保存到起来。删除完成之后重新拷贝。如果不清楚看下面代码。
....
static u8 clean_trace[MAP_SIZE];
u8 needs_write = 0
/* 从文件长度1/16开始最到最小1/1024步长,设置移除文件的大小 */
while (remove_len >= MAX(len_p2 / TRIM_END_STEPS, TRIM_MIN_BYTES)) {
/*按选定的步长,移除,然后循环该文件*/
while (remove_pos < q->len) {
//删除
write_with_gap(in_buf, q->len, remove_pos, trim_avail);
//执行
fault = run_target(argv, exec_tmout);
/* 检查trace_bit是否不一样 */
cksum = hash32(trace_bits, MAP_SIZE, HASH_CONST);
/* 如果删除对跟踪没有影响,则使其永久。作者表明可能可变路径会对此产生一些影响,不过没有大碍*/
if (cksum == q->exec_cksum) {
memmove(in_buf + remove_pos, in_buf + remove_pos + trim_avail, move_tail);
/* 保存之前的trace_bits,因为执行如果改变了trace_bits*/
if (!needs_write) {
memcpy(clean_trace, trace_bits, MAP_SIZE);}
} else remove_pos += remove_len;
}
remove_len >>= 1; //增加步长
}
}
用户可能会提供低质量的起始语料库,某些类型的突变可能会产生迭代地增加生成文件的大小的效果,因此应对这一趋势是很重要的。
幸运的是,插装反馈提供了一种简单的方法来自动删除输入文件,同时确保对文件的更改不会对执行路径产生影响。
在afl-fuzz中内置的修边器试图按可变长度和stepover顺序删除数据块;任何不影响跟踪映射校验和的删除都被提交到磁盘。修剪器的设计并不是特别彻底;相反,它试图在精度和在进程上花费的execve()调用的数量之间取得平衡,选择块大小和stepover来匹配。每个文件的平均增益大约在5%到20%之间。
首先,工具自动选择操作模式。如果初始输入崩溃了目标二进制文件,afl-tmin将以非插装模式运行,只需保留任何能产生更简单文件但仍然会使目标崩溃的调整。如果目标是非崩溃的,那么这个工具使用一个插装的模式,并且只保留那些产生完全相同的执行路径的微调。
因为变异阶段没有什么太多逻辑性的东西,我觉得不需要再进行太多补充解释。此处内容大多来源于:https://blog.csdn.net/Chen_zju/article/details/80791268 也可以前往观看。
作为精妙构思的fuzzer,AFL不会放过每一个获取文件信息的机会。这一点在bitflip过程中就体现的淋漓尽致。具体地,在上述过程中,AFL巧妙地嵌入了一些对文件格式的启发式判断。包括自动检测token和生成effector map。
例如,PNG文件中用IHDR作为起始块的标识,那么就会存在类似于以下的内容
.......IHDR........
当翻转到字符I的最高位时,因为IHDR被破坏,此时程序的执行路径肯定与处理正常文件的路径是不同的;随后,在翻转接下来3个字符的最高位时,IHDR标识同样被破坏,程序应该会采取同样的执行路径。由此,AFL就判断得到一个可能的token:IHDR,并将其记录下来为后面的变异提供备选。
AFL采取的这种方式是非常巧妙的:就本质而言,这实际上是对每个byte进行修改并检查执行路径;但集成到bitflip后,就不需要再浪费额外的执行资源了。此外,为了控制这样自动生成的token的大小和数量,AFL还在config.h中通过宏定义了限制.
对于一些文件来说,我们已知其格式中出现的token长度不会超过4,那么我们就可以修改MAX_AUTO_EXTRA为4并重新编译AFL,以排除一些明显不会是token的情况。遗憾的是,这些设置是通过宏定义来实现,所以不能做到运行时指定,每次修改后必须重新编译AFL。
EFF_APOS - position of a particular file offset in the map. 文件偏移
EFF_ALEN - length of a map with a particular number of bytes. 特殊字符的长度
EFF_SPAN_ALEN - map span for a sequence of bytes. 一个字节序列的映射
*/
#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)
这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。
由此,通过极小的开销(没有增加额外的执行次数),AFL又一次对文件格式进行了启发式的判断。看到这里,不得不叹服于AFL实现上的精妙。
不过,在某些情况下并不会检测有效字符。第一种情况就是dumb mode或者从fuzzer,此时文件所有的字符都有可能被变异。
arith 8/8,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异 arith 16/8,每次对16个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个word进行整数加减变异 arith 32/8,每次对32个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个dword进行整数加减变异
加减变异的上限,在config.h中的宏ARITH_MAX定义,默认为35。所以,对目标整数会进行+1, +2, …, +35, -1, -2, …, -35的变异。特别地,由于整数存在大端序和小端序两种表示方式,AFL会贴心地对这两种整数表示方式都进行变异。
此外,AFL还会智能地跳过某些arithmetic变异。第一种情况就是前面提到的effector map:如果一个整数的所有bytes都被判断为“无效”,那么就跳过对整数的变异。
interest 8/8,每次对8个bit进替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
interest 16/8,每次对16个bit进替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
interest 32/8,每次对32个bit进替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换
而用于替换的”interesting values”,是AFL预设的一些比较特殊的数。这些数的定义在config.h文件中,可以看到,用于替换的基本都是可能会造成溢出的数。
与之前类似,effector map仍然会用于判断是否需要变异;此外,如果某个interesting value,是可以通过bitflip或者arithmetic变异达到,那么这样的重复性变异也是会跳过的。
其中,用户提供的tokens,是在词典文件中设置并通过-x选项指定的,如果没有则跳过相应的子阶段。
if ((extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) ||
extras[j].len > len - i ||
!memcmp(extras[j].data, out_buf + i, extras[j].len) ||
!memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) {
stage_max--;
continue;
}
由上述代码也可以看到,effector map在这里同样被使用了:如果要替换的目标bytes全部是“无效”的,那么就跳过这一段,对下一段目标执行替换。
但是,对于插入这种变异方式,恢复起来则复杂的多,所以AFL采取的方式是:将原文件分割为插入前和插入后的部分,再加上插入的内容,将这3部分依次复制到目标缓冲区中(当然这里还有一些小的优化,具体可阅读代码)。
随机选取某个bit进行翻转 随机选取某个byte,将其设置为随机的interesting value 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value 随机选取某个byte,对其减去一个随机数 随机选取某个byte,对其加上一个随机数 随机选取某个word,并随机选取大、小端序,对其减去一个随机数 随机选取某个word,并随机选取大、小端序,对其加上一个随机数 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数 随机选取某个byte,将其设置为随机数 随机删除一段bytes 随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数 随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
怎么样,看完上面这么多的“随机”,有没有觉得晕?
common_fuzz_stuff(char** argv, u8* out_buf, u32 len) 编写修改后的测试用例,运行程序,处理结果。
步骤:
static u8 save_if_interesting(char** argv, void* mem, u32 len, u8 fault)判断是否为感兴趣的输入,判断一个文件是否是感兴趣的输入(has_new_bits),即是否访问了新的tuple或者tuple访问次数发生变化,如果是则保存输入文件(放到队列queue中)。
步骤:
判断queue_cur是否为空,如果是,则表示已经完成对队列的遍历,初始化相关参数,重新开始遍历队列
fuzz_one() 函数会对queue_cur所对应文件进行fuzz,包括(跳过-calibrate_case-修剪测试用例-对用例评分-确定性变异或直接havoc&ssplice)
判断是否结束,更新queue_cur和current_entry
当队列中的所有文件都经过变异测试了,则完成一次”cycle done”。整个队列又会从第一个文件开始,再次继续进行变异
个人感觉的AFL难点在于forkserver和fuzzer交互运行之间的关系那一块,另外就是bitmap相关的,trace_bits,trace_mini,virgin_bits,top_rate[],这几个变量都是干什么的?在那个阶段改变的?哪个函数改变的?又是谁对它们进行的操作?如果这几点理解了,那么整个AFL就比较容易理解了。
https://www.cnblogs.com/0xHack/p/9414444.html
https://bbs.pediy.com/thread-249179.htm
https://bbs.pediy.com/thread-250866.htm
https://paper.seebug.org/841/
https://paper.seebug.org/842/
https://www.cnblogs.com/WangAoBo/p/8280352.html
http://zeroyu.xyz/2019/05/15/how-to-use-afl-fuzz/
https://spin.atomicobject.com/2015/08/23/fuzz-testing-american-fuzzy-lop/
https://blog.csdn.net/youkawa/article/details/45696317
https://blog.csdn.net/youkawa/article/details/76405468
https://blog.csdn.net/youkawa/article/details/76615480
https://ljie.space/2018/01/22/afl%E6%8A%80%E6%9C%AF%E7%99%BD%E7%9A%AE%E4%B9%A6%E7%AC%94%E8%AE%B0-1/
https://blog.csdn.net/gengzhikui1992/article/details/50844857
https://bbs.pediy.com/thread-249912.htm
https://bbs.pediy.com/thread-218671.htm
https://blog.csdn.net/qq_32464719/article/details/80592902#comments
http://rk700.github.io/2017/12/28/afl-internals/
http://rk700.github.io/2018/01/04/afl-mutations/
http://rk700.github.io/2018/02/02/afl-enhancement/
https://blog.csdn.net/Chen_zju/article/details/80791268
https://xz.aliyun.com/t/4628
https://ch4r1l3.github.io/2019/03/05/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%901%E2%80%94%E2%80%94afl-gcc-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
https://ch4r1l3.github.io/2019/03/06/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%902%E2%80%94%E2%80%94afl-as-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
https://ch4r1l3.github.io/2019/03/08/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%903%E2%80%94%E2%80%94afl-as-h%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90/
https://ch4r1l3.github.io/2019/03/09/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%904%E2%80%94%E2%80%94afl-fuzz-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%901/
https://ch4r1l3.github.io/2019/03/10/AFL%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%905%E2%80%94%E2%80%94afl-fuzz-c%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%902/
https://www.cnblogs.com/jg01/p/9772700.html
https://barro.github.io/2018/06/afl-fuzz-on-different-file-systems/
https://foxglovesecurity.com/2016/03/15/fuzzing-workflows-a-fuzz-job-from-start-to-finish/
https://github.com/google/AFL
https://bbs.pediy.com/thread-251051.htm
https://github.com/vanhauser-thc/AFLplusplus
https://bbs.pediy.com/thread-249986.htm
最后欢迎各位研究fuzz以及漏洞挖掘的朋友讨论和指正。
看雪ID:0saber0
https://bbs.pediy.com/user-760932.htm
推荐文章++++