AFL分享

Fuzzing是漏洞挖掘领域最有效的方法之一,可以用来发现大量的远程代码执行和提权的漏洞。然而,fuzzing优势相对肤浅和盲目的。随机变异使得我们很难实现达到测试程序特定的代码路径。这就使得测试的代码覆盖率很低。
有很多人试图去解决这个问题,Tavis Ormandy曾经提出一种:根据代码覆盖率,从大量高质量的输入文件语料中选取一个子集,然后按照传统方法去fuzz。这种方法很有效,但前提是需要一个这样的语料。另一方面,代码覆盖率只提供了一个很简单的对程序状态的描述,当Fuzzing测试到了一定的程度,代码覆盖率就没什么作用了。
所以,大家都在探索更复杂的技术,比如:程序控制流分析,符号执行或静态分析。这些技术在实验中是很有前途的,但是在实际应用中显得效率低下、缺乏可靠性。

AFL

1.介绍afl大致流程

①从源码编译程序时进行插桩,以记录代码覆盖率(Code Coverage);

②选择一些输入文件,作为初始测试集加入输入队列(queue);

③将队列中的文件按一定的策略进行“突变”;

④如果经过变异文件更新了覆盖范围,则将其保留添加到队列中;

⑤上述过程会一直循环进行,期间触发了crash的文件会被记录下来。

在这里插入图片描述

二、插桩:

为什么要插桩?:

1.代码覆盖率
代码覆盖率是一种度量代码的覆盖程度的方式,也就是指源代码中的某行代码是否已执行。这里涉及到三个概念,基本块(basic-block)、边界(edge)、元组(tuple)。

2.基本块(Basic Block)
将程序的代码划分为一个个基本块,在这个基本块中第一条指令被执行后,后续的指令也会被全部执行,每个基本块中所有指令的执行次数是相同的,下图就是对分块的一个简单示例。
在这里插入图片描述

3.边(edge)
AFL的技术白皮书中提到fuzzer通过插桩代码捕获边(edge)覆盖率。那么什么是edge呢?我们可以将程序看成一个控制流图(CFG),图的每个节点表示一个基本块,而edge就被用来表示在基本块之间的跳转。知道了每个基本块和跳转的执行次数,就可以知道程序中的每个语句和分支的执行次数,从而获得详细的覆盖率信息。
在这里插入图片描述

4.元组(tuple)
具体到AFL的实现中,使用二元组(branch_src, branch_dst)来记录当前基本块 + 前一基本块 的信息,从而获取目标的执行流程和代码覆盖情况。

怎么插桩?:

我们都知道一个程序从源代码到可执行文件要经过四个步骤:预处理、编译、汇编、链接。
AFL在程序编译形成汇编代码后,将插桩代码(也是汇编代码),插入到每个基本块的前面,之后在进行汇编形成计算机可以识别的机器指令。

插什么?:

插桩代码的伪代码如下:

cur_location = <COMPILE_TIME_RANDOM>;
shared_mem[cur_location ^ prev_location]++; 
prev_location = cur_location >> 1;

cur_location的值是随机产生的,为的是简化连接复杂对象的过程和保持XOR输出分布是均匀的,后者可用减少tuple映射到shared_mem[] 时发生冲突的概率。

shared_mem[] 数组是一个64kb的共享空间。其中的每一字节里面存储着对于特定的Edge也就是tuple的命中次数,所以最多可用处理64k个Edge。

选择这个数组大小的原因是让冲突(collisions)尽可能减少。这样通常能处理2k到10k的分支点。同时,它的大小也足以达到毫秒级的分析,不会导致分析过久。

上图说明了不同数量级的Edge数下,冲突的概率,在程序不是非常大的情况下,冲突概率都是可用接受的,至于冲突后是怎么处理的我还不清楚。

prev_location = cur_location >> 1; 如果不右移,A->B与B->A这两个不一样的执行路径生成的异或值是一样的,会导致无法区分。

三、准备测试用例:

在read_testcases函数中,会对用户给出的测试用例做筛选,比如判断是否是常规文件,而不是目录文件,设备文件等,判断测试用例是否为空文件等。因此部分无效的初始测试用例会被抛弃。

四、变异-确定性变异:

4.1确定性变异:(如果当前测试用例做过确定性变异,就会直接做非确定性变异,也就是每个测试用例只做依次确定性变异)

1.bitflip,位反转

基本原理:bitflip,按位翻转,1变为0,0变为1。

拿到一个原始文件,打头阵的就是bitflip,而且还会根据翻转量/步长进行多种不同的翻转,按照顺序依次为:

  • bitflip 1/1,每次翻转1个bit,按照每1个bit的步长从头开始
//stage_max是当前测试用例的bit数
  for (stage_cur = 0; stage_cur < stage_max; stage_cur++) {
   
	//翻转第stage_cur 位bit
    FLIP_BIT(out_buf, stage_cur);
	//将修改后的测试用例拿去测一下目标二进制,如果有触发新的tuple或者使某个tuple的命中数增加了合理值,就把翻转后的内容作为新的测试用例加入到queue种,
	//如果用户主动结束测试,或者用户主动请求放弃当前测试用例等,common_fuzz_stuff就返回1,执行abandon_entry
    if (common_fuzz_stuff(argv, out_buf, len)) goto abandon_entry;
	//没出错就把修改的那一位改回来
    FLIP_BIT(out_buf, stage_cur);
    .....
    }
  • bitflip 2/1,每次翻转相邻的2个bit,按照每1个bit的步长从头开始

  • bitflip 4/1,每次翻转相邻的4个bit,按照每1个bit的步长从头开始

  • bitflip 8/8,每次翻转相邻的8个bit,按照每8个bit的步长从头开始,即依次对每个byte做翻转

  • bitflip 16/8,每次翻转相邻的16个bit,按照每8个bit的步长从头开始,即依次对每个word做翻转

  • bitflip 32/8,每次翻转相邻的32个bit,按照每8个bit的步长从头开始,即依次对每个dword做翻转

每次翻转后会将翻转后测试用例作为输入检测一次程序,如果导致崩溃就放弃对这个测试用例的继续变异,直接对下一个测试用例进行如上操作

自动检测token

在进行bitflip 1/1变异时,对于每个byte的翻转还会进行了额外的处理:如果连续多个bytes被翻转后,程序的执行路径都未变化,而且与原始执行路径不一致,那么就把这一段连续的bytes判断是一条token。
例如,PNG文件中用IHDR作为起始块的标识,那么就会存在类似于以下的内容
…IHDR…
当翻转到字符I时,因为IHDR被破坏,此时程序的执行路径肯定与处理正常文件的路径是不同的;随后,在翻转接下来3个字符时,IHDR标识同样被破坏,程序应该会采取同样的执行路径。由此,AFL就判断得到一个可能的token:IHDR,并将其记录下来为后面的变异提供备选。

生成effector map

在进行bitflip 8/8变异时,AFL还生成了一个非常重要的信息:effector map。这个effector map几乎贯穿了整个deterministic fuzzing的始终。

具体地,在对每个byte进行翻转时,如果其造成执行路径与原始路径不一致,就将该byte在effector map中标记为1,即“有效”的,否则标记为0,即“无效”的。

这样做的逻辑是:如果一个byte完全翻转,都无法带来执行路径的变化,那么这个byte很有可能是属于"data",而非"metadata"(例如size, flag等),对整个fuzzing的意义不大。所以,在随后的一些变异中,会参考effector map,跳过那些“无效”的byte,从而节省了执行资源。

2.arithmetic,进行加减操作

在bitflip变异全部进行完成后,便进入下一个阶段:arithmetic。与bitflip类似的是,arithmetic根据目标大小的不同,也分为了多个子阶段:

  • arith 8/8,每次对8个bit进行加减运算,按照每8个bit的步长从头开始,即对文件的每个byte进行整数加减变异
 for (i = 0; i < len; i++) {
   

    u8 orig = out_buf[i];//指向当前操作的字节

    /* Let's consult the effector map... */

    if (!eff_map[
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值