符号测执行软件测试,基于符号执行与模糊测试的混合测试方法

1 引言

随着信息技术的发展, 软件已经渗透到现代社会的方方面面, 而由于开发不当引入的软件漏洞也日益增多.据统计, 最近5年内软件漏洞数增加了38%, 而仅在2016年~2017年间就增加了14%[.软件测试是检测软件漏洞的一种主要方法, 当前工业界的主流方法还是通过手工设计测试用例来提高软件产品的质量, 然而, 手工生成测试用例通常效率较低、成本高昂并且容易出错[.每年成千上亿的资金被投入到软件行业, 其中软件测试一般需要占据50%以上的成本预算[.

软件漏洞可以被看作是隐藏在某个条件下的错误语句, 通过提升测试用例的代码覆盖率可以提高软件漏洞的检测概率.软件测试致力于为待测程序生成高代码覆盖率(例如语句覆盖、分支覆盖等)的测试用例以发现软件漏洞, 当被测程序配套的测试用例覆盖率高且均执行通过时, 则认为该程序在一定程度上具有高可靠性.

基于覆盖率引导的模糊测试(coverage-guided fuzz testing)[(下文中所提到的模糊测试均指基于覆盖引导的模糊测试)与符号执行(symbolic execution)[是目前两种被广泛研究和使用的测试技术.给定初始测试用例, 模糊测试(例如AFL工具)动态地执行目标程序, 并基于覆盖率选择已有测试用例进行随机变异(mutation), 从而生成新的测试用例.常见的变异操作包括字节翻转等.该过程将不断被重复, 直到不能覆盖到更多的分支或语句为止.在动态执行测试用例时, 如果检测到异常崩溃(crash), 则认为发现了漏洞.由于模糊测试采取随机变异, 因此难以生成可以覆盖到复杂条件的测试用例.例如, 32次才能覆盖到条件input=123456789.

图 1

Fig. 1

ffe1053ce01d91052eed61a6d42be107.png

Fig. 1 Code coverage comparison

图 1 代码覆盖率比较

符号执行通过符号化执行程序来收集约束条件, 并借助约束求解器input=123456789分支并触发漏洞error1的测试用例.但其缺点是, 由于需要符号化地执行程序, 因此在遇到循环, 尤其是循环次数依赖于输入的循环(例如

当前已有多个工作[、SAGE[、SYMFUZZ[以及HCT[将随机测试与符号执行一起使用, 但这些方法的随机测试中并未考虑程序的覆盖信息.而且, DART、SAGE以及SYMFUZZ都属于单向结合.具体来说, DART与SAGE利用随机测试提高符号执行, 而SYMFUZZ利用符号执行辅助随机测试.Munch[将符号执行与模糊测试相结合, 但其目的在于提高函数覆盖率, Driller[将混合执行与模糊测试相结合, 其效果依赖于混合执行所选取的测试用例.不同于上述方法, 本文通过分析模糊测试与符号执行的优缺点, 以分支覆盖信息为引导, 提出了一种符合执行与模糊测试多次交互与结合的混合测试方法.

模糊测试和符号执行各有优缺点, 通过结合双方优点可以在一定程度上弥补不足, 从而获得更高的覆盖率.直观上, 程序的运行空间可以看作是一棵执行树, 模糊测试通过随机变异的方法可以覆盖执行树上的多条路径, 并且每条路径都可以执行到较深的位置; 而由于一些复杂的条件分支, 使得执行树中较浅的位置无法被突破.而符号执行可以突破执行树内较浅位置的复杂条件分支, 但却难以深入到较深的位置.因此, 本文的出发点是通过模糊测试探索程序执行树中的多个易覆盖的以及较深位置的分支, 通过符号执行来系统地搜索执行树, 以尽可能地覆盖模糊测试未覆盖到的分支.例如, 通过结合模糊测试和符号执行, 本文所提方法可以覆盖

具体来说, 本文提出了一种将模糊测试(基于american fuzzy lop[, 简称AFL)与符号执行(基于KLEE[)相结合的方法, 并开发出相应的工具Afleer.其中, AFL进行标准的模糊测试, KLEE依据AFL当前的覆盖信息调整搜索策略, 当遇到未覆盖的分支时生成测试用例并发送给AFL.AFL通过读取新的测试用例进而探索该分支下更多的分支.在实证研究中, 将Afleer工具应用到标准程序集LAVA-M[以及1个实际项目oSIP[上, 以分析本方法的代码覆盖能力和漏洞检测能力.实证研究结果表明:与AFL相比, 在标准程序集上, Afleer可以检测到755个漏洞而AFL仅能发现1个.并且, 在分支覆盖与路径覆盖上都有不同程度的提升.其中, LAVA-M在最好情况下(项目who中), 分支覆盖率提升了35%, 路径覆盖率提升了492%.在实际项目oSIP中, 分支覆盖率提升了2.4倍, 路径覆盖率提升了6.1倍.除此之外, 在oSIP中, Afleer发现了一个新漏洞并已被提交.上述实证研究结果证明了Afleer的有效性.

本文的主要贡献可总结如下.

●本文提出了一种将模糊测试与符号执行相结合的新方法, 即通过模糊测试的覆盖率信息来引导符号执行的搜索, 从而生成具有更高代码覆盖率的测试用例, 并有助于找到更多的软件漏洞.

●基于该方法, 实现了工具Afleer, 并设计了相关实验, 在标准程序集与实际项目上证明了本方法的有效性.

●本文深入分析了影响代码覆盖率的因素, 在此基础上讨论了符号执行和模糊测试的优缺点, 为之后两种技术的研究以及结合提供了指导性建议.

本文第2节介绍研究背景.第3节介绍所提方法的框架和具体实现细节.第4节介绍本文的实证研究并对实验结果进行分析与讨论.第5节对相关工作进行总结, 并与本文所提方法进行比较.最后进行总结并对未来工作加以展望.

2 研究背景和研究动机

2.1 模糊测试与AFL工具

基于覆盖率引导的模糊测试是一种高效的自动化测试技术, 它通过随机变异的方式来生成大量测试用例, 依靠覆盖信息来决定新生成的测试用例是否应该被保留.其中, American Fuzzy Lop(AFL)[工具是当前最先进和最流行的模糊测试工具之一, 并已在大量实际项目中检测到了新的漏洞.

AFL在编译过程中对程序进行插桩来记录程序的覆盖信息, 并使用遗传算法来变异已有测试用例以生成最有可能探索到新的程序状态的测试用例.具体来说, AFL在编译过程中为待测程序的每个基本块(basic block)插入一个编号, 当一个条件分支(prev, cur)被覆盖一次时, 其执行次数加1(即trace_bits[h(prev, cur)]++).其中, prev和cur分别表示先前基本块与当前基本块的编号, trace_bits是记录当前测试用例覆盖信息的数组, 索引值表示分支的哈希值.这样, 在执行一个测试用例后, trace_bits记录了程序中每个分支的覆盖次数.同时, 在执行多个测试用例后, AFL也会记录全局覆盖信息数组virgin_bits, 它表示已执行的所有测试用例的覆盖信息.通过比较trace_bits与virgin_bits, 可以判断出当前测试用例是否可以覆盖到新状态, 从而决定是否保留该测试用例.

AFL通过以下变异操作来对一个已有测试用例进行变异, 对于变异后的新测试用例, 如果其可以覆盖到新状态, 则保留该测试用例.

●变异操作1:针对位或者字节进行翻转.

●变异操作2:以单字节、双字节或者四字节为单位进行加减操作.

●变异操作3:删除、复制、重写或者插入新的字节块.

●变异操作4:两个测试用例随机选取位置并进行拼接.

2.2 符号执行与KLEE工具

动态执行时, 一个具体测试用例对应于程序控制流图(control flow graph, 简称CFG)中的一条路径.因此, 模糊测试通常只能执行到程序的部分行为(即under-approximation).

与此相反, 符号执行的输入不是一个具体测试用例, 而是一个符号值, 这样可以模拟执行程序的多条路径.符号执行在执行过程中为每条路径记录一个路径条件(path condition)ϕ, 当遇到一个分支条件时, 该分支条件ρ分叉(fork)为两条路径, 其路径条件被更新为ϕ^ρ以及ϕ^$\neg $ρ.通过约束求解器[是基于LLVM[实现的符号执行工具.被测程序被编译为LLVM位码, KLEE直接解释指令集并将其转为相应的约束条件.

2.3 优缺点分析及研究动机

正如引言所述, 模糊测试的优点在于动态执行, 因此执行一个测试用例, 其可以覆盖到较深的位置; 而缺点是当遇到复杂的分支条件时, 则很难通过随机变异的方法来生成满足该条件的测试用例.符号执行的优点是其可以突破复杂分支条件, 由于维护了路径条件, 因此通过约束求解器可以生成满足该条件的测试用例.而缺点是由于所有值都需要进行符号化, 因此, 当遇到依赖符号输入的循环或者递归时, 其无法确定迭代次数, 从而陷入到循环展开中.此外, 随着路径深度的增加, 路径条件也会越来越复杂, 其对于约束求解器也会构成很大的挑战.

综上所述, 如果将程序看作执行树, 一个分支能否被覆盖, 主要受到两个因素的影响:分支所在路径的深度以及分支所在路径的路径条件复杂度.该因素主要受到两种方法的本质特征所决定.从概率上来讲, 符号执行属于白盒测试技术, 其符号化地遍历程序的整个执行树.当分支所在路径的深度较深时, 符号执行遍历到该分支的概率也会降低, 并且约束求解的难度也会提高, 符号执行此时会比较低效.而模糊测试属于灰盒测试技术, 其本质是在程序的输入集内进行随机枚举, 此时, 基于执行树上的路径, 把输入集划分为不同的区域(即不同的输入会执行不同的路径).路径条件复杂可以理解为该路径可执行的输入集较小, 因此模糊测试变异到的概率也会变小, 此时, 模糊测试会比较低效.

图 2

Fig. 2

092aac825b6384a503f11825e9999f57.png

Fig. 2 Analysis of branch coverage on program execution tree

图 2 程序执行树中的分支覆盖分析

3 主要方法

3.1 方法框架

本文所提出的Afleer方法的框架如p, c)未被覆盖时, 则生成相应的测试用例T并加入到测试用例集中.模糊测试监控新增测试用例集, 当有新的测试用例T时, 将其读入并加入到自己的测试用例队列中, 此时, (p, c)已被覆盖.基于新的测试用例, 模糊测试可以继续探索(p, c)后面的分支.通过不断的迭代, 最终Afleer可以覆盖到更多的分支.下面, 我们将详细介绍Afleer方法的实现细节.

图 3

Fig. 3

707208d252e58440181ca30d2190937c.png

Fig. 3 Overview of Afleer

图 3 Afleer的主要框架

3.2 代码插桩处理

插桩一致性.符号执行与模糊测试结合的前提是其双方版本一致.具体来说, 在源文件以及可执行文件这两个版本中, 需要保证控制流图以及每个基本块的插桩编号相同, 使得双方在覆盖信息上保持一致.

例如, KLEE的输入是LLVM位码, 而AFL可以基于LLVM进行插桩, 为了保证插桩信息一致, 该项工作修改了编译工具Whole Program LLVM[(简写为WLLVM).WLLVM是一个可以为项目生成LLVM位码以及可执行文件的工具, 但是, 由于WLLVM生成LLVM位码与可执行代码是分两次编译的, 从而导致同一个基本块在两次编译中随机生成两个不同的编号.最终, 模糊测试与符号执行无法同步覆盖信息.修改该工具如下:首先插桩并编译为LLVM位码, 之后再将位码编译为目标代码(object文件), 最终将目标文件链接为可执行文件.修改后的WLLVM中仅插桩一次, 从而保证了LLVM位码与可执行代码的插桩信息保持一致.

插桩预处理.经过模糊测试插桩后, 每个基本块都加入了统计覆盖信息的代码.例如, 在AFL插桩后会引入两个外部全局变量, 并且LLVM位码中的每个基本块会多出8条计算指令.而符号执行不需要这些动态计算覆盖信息, 因此, 这些指令会极大地影响符号执行的执行效率.我们给出算法来获得插桩信息中的编号, 并删除其他无用指令.

算法1给出了针对AFL插桩的预处理算法, 其输入是LLVM的模块M.在第1行删除AFL引入的外部全局变量(即afl_prev_loc与afl_area_ptr), 由于这两个全局变量都是外部变量, 会造成符号执行运行时无法找到变量而报错, 第1行将其删除.其次, 在模块M中的每一个基本块中, 删除AFL插桩的额外代码.对于每个基本块B, 首先解析插桩指令, 并得到随机插桩编号ID(第3行), 然后删除插桩指令(第4行), 并插入一条指令(第5行)以保存ID的值.针对KLEE, 我们选择加入一条KLEE未支持的指令AtmoicRMW来保存ID.因此, 在经过预处理后, 在每个基本块中8条指令将减少为1条.

算法1.插桩预处理.

Input: M: LLVM模块(Module).

1    删除M中模糊测试引入的全局变量;

2    foreach B∈M do

3      ID=从插桩代码中解析基本块的编号;

4      删除所有插桩的代码;

5      插入一条指令以保存ID;

6  end

3.3 Afleer模糊测试模块

算法2展示了Afleer中模糊测试模块.Afleer主要扩展了AFL的算法, 输入为已有测试用例Seeds、待测程序P以及来自符号执行的新增测试用例集SE_Seeds.输出是生成的所有测试用例, 其中, 包含可以触发漏洞的输入.AFL依次遍历Seeds中的每一个测试用例seed.首先对其进行变异, 得到新的测试用例newseed (第2行).注意, 一个测试用例可以通过多种变异操作得到多个测试用例, 不失一般性, 这里假设其结果为newseed.然后程序P执行新的测试用例newseed(第3行), 得到覆盖信息以及运行结果.如果运行结果发现异常, 则找到了触发漏洞的测试用例(第4行).如果该测试用例使得新的分支被覆盖(第7行), 则将其加入测试用例集合(第8行)并更新覆盖信息(第9行).在完成一个测试用例的处理后, 将检查是否符号执行探索到新的未覆盖分支, 如果发现(第11行)新的测试用例, 则将其加入Seeds的最前列, 并将SE_Seeds清空.这样, 在符号执行发现新的测试用例后, 模糊测试将对其进行变异, 以探索该分支下更深位置的分支.

算法2. Afleer模糊测试模块.

Input: Seeds:己有测试用例, P:待测程序, SE Seeds:新增测试用例;

Output:测试用例.

1 foreach seed∈Seeds do

2    newseed=mutate(seed);

3    (trace_bits, result)=Execute(P, newseed);

4    ifresult==crash then

5      发现漏洞;

6    end

7    if hasNewCov(trace_bits, virgin_bits)then

8      Seeds=Seeds∪{newseed};

9      update(virgin_bits, trace_bits);

10  end

11  if SE_Seeds≠∅ then

12      Seeds=Seeds∪SE_Seeds;

13      SE_Seeds=∅;

14  end

15 end

3.4 Afleer符号执行模块

算法3展示了Afleer中符号执行模块的主要思想.Afleer主要扩展了KLEE的算法, 算法输入是LLVM的模块M, 覆盖信息virgin_bits, 输出为新生成的测试用例SE_Seeds.首先创建一个初始状态(第1行), 并将其加入到状态集合States中(第2行).此时, 从初始状态开始进行遍历, 在集合States中选择一个状态(第4行), 这里, 搜索方法可以采取不同的策略(例如, 深度优先策略或者广度优先策略等).接下来得到当前状态的指令inst, 根据指令的不同类型对其进行解释执行(第6行~第30行).例如, 当指令为分支条件时(第7行), 当前状态分叉为两个状态并更新路径条件state.pc.如果当前分支的路径条件可满足(第8行与第12行), 则将生成的新状态加入状态集合.当遇到指令AtmoicRMW(来自算法1)时解析得到基本块编号(第18行), 基于该编号可以检查当前分支是否被覆盖.第18行~第20行计算当前分支的哈希值(与AFL中的插桩计算等价).如果覆盖到新的分支(第21行), 则生成测试用例(第22行), 并将其加入到测试用例集合SE_Seeds(第23行).当该路径生成新的分支后, 我们将该状态优先级降低(第24行), 因为在符号执行突破新的分支后, 我们期待模糊测试基于该测试用例进行更深位置的探索.因此, 为了提高效率, 此时, 符号执行将优先探索其他分支, 从而避免与模糊测试进行同位置的探索.第4行的搜索算法将同时考虑各个状态的优先级.对于其他类型的指令, 符号执行将根据相应的语义进行解释与执行(第28行).

算法3. Afleer符号执行模块.

Input:M :LLVM模块, virgin bits:覆盖信息;

Output:SE_Seeds:新增测试用例.

1  start=createNewState(M);

2  States={start};

3    while States≠∅ do

4      state=select(States);

5      inst=state.ins;

6      switch inst do

7        case BranchInst

8          if state.pc∧pathCond(inst, true) then

9                state1=fork(state, inst, true);

10              States=State∪{state1};

11          end

12          if state.pc∧pathCond(inst, false) then

13              state2=fork(state, inst, false);

14              States=State∪{state2};

15          end

16          endsw

17          case AtmoicRMWInst

18          cur=getID(inst);

19          hash=cur⊕state.pre;

20          state.prev=cur > > 1;

21          if hasNew(virgin bits, hash) then

22            seed=generateTestCase(state);

23            SE_Seedss=SE_Seed∪{seed};

24            state.priority--;

25          end

26        endsw

27        otherwise

28            handle(state, inst);

29        endsw

30      endsw

31    end

4 实证研究

本节介绍本文的实证研究, 首先提出研究问题, 接着介绍实验设计的具体方法, 然后总结并分析实验结果, 最后对Afleer的优缺点进行讨论, 并对符号执行及模糊测试相结合的未来研究工作进行分析.

4.1 实验设计

基准方法的选择.Afleer方法的目的是通过结合符号执行帮助模糊测试能够覆盖到更多的分支, 从而发现更多的漏洞.在实验中我们选择AFL(本文提供的方法是通用的, 可以很容易地扩展到其他AFL优化的方法上, 例如AFLfast[、Steelix[以及Skyfire[等)作为基准方法, 而未选择KLEE作为基准方法, 其主要原因总结如下.

(1) 在Afleer中, 主要使用KLEE增强AFL寻找未覆盖边的能力.而不是用AFL去辅助KLEE生成测试用例, 因此, 本文将侧重点放在对模糊测试方法的改进上;

(2) 为了提高效率, Afleer中KLEE未对AFL已覆盖的边生成测试用例, 因此, Afleer生成的测试用例更适合与AFL生成的测试用例进行对比;

(3) 即使将Afleer中的KLEE按标准方式加以运行, 其生成的测试用例一定是Afleer生成的测试用例的子集, 因此, 将两者进行比较没有意义.

除此之外, 将符号执行与动态测试结合的技术还有Driller[以及配套工具HCT[等, 然而, 由于HCT工具没有开源, Driller仅能够运行在DECREE的二进制程序上, 因此, 在我们设计的实验中并没有与其直接进行比较, 在相关工作中, 我们将从方法上进行比较.

研究问题.为了验证Afleer的有效性, 本文尝试回答如下研究问题.

● RQ1. Afleer在漏洞检测上的效果如何?与AFL相比, 其是否可以在相同的指定时间内检测到更多的漏洞?或者对于同样的漏洞, 其是否可以更快地检测到?

● RQ2. Afleer在测试覆盖能力上的效果如何?是否借助符号执行可以有效辅助Afleer覆盖到更多的分支?

实验数据集.为了回答上述研究问题, 本文选择了标准程序集LAVA-M[以及一个实际项目oSIP-4.0.0[作为实验对象.其中, LAVA-M中的每个程序均包含了大量的已知漏洞, 因此可以用来公平地比较各种方法的漏洞检测能力, 实际项目oSIP则可用来验证Afleer的可扩展性以及有效性.标准程序集LAVA-M和项目oSIP的具体信息总结如下.

● LVAM-M包含4个带有漏洞的Linux系统工具程序:base64、md5sum、uniq以及who.数据集的搜集人员按照一定标准在每个程序中插入了大量的难以检测到的漏洞.其中, 每个漏洞都有一个唯一编号, 当该漏洞被触发时则打印对应漏洞编号.我们使用LAVA-M来比较Afleer以及AFL的漏洞检测能力, 由于这些程序都来自实际项目, 因此, 它们同时也被用来比较不同方法的代码覆盖能力.注意, 在运行base64与md5sum时, 我们分别使用了参数-d与-c.

● oSIP实现了SIP协议[, 其为用户提供了初始化以及控制SIP会话的接口实现.由于其包含了SIP协议的语法解析器, 因此, AFL难以生成可以通过复杂语法检查的测试用例, 我们将分析Afleer在该项目中的代码覆盖能力.

评测指标.针对漏洞检测能力以及代码覆盖能力, 本文通过以下指标来评测不同方法.

●漏洞检测数(#bugs):在指定时间内找到的漏洞总数.

●边覆盖数(#edges):AFL中的边指的是分支, 边覆盖数表示在指定时间内的分支覆盖数.

●路径覆盖数(#path):路径数表示AFL在指定时间内生成的测试用例数.注意, AFL采用了循环桶(loop bucket)的思想, 部分路径的测试用例不会被保留.

运行实验的机器配置信息总结如下:CPU Intel Xeon E5-1650 v3, 内存16GB, 操作系统64位Ubuntu 16.04LTS, 并且运行时间固定为5小时.

4.2 实验结果分析

4.2.1 LAVA-M程序集上的漏洞检测能力比较(RQ1)

表 1(Table 1)

6f8671eee0ef76504a7c4aef57678b26.gif

Table 1 Results of bugs found on benchmark LAVA-M

表 1 LAVA-M程序集上的漏洞检测结果比较

LAVA-M

base64

md5sum

uniq

who

Total

Total Bugs

44

57

28

2 136

2 265

Afleer

#kleebugs

6

0

8

9

23

#afleerbugs

11(25%)

0

8(28.57%)

736(34%)

755(33%)

AFL

#aflbugs

0

0

0

1

1

Table 1 Results of bugs found on benchmark LAVA-M

表 1 LAVA-M程序集上的漏洞检测结果比较

基于

图 4

Fig. 4

32174974f9b529f12fcbf47388e470b2.png

Fig. 4 Increasing of bugs finding on LAVA-M

图 4 LAVA-M漏洞检测趋势图

通过查看LAVA-M中漏洞插入的代码, 我们分析了AFL难以检测到漏洞而Afleer可以检测到漏洞的原因.LAVA-M中插入的漏洞的触发条件主要是针对魔术字节(magic byte)的比较, 例如, 0x6c617564==lava_get(253).因此, 普通AFL很难突破这些分支, 从而无法找到漏洞.而在Afleer中, KLEE可以为其生成相应的测试用例0x6c617564.并且, 这些漏洞之间是有一定的关系的, 例如, AFL读取KLEE发送的测试用例0x6c617564后, 在其基础上很容易变异出一个可以覆盖条件0x6c617562==lava_get(255)的测试用例(其只有一个字节是不同的), 并且不受该条件所处深度的影响.例如, 在uniq中, KLEE提供了8个触发漏洞的测试用例, 而AFL基于该测试用例并没有发现新的漏洞.其主要原因是, 触发其他漏洞的测试用例与KLEE已生成的测试用例在字节上相差很大, 因此, 基于变异策略的AFL无法更快地生成新的漏洞.而如果其处于深位置, 则KLEE也将很难搜索到该位置.该结果表明了Afleer的有效性.

同时, 通过分析现有测试用例的覆盖率, 我们进一步获得了其他漏洞未被检测到的原因, 主要原因是由于符号执行与模糊测试本身的弱点导致某些分支难以被覆盖.具体地, 我们从符号执行与模糊测试两方面来进行分析.

(1) 从符号执行方面进行分析, 其主要由两个原因导致.首先, 符号执行面临状态爆炸问题, 由于一些分支在有限时间内无法被遍历到, 因此无法生成相应的测试用例.其次, 某些分支条件依赖于其相应的前置条件, 即使这些分支可以被覆盖到, 但在某些前置条件下, 分支条件永远无法得到满足, 因此也会导致无法生成相应的测试用例.例如, KLEE与AFL在md5sum程序上都没有检测到漏洞, 其原因主要是无法突破开始时的哈希函数, 因此难以覆盖到含有漏洞的分支.

(2) 在模糊测试方面, 由于LAVA数据集中都是较难的数字比较条件, 因此, 模糊测试通过随机变异很难生成相应的测试用例, 从而无法发现漏洞.

在Afleer总共发现的755个漏洞中, 通过符号执行Aflee_KLEE找到了23个, 而通过模糊测试Afleer_Afl找到了722个.因此, 与单独使用符号执行工具KLEE相比, Afleer多发现了722(超过31倍)个漏洞.

基于上述分析我们认为, 通过结合KLEE与AFL, Afleer与单独使用KLEE及AFL相比可以显著提高漏洞检测的能力(即基于LAVA-M评测集可以比AFL多发现754个漏洞, 比KLEE多发现722个漏洞).

4.2.2 LAVA-M程序集上的代码覆盖能力比较(RQ2)

本节我们将依次比较Afleer与AFL在LAVA-M程序集上的分支覆盖能力以及路径覆盖能力.

图 5

Fig. 5

02114f09687e7ddcbc52c87229b14518.png

Fig. 5 Increasing of branch coverage on LAVA-M

图 5 LAVA-M分支覆盖趋势图

表 2(Table 2)

6f8671eee0ef76504a7c4aef57678b26.gif

Table 2 Comparison of the total number of test cases generated by Afleer and AFL

表 2 Afleer与AFL生成的测试用例总数比较结果

Afleer_AFL

Afleer_KLEE

Afleer Total

AFL

base64

171(88%)

24(12%)

195(+28%)

153

md5sum

353(97%)

10(3%)

363(+35%)

269

uniq

75(79%)

20(21%)

95(+4%)

91

who

815(97%)

26(3%)

841(+492%)

142

Table 2 Comparison of the total number of test cases generated by Afleer and AFL

表 2 Afleer与AFL生成的测试用例总数比较结果

图 6

Fig. 6

82d1251b9608905c164c49af7b43f199.png

Fig. 6 Increasing of branch coverage and bug finding on oSIP

图 6 LAVA-M路径覆盖趋势图

基于上述及

4.2.3 基于实际项目的比较(RQ1和RQ2)

我们将Afleer应用在实际项目oSIP中, 并发现了一个新的漏洞, 该漏洞目前已被提交给GNU处理[.在实验中, 我们对输入规模进行了限制(即将输入规模限定为10字节或者128字节), 并通过不同的输入规模来分析其对覆盖能力以及漏洞检测能力的影响.

图 7

Fig. 7

320a41d57b352164f50c13d2aa490f82.png

Fig. 7 Increasing of branch coverage and bug finding on oSIP

图 7 oSIP分支覆盖以及漏洞检测趋势图

同时, 我们也分析了Afleer中AFL与KLEE的各自贡献率(记为KLEE/AFL).其中, 在测试用例生成方面, 当输入长度为10字节时为60/78, 当输入长度为128字节时为2/680.可以看到, 在输入长度较小时, KLEE会快速生成更多的测试用例, 因为当输入长度较小时, KLEE的约束复杂度较低, 从而使得求解速度快速提高.在漏洞发现方面, KLEE没有发现该漏洞.

总的来说, Afleer在实际项目中比AFL和KLEE在代码覆盖能力及漏洞检测能力上都具有显著的提升, 因此证明了Afleer的可扩展性和有效性.

4.3 进一步讨论

本节将结合上述实验结果进一步讨论影响Afleer有效性的因素, 在Afleer中, KLEE的方式是搜索执行树而AFL的方式是变异测试用例, 因此, Afleer的有效性受制于KLEE与AFL本身在目标程序上的搜索效率与变异效率.

从KLEE角度出发, 其在搜索时会存在状态爆炸问题.当目标分支在目标程序的较浅位置时, KLEE比较容易搜索到该分支, 从而生成相应的测试用例.例如,

从AFL的角度出发, 测试用例在内容上的关联程度对其影响较大.假设KLEE已为某个目标分支生成测试用例并提交给AFL, 如果剩余未覆盖分支的测试用例与当前测试用例关联程度较强(即相似度较高), 则AFL通过变异可以很快覆盖到其他分支, 并且不受深度的影响.例如, 在上一节所述内容中, KLEE生成了测试用例0x6c617564后, 对于另一个处于较深位置的未覆盖分支0x6c617562==lava_get(255)则很难搜索到.但是, AFL通过变异可以很快地为其生成测试用例, 在项目who中, KLEE为其找到了9个漏洞, 在此基础上, AFL仍多检测到727个漏洞(见

总的来说, 当符号执行发现某个未覆盖的分支, 而模糊测试基于该分支可以找到更多的未覆盖分支时, 双方结合的方式将会更加高效.当KLEE无法搜索到未覆盖的分支, 或者AFL无法基于KLEE生成的测试用例找到更多的测试用例时, 双方结合的方式将会低效.

此外, 在Afleer的设计中, KLEE的不同之处在于:(1)对于AFL已经覆盖到的边, KLEE仍然可以到达该边但是不会生成测试用例; (2)对于AFL未覆盖的边, 则生成测试用例并发送给AFL.KLEE在Afleer中执行的先后顺序仅影响AFL与KLEE中都容易生成的测试用例, 而不影响整体效率.具体来讲, 如果KLEE先执行, 则KLEE将会优先生成多个AFL未遍历的测试用例(假设测试用例集合为P+Q, P表示对AFL容易覆盖到的测试用例集合, Q表示AFL较难覆盖到的测试用例集合).由于此时AFL还未执行, P与Q执行的路径并未被覆盖, 因此, 将由KLEE优先生成.而如果AFL先执行, 则P将由AFL快速生成, 此时, KLEE在遍历时如果遇到这些边则不再需要生成P, 仅只生成AFL难以生成的测试用例Q.总的来说, KLEE与AFL的先后执行顺序仅影响P由哪部分优先生成; 而对于AFL难以生成的测试用例Q, 理论上无论哪部分先执行, 其基本上都将被KLEE生成.

为了解决Afleer现有的不足, 未来可以从以下几个方面进行改进.在符号执行方面, 可以采用加入静态分析(例如文献[[, Angora[)、定向模糊测试(AFLgo[, Hawkeye[)以及其他策略(Skyfire[, AFLFast[)来提高模糊测试的能力, 从而提高混合测试的整体性能.

5 相关工作

本节将分别从模糊测试、符号执行以及混合测试等角度对已有研究工作进行总结.

5.1 模糊测试

目前存在很多基于遗传算法并基于覆盖率引导的模糊测试工具, 例如:AFL[、libFuzzer[以及honggfuzz[.污点分析(taint analysis)用于分析程序输入与程序逻辑之间的关系, 目前, 很多基于污点分析的方法已被用来提高模糊测试的能力.BuzzFuzz[以及FairFuzz[使用动态污点分析的方法来定位输入中感兴趣的字节, 随后重点对这些字节进行变异, 从而提高效率.Taintscope[通过污点分析来识别验证校验和(checksum)数字的分支, 并通过改变控制流来绕过这些难以通过的分支.与BuzzFuzz和FairFuzz类似, 在绕过校验和的检验后, 其识别出输入中与危险操作相关的字节并进行重点变异.Dowser[对缓冲区溢出漏洞进行检查, 通过污点分析检查输入中影响到数组索引变化的字节.污点分析虽然可以识别出重要字节, 但同时也会付出很高的性能代价, 并减缓模糊测试的执行速度.VUzzer[通过污点分析识别出魔术字节(magic bytes)在输入中的位置, 从而对这些字节进行变异, 其缺点是魔术字节在输入中的位置是固定的.Steelix[在固定位置通过细粒度的插装方式来拆分程序中魔术字节的比较状态.例如, 一个32位的整数比较可以记录为4个8位整型的比较, 从而加快魔术字节的匹配速度, Steelix适用于魔术字节比较中变量直接来自输入的场景.Angora[提供了一种不通过符号执行来求解路径约束的方法, 通过污点分析, 借助梯度下降来搜索可以满足约束条件的测试用例.基于上述分析可以看出, 基于污点分析的模糊测试与本文所提出的Afleer的目标相似, 都是针对难以覆盖的分支来生成测试用例.其优势在于, 当分支条件中的变量直接来自输入的某些字节时, 即使该分支处于较深的位置都可以快速生成测试用例.而如果分支条件中的变量依赖于输入变量的所有字节或者依赖于部分字节的复杂计算, 则基于污点分析的方法将会失效, 而Afleer通过符号执行则可以有效解决该问题.

除此之外, 研究人员还提出了一些用来提高模糊测试效率的方法.例如, Rebert[等人以及Woo等人[在模糊测试中, 借助测试用例选择以及调度策略来提升漏洞检测能力.AFLFast[发现模糊测试在大部分时候都在尝试高概率路径, 而很难触发到新的低概率路径, 因此其通过给覆盖低概率路径的测试用例赋予更多的时间进行变异来提升效率.AFLgo[是一种针对特定目标点的模糊测试技术, 其通过为测试用例排序来进行高效率的有针对性的测试.Skyfire[从现有测试用例中学习出基于概率的上下文敏感语法(probabilistic context sensitive grammar), 以指导生成高质量的结构型测试用例.CollAFL[提出了一种可以解决模糊测试中路径碰撞(path collisions)问题的方法, 路径碰撞问题主要由插装过程中的基本块编号碰撞所导致, 该问题会对新漏洞检测能力产生影响.不难看出, 这些方法与Afleer存在正交关系, 在下一步工作中, 我们可以进一步融合这些方法来提升Afleer中模糊测试的能力.

5.2 符号执行

符号执行已得到广泛研究, 并在工业界与学术界中涌现出了很多优秀工具, 例如:KLEE[、JPF[、CUTE[、S2E[、PEX[以及Angr[等.符号执行时最大的问题是状态爆炸或者路径爆炸, 目前, 存在很多针对该问题的研究工作.

使用更好的搜索策略是解决状态爆炸问题的一种方法, 例如:随机路径探索[、深度优先探索[、广度优先探索[、基于覆盖信息的探索[.谢涛等人[提出了一种基于适应度函数的路径探索方法.陈振邦等人[提出了一种基于待验证性质引导的搜索策略.李宣东等人[提出了一种优先搜索最少被遍历过的路径从而提高整个覆盖率的方法.王海军等人[提出了一种基于路径依赖分析以删除冗余路径的方法, 从而缓解了状态爆炸问题.实证研究结果表明, 这些策略可以在一定程度上缓解路径爆炸的问题.

通过状态约减[也是解决路径爆炸问题的一种方法, 如果一条路径不可达或者约束条件与之前遍历的路径的约束条件相同, 则可以不必执行该路径.甘水滔等人[提出了一种根据程序功能进行切片分析的方法, 进而引导符号执行并约减无关状态.Trabish等人[提出的方法, 可以在符号执行过程中实时地过滤无关代码从而提高效率.此外, 合并路径[以及循环总结技术[.

不难看出, 上述针对符号执行的研究工作与本文所提出的Afleer同样存在正交关系, 在下一步工作中, 我们可以进一步融合这些方法, 帮助Afleer更高效地搜索到未覆盖的分支, 从而提升Afleer的效率.

5.3 符号执行与模糊测试结合的混合测试

目前已有很多关于符号执行与模糊测试(或随机测试)结合的混合测试.DART[与SAGE[通过随机测试产生一定数量的测试用例, 之后选取测试用例并运用混合执行来系统性地遍历程序.与Afleer相比, DART与SAGE使用随机测试以及混合执行, 而Afleer使用符号执行与基于覆盖率的灰盒测试.DART与SAGE是单向的, 其仅将随机测试生成的测试用例用于混合执行, 而Afleer是双向交互的, 因此能够更高效地生成测试用例.SYMFUZZ[通过符号执行分析两个程序输入之间的依赖关系, 基于依赖信息计算测试用例的变异率, 并用随机测试来进行变异.SYMFUZZ不是直接将符号执行与随机测试结合, 而是使用符号执行计算出一定的信息来提高随机测试的效率.Brian等人[提出了一种结合符号执行与模糊测试的混合测试方法.与Afleer不同的地方在于:(1)该方法使用随机测试生成测试用例来发现哪些基本块较容易被覆盖.基于该信息, 利用符号执行来搜索难以覆盖的基本块.而Afleer直接使用模糊测试的覆盖信息来指导符号执行的搜索过程.(2)不同于传统模糊测试, 在该方法中, 模糊测试不进行测试用例的变异操作, 而仅用来运行符号执行生成的测试用例, 以检测程序是否出现崩溃, 因此, 其并未充分利用模糊测试的信息及优势.

Majumdar等人[提出了一种混合执行与随机测试交互执行的方法(简称HCT), 其通过随机测试产生测试用例, 当随机测试收敛时(即无法生成新的测试用例)转到混合执行.一旦混合执行找到新的测试用例, 则返回随机测试继续进行.Yun等人[提出了一种适合混合测试的混合执行工具QSYM, 他们深入分析了影响混合测试的主要性能瓶颈:即混合执行测试.该方法与我们的方法是正交关系, 通过引入其改进的混合执行测试方法QSYM, 可以提升Afleer的整体效率.Chao-Chun等人[提出了一种以发现漏洞为目标的混合执行方法CRAXfuzz.首先对一些与安全密切相关的函数进行hook, 例如malloc、strcpy以及printf等.之后基于某个种子进行混合执行测试, CRAXfuzz采用目标驱动的搜索策略来检测被hook的函数是否具有安全问题.如果当前种子未发现漏洞, 则用模糊测试或者符号执行生成新的种子进行尝试.

与Afleer比较类似的工作是Saahil等人[提出的方法Munch.Munch也是使用KLEE与AFL进行结合来提高测试的覆盖率, 但是, 这两种方法具有很大的不同:(1)两者最大的不同在于目标不同, Afleer侧重于提高分支覆盖率, 而Munch侧重于提高函数覆盖率.因此, Afleer采用细粒度的覆盖信息(即分支覆盖)来指导KLEE的搜索, 而Munch采用粗粒度的函数覆盖来引导KLEE搜索(即sonar-search).(2)由于AFL默认也使用分支覆盖引导并生成分支覆盖信息, 因此, 通过分支覆盖则可以更直接、更有效地将KLEE与AFL结合.例如, Afleer中KLEE的搜索利用AFL的分支覆盖信息进行引导, 从而避免搜索AFL已覆盖的分支.因此, KLEE新生成的测试用例一定是AFL感兴趣的测试用例(该测试用例会覆盖AFL未覆盖的分支).因此, Afleer中AFL与KLEE通过多次双向交互, 可以更高效地提高分支覆盖率.在AFL中未考虑函数覆盖信息, Munch中的两种策略都分别属于单向交互(即KLEE到AFL或者AFL到KLEE).而在Munch的实验结果中(原文

Driller[将混合执行工具Angr与基于覆盖率的AFL相结合.首先运行AFL, 当AFL无法生成新的测试用例时, 则Angr选取一个测试用例进行混合执行, 直到发现新的测试用例, 之后将新的测试用例再转交给AFL来继续进行.这两种方法与Afleer类似, 都是通过符号执行/混合执行与灰盒测试/随机测试结合, 并且双向交互.与HTC不同的是, Afleer与Driller使用了灰盒测试, 因此可以利用覆盖率信息来指导符号执行或混合执行的搜索, 进而效率更高.而Afleer与Driller的不同之处在于:Afleer运行在源码上, 而Driller运行在二进制代码上; 并且Afleer使用的是符号执行, 而Driller使用的是混合执行.混合执行的优势是, 如果从AFL的结果中选取高质量的测试用例执行, 则有助于找到新分支.但在实际执行过程中, 不是所有测试用例都是高质量的, 例如:即使某个测试用例在控制流图上可以到达未覆盖的分支, 但该测试用例在该路径下产生的约束条件仍可能无法使分支条件得到满足, 此时就会影响效率.因此, 其是一个平衡的过程, 引入混合执行在某些情况下可以快速找到并覆盖处于深处的分支, 但在某些条件下会出现大量的可到达但无法覆盖深分支的情况.基于上述分析, Afleer没有采用混合执行方式, 而是使用符号执行并依据覆盖信息(即AFL实时更新的文件)进行系统性的搜索, 该策略首先可以保证更快速地覆盖浅位置的新分支.同时, 基于覆盖信息以及控制流图可以运用更通用的搜索算法来遍历程序执行树.

6 总结与展望

本文深入分析了现有符号执行与模糊测试的优缺点, 并提出了一种将符号执行与模糊测试相结合的新颖方法Afleer.该方法综合利用了符号执行可以解决复杂条件的能力, 模糊测试可以覆盖深分支的能力进而生成具有高覆盖率的测试用例.具体来说, 模糊测试可以快速生成大量的测试用例, 符号执行基于模糊测试的覆盖信息进行搜索, 当搜索到新的未覆盖分支时则生成测试用例, 模糊测试基于符号执行生成的测试用例继续变异.本文将Afleer用于标准程序集LAVAM-M以及实际项目oSIP, 最终结果验证了该方法的有效性.

我们在下一步工作中, 将从符号执行与模糊测试本身存在的不足之处出发, 进一步增强Afleer的能力.首先, 将现有的基于污点分析以及有目标性的策略集成到模糊测试中, 其可以提高模糊测试处理复杂分支的能力.其次, 将前期的循环分析与总结工作

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值