本文适合对象:已经对afl的流程有一定了解,自己跑过afl的各个功能;具有一定C编程基础,浏览过afl源码或者某个模块的源码。笔记分为五个大块对 afl-fuzz.c 进行分析:文件引用、预备工作、fuzzing的整体结构、关键函数实现原理、main函数。当然其中涉及到很多其他的头文件,也会对相关部分进行详述。
这部分几个头文件都比较重要,源码都是需要看的
标准库文件,基本跟 Windows 下的 C 编程都差不多,只有个别是Linux C下才会用到的
这一部分,引用了一些Linux环境下的特殊头文件,跟上一部分会有一些重叠,但是各司其职,实际编程两边的函数都可以,但是偏向于用这部分。对应Linux环境的头文件位置 /usr/include/sys/
需要深入了解看源代码的头文件是
这三个都需要查看源码,对后续查看、按需修改 afl源码 一定都会很有帮助。
这一部分,差不多前三百多行左右,到枚举类型结束。
Linux环境下独有的宏定义
在之后代码中,可以用 ifdef HAVE_AFFINITY
判断是否是Linux环境,这里的 AFFINITY 是亲和性的意思,跟cpu的亲和性,与运行性能有关 。 在定义之后的源代码中一共出现了五次 HAVE_AFFINITY
,因为是特定条件下的宏定义,所以也可以当是跟cpu操作相关的bool用,都是成对出现,比如:
只有在启用这种亲和性的情况下,才会定义 cpu_aff 用来标记选择的cpu核。
AFL_LIB库处理
不常用,当afl被编译成库的时候,用来保证变量的输出。//A toggle to export some variables
整个部分代码块最开始的注释块: / Lots of globals, but mostly for the status UI and other things where it really makes no sense to haul them around as function parameters. / 可以看出作者很谦虚的指出,这部分的全局变量大部分是跟 UI 展示的状态部分有关的,还有一部分是没必要 去把他们当作函数参数 (有些情况会造成代码冗余)。
这部分从四个方面考虑:
这部分跟头文件 "types.h"
息息相关,变量类型不清楚的时候就去那个头文件找答案。比如:
u8: typedef uint8_t u8;
u32: typedef uint32_t u32;
u64: 下面代码的意思,我猜测是为了兼容32和64不同结构情况下的定义
s32: typedef int16_t s16;
这里作者的变量定义是按代码块的结构写的, 【此部分每块变量的含义,待补】
在之后的整个fuzzing过程中,都会维护一些queue_entry类型的队列。
枚举类型的好处是可读性强,易于使用,作者定义了三部分枚举类型,用来表示自己想要展现的状态。具体用法会分析后举例说明 :
比如 DI(stage_finds[STAGE_FLIP1]), DI(stage_cycles[STAGE_FLIP1]),
其中 stage_finds
的定义static u64 stage_finds[32]
是一个大数组,用来保存不同的 fuzz stage
状态下的模式发现。
状态的值类型,这里是用来辅助、修饰上一个枚举fuzzing状态的,用来识别当前的状态枚举对应变异方式的类型
执行状态(针对错误fault),每个状态都有其特殊含义 【此部分每个状态的含义,待补】
这部分最重要的是对于全局变量四部分的理解,afl-fuzz.c文件最常用的变量都在这里面了,特别是测试用例的队列、枚举代表的含义。 还有就是要注意宏定义的内容,对afl的环境配置、整体的流程的分析都很有帮助。
后面的代码就是真正的 afl fuzz
过程代码,在开始分析详细的代码之前,需要从整体结构上了解掌握 afl-fuzz.c 的主要流程,这也是师兄给的主要任务。这里先不管 main
函数之前的函数是怎么实现原理的,先就通过简单的 main
函数的分析,结合各函数的原注释进行主要流程分析。
设置各种main函数需要的主要局部变量,一部分跟要调用函数的参数有关,一部分跟main函数主体相关。
这部分的设置变量,主要是main内使用,跟之外的都无关系,魔改的时候要注意这里。
while的判断条件是“命令行”里是否还有输入的参数,while内的命令判断是通过 switch - case
实现的:
i:输入文件夹,包含所有的测试用例 testcase o:输出文件夹,用来存储所有的中间结果和最终结果 M:设置主(Master)Fuzzer S:设置从属(Slave)Fuzzer f:testcase的内容会作为afl_test的stdin x:设置用户提供的tokens t:设置程序运行超时的时间,单位为ms m:设置分配的内存空间 d: B: C: n: T: Q: default: 如果输入错误,提示使用手册(当进行魔改的时候,可以在这个函数 usage(argv[0]);
里进行修改) 对afl进行魔改的时候,需要外界输入参数的话,可以从这里先入手,然后倒推到开头,整理出afl实现过程中的参数变化过程。
首先应该知道的是,AFL是根据二元tuple(跳转的源地址和目标地址)来记录分支信息,从而获取target的执行流程和代码覆盖情况。起始阶段 fuzzer
会进行一系列的准备工作,为记录插桩得到的目标程序执行路径,即 tuple
信息。
其中 trace_bits
记录当前的tuple信息;virgin_bits
用来记录总的tuple信息;virgin_tmout
记录fuzz过程中出现的所有目标程序的timeout时的tuple信息;virgin_crash
记录fuzz过程中出现的crash时的tuple信息;
初始化阶段有个重要的函数调用不得不提 init_forkserver
(关于此函数的详细解读在之后的函数实现原理part会详细说明),因为这个函数操作就是初始化 forkserver
。下面说一下forkserver为什么重要,以及到底是用来干嘛的。 fuzzing的大致思路是,对输入的文件不断变异,然后输入喂给target执行,检查是否会造成崩溃,这其中会涉及大量的fork和执行target过程。(在“[文件/数据]变异”部分就会见识到有多多了) AFL实现的这套 fork server
机制就是为了提高效率,把 fork
相关的操作集成起来,fuzzer不需要对fork负责,只需要与fork server交互即可。关于这部分的具体运行原理在之后会有详细说明。
对输入目录输出目录进行检查,检查cpu,检查内核数量等等。
正好这里涉及了对输入文件的变异策略,可以单独先提一下。在AFL的fuzz过程中,维护了一个 testcase 队列,每次把队列里的文件取出来之后,对其进行变异,下面就讲一下各个阶段的变异是怎样的。 注: bitflip、arithmetic、interest、dictionary
是 deterministic fuzzing
过程,属于dumb mode(-d) 和主 fuzzer(-M) 会进行的操作; havoc、splice
与前面不同是存在随机性,是所有fuzz都会进行的变异操作。文件变异是具有启发性判断的,应注意“避免浪费,减少消耗”的原则,即之前变异应该尽可能产生更大的效果,比如 eff_map
数组的设计;同时减少不必要的资源消耗,变异可能没啥好效果的话要及时止损。
STAGE_FLIP1
每次翻转一位(1 bit
),按一位步长从头开始。STAGE_FLIP2
每次翻转相邻两位(2 bit
),按一位步长从头开始。STAGE_FLIP4
每次翻转相邻四位(4 bit
),按一位步长从头开始。STAGE_FLIP8
每次翻转相邻八位(8 bit
),按八位步长从头开始,也就是说,每次对一个byte做翻转变化。STAGE_FLIP16
每次翻转相邻十六位(16 bit
),按八位步长从头开始,每次对一个word做翻转变化。STAGE_FLIP32
每次翻转相邻三十二位(32 bit
),按八位步长从头开始,每次对一个dword做翻转变化。
这一部分在 5135
行的 #define FLIP_BIT(_ar, _b) do {***}
有详细的代码实现。
源码中有一段关于这部分的注释,意思是说在进行为翻转的时候,程序会随时注意翻转之后的变化。比如说,对于一段 xxxxxxxxIHDRxxxxxxxx
的文件字符串,当改变 IHDR
任意一个都会导致奇怪的变化,这个时候,程序就会认为 IHDR
是一个可以让fuzzer很激动的“神仙值”--token。
/ While flipping the least significant bit in every byte, pull of an extra trick to detect possible syntax tokens. In essence, the idea is that if you have a binary blob like this: xxxxxxxxIHDRxxxxxxxx ...and changing the leading and trailing bytes causes variable or no changes in program flow, but touching any character in the "IHDR" string always produces the same, distinctive path, it's highly likely that "IHDR" is an atomically-checked magic value of special significance to the fuzzed format. We do this here, rather than as a separate stage, because it's a nice way to keep the operation approximately "free" (i.e., no extra execs). Empirically, performing the check when flipping the least significant bit is advantageous, compared to doing it at the time of more disruptive changes, where the program flow may be affected in more violent ways. The caveat is that we won't generate dictionaries in the -d mode or -S mode - but that's probably a fair trade-off. This won't work particularly well with paths that exhibit variable behavior, but fails gracefully, so we'll carry out the checks anyway.
/
其实token的长度和数量都是可以控制的,在 config.h
中有定义,但是因为是在头文件宏定义的,修改之后需要重新编译使用。
/ Maximum number of auto-extracted dictionary tokens to actually use in fuzzing (first value), and to keep in memory as candidates. The latter should be much higher than the former. /
在这里值得一提的是 effector map
,在看源码数据变异这一部分的时候,一定会注意的是在 bitflip 8/8 的时候遇到一个叫 eff_map
的数组,这个数组的大小是 EFF_ALEN(len)
(也就是【??】),数组元素只有 0/1 两种值,很明显是标记的意思,到底是标记什么呢? 要想明白 effector map
的原理需要了解三个点:
bitflip
结束之后,就进入 arithmetic
阶段,目标大小和阶段与 bitflip
非常类似:
arith 8/8,每次8bit进行加减运算,8bit步长从头开始,即对每个byte进行整数加减变异; arith 16/8,每次16bit进行加减运算,8bit步长从头开始,即对每个word进行整数加减变异; arith 32/8,每次32bit进行加减运算,8bit步长从头开始,即对每个dword进行整数加减变异;
其中对于加减变异的上限,在 config.h
中有所定义:
/ Maximum offset for integer addition / subtraction stages: /
注:跟bitflip相同的,如果需要修改此值,在头文件中修改完之后,要进行编译才会生效。
在这里对整数目标进行+1,+2,+3...+35,-1,-2,-3...-35的变异。由于整数存在大端序和小端序两种表示,AFL会对这两种表示方式都进行变异。 前面也提到过AFL设计的巧妙之处,AFL尽力不浪费每一个变异,也会尽力让变异不冗余,从而达到快速高效的目标。AFL会跳过某些arithmetic变异:
interest的三个步骤跟arithmetic相同:
interest 8/8,每次8bit进行加减运算,8bit步长从头开始,即对每个byte进行替换; interest 16/8,每次16bit进行加减运算,8bit步长从头开始,即对每个word进行替换; interest 32/8,每次32bit进行加减运算,8bit步长从头开始,即对每个dword进行替换;
用于替换的叫做 interesting values
,是AFL预设的特殊数:
/ Interesting values, as per config.h /
其中 interesting values
在 config.h
中的设定:
可以看到,基本是些会造成溢出的数值。与前面的思想相同的,本着“避免浪费,减少消耗”的原则,eff_map
数组中已经判定无效的就此轮跳过;如果 interesting value
达到的效果跟 bitflip
或者 arithmetic
效果相同,也被认为是重复消耗,跳过。
此阶段已经是确定性变异 deterministic fuzzing
的结尾:
user extras (over),从头开始,将用户提供的tokens依次替换到原文件中 user extras (insert),从头开始,将用户提供的tokens依次插入到原文件中 auto extras (over),从头开始,将自动检测的tokens依次替换到原文件中
其中 “用户提供的tokens” 是一开始通过 -x 选项指定的,如果没有则跳过对应的子阶段;“自动检测的tokens” 是第一个阶段 bitflip
生成的。
havoc
意味着随机的开始,与后面的 splice
是任何模式下都要进行的变异,具体来说 havoc 包含了多轮变异,每一轮都是组合拳:
随机选取某个bit进行翻转 随机选取某个byte,将其设置为随机的interesting value 随机选取某个word,并随机选取大、小端序,将其设置为随机的interesting value 随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value 随机选取某个byte,对其减去一个随机数 随机选取某个byte,对其加上一个随机数 随机选取某个word,并随机选取大、小端序,对其减去一个随机数 随机选取某个word,并随机选取大、小端序,对其加上一个随机数 随机选取某个dword,并随机选取大、小端序,对其减去一个随机数 随机选取某个dword,并随机选取大、小端序,对其加上一个随机数 随机选取某个byte,将其设置为随机数 随机删除一段bytes 随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数 随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换 随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
之后AFL会生成一个随机数,作为变异组合的数量,每次从上面随机选取作用于当前文件。
具体来说,AFL会在文件队列中随机选择一个文件与当前文件进行对比,如果差别不大就重新再选;如果差异明显,就随机选取位置两个文件都一切两半。最后将当前文件的头与随机文件的尾拼接起来得到新文件【为什么不是当前的尾拼接随机文件的头【??】】。当然了本着“减少消耗”的原则拼接后的文件应该与上一个文件对比,如果未发生变化应该过滤掉。
AFL状态栏右上角的 cycles done
意味着完成的循环数,每次循环是对整个队列的再一次变异,不过只有第一次 cycle 才会进行 deterministic fuzzing
,之后的只有随机性变异了。
这部分跟 mutation
相关,会多写一点(不断更新),将会分为两部分进行展开:现有AFL的fuzzing的策略 和论文相关魔改AFL的fuzzing策略 。
在上一节中已经说完了文件/数据的各种变异类型,其实还有一个点没说,就是一次变异操作到底是怎样的流程:(一次变异到底是变一个比特还是从一比特到32比特都结束了才算事【??】)
fuzz_one
:进行一次测试用例变异过程common_fuzz_stuff
:变异完成后的一般操作处理write_to_testcase
:变异后的内容写入测试文件run_target
:运行目标进程save_is_insteresting
:判断是否保存这个测试用例has_new_bits
:判断测试用例是否产生新状态
随着fuzzing的不断进行,cycle一轮又一轮,势必会产生更多的变异测试用例,这些用例并不是一股脑全都放进队列里的,对于没有新状态(由tuple信息可得)的测试用例应该抛弃;而且对于较优的用例应该更有可能被利用(这也是启发性的体现)。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2020-5-23 11:02
被张公子T40编辑
,原因: