CollAFL: Path Sensitive Fuzzing

摘要

覆盖导引模糊测试是一种广泛应用和有效解决软件漏洞的方法。跟踪码覆盖和利用其导引模糊测试是覆盖制导模糊测试的关键。然而,由于仪器开销很大,在实际应用中不可能跟踪完整和准确的路径覆盖。流行的模糊测试(如AFL)通常使用粗糙的覆盖信息,例如存储在紧凑位图中的边缘命中计数,以实现高效的灰盒测试。这种覆盖的不准确和不完整给模糊测试带来了严重的局限性。首先,它会导致路径冲突,从而阻止模糊测试发现导致新崩溃的潜在路径。更重要的是,它阻止了模糊测试对模糊测试策略做出明智的决定。

本文提出了一种覆盖敏感的模糊解collafl。它通过提供更准确的平均信息来减少路径冲突,同时仍然保持较低的仪器开销。它还利用覆盖信息应用三种新的模糊策略,提高发现新路径和漏洞的速度。我们实现了一个基于流行模糊测试AFL的collafl原型,并在24个流行应用中进行了评估。结果表明,路径碰撞是常见的,在某些应用中,高达75%的边缘可能与其他边缘发生碰撞,colafl可以将边缘碰撞率降低到接近零。此外,在三种模糊策略的支持下,collafl在代码覆盖率和漏洞发现方面都优于afl。平均而言,collafl在200小时内覆盖的程序路径比afl多20%,发现独特的崩溃比afl多320%,错误比afl多260%。总共,collafl发现157个新的安全漏洞,分配了95个新的CVE。

一、引言

内存损坏漏洞是许多严重威胁程序的根本原因,包括控制流劫持攻击[12、37、38]和信息泄漏攻击[36]。攻击者依靠漏洞破坏目标程序的执行并执行恶意操作。如果防御者能够提前发现漏洞,他们可以修补漏洞以击败潜在攻击。

覆盖引导模糊测试是最流行的漏洞发现解决方案之一,广泛应用于工业领域。例如,Google的OSS Fuzz平台[35]采用了多种最先进的覆盖制导模糊测试,包括libfuzzer[34]、honggfuzz[40]和afl[45],来持续测试开源应用程序。它在5个月内发现了1000多个bug[3],其中有数千个虚拟机。

首先,跟踪代码覆盖对于覆盖引导的模糊器至关重要。准确的路径覆盖信息可以帮助模糊测试人员感知所有独特路径并探索它们以发现漏洞。然而,由于极高的仪表开销,在实践中跟踪路径覆盖是不可行的。 Fuzzers将覆盖精度与性能进行权衡。 LibFuzzer [34]和honggfuzz [40]利用Clang编译器提供的SanitizerCoverage [4]工具来跟踪块覆盖。 VUzzer [29]使用动态二进制检测工具PIN [24]来跟踪块覆盖。 AFL [45](在GCC和LLVM模式下)使用带有紧凑位图的静态检测来跟踪边缘覆盖,提供比块覆盖更多的信息。即使对于AFL,也存在已知的哈希冲突问题,其中两个不同的边可以具有相同的哈希,因此在覆盖位图中共享相同的记录。它会导致边缘覆盖精度的损失。我们的实验表明,在某些应用中,高达75%的边缘可能会与其他边缘碰撞。

更重要的是,利用覆盖信息来指导模糊测试对于覆盖引导的模糊器至关重要。 AFL利用边缘覆盖信息来识别种子(即,有助于覆盖的良好测试用例),并将它们添加到等待进一步突变和测试的种子池中。 AFLfast [11]进一步利用边缘覆盖信息来优先选择和选择种子(来自池)运行频率较低的突变路径,以提高路径发现的效率。 VUzzer利用块覆盖信息来优先考虑行使错误处理块的测试用例和执行频繁路径的测试用例。但是,鉴于代码覆盖率信息不准确,模糊器无法做出最佳决策。此外,很少有模糊测试器利用代码覆盖率信息直接驱动模糊到未探索的路径。

据我们所知,覆盖不准确的后果被模糊器的巨大成功所掩盖,因此没有得到系统的评估。在本文中,我们证明它实际上对模糊器的能力有着至关重要的影响。我们还证明,如果能够以较低的开销实现准确的边缘覆盖并且部署了适当的覆盖引导的模糊测试策略,则模糊器可以显着提高他们探索路径和发现错误的能力。

A.覆盖不准确性如何模糊错误发现?

首先,覆盖不准确可能导致模糊器在某些情况下无法区分两个不同的程序路径。如果测试用例行使与先前探索过的路径发生碰撞的新路径,则模糊测试器可能会将其错误地归类为不感兴趣,并错过彻底测试此路径或探索相关路径的机会。因此,它将导致代码覆盖率的损失,甚至错过隐藏在这些路径中的潜在漏洞。同样,模糊测试程序也可能错误地将新发现的漏洞分类为不感兴趣,因为它的路径与先前发现的漏洞相冲突。

其次,更重要的是,覆盖不准确性模糊了模糊策略。例如,它可以防止模糊器在选择种子进行变异和测试时做出最佳决策。例如,AFLfast [11]优先考虑运行频率较低的路径的种子,这可能是不准确的,因为覆盖范围是由近似位图捕获的。 AFLgo [10]将种子优先于目标位置,也需要准确的路径覆盖信息。因此,覆盖不准确性问题将使这些模糊测试器的种子选择策略效率低下,从而降低了查找错误的速度。

B.如何提高覆盖精度和引导模糊器?

如上所述,在实践中跟踪准确的路径覆盖是不可行的,但是跟踪边缘和块覆盖是可能的。 AFL提供的边缘覆盖提供了比块覆盖解决方案更多的信息。此外,AFL的覆盖范围跟踪解决方案也比其他解决方案引入更低的运行时开销。因此,我们可以将AFL的覆盖跟踪解决方案移植到其他模糊器,以提高其覆盖精度。但是,由于哈希冲突问题,AFL的边缘覆盖本身并不完美。这个问题的直接解决方案是扩大AFL用于存储覆盖范围的位图的大小。正如我们的实验所示,此解决方案无法消除已知边缘的所有哈希冲突,但会引入显着的开销。

本文介绍了一种覆盖敏感的模糊测试解决方案CollAFL,它可以解决AFL的哈希冲突问题并提高其覆盖精度而不会降低性能。此外,CollAFL不仅利用准确的边缘覆盖信息,还使用三种新设计的模糊测试策略来驱动模糊器到非探索路径,从而提高漏洞发现的效率。

CollAFL通过确保目标程序中的每个边都具有唯一的散列来解决AFL中的散列冲突问题,以便AFL可以区分任何两个边。更具体地说,我们分析目标应用程序的控制流图以获得已知边的列表。精心设计的散列方案用于为所有边缘分配ID到基本块和计算散列,确保仪器成本低,并消除已知边缘的冲突。

一旦解决了哈希冲突问题,模糊器就可以获得准确的边缘覆盖信息,从而实现覆盖敏感的模糊测试策略,例如种子选择策略。 准确的边缘覆盖允许模糊器基于与种子的执行路径相关联的细粒度属性来对种子进行优先级排序,例如,沿着每条路径的存储器访问操作的数量,未触摸的邻居分支和未触摸的邻居后代。 相应地,本文提出了三种新的种子选择策略:记忆访问引导,未触发分支引导和非引导后代引导,每种策略基于上述路径属性优先考虑种子选择。 所有这些策略都显示了模糊器的路径和漏洞发现效率的改进。

C.覆盖敏感模糊测试的表现如何?

我们基于流行的覆盖引导模糊AFL实现了CollAFL的原型。我们在LAVA-M数据集[14]和24个开源应用程序上评估了CollAFL。评估结果表明:

  • 哈希冲突问题在现实世界的应用程序中很普遍,高达75%的边缘可能与其他应用程序冲突;
  • 我们提出的碰撞缓解解决方案可以解决已知边缘的所有哈希冲突,并可以帮助模糊器探索9.9%以上的代码,发现250%的独特崩溃和平均140%的安全漏洞。
  • 未受影响的分支引导种子选择策略(连同碰撞缓解)可以进一步提高模糊器的路径和错误发现的效率。平均而言,它可以帮助模糊器探索20%以上的代码,发现320%的独特崩溃和260%的安全漏洞。

总的来说,我们在这24个开源应用程序中发现了157个安全漏洞,并将它们报告给上游供应商。其他研究人员报告了其中23例,但没有公开曝光。在剩余的134个漏洞中,其中95个由CVE确认。总之,本文做出以下贡献:

  • 我们研究了覆盖引导模糊器中覆盖不准确性的负面影响。 特别是,我们证明了AFL中的哈希冲突问题严重限制了其路径和漏洞发现的效率。
  • 我们设计了一种算法来解决AFL中的哈希冲突问题,通过低开销的检测方案(在大多数情况下比AFL更快)提高其边缘覆盖精度。
  • 我们提出了三种新的覆盖敏感种子选择政策。 我们的实证结果证实,基于准确的边缘覆盖信息对种子进行优先级排序可以显着提高模糊器的性能。
  • 我们实现了基于AFL的CollAFL原型,并在24个开源应用程序上进行了评估。 CollAFL的有效性部分得到了验证,因为它能够在之前经过充分测试的应用程序中找到超过一百个新的安全漏洞。

II。背景和相关工作

A.模糊

Fuzzing是目前最划算最有效的漏洞发现解决方案。通常,模糊器将首先生成大量测试用例来测试目标应用程序,然后监视应用程序的运行时执行并在检测到安全违规时报告错误。

模糊器通常易于设置,可以扩展到大型应用程序。因此,模糊测试已成为业界主要的漏洞发现解决方案。 Fuzzers通常使用两种类型的测试用例生成解决方案:基于语法和基于突变。基于语法的模糊器[15,17]基于已知的输入语法生成测试用例。根据语法,fuzzers可以生成有效的测试用例并覆盖程序路径的主要部分。但是他们需要很多工程工作来翻译输入语法,并且无法处理没有已知语法的应用程序。

另一方面,基于突变的模糊器[20,34,45]改变现有的测试用例以生成新的测试用例而不依赖于输入语法,因此具有更好的可扩展性。由于简单性和可扩展性,基于突变的解决方案在实践中被广泛采用。

然而,基于琐碎突变的模糊器通常具有较差的代码覆盖率。 例如,它们可能会被输入格式检查阻止,并且无法触发更深层次的漏洞。 因此,研究人员在改进这些模糊测试器的代码覆盖率和模糊测试效率方面做了大量工作。

B.覆盖范围引导的模糊测试

为了改善代码覆盖率,最成功的解决方案之一是覆盖引导模糊测试。 它采用不断发展的算法来驱动模糊器,以实现高代码覆盖率。 AFL [45],libFuzzer [34],honggfuzz [40]和VUzzer [29]是一些最先进的覆盖引导模糊器。

图1显示了覆盖引导模糊器的一般工作流程。它通常维护一个种子测试池并执行连续的模糊循环:(1)从具有特定策略的池中选择种子,(2)变异种子以生成一批新的测试用例,(3)使用这些新测试用例高速测试目标应用程序,(4)用仪器监控程序执行情况,跟踪代码覆盖率和安全违规情况;(5)报告检测到安全违规时的漏洞,(6)过滤有助于代码覆盖的良好测试用例将它们放入池中,然后转到步骤1.在这个连续循环之后,模糊器优先考虑种子覆盖范围,并向高代码覆盖范围发展。

研究表明,对该循环的每个步骤的改进可以提高模糊器的效率和效率。模糊测试的成功有很多因素,包括测试速度,覆盖准确性,种子选择策略,种子变异策略以及对安全违规的敏感性等。

例如,在步骤3中,提出了几种优化以提高模糊器的速度和吞吐量。 AFL利用Linux提供的fork机制加速,并进一步采用forkserver模式和persistent模式来减轻fork的负担。此外,AFL还支持并行模式3,使多个模糊器实例能够相互协作。文旭等。提出了几个新的原语[44],使AFL加速6.1到28.9倍。

在步骤4中,模糊器可以使用不同的机制,例如静态检测,动态二进制检测,调试或甚至系统仿真,来检测目标应用并跟踪有用信息。 AFL利用GCC和Clang编译器执行静态源检测,并利用QEMU执行动态二进制检测VUzzer利用工具PIN [24]执行动态二进制检测。 Syzkaller [5]和kAFL [31]利用QEMU以及硬件功能(例如Intel PT)进行检测。

在步骤5中,模糊器经常使用程序崩溃作为漏洞的指示器,因为即使没有仪器也很容易检测到它们。但是,当触发漏洞时,例如,当覆盖数组后面的填充字节时,程序并不总是崩溃。研究人员提出了几种解决方案来检测各种安全违规行为。例如,广泛使用的AddressSanitizer [32]可以检测缓冲区溢出和释放后使用漏洞。还有许多其他Sanitizer,包括UBSan [22],MemorySanitizer [39],LeakSanitizer [30],DataFlowsanitizer [2],ThreadSanitizer [33]和HexVASan [9]。

在以下部分中,我们将详细介绍其他步骤。

C.覆盖范围跟踪

覆盖引导的模糊器利用覆盖信息来驱动模糊测试(步骤6)。如前所述,覆盖不准确性会使错误发现变得模糊。因此,模糊测试员必须跟踪准确的覆盖范围。但读者也应该意识到,覆盖精度只是模糊器成功的一个因素。

不同的程序路径表现出不同的程序行为,因此可能具有不同的漏洞。准确的路径覆盖可以帮助模糊测试人员感知不同的路径。但是,在运行时跟踪所有路径覆盖(特别是边缘的顺序)是不可行的,因为路径的数量非常高并且每个路径的存储开销很高。
在实践中,覆盖引导的模糊器跟踪不同级别的代码覆盖。例如,LibFuzzer [34]和honggfuzz [40]利用Clang编译器提供的SanitizerCoverage [4]检测方法来跟踪块覆盖。 VUzzer [29]使用PIN来跟踪块覆盖。 AFL [45]使用静态/动态仪器来跟踪边缘覆盖。

给定边缘覆盖,我们当然可以推断块覆盖。在某些情况下,我们甚至可以从块覆盖中推断出边缘覆盖。 SanitizerCoverage进一步消除了关键边缘以确保后者的推理,并声称支持边缘覆盖。但它只是块覆盖的增强版本。块覆盖提供的信息少于边缘覆盖。临界边缘只是妨碍从块覆盖中推断边缘覆盖的一个因素。如图2所示,函数foo中没有关键边。两个程序路径P1和P2共享其大部分边缘,除了它们在函数foo中采用不同的子路径。因此P1和P2的块覆盖范围完全相同,但它们的边缘覆盖范围不同。例如,边缘B1-> C1仅存在于路径P1中。

对于跟踪块覆盖的模糊器,例如libFuzzer,honggfuzz和VUzzer,提高其覆盖精度的解决方案是用边缘覆盖跟踪(例如AFL使用的跟踪覆盖方案)替换它们的跟踪方案。 然而,AFL提供的边缘覆盖是不完美的。

a)散列碰撞问题:AFL利用位图(默认大小为64KB)来跟踪应用程序的边缘覆盖范围。 位图的每个字节表示特定边缘的统计数据(例如,命中计数)。 为每个边计算哈希值,并将其用作位图的关键字。 在该方案中存在哈希冲突问题,即,两个边可以具有相同的哈希。 因此,模糊器无法区分这些边缘,导致覆盖不准确。

更具体地说,AFL检测目标应用程序并将随机密钥分配给其基本块。 给定边A-> B,然后AFL计算其散列如下:

cur\bigoplus (prev\gg 1)

其中prev和cur分别是基本块A和B的键。 由于密钥的随机性,两个不同的边可以具有相同的散列。 此外,边缘的数量很高(即,与位图大小64K相当),考虑到生日攻击[16],碰撞率会非常高。

据我们所知,覆盖不准确的后果被模糊器的巨大成功所掩盖,因此没有得到系统的评估。 我们的实验表明,由于哈希冲突问题,在实际应用中高达75%的边缘对于AFL是不可见的,这极大地限制了AFL的能力。 本文讨论了这个问题。

D.种子选择政策

最近的研究[10,11]表明,种子选择策略(即模糊测试循环中的步骤1)对于基于覆盖的模糊器是至关重要的。一个好的种子选择政策可以提高模糊测试的路径探索和bug发现的速度。

AFL优先考虑较小且执行速度较快的种子,因此可能在给定时间内测试更多的测试用例。 Honggfuzz按顺序选择种子,Lib-Fuzzer优先考虑击中更多新块的种子。 VUzzer [29]优先考虑行使更深路径的种子,并优先考虑行使错误处理块和频繁路径的测试用户,因此可能会测试难以到达的路径并避免无用的错误处理路径。 AFLfast [11]优先考虑种子行走频率较低且选择较少的种子,因此很可能可以彻底测试冷路径,并且在热路径上浪费的能量更少。

种子选择政策还可以加强模糊器在特定方向上的能力。例如,QTEP [42]优先考虑通过静态分析识别的更多错误代码的种子,增加了在测试期间触发漏洞的可能性。 SlowFuzz [27]优先考虑使用更多资源(例如,CPU,内存和能量)的种子,从而增加了触发算法复杂性漏洞的可能性。 AFLgo [10]优先考虑更接近预定目标位置的种子(例如,等待评论的新提交),从而实现有效的定向模糊测试。

但是,鉴于代码覆盖率信息不准确,模糊器无法对种子选择做出最佳决策。例如,如果冷却路径发生碰撞,AFLfast可能会错误地将冷路分类为热路径,从而导致此冷路测试不良并且漏掉了潜在的漏洞。此外,很少有模糊测试器利用代码覆盖率信息直接驱动模糊到未探索的路径。本文提出了几个解决此问题的新政策。

E.种子突变政策

变异种子(即模糊环中的步骤2)对于覆盖引导的模糊器是必不可少的。 AFL和libFuzzer等基本上使用一组确定性和随机算法来变异种子并生成新的测试用例。种子突变政策与几个核心问题有关:(1)种子来源,(2)变异的地方,以及(3)突变使用的价值。

一组好的种子可以帮助产生良好的突变。 IMF [19]从正常的应用程序执行中学习系统调用之间的顺序和值依赖关系,然后相应地生成测试用例,从而能够找到许多深层内核错误。 Skyfire [41]从丰富的输入中学习概率上下文敏感语法,以指导测试用例的生成。 DIFUZE [13]利用静态分析在用户空间中组合有效输入来测试内核驱动程序。最近,研究人员利用AI技术来帮助模糊测试。 Patrice Godefroid et.al.提出了一种RNN(递归神经网络)解决方案[18]来生成有效的种子文件,并且可以帮助生成输入以通过格式检查,从而改善代码覆盖率。 Nicole Nichols等。提出了一个GAN(生成对抗网络)解决方案[26],用额外的种子来争论种子库,展示了另一个有希望的解决方案。但是,需要更多的研究来进一步提高种子投入的质量。

突变的另一个核心问题是在哪里发生变异。 VUzzer [29]使用控制流和数据流特征来推断要变异的字节(例如,魔术字节),这对某些类型的数据字段很有用。志强等提出了一种解决方案[23],用于识别使用静态数据沿袭分析进行变异的敏感字节。 Mohit Rajpal等。提出了一个DNN(深度神经网络)解决方案[28]来预测要变异的字节,显示出有希望的改进。 TaintScope [43]使用污点分析识别校验和字节并在测试期间修复它们。

突变的另一个核心问题是用于突变的价值。 VUzzer [29]使用动态分析来推断用于变异的有趣值(例如,幻数)。 Honggfuzz [40]采用类似的策略来在运行时识别有趣的值(即cmp指令的操作数)并大大改善其路径覆盖。 Laf-intel [1]转换目标应用程序,将长字符串或常量比较分成几个小的比较语句,使模糊器能够找到匹配的变异并更快地运行新的路径。

F.本文的重点

为了提高模糊测试器的查找效率,我们提出了CollAFL,一种覆盖敏感的模糊测试解决方案。 图1中的黄色组件展示了我们解决方案的重点。 简而言之,它首先提高了代码覆盖率跟踪的准确性,然后通过替换种子选择策略,利用准确的覆盖率信息来指导模糊器。 更多细节将在以下部分中讨论。

对种子突变政策,测试性能优化和仪器方案以及细粒度安全sanitizers,的研究与我们提出的工作是正交的。 我们的解决方案也可以从这些工作中受益

III.提高覆盖准确度

如前所述,覆盖不准确性模糊了模糊测试者发现错误的能力,导致模糊器无法看到某些路径。 CollAFL优于现有覆盖引导模糊器的第一个改进是覆盖精度。 它可以帮助模糊测试人员探索更多路径并找到更多漏洞。

我们已经研究了不同类型的覆盖粒度,并且找出边缘覆盖是最佳选择,其在仪器开销和覆盖精度之间达到了良好的平衡。 我们进一步指出当前边缘覆盖实现中的不准确性问题,并提出解决方案。

A.覆盖粒度

有三种常见类型的覆盖粒度,即块覆盖,边缘覆盖和路径覆盖。他们每个人都有其优点和缺点。

典型的块覆盖解决方案将在测试期间跟踪每个块的命中数。它被模糊器广泛采用,例如VUzzer,libFuzzer和honggfuzz。但是,它不跟踪块的顺序,导致覆盖信息丢失。图2显示了两个不同的路径共享完全相同数量的块命中,因此其中一个块对于块覆盖模糊器是不可见的。

典型的边缘覆盖解决方案将跟踪每个边缘的命中计数。代表性实施是AFL使用的实施。仪表开销类似于块覆盖解决方案。但是,它不会跟踪边缘的顺序,也会丢失一些信息。

路径覆盖解决方案将跟踪边缘的顺序,提供最完整的代码覆盖率信息。但是,路径的长度非常长,并且应用程序中的路径数量很大,因此跟踪路径覆盖的运行时开销和内存开销非常高。实际上,跟踪路径覆盖是不可行的。

因此,边缘覆盖解决方案在效率和覆盖范围信息之间达到某种平然而,即使对于代表性边缘覆盖解决方案AFL,也存在导致不准确的哈希冲突问题。 CollAFL采用边缘覆盖跟踪方案并修复了碰撞问题。其他模糊器(例如VUzzer)也可以从该方案中受益。

B.哈希碰撞的平凡解决方案

这个问题的直接解决方案是扩大散列的空间,即AFL实现中的位图大小。但是,正如AFL本身所解释的那样,当前的默认位图大小(即64KB)是性能的折衷。

选择地图的大小,使得几乎所有预定目标的碰撞都是零星的,这些目标通常在2k到10k之间发现可发现的分支点。同时,它的大小足够小,可以在接收端以微秒为单位分析地图,并毫不费力地适应L2缓存。

我们已经评估了该解决方案的效率,并确认如果我们扩大位图大小,模糊器的性能会迅速下降。如第V-A节所示,为了将哈希冲突率降低到5%,我们必须将位图大小从64KB增加到4MB,从而导致60%的执行速度下降。更糟糕的是,由于随机性,我们不能保证仅通过放大位图来消除冲突。因此,这不是哈希冲突问题的正确解决方案。

C. CollAFL解决哈希冲突的方法

如等式1所示,AFL使用固定公式来计算每条边的散列,这很快但很容易发生碰撞。 我们通过仔细地为不同的边缘应用不同的哈希公式来细化它,以消除哈希冲突,同时保持哈希计算和覆盖跟踪的速度。

通常,给定两个具有键prev和cur的块A和B,我们计算边A-> B的哈希,如下所示:

Fmul(cur,prev)=(cur \gg x)\oplus (prev \gg y)+z

其中<x,y,z>是要确定的参数,对于不同的边可能是不同的。 AFL使用的等式1是该算法的特定形式,即,对于所有边/块,<x = 0,y = 1,z = 0>。 Fmul的计算过程与AFL相同,具有相同的开销。

如图3所示,我们可以为每个结束块而不是每个边选择一组参数来计算边缘哈希值。 为简单起见,将在块之间共享相同的参数y,并将值(prev \gg y)缓存在全局变量_prev中。 每个块可以具有不同的参数集<x,z>。

因此,给定一个应用程序,我们可以尝试找到每个基本块的参数解决方案,确保通过Fmul计算的所有边缘哈希值都不同。我们使用贪婪算法逐个搜索每个块的参数。一旦找到所有块的解,我们就可以使用它们的哈希来区分任何两个边,从而解决哈希冲突问题。

但是,我们无法保证为给定的应用程序找到解决方案,因为应用程序中的基本块太多而我们无法遍历所有可能的参数。 即使我们可以这样做,我们也无法保证存在解决方案,因为基本块的密钥是随机分配的。 因此,我们进一步细化所提出的散列计算算法如下。

1)具有单个先例的块的散列算法:如果一个块只有一个先例,如图3(3)所示,我们可以在结束块中直接为该边分配散列,而不是使用等式2来计算一个 ,只要这个哈希不与任何其他边缘碰撞'。

因此,对于仅具有一个先前块A的块B,我们不需要找到参数<x,y,z>的组合,而只需要其唯一的入口边缘a \rightarrow b的唯一散列。 因此,我们为它引入了一个不同的哈希算法如下:

Fsingle(cur,prev):c

其中prev和cur是分配给块A和B的键,参数c是要确定的唯一常量。

这个散列值c可以离线解析,然后在结束块B中硬编码。因此,CollAFL比AFL快得多,以获得这样的边缘散列。 正如我们的实验所示,在大多数应用中,超过60%的基本块只有一个先例块。 因此,它可以节省大量的运行时开销,从而提高了模糊器的吞吐量。

此外,这些哈希值可以随时解决。 因此,为了避免冲突,我们可以等到确定所有其他边的散列,然后选择未使用的散列并将它们分配给只有一个先前块的块。

2)具有多个先例的块的散列算法:如果块B具有多个先前块,即B具有多个入射边缘,则我们必须动态计算块B中的散列,因为被击中的入射边缘仅在运行时已知。 通常,我们将使用上述等式2来计算散列。

如前所述,我们无法保证找到这个等式的解决方案以避免冲突,即使在仅使用一个先例删除块之后也是如此。 我们使用贪心算法来解析这些块的参数。 我们将可以解析的块表示为可解块,并表示我们无法解析为无法解析块的块。

对于不可解析的块B,我们为其入口边A-> B引入另一个散列算法,如下所示:

Fhash(cur,prev):hash_table_lookup(cur,prev)

其中prev和cur是块A和B的键。它离线构建一个哈希表,所有边的唯一哈希以不可解析的块结尾,不同于所有其他边的哈希。 在运行时,它会查找此预先计算的哈希表,以使用它们的开始和结束块作为键来获取此类边缘的哈希值。

在运行时,哈希表查找操作比以前的算法Fmul和Fsingle慢得多。 因此,我们应该将不可解析的块集限制为尽可能小。 根据我们的实验,这套通常是空的。

3)整体缓解解决方案:首先,我们应该确保位图大小(即散列值空间的大小)大于边数,否则无法避免散列冲突。 然后,使用三个提议的哈希公式,即Fmul,Fsingle和Fhash,我们可以通过根据它们的类型对它们应用不同的公式来解决所有边的哈希冲突,如下所示:

F=\left\{\begin{matrix} fmul &Solvable blocks with multi pred \\ fhash & Unsolvable blocks with ...\\ fsingle & Blocks with single precedent \end{matrix}\right.

Algorithm 1 The collision mitigation algorithm.
Input: Original program
Output: Instrumented program
(BBS, SingleBBS, MultiBBS, Preds) = GetCFG()
Keys = AssignUniqRandomKeysToBBs(BBS)
// Fixate algorithms. Preds and Keys are common arguments
(Hashes, Params, Solv, Unsolv) = CalcFmul(MultiBBS)
(HashMap, FreeHashes) = CalcFhash(Hashes;Unsolv)
// Instrument program with coverage tracking.
InstrumentFmul(Solv, Params)
InstrumentFhash(Unsolv, HashMap)
InstrumentFsingle(SingleBBS, FreeHashes)

a)预处理应用程序:我们首先检索由任何静态分析工具或编译器提供的目标应用程序的基本块和先前信息。如第1行所示,我们可以在程序中获得一组基本块BBS,并将其拆分为两个子集SingleBBS和MultiBBS,具体取决于块是单个还是多个先例。每个基本块的先前信息存储在地图Preds中。在第2行中,它为程序中的每个基本块分配唯一的随机密钥。此分配信息存储在映射键中。

