AFL(American Fuzzy Lop)源码详细解读(3)
本篇是关于主循环阶段的内容,整个AFL最核心的部分,篇幅较长。最后简述一下afl_fuzz整体流程。
多亏大佬们的文章,对读源码帮助很大:
http://rk700.github.io/
https://eternalsakura13.com/2020/08/23/afl/
三、主循环
1. cull_queue()
精简队列,前面已经介绍过了,值得一提的是,第一轮循环时,循环上面执行了一次cull_queue,已经置score_changed = 0,直接返回了。后续再次循环时,根据score_changed 的值决定是否执行这个函数
2. 初始化相关参数
如果queue_cur为空
- queue_cycle 队列fuzz轮数加1
- current_entry 当前条目的ID,从0开始
- cur_skipped_paths 本轮循环中跳过的变异case数量
- queue_cur 置为队列首
- 如果是resume session ,从seek_to 指定的位置开始fuzz
- 调用 show_stats 刷新终端展示界面
- 如果not_on_tty = 1,刷新输出缓冲区
- 如果上一轮循环完毕队列长度和上上一轮循环完毕后的队列长度相同,表示上一轮循环没有发现新的case
- 如果use_splicing非0,即上一轮循环采用了splice,没有发现新的case的循环轮数 cycles_wo_finds加1
- 否则,置use_splicing = 1,表示本轮要采用splice
- 否则,发现新的case,置 cycles_wo_finds = 0
- 更新 prev_queued = queued_paths 为上一轮循环结束后的队列长度
- 如果并行fuzz并且是第一轮循环,并且存在环境变量"AFL_IMPORT_FIRST",启动并行fuzz
- skipped_fuzz = fuzz_one(use_argv) 调用fuzz_one 开始fuzz当前case,skipped_fuzz表示是否跳过当前case
3. fuzz_one fuzz当前case
(1)准备
- 如果队列中存在被标记为favored的case等待fuzz
- 如果当前case已经被fuzz过,或当前case 不是 favored,则99%的概率跳过当前case
- return 1
- 如果队列中不存在被标记为favored的case等待fuzz,并且在插桩模式下,并且当前case没被标记为favored,并且队列长度大于10
- 如果在非首轮循环并且当前case没有被fuzz过,则75%的概率跳过当前case(很明显,首轮循环为了fuzz到更多的种子,就不进入这个代码块)
- 否则,即当前case被fuzz过,95%的概率跳过
- 只读形式打开当前case的文件,映射到内存中,起始地址为orig_in和in_buf
- 为out_buf分配等长的内存
- 置subseq_tmouts = 0,这个变量表示当前case变异出来的case执行结果连续为FAULT_TMOUT的数量
- cur_depth 置为当前case的深度
- 假如当前项有校准错误,并且校准错误次数小于3次,那么校验和清零,调用calibrate_case再次校准。如果手动终止或是res 不等于0,直接放弃该case,cur_skipped_paths加1(第一轮循环应该是不进入这个代码块)
- 如果测试用例没有修剪过,那么调用函数trim_case对测试用例进行修剪,如果修剪成功置 trim_done = 1,并且更新修剪后的文件长度,将修剪后的内容拷贝到out_buf地址指向的内存中。(需要注意的是,修剪后in_buf中存放的就是新内容了)
- 调用calculate_score对当前case进行打分,结果保存到orig_perf 和 perf_score。大概是执行时间越短初始分数越高;然后覆盖路径越多,分数乘上更高的倍数;再根据handicap,乘上相应的倍数;最后深度越深,乘上更高的倍数。如果分数超过了默认上限值,则置为默认上限值。
- 如果命令行输入参数制定了-d,或是当前case已经被fuzz过了,或是当前case已经经过确定性变异,则直接跳过确定性变异,到havoc阶段
- 如果校验和模master_max != master_id - 1,则直接跳过确定性变异,到havoc阶段,(这里没太看懂)
- 置doing_det = 1,表示执行确定性变异
确定性变异
(2)BITFLIP
-
FLIP_BIT 还是在byte的基础上,对每bit进行操作
- _arf = (u8*)(_ar); 每次取一个字节
- _arf[(_bf) >> 3] 每个字节操作8次,就是分别对每一bit操作
- (128 >> ((_bf) & 7)) 产生len组下列数据
- 10000000
- 1000000
- 100000
- 10000
- 1000
- 100
- 10
- 1
- _arf[(_bf) >> 3] ^= (128 >> ((_bf) & 7)); 依次对每个字节的最高位至最低为进行翻转。(与1异或翻转,与0异或保持不变)
-
每次翻转1个bit,按照每1个bit的步长从头开始
-
stage_max = len << 3,len表示的是以byte为单位的长度,这里要对每一bit进行翻转,所以翻转次数为len*8
-
stage_val_type = STAGE_VAL_NONE = 0
-
更新 orig_hit_cnt ,队列长度加上路径不同的crash数量
-
保存当前case的校验和 prev_cksum,用于在调用calibrate_case函数时与编译后的用例的校验和进行比较
-
进入循环,遍历每个bit
- stage_cur_byte = stage_cur >> 3; 表示当前处理的字节
- 调用FLIP_BIT对每一位进行翻转
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 再调用FLIP_BIT翻转回来
- 如果插桩模式,并且(stage_cur & 7) == 7 就是在处理每个字节的最低位的时候
- 计算该变异case的校验和
- 如果已经在文件末尾并且校验和与先前的校验和相同 prev_cksum(上一个字节最低位翻转后的校验和)
- 如果a_len 小于 MAX_AUTO_EXTRA = 32 就将当前字符先存放到a_collect
- a_len++
- a_len >= MIN_AUTO_EXTRA = 3 并且 a_len <= MAX_AUTO_EXTRA = 32,调用maybe_add_auto将a_collect 已经存放的字符作为token加入到 a_extras中,(a_extras 存放的是自动生成的token,这一步是将符合要求的token才能加入到a_extras中)
- 如果不是在文件末尾,并且与前一字节校验和不同
- a_len >= MIN_AUTO_EXTRA = 3 并且 a_len <= MAX_AUTO_EXTRA = 32,调用maybe_add_auto将a_collect 已经存放的字符作为token加入到 a_extras中
- 置a_len = 0
- 更新prev_cksum
- 如果不是在文件末尾,并且cksum == prev_cksum,并且cksum != queue_cur->exec_cksum
- 将当前字符追加到a_collect 中
- a_len++
- (上面这一段是一个很有意义的骚操作,如果连续的几个字节翻转后都与原始的case覆盖路径不同并且这几个字节走的路径相同,就认为这是一个token,所以这个cksum == prev_cksum判断就能解释通了,是在判断和前一个字节的执行路径是否相同。(下面的例子摘自https://eternalsakura13.com/2020/08/23/afl/,可以说是很清晰易懂了)比如对于SQL的
SELECT *
,如果SELECT
被破坏,则肯定和正确的路径不一致,而被破坏之后的路径却肯定是一样的,比如AELECT
和SBLECT
,显然都是无意义的,而只有不破坏token,才有可能出现和原始执行路径一样的结果,所以AFL在这里就是在猜解关键字token)
-
更新new_hit_cnt,队列长度加上路径不同的crash数量
-
stage_finds[STAGE_FLIP1]的值加上在整个FLIP_BIT中新发现的路径和路径不同的crash总和
-
stage_cycles[STAGE_FLIP1]的值加上在整个FLIP_BIT中执行的target次数stage_max
-
每次翻转相邻的2个bit,按照每1个bit的步长从头开始
-
stage_max = (len << 3) - 1
-
更新 orig_hit_cnt
-
进入循环,遍历每2个bit
- 记录stage_cur_byte 当前处理的字节
- 翻转当前bit和下一bit
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 翻转回来
-
更新new_hit_cnt
-
保存stage_finds[STAGE_FLIP2]和stage_cycles[STAGE_FLIP2]
-
每次翻转相邻的4个bit,按照每1个bit的步长从头开始
-
stage_max = (len << 3) - 3
-
更新 orig_hit_cnt
-
进入循环,遍历每4个bit
- 记录stage_cur_byte 当前处理的字节
- 翻转相邻的4个bit
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 翻转回来
-
更新new_hit_cnt
-
保存stage_finds[STAGE_FLIP4]和stage_cycles[STAGE_FLIP4]
构建eff_map
- EFF_APOS(_p) ((_p) >> EFF_MAP_SCALE2) EFF_MAP_SCALE2 = 3, 右移三位,也就是除以八
- EFF_REM(_x) ((_x) & ((1 << EFF_MAP_SCALE2) - 1)) 与7按位与
- EFF_ALEN(_l) (EFF_APOS(_l) + !!EFF_REM(_l)) 这里我的理解是,将每八个字节映射到eff_map中的一个字节,最后剩下的不到八个字节映射到一个字节。简而言之 _l/8,向上取整(但是很无厘头,不知道为什么这样映射呀,为什么不将每个字节对应到eff_map里的每个字节呢?或许是我的理解有误,或是理解不到位,没有get到这样做的理由)
- 分配内存,比如len为64bytes,eff_map大小则为8byte;len为65bytes,eff_map大小则为9byte
- 将第一个字节置为1,需要说明的是eff_cnt初始为0,所以这里就没有++eff_cnt
- 如果映射出来的eff_map大小不止一个字节,将eff_map最后一个字节置1,eff_cnt 标记数量加1
eff_map作用:还是摘自https://eternalsakura13.com/2020/08/23/afl/。在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于”data”,而非”metadata”(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。
-
每次翻转相邻的8个bit,按照每8个bit的步长从头开始
-
stage_max = len
-
更新 orig_hit_cnt
-
进入循环,遍历每个字节
- 记录stage_cur_byte 当前处理的字节
- 翻转一个字节
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 如果当前字节在eff_map中的映射为0
- 如果在插桩模式并且case长度大于EFF_MIN_LEN = 128,计算校验和
- 否则,即在非插桩模式或者是文件长度非常短的情况下 小于等于128,直接将eff_map中所有的字节都标记为1,具体地 cksum = ~queue_cur->exec_cksum,让cksum等于当前case校验和的按位取反结果,后续会根据校验和不相同在eff_map中标记为1
- 如果校验和不相同则置aff_map中相应的位为1,并且eff_cnt 加1
- 翻转回来
-
如果一个eff_map超过90%的bytes都是“有效”的,干脆把所有字符都划归为“有效”,并且更新blocks_eff_select 加上当前eff_map的长度
-
否则,blocks_eff_select += eff_cnt 更新blocks_eff_select 加上标记为1的字节数
-
更新blocks_eff_total,所有用例映射出来的总字节数
-
更新new_hit_cnt
-
保存stage_finds[STAGE_FLIP8]和stage_cycles[STAGE_FLIP8]
-
每次翻转相邻的16个bit,按照每8个bit的步长从头开始,即依次对每个word做翻转
-
如果len长度小于2,直接跳过后续翻转
-
stage_max = len - 1
-
更新 orig_hit_cnt
-
进入循环,遍历每个word
- 如果两个字节在eff_map中都被标记为0,即无效的,stage_max–,执行目标二进制文件数量减1,continue
- 记录stage_cur_byte 当前处理的字节
- 翻转两个字节
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 翻转回来
-
更新new_hit_cnt
-
保存stage_finds[STAGE_FLIP16]和stage_cycles[STAGE_FLIP16]
-
每次翻转相邻的32个bit,按照每8个bit的步长从头开始,即依次对每个dword做翻转
-
如果len长度小于4,直接跳过后续翻转
-
stage_max = len - 3
-
更新 orig_hit_cnt
-
进入循环,遍历每个dword
- 如果四个字节在eff_map中都被标记为0,即无效的,stage_max–,执行目标二进制文件数量减1,continue
- 记录stage_cur_byte 当前处理的字节
- 翻转四个字节
- 调用 common_fuzz_stuff 检查翻转后的结果,如果为1,则丢弃
- 翻转回来
-
更新new_hit_cnt
-
保存stage_finds[STAGE_FLIP32]和stage_cycles[STAGE_FLIP32]
(3)ARITHMETIC
如果禁用ARITHMETIC,则跳过ARITHMETIC
-
每次对8个bit进行计算,按照每8个bit的步长从头开始,即对文件的每个byte进行计算
-
stage_max = 2 * len * ARITH_MAX 每个字节都要加一次再减一次 1—35
-
stage_val_type = STAGE_VAL_LE = 1
-
更新 orig_hit_cnt
-
进入循环,遍历每个byte
- 如果该字节在eff_map中都被标记为0,即无效的,stage_max -= 2 * ARITH_MAX,continue
- 记录stage_cur_byte 当前处理的字节
- 进入循环,遍历1—35
- 如果当前字节加上某个数之后,其效果与之前的某种bitflip相同,就直接跳过,stage_max–,这里for循环里条件是i < len,所以更改stage_max没事
- 否则out_buf[i] = orig + j,调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 如果当前字节减去某个数之后,其效果与之前的某种bitflip相同,就直接跳过,stage_max–
- 否则out_buf[i] = orig - j,调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 恢复
-
更新new_hit_cnt
-
保存stage_finds[STAGE_ARITH8]和stage_cycles[STAGE_ARITH8]
先介绍一下大端序和小端序,具体解释见: https://zhuanlan.zhihu.com/p/77436031
大端序(Big-endian):高位字节存入低地址,低位字节存入高地址
小端序(Little-endian):低位字节存入低地址,高位字节存入高地址
-
每次对16个bit进计算,按照每8个bit的步长从头开始,即对文件的每个word进行计算
-
如果len小于2,直接跳过后续运算
-
stage_max = 4 * (len - 1) * ARITH_MAX 每两个字节都要分别按照大端序和小端序加减35次
-
更新 orig_hit_cnt
-
进入循环,遍历每个word
-
如果两个字节在eff_map中都被标记为0,即无效的,stage_max -= 4 * ARITH_MAX,continue
-
记录stage_cur_byte 当前处理的字节
-
进入循环,遍历1—35
- r1 小端加,r2 小端减,r3 大端加,r4 大端减
小端序:
-
stage_val_type = STAGE_VAL_LE 表示在处理小端序
-
如果相加后效果与之前的某种bitflip相同,并且加完后影响到另一个字节,就运算 *(u16*)(out_buf + i) = orig + j,调用 common_fuzz_stuff 检查,如果为1,则丢弃。(orig & 0xff) + j > 0xff 这里解释一下-,当前是每次对16个bit进行处理,如果低8bits 加上 j 之后大于 0xff 则会产生进位让高8bits发生变化,加上 j 之后小于等于 0xff,就是没有产生进位让高8bits发生变化。
-
否则,直接跳过,stage_max–
-
如果相减后效果与之前的某种bitflip相同,并且减完后影响到另一个字节,就运算 *(u16*)(out_buf + i) = orig + j,调用 common_fuzz_stuff 检查,如果为1,则丢弃。 (orig & 0xff) < j 和上面解释同理。
-
否则,直接跳过,stage_max–
大端序:
-
stage_val_type = STAGE_VAL_BE 表示在处理大端序
-
如果相加后效果与之前的某种bitflip相同,并且加完后影响到另一个字节,就运算 *(u16*)(out_buf + i) = orig + j,调用 common_fuzz_stuff 检查,如果为1,则丢弃。(orig >> 8) + j > 0xff 和上面同理,需要注意的是,因为是大端序,所以右移8位。
-
否则,直接跳过,stage_max–
-
如果相减后效果与之前的某种bitflip相同,并且减完后影响到另一个字节,就运算 *(u16*)(out_buf + i) = orig + j,调用 common_fuzz_stuff 检查,如果为1,则丢弃。 (orig >> 8 & 0xff) < j 和上面解释同理。
-
否则,直接跳过,stage_max–
-
恢复
-
-
更新new_hit_cnt
-
保存stage_finds[STAGE_ARITH16]和stage_cycles[STAGE_ARITH16]
-
每次对32个bit进计算,按照每8个bit的步长从头开始,即对文件的每个dword进行计算
-
如果len小于4,直接跳过后续运算
-
stage_max = 4 * (len - 3) * ARITH_MAX 每四个字节都要分别按照大端序和小端序加减35次
-
更新 orig_hit_cnt
-
进入循环,遍历每个dowrd
-
如果四个字节在eff_map中都被标记为0,即无效的,stage_max -= 4 * ARITH_MAX,continue
-
记录stage_cur_byte 当前处理的字节
-
进入循环,遍历1—35
- r1 小端加,r2 小端减,r3 大端加,r4 大端减
小端序:
-
stage_val_type = STAGE_VAL_LE 表示在处理小端序
-
如果相加后效果与之前的某种bitflip相同,并且加完后影响到另一个字节,则运算 ,调用 common_fuzz_stuff 检查,如果为1,则丢弃。(orig & 0xffff) + j > 0xffff 同理,低16bits能否影响高16bits
-
否则,直接跳过,stage_max–
-
如果相减后效果与之前的某种bitflip相同,并且减完后影响到另一个字节,则运算,调用 common_fuzz_stuff 检查,如果为1,则丢弃。
-
否则,直接跳过,stage_max–
大端序:
-
stage_val_type = STAGE_VAL_BE 表示在处理大端序
-
如果相加后效果与之前的某种bitflip相同,并且加完后影响到另一个字节,则运算,调用 common_fuzz_stuff 检查,如果为1,则丢弃。
-
否则,直接跳过,stage_max–
-
如果相减后效果与之前的某种bitflip相同,并且加完后影响到另一个字节,则运算,调用 common_fuzz_stuff 检查,如果为1,则丢弃。
-
否则,直接跳过,stage_max–
-
恢复
-
-
更新new_hit_cnt
-
保存stage_finds[STAGE_ARITH32]和stage_cycles[STAGE_ARITH32]
(4)INTERESTING VALUES
-
每次对8个bit进行替换,按照每8个bit的步长从头开始,即对文件的每个byte进行替换
-
stage_max = len * sizeof(interesting_8) 每个字节用interesting_8中的每项替换一次
-
更新orig_hit_cnt = new_hit_cnt
-
进入循环,遍历每个byte
- 如果该字节在eff_map中都被标记为0,即无效的,stage_max -= sizeof(interesting_8),continue
- 记录stage_cur_byte 当前处理的字节
- 进入循环,遍历interesting_8每一项
- 如果替换后效果与之前的某种bitflip或是arithmetic相同,stage_max–,continue
- 否则,替换out_buf[i] = interesting_8[j]; 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 恢复
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
-
每次对16个bit进行替换,按照每8个bit的步长从头开始,即对文件的每个word进行替换
-
如果禁用arith即no_arith或是len < 2,跳过后续interesting
-
stage_max = 2 * (len - 1) * (sizeof(interesting_16) >> 1) 乘2是因为分大小端序,右移一位(即除以2)是因为sizeof求出来是字节数,除以二才是数组元素个数
-
更新orig_hit_cnt = new_hit_cnt
-
进入循环,遍历每个word
-
如果这两个字节在eff_map中都被标记为0,即无效的,stage_max -= sizeof(interesting_16),continue(其实这里写是2*(sizeof(interesting_16))>>1)
-
记录stage_cur_byte 当前处理的字节
-
进入循环,遍历interesting_16每一项
小端序:
- 如果替换后效果与之前的某种bitflip、arithmetic和interest不相同,
- stage_val_type = STAGE_VAL_LE 小端序
- 替换
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 否则,stage_max–
大端序:
- 如果大端序与小端序不相同并且替换后效果与之前的某种bitflip、arithmetic和interest不相同
- stage_val_type = STAGE_VAL_LE 大端序
- 替换
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 否则,stage_max–
- 如果替换后效果与之前的某种bitflip、arithmetic和interest不相同,
-
恢复
-
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
-
每次对32个bit进行替换,按照每8个bit的步长从头开始,即对文件的每个dword进行替换
-
如果len < 4,跳过后续interesting
-
stage_max = 2 * (len - 3) * (sizeof(interesting_32) >> 2) 乘2是因为分大小端序,右移2位(即除以4)是因为sizeof求出来是字节数,除以4才是数组元素个数
-
更新orig_hit_cnt = new_hit_cnt
-
进入循环,遍历每个dword
-
如果这四个字节在eff_map中都被标记为0,即无效的,stage_max -= sizeof(interesting_32)>>1,continue(2* sizeof(interesting_32)>>2)
-
记录stage_cur_byte 当前处理的字节
-
进入循环,遍历interesting_16每一项
小端序:
- 如果替换后效果与之前的某种bitflip、arithmetic和interest不相同,
- stage_val_type = STAGE_VAL_LE 小端序
- 替换
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 否则,stage_max–
大端序:
- 如果大端序与小端序不相同并且替换后效果与之前的某种bitflip、arithmetic和interest不相同
- stage_val_type = STAGE_VAL_LE 大端序
- 替换
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 否则,stage_max–
- 如果替换后效果与之前的某种bitflip、arithmetic和interest不相同,
-
恢复
-
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
(5)DICTIONARY STUFF
如果没有自定义的token,跳过此阶段
-
将用户提供的tokens依次复写到原文件中,按照每8个bit的步长从头开始
-
stage_max = extras_cnt * len
-
stage_val_type = STAGE_VAL_NONE
-
更新orig_hit_cnt = new_hit_cnt
-
进入循环,遍历每个byte
- 记录stage_cur_byte 当前处理的字节
- 进入循环,遍历每个token
- (extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) 如果token数量大于200,则有概率跳过 || extras[j].len > len - i 放不下 || !memcmp(extras[j].data, out_buf + i, extras[j].len) token与原本内容相同 || !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) 要替换的内容在eff_map中的映射全都为0,即无效的
- stage_max–; continue;
- 否则,last_len = extras[j].len; 保存当前token长度
- memcpy(out_buf + i, extras[j].data, last_len); 替换,值得一提的是,AFL提前将token按照长度从小到大进行排序。这样做的好处是,只要按照顺序使用排序后的tokens,那么后面的token不会比之前的短,从而每次覆盖替换后不需要再恢复到原状
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- (extras_cnt > MAX_DET_EXTRAS && UR(extras_cnt) >= MAX_DET_EXTRAS) 如果token数量大于200,则有概率跳过 || extras[j].len > len - i 放不下 || !memcmp(extras[j].data, out_buf + i, extras[j].len) token与原本内容相同 || !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, extras[j].len))) 要替换的内容在eff_map中的映射全都为0,即无效的
- 恢复
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
-
将用户提供的tokens依次插入到原文件中,按照每8个bit的步长从头开始
-
stage_max = extras_cnt * (len + 1)
-
更新orig_hit_cnt = new_hit_cnt
-
ex_tmp = ck_alloc(len + MAX_DICT_FILE); 开辟空间,用于后续
-
进入循环,遍历每个byte
- 记录stage_cur_byte 当前处理的字节
- 进入循环,遍历每个token
- len + extras[j].len > MAX_FILE 如果插入token后长度大于1024*1024
- stage_max–; continue;
- 否则,last_len = extras[j].len; 保存当前token长度
- 替换,具体地,先头部保持不变,再插入token,最后尾部复制过来
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃,释放内存
- len + extras[j].len > MAX_FILE 如果插入token后长度大于1024*1024
- 复制头部
-
释放内存
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
如果没有自动生成的token,跳过此阶段
-
将自动生成的tokens依次复写到原文件中,按照每8个bit的步长从头开始
-
stage_max = MIN(a_extras_cnt, USE_AUTO_EXTRAS) * len 上限50个
-
stage_val_type = STAGE_VAL_NONE
-
更新orig_hit_cnt = new_hit_cnt
-
进入循环,遍历每个byte
- 记录stage_cur_byte 当前处理的字节
- 进入循环,遍历每个token
- (a_extras[j].len > len - i 放不下 || !memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) token与原本内容相同 || !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) 要替换的内容在eff_map中的映射全都为0,即无效的
- stage_max–; continue;
- 否则,last_len = extras[j].len; 保存当前token长度
- memcpy(out_buf + i, a_extras[j].data, last_len); 替换,也是从小到大排好序的
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- (a_extras[j].len > len - i 放不下 || !memcmp(a_extras[j].data, out_buf + i, a_extras[j].len) token与原本内容相同 || !memchr(eff_map + EFF_APOS(i), 1, EFF_SPAN_ALEN(i, a_extras[j].len))) 要替换的内容在eff_map中的映射全都为0,即无效的
- 恢复
-
更新new_hit_cnt
-
保存stage_finds[STAGE_INTEREST8]和stage_cycles[STAGE_INTEREST8]
-
调用 mark_as_det_done 将当前case标记为经过确定性变异
确定性变异结束
(6)HAVOC
- 如果不是在文件拼接,stage_max = (doing_det ? HAVOC_CYCLES_INIT : HAVOC_CYCLES) * perf_score / havoc_div / 100 如果在havoc之前进行了确定性变异 stage_max = 1024 * perf_score / havoc_div / 100,否则 stage_max = 256 * perf_score / havoc_div / 100 (每个case在第每轮循环fuzz时先走一次这里)
- 否则,stage_max = SPLICE_HAVOC * perf_score / havoc_div / 100 ,stage_max = 32 * perf_score / havoc_div / 100
- stage_max = HAVOC_MIN ,stage_max下限为16
- temp_len = len; temp_len 为当前case长度
- 更新orig_hit_cnt
- havoc_queued = queued_paths; havoc_queued为队列长度,后续用于判断对一个case进行一次havoc变异后queued_paths长度是否发生变化
- 进入循环
- use_stacking = 1 << (1 + UR(HAVOC_STACK_POW2)) 随机组合轮数 (2—128)
- 进入循环,遍历组合轮数,switch判断,如果无token,随机选择0—14,有token,随机选择0—16
- case 0:随机选取某个bit进行翻转
- case1:随机选取某个byte,将其替换为随机的interesting
- case2:随机选取某个word,并随机选取大、小端序,将其替换为随机的interesting value
- case3:随机选取某个dword,并随机选取大、小端序,将其设置为随机的interesting value
- case4:随机选取某个byte,对其减去一个随机数
- case5:随机选取某个byte,对其加上一个随机数
- case6:随机选取某个word,并随机选取大、小端序,对其减去一个随机数
- case7:随机选取某个word,并随机选取大、小端序,对其加上一个随机数
- case8:随机选取某个dword,并随机选取大、小端序,对其减去一个随机数
- case9:随机选取某个dword,并随机选取大、小端序,对其加上一个随机数
- case10:随机选取某个byte,将其设置为随机数(异或上一个大于1的数,保证结果不会等于原始值)
- case11:随机删除一段bytes
- case12:随机删除一段bytes(11,12是相同的处理操作,概率相当于是其他的选项的二倍,因为AFL希望保持文件相当小)
- case13:随机选取一个位置,插入一段随机长度的内容,其中75%的概率是插入原文中随机位置的内容,25%的概率是插入一段随机选取的数,也是采用复制头部,插入,复制尾部的操作
- case14:随机选取一个位置,替换为一段随机长度的内容,其中75%的概率是替换成原文中随机位置的内容,25%的概率是替换成一段随机选取的数
- case15:随机选取一个位置,用随机选取的token(用户提供的或自动生成的)替换
- case16:随机选取一个位置,用随机选取的token(用户提供的或自动生成的)插入
- 调用 common_fuzz_stuff 检查,如果为1,则丢弃
- 如果一轮组合过后,原始case中的内容变化了,则恢复
- 更新temp_len = len;
- 如果队列长度发生了变化,即这轮组合发现了新路径
- 如果perf_score <= HAVOC_MAX_MULT * 100 =1600,则havoc次数翻倍,分数翻倍,
- 更新havoc_queued
- 更新new_hit_cnt
- 如果不是在文件拼接,保存stage_finds[STAGE_HAVOC]和stage_cycles[STAGE_HAVOC] (每个case在第每轮循环fuzz时先走一次这里)
- 否则,保存stage_finds[STAGE_SPLICE]和stage_cycles[STAGE_SPLICE]
(7)SPLICING
-
如果进行文件拼接,并且已拼接次数小于15次,并且队列中有不止一个case,并且当前case长度大于1
- 如果in_buf 发现了变化则恢复
- 随机选择一个不同于当前的种子
- splicing_with = tid,splicing_with置为目标种子的ID;
- target置为目标种子
- 如果目标种子的长度小于2或是和当前种子是同一个,则循环令目标种子的下一个种子作为target
- 如果一番操作后target为null,重新找target
- 将target读到缓冲区new_buf中
- 找到一个合适的拼接位置,具体操作为找到两个case相同位置的第一个不同内容f_diff 和最后一个不同内容 l_diff
- 如果f_diff < 0 没有不同内容,或者 l_diff < 2 在前两个字节, 或是f_diff == l_diff 位置相同,则释放缓冲区,重新找target
- split_at = f_diff + UR(l_diff - f_diff); 在第一个和最后一个不同的字节之间随机取一个拆分位置
- memcpy(new_buf, in_buf, split_at); 将当前case的前半部分复制到new_buf中,即用当前case的前半部分拼接target的后半部分作为新的变异case
- in_buf = new_buf; 更新in_buf 为新的拼接后的case
- 将新的变异case 拷贝到 out_buf 中后跳到 havoc 阶段
-
ret_val = 0; 该变量表示是否跳过当前case了,初始为1,如果执行到这里了说明不跳过,将其置0
abandon_entry:
- splicing_with = -1; 没有进行文件拼接
- 如果没有手动停止,并且没有校准失败,并且没被标记为fuzz过的
- 将当前case标记为已经fuzz过
- 并且更新pending_not_fuzzed
- 如果当前case是被标记为favored,更新pending_favored
- munmap(orig_in, queue_cur->len); 解除内存映射
- 释放内存
- return ret_val
4. tirm_case 修剪用例
- 如果种子长度小于5,直接返回
- stage_name = tmp; bytes_trim_in += q->len;该变量表示被trim过的字节
- len_p2 为第一个大于等于种子长度的2的次幂
- TRIM_START_STEPS=16, TRIM_MIN_BYTES=4. remove_len 为(len_p2/16, 4)中的较大者,该变量表示修剪掉的长度,
- 进入循环, TRIM_MIN_BYTES=1024,remove_len 小于(len_p2/1024, 4)中的较大者时结束循环,即最多修剪1024次
- remove_pos = remove_len,修剪位置
- 向tmp中写入字符串内容
- 进入循环,当修剪位置没有超过种子长度
- trim_avail = MIN(remove_len, q->len - remove_pos); 确保不会超长
- 调用
write_with_gap(in_buf, q->len, remove_pos, trim_avail)
将修剪后的内容存入.cur_input
文件里,具体地,先将 [0, remove_pos) 内容写入文件,然后跳过 trim_avail 长度,再把剩余的尾部写入文件,其他的和write_to_testcase
一样。 run_target
运行目标程序- trim_execs++; 修剪次数加一
- 如果终止 或是 fault == FAULT_ERROR,跳转至 abort_trimming
- 计算校验和
- 如果修剪用例校验和cksum与种子原本的校验和q->exec_cksum相等(即覆盖了相同的路径)
- move_tail 为剩余的尾部内容
- 更新 q->len 和 len_p2
- 调用
memmove
直接用尾部内容将修剪掉的内容覆盖掉,这里如果尾部内容比修剪掉的内容短的话也不影响,因为有q->len 的限制。也是因为这里已经更新过了,所以不用执行remove_pos += remove_len了。 - 如果 needs_write 为0,该变量表示是否需要更新种子
- needs_write 置1,表示需要更新种子
- 将trace_bits 的内容复制到 clean_trace 中
- 否则,即如果修剪用例校验和cksum与种子原本的校验和q->exec_cksum不相等(即覆盖了不同的路径)
- remove_pos += remove_len 去检查下一个位置
- 每执行stats_update_freq/2 次trim就需要刷新显示(因为上面trim_exec已经自加过了,这里又自加一次,所以是每stats_update_freq/2 次就刷新)
- stage_cur++;
- remove_len >>=1 ,当前步长执行完后,更新步长
- 如果需要更新种子
- 将原来的q->fname删除
- 新建q->fname文件并将修剪后的内容写入、
- 将clean_trace 复制到 trace_bits中
- 调用
update_bitmap_score
更新位图得分
- abort_trimming:
- bytes_trim_out += q->len; 该变量表示经过修剪后输出的字节数。对于每个种子,如果没有修剪bytes_trim_out=bytes_trim_in,如果修剪过,则小于。
- return fault
5. common_fuzz_stuff 变异后的处理
-
调用 write_to_testcase 将当前变异的case写入 out_dir/.cur_input
-
运行目标二进制程序,结果存至 fault
-
如果 fault == FAULT_TMOUT = 1
- 如果subseq_tmouts 已经超过250,(subseq_tmouts该变量表示一个case在变异执行过程中连续超时数量)就直接放弃这个变异的case,cur_skipped_paths 本轮循环中跳过的变异case数量加1,并return 1
-
否则置subseq_tmouts = 0
-
如果skip_requested为1(信号为SIGUSR1时),则设置skip_requested为0,然后将cur_skipped_paths加一,直接返回1
-
调用save_if_interesting 判断当前变异case是否保存,queued_discovered 存放新加入队列中的case数量数量
-
return 0
6. save_if_interesting
- 如果fault == 0 并且没有走任何路径,直接return 0
- 调用add_to_queue(fn, len, 0)将当前变异case添加到队列中,需要注意的是,添加的变异case是还未经过确定性变异的,所以传入的最后一个参数为0
- 如果发现了新路径,刚添加到队列中的case的has_new_cov 置1; 并且queued_with_cov加1
- 计算新case的校验和
- 校验新case
- 正常则写入文件,置keeping = 1; 该变量代表是否将该变异case加入了队列
- 进入switch
- 如果fault == FAULT_TMOUT = 1
- total_tmouts 总的超时数量加1
- 如果挂起数量达到500,直接return 0
- 如果在插桩模式下
- 调用 simplify_trace 对trace_bits进行规整。该函数作用就是,trace_bits为0的字节规整为1(0000 0001),非0字节规整为128(1000 0000)
- trace_bits中没有新的超时路径也直接return 0
- 否则,代表发现了新的超时路径,unique_tmouts加1
- 如果超时时限小于挂起时限 exec_tmout < hang_tmout
- 调用write_to_testcase 将当前case写入out_dir/.cur_input
- 再次执行一遍该变异case
- 如果 结果为FAULT_CRASH,跳转到keep_as_crash
- 如果结果不是FAULT_CRASH,直接return 0
- 否则,超时时限大于挂起时限,就代表当前变异case已经算是挂起了
- 就将该变异case 写入out_dir/hangs 下相应的文件内
- unique_hangs 挂起数量加一
- 更新last_hang_time的值
- 如果fault == FAULT_CRASH = 2 (keep_as_crash:)
- total_crashes 数量加1
- 如果crash数量已经达到5000,直接return 0
- 如果是在插桩模式下
- 调用 simplify_trace规整trace_bits
- 没有新的崩溃路径也直接return 0
- 否则,有新路径,在crashes文件夹下编写一个readme文件
- 就将该变异case 写入out_dir/crashes 下相应的文件内
- unique_crashes 路径不同的crash数量加一
- 更新last_crash_time 上一次崩溃时间 和last_crash_execs 上一次崩溃的执行时间
- 如果fault == FAULT_ERROR = 3, 不饿能执行目标二进制文件
- default: return keeping 需要注意的是,fault == 0 走到这里 return 1,fault == 1 或是 fault == 2 走到这里 return 0,因为没有加入到队列中
- 如果fault == FAULT_TMOUT = 1
7. fuzz_one 结束后的处理
-
循环内
-
如果没有结手动束,并且并行fuzz,并且没有跳过当前case
-
sync_interval_cnt计数器加一,如果其结果是SYNC_INTERVAL(默认是5)的倍数,就进行一次sync fuzz
-
if (!stop_soon && exit_1) stop_soon = 2; 如果没有手动停止,但是exit_1非0,就置stop_soon = 2(exit_1和环境变量AFL_BENCH_JUST_ONE有关,大概意思是只fuzz一轮。但exit_1通常为0)
-
如果stop_soon 非零,退出循环
-
遍历下一个case,current_entry更新为下一个case的ID
-
-
循环外
- 如果queue_cur非空,最后刷新一次状态、
- 如果stop_soon == 2,即非手动结束fuzz,终止子进程 和 forkserver(手动结束fuzz是通过信号处理函数终止子进程 和 forkserver的)
- 调用write_bitmap,如果位图有变化,就将virgin_bits写到out_dir/fuzz_bitmap中,可以用于后续命令行参数中指定 -B
- 调用write_stats_file ,保存状态信息
- 调用save_auto,保存自动生成的token
stop_fuzzing:
- 打印结束方式
- 如果在首轮,打印提示
- 关闭资源,释放内存
- 结束!!!
四、afl-fuzz流程简述
- 环境准备。包括注册信号处理函数、内存检查、保存命令行参数、环境变量检查、设置共享文件夹、准备输出文件夹
- 将输入文件夹下的文件扫描至队列中
- 校准输入文件夹下的所有种子,启动fork server 并初次运行输入种子。在这里会找出输入中有问题的种子,共享位图
trace_bits
和全局位图virgin_bits
以及优秀种子集top_rated
就开始使用了 - 调用
cull_queue
精简队列,将优秀种子集中的所有种子标记为favored,其余的标记为redundant - 进入模糊测试循环
- 在每个种子被模糊之前都会精简队列,虽然叫精简,但实际上并不会剔除队列中的任何种子,而是重新选择favored种子,并将剩下的标记为redundant,待测试的青睐种子
pending_favored
和队列中的青睐种子queued_favored
当然也会重新计数 - 如果一轮循环过后没有任何新的发现,将启用拼接并且不再关闭
- 按照队列中的顺序模糊种子,有一定概率跳过
- 如果种子校准有错误并且小于3次,将校验和清零后调用
calibrate_case
再次校准;大于3次就直接跳过当前种子 - 如果种子没有修剪过,调用
trim_case
进行修剪 - 调用
calculate_score
对种子进行打分,执行速度越快、覆盖边数越多、越晚被发现、深度越深的种子得分越高 - 进入变异阶段,其中随机性变异的轮数根据种子得分决定
- 调用
run_target
执行每个确定性变异出的每个用例,trace_bits 在这里就直接被分桶 - 调用
save_if_interesting
判断是否保留用例作为新的种子存入队列。- 加入队列,新种子写入文件,全局位图更新、崩溃和挂起的测试用例写入输出文件夹都是在这里进行的,新种子的校验和也直接被计算,然后调用
calibrate_case
进行校验。在calibrate_case
中完成校验部分后,还会调用update_bitmap_score
更新优秀种子集(top_rated)
- 加入队列,新种子写入文件,全局位图更新、崩溃和挂起的测试用例写入输出文件夹都是在这里进行的,新种子的校验和也直接被计算,然后调用
- 调用
- 下一个种子
- 在每个种子被模糊之前都会精简队列,虽然叫精简,但实际上并不会剔除队列中的任何种子,而是重新选择favored种子,并将剩下的标记为redundant,待测试的青睐种子