b)确定块的算法:如第4行所示,我们首先尝试使用CalcFmul为具有多个先例的块找到适当的参数,并获得可解块的集合Solv和不可解析的块Unsol,以及可解块的参数Params到目前为止,边缘采用的哈希解决了。在第5行中,我们使用CalcFhash为不可解析的块Unsol构建哈希映射HashMap,并获取到目前为止未解析的任何边缘未使用的未使用的哈希FreeHashes集合。

算法2演示了CalcFmul的工作流程,即如何搜索具有多个先例的块的参数。它首先选择参数y然后迭代每个块BB,并遍历<x,z>的所有组合以找到组合,使得以该块结束的所有边的哈希值与其他块不同。如果找不到任何组合,则该块将被归类为无法解析并放入Unsol。否则,该块将被放入Solv,解决方案将被放入Params。一旦处理了具有多个先例的所有基本块,并且Unsol集足够小,我们就找到了问题的解决方案。否则,我们将选择另一个参数y并继续前一个过程。

算法3(附录A)演示了CalcFhash,即如何为不可解析的块Unsol构建哈希表。 简而言之,它为每个以无法解析的块结尾的边选择随机未使用的哈希值,并将其存储在哈希映射HashMap中。 它还返回一组未使用的哈希FreeHashes。

c)工具块:我们然后检测应用程序以跟踪边缘覆盖,如图3所示。对于可解块,我们使用Fmul对它们进行测量,即与AFL相同,但具有不同的参数<x,y,z >在Params。 对于不可解析的块Unsolv,我们使用Fhash对每个块进行检测,以在HashMap中搜索在运行时以这些块结尾的边的哈希值。 对于具有单个先例的块,我们在FreeHashes中为每个块硬编码未使用的哈希。 通过这种方式,我们可以消除所有已知边缘的哈希冲突。

D.业绩分析

如前所述,这三种提议的哈希算法的性能开销如下,

cost(Fhash)> cost(Fmul)>cost(Fsingle)\approx 0(6)

另一方面,根据实验,大多数基本块只有一个先例块,无法解析的块数非常少。

num(Fsingle)> num(Fmul)\gg num(Fhash)\approx 0(7)

总的来说,CollAFL使用的哈希计算引入的整体性能成本很小。如评估表II所示,对于大多数应用,我们的解决方案比AFL引入的指令更少,性能成本更低。


E.实施细节

CollAFL基于边缘覆盖引导的模糊AFL构建。我们扩展了AFL的llvm_mode,并编写了一个Clang链接时间优化传递给(1)检索所需的基本块和边缘信息,(2)为每个基本块分配唯一的密钥,(3)解决每个基本的哈希计算算法阻止取决于其类型,以及(4)使用散列计算和覆盖跟踪代码对每个块进行检测。通过遵循第III-C节和第3节中的算法,可以轻松实现最后三个步骤。

对于第一步,我们使用Clang的默认实现来获取后继和先前信息,例如,通过API llvm :: TerminatorInst :: getSuccessor。然而,离线控制间接控制转移的目标是一个开放的挑战,影响了我们所需的先前信息的精确性。例如,它可能错误地将一些基本块分类为单个(或没有)先前块。

因此,我们采取两个额外步骤来改进结果。首先,我们将未被任何人直接调用的函数的入口块标记为多先前块。此外,我们将间接调用指令展开到一组直接调用和间接调用指令,类似于去虚拟化技术[25]。因此,它将一些基本块连接在一起,减少了单个先前块的数量。因此,我们将使用Fmul而不是Fsingle来计算这些块的哈希值,从而降低运行时发生冲突的可能性。

边缘信息的准确性会影响我们所知道的边缘数量。由于我们的碰撞缓解解决方案仅确保消除已知边缘的碰撞,因此即使使用CollAFL,也可能在运行时存在一些边缘碰撞。此外,CollAFL目前仅适用于具有源代码的应用程序。但它也应该用于二进制文件,除了边缘信息不太准确。我们将在未来的工作中评估其在二进制文件上的表现。

IV。优先种子选择

现有的种子选择策略主要关注执行速度,路径频率和路径深度,但没有一个专注于直接驱动模糊器到非探索路径。为实现这一目标,我们有两个可以提供帮助的直觉:

  • 如果一条路径有许多未探索(或未触及)的邻居分支,那么这条路径的突变很可能会探索那些未探索的分支。
  • 如果一条路径有许多未探索(或未触及)的邻居后代,那么这条路径的突变很可能会探索那些未探索的后代。

最终目标是提高漏洞发现的有效性。为实现这一目标,我们有另一种直觉:

  • 如果路径具有许多内存访问操作,则可能会触发潜在的内存损坏漏洞,因此其突变也是如此。

遵循这些直觉的突变可以指导模糊测试人员探索更多路径并发现更多漏洞。因此,我们基于这些直觉提出了三种新的种子选择政策。


值得注意的是,这些政策不仅限于任何模糊器。只要提供边缘覆盖信息,我们就可以将这些策略应用于模糊器并提高其在漏洞发现方面的效率。

A.未触及的邻居分支指导政策
A.未触及的邻居分支引导策略在此策略中,具有更多未触及的邻居分支的种子将优先化为模糊。 我们相信基于这些种子的突变有更高的概率来探索那些未触及的邻居分支。 为简单起见,我们将此政策表示为CollAFL-br。
更具体地说,我们使用未触摸的邻居分支的数量作为测试用例T的权重,如下所示:


在此策略中,具有更多未触及的邻居分支的种子将优先于模糊。 我们相信基于这些种子的突变有更高的概率来探索那些未触及的邻居分支。 为简单起见,我们将此政策表示为CollAFL-br。


更具体地说,我们使用未触摸的邻居分支的数量作为测试用例T的权重,如下所示:

WeightBr(T)=\sum_{\begin{matrix} bb\in Path(T)\\<bb,bb_{i}> \in EDGES \end{matrix}}IsUntouched(<bb,bb_{i}>)

当且仅当边<bb,bb_{i}>未被任何先前的测试用例覆盖时,函数IsUntouched返回1,否则返回0。

在本策略中,将优先考虑权重较高的种子进行模糊测试。 值得注意的是,先前运行的测试用例集将随着测试的进行而改变,因此函数IsUntouched的返回值也将改变。 结果,测试用例的权重是动态的。

值得注意的是,如果测试用例被多次击中,我们将多次迭代一个基本块。 因此,循环中的块将对总体权重贡献更多。

B.未触及的邻居后裔指导政策

在这项政策中,具有更多未触及的邻居后代的种子将优先考虑模糊。 来自这些种子的突变有更高的概率来探索那些未触及的邻居后代。 我们将此政策表示为CollAFL-desc。

更具体地说,我们将使用未触及的邻居后代的数量作为测试用例T的权重,如下所示:

WeightBr(T)=\sum_{\begin{matrix} bb\in Path(T)\\IsUntouched<bb,bb_{i}>\inEDGES \end{matrix}}NumDesc(bb_{i})

函数IsUntouched与CollAFL-br策略中使用的函数相同,函数NumDesc返回从参数基本块开始的后代路径数。 其正式定义如下:

NumDesc(bb)=\sum_{<bb,bb_{i}> \in EDGES}NumDesc(bb_{i})

这里的权重不是确定性的,因为函数IsUntouched是动态的。 但是,后代子路径的数量对于每个基本块是确定的。 我们可以使用静态分析来计算此值,而无需运行时开销。 类似地,如果测试用例多次触发基本块,我们将多次迭代它。

C.记忆访问指导政策

在此策略中,表示为CollAFL-mem,具有更多内存访问操作的种子将优先于模糊。

更具体地说,我们使用内存访问操作的数量作为测试用例T的权重,如下所示:

WeightMem(T)=\sum_{bb \in Path(T)}NumMemInstr(bb)

其中函数NumMemInstr返回参数基本块中的内存访问操作数,可以静态计算。因此,与前两个策略不同,以这种方式计算的权重是确定性的。类似地,如果测试用例多次触发基本块,我们将多次迭代它。

D.实施细节

值得注意的是,只要可以提供边缘覆盖和块信息,这些策略就可以应用于任何覆盖引导的模糊器。

我们通过替换其默认的种子选择策略,在AFL中实施这三个策略。如前所述,我们可以在编译时获取每个基本块的内存访问操作数和后代子路径数。

在运行时,在测试种子测试用例T之后,我们将计算其未触及的邻居分支和后代子路径,以及代表性地沿着路径的存储器访问操作。更具体地说,我们将首先检查测试用例的覆盖位图,并获得此测试用例覆盖的所有边缘以及命中计数。由于每个边都有不同的散列,我们可以从散列中解码每个边的起始​​和结束块。然后,对于每个块,我们将根据整体覆盖位图获取其未触及的邻居分支列表。连同我们已经收集的后代子路径和内存访问操作的数量,我们可以相应地计算所有三个策略的权重。

VI。结论

在本文中,我们研究了覆盖引导模糊器中覆盖不准确性的负面影响。我们提出了一种覆盖敏感的模糊测试解决方案CollAFL,它解决了最先进的模糊AFL中的哈希冲突问题,能够在保持低仪器开销的同时实现更准确的边缘覆盖信息。基于准确的覆盖信息,我们提出了三种新的种子选择策略,以便将模糊器直接驱动到非探索路径。实验表明,就路径发现,崩溃发现和漏洞发现而言,该解决方案既有效又高效。我们在24个真实世界的应用程序中发现了157个新的安全漏洞,其中95个由CVE确认。

发布了43 篇原创文章 · 获赞 23 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zhang14916/article/details/90601317