Qsym : A Practical Concolic Execution Engine Tailored for Hybrid Fuzzing 精翻

QSYM:为混合模糊测试量身定制的实用符号执行引擎

摘要

   最近,混合模糊测试被提出用于解决模糊测试和符号执行的局限性,它通过结合这两种方法来实现。混合方法在各种合成基准测试(如DARPA网络大奖赛(CGC)二进制文件)中展示了其有效性,但在处理复杂的现实世界软件时,仍然面临难以扩展以发现漏洞的问题。我们观察到,现有符号执行器的性能瓶颈是其主要限制因素。

    为了解决这一问题,我们设计了一个快速的符号执行引擎,称为QSYM,以支持混合模糊测试。其关键理念是通过动态二进制翻译,将符号仿真与本地执行紧密结合,从而实现更细粒度、更快速的指令级符号仿真。此外,QSYM放宽了传统符号执行器对严格正确性的要求,以换取更高的性能,同时利用更快的模糊测试器进行验证,从而提供了前所未有的性能优化机会,例如乐观地求解约束条件以及修剪无关的基本块。

    我们的评估表明,QSYM不仅在性能上超越了最先进的模糊测试工具(例如,在LAVA-M数据集中发现的漏洞数量是VUzzer的14倍,并在126个二进制文件中有104个超过了Driller),还在已经被最先进的模糊测试工具(如AFL和OSS-Fuzz)广泛测试的八个真实世界的程序中(如Dropbox Lepton、ffmpeg和OpenJPEG)发现了13个此前未知的安全漏洞。

1 引言

    计算机科学界已经开发了两种显著的技术来自动发现软件中的漏洞:即覆盖率引导的模糊测试和符号执行。模糊测试能够以接近原生速度快速探索输入空间,但它只擅长发现导致执行路径具有松散分支条件的输入,例如 x > 0。相反,符号执行擅长找到将程序引导到紧密且复杂的分支条件下的输入,例如 x == 0xdeadbeef,但其代价昂贵且在构建和求解这些约束时速度较慢。

    为了同时利用这两种技术的优势,近期提出了一种混合方法,称为混合模糊测试。该方法结合了模糊测试和符号执行,旨在让模糊测试快速探索简单的输入空间(即松散条件),而符号执行则解决复杂的分支条件(即紧密条件)。例如,Driller展示了混合模糊测试在DARPA网络大奖赛(CGC)二进制文件中的有效性——在126个二进制文件中生成了6个新的崩溃输入,这些输入是单独使用模糊测试或符号执行都无法发现的。

    不幸的是,这些混合模糊测试工具在那些复杂而难以处理的应用程序中发现漏洞时仍然面临扩展性问题。我们观察到,它们的符号执行器的性能瓶颈是阻碍它们超越合成基准测试的主要限制因素。与符号执行器所承诺的效果不同,它们未能扩展到实际应用中:符号仿真在制定路径约束(例如图1中的libjpeg和libpng)时过于缓慢,或者由于环境模型的不完整和错误(见表4),这些约束往往无法生成(例如图1中的libtiff和file)。

图1 我们对流行开源软件在最先进的符号执行器(Driller 和 S2E)以及我们的系统 QSYM 下的新发现的行覆盖进行了评估,直到覆盖饱和为止。我们在每个项目中使用了五个具有最大代码覆盖率的测试用例。结果表明,QSYM生成的测试用例覆盖的代码行数明显多于这两种符号执行器。在libtiff中,由于对mmap()的建模不完整,Driller无法生成任何测试用例。

    在本文中,我们系统地分析了符号执行的性能瓶颈,并通过构建符号执行器以支持混合模糊测试来解决这些问题(第2节)。核心思想是通过动态二进制翻译将符号仿真与本地执行紧密集成。这种方法提供了前所未有的机会,能够实现更细粒度的指令级符号仿真,从而最小化昂贵的符号执行使用(第3.1节)。与我们的方法不同,目前的符号执行器采用了粗粒度的基本块级污点跟踪和符号仿真,这给符号执行带来了不可忽视的开销。

    此外,我们放宽了传统符号执行器的严格正确性要求,以实现更好的性能,并使其能够扩展到真实世界的程序中。这种约束的不完整性或不正确性在混合模糊测试中并不是问题,因为共同运行的模糊测试器可以快速验证新生成的测试用例;如果测试用例无效,模糊测试器可以迅速丢弃它们。此外,这种方法还使得实施一些实用的技术成为可能,例如,通过乐观地解决部分约束(第3.2节)来生成新的测试用例,以及通过修剪无关的基本块(第3.3节)来提高性能。这些新技术和优化共同使QSYM能够扩展到真实世界的程序测试中。

    我们的评估显示,基于我们的符号执行器和最先进的模糊测试工具AFL构建的混合模糊测试工具QSYM,优于所有现有的模糊测试工具,如Driller和VUzzer。QSYM在126个DARPA CGC二进制文件中,在其中104个文件取得了显著优于Driller的代码覆盖率(在五个挑战中并列)。此外,QSYM在LAVA-M测试集中的2,265个漏洞中发现了1,368个漏洞,而VUzzer仅发现了95个漏洞。

    更重要的是,QSYM能够扩展到测试复杂的真实世界应用程序。它在八个非平凡程序中发现了13个此前未知的漏洞,包括ffmpeg和OpenJPEG。值得注意的是,这些程序已经被其他最先进的模糊测试工具(如AFL和OSS-Fuzz)彻底测试过,这突显了我们符号执行器的有效性。虽然OSS-Fuzz在一个拥有数百台服务器的分布式模糊测试基础设施上运行,但仍未能发现这些漏洞,而QSYM仅通过一台工作站就找到了这些漏洞。为了进一步研究,我们将QSYM的原型开源,地址为 https://github.com/sslab-gatech/qsym。

    本文主要贡献如下:

    通过高效仿真快速符号执行:我们通过优化仿真速度和减少仿真使用来提高符号执行的性能。我们的分析发现,符号生成仿真是符号执行的主要性能瓶颈,因此我们通过指令级选择性符号执行、先进的约束优化技术以及将符号执行(symbolic)与符号执行(concolic)结合解决了这一问题。

    高效的重复测试和具体环境:QSYM的高效性使得基于重新执行的重复测试和外部环境的具体执行变得可行。因此,QSYM避免了快照引起的显著性能下降和由于环境模型不完整而导致的错误符号执行问题,这些问题源于其不可重用的特性。

    针对混合模糊测试的新启发式方法:我们提出了针对混合模糊测试的新启发式方法,以乐观地解决不可满足的路径并修剪计算密集型的基本块,从而使QSYM能够继续进行测试。

    现实世界中的漏洞:基于QSYM的混合模糊测试工具在DARPA CGC和LAVA测试集上超越了最先进的自动漏洞发现工具(例如Driller和VUzzer)。此外,QSYM在八个真实世界的软件中发现了13个新漏洞。我们认为这些结果清楚地展示了QSYM的有效性。

    本文其余部分的组织结构如下:第2节分析了当前混合模糊测试的性能瓶颈;第3节和第4节分别描述了QSYM的设计和实现;第5节通过基准测试、测试集和实际测试用例评估了QSYM;第7节解释了QSYM的局限性及可能的解决方案;第8节介绍了相关工作;第9节总结了本文内容。

2 动机:性能瓶颈

    在本节中,我们系统地分析了混合模糊测试工具中使用的传统符号执行器的性能瓶颈。以下是阻碍混合模糊测试工具在小规模研究之外应用于现实世界的主要原因。

2.1 性能问题1:符号仿真速度慢

    传统符号执行器中的仿真层用于处理符号内存模型,速度极其缓慢,导致整体符号执行显著变慢。这令人意外,因为业界通常认为符号执行和符号执行速度慢是由于路径爆炸和约束求解造成的。表1展示了在一些广泛使用的符号执行器(如KLEE和angr)中执行多个程序时,即使不分支到其他路径(无路径爆炸)或不在路径上求解约束时,符号仿真仍然带来了显著的开销。与本地执行相比,KLEE的执行速度慢了大约3,000倍,而angr则慢了超过321,000倍,这种差距是非常显著的。

    为什么符号仿真如此缓慢?在我们的分析中,我们发现当前符号执行器的设计,尤其是在符号仿真中采用中间表示(IR),导致仿真速度变慢。现有的符号执行器采用IR来显著降低实现的复杂性,但这牺牲了性能。此外,加速IR使用的优化措施限制了进一步的优化机会,特别是通过基本块粒度将程序转换为IR的方式。这种设计不允许跳过那些与符号执行无关的指令逐条仿真。我们将在以下部分详细描述这些问题。

    为什么使用中间表示(IR):IR 使仿真器实现更简单。现有的符号仿真器在仿真执行之前,将一条机器指令转换为一条或多条IR指令。这主要是为了简化符号建模的实现。为了对符号内存进行建模,仿真器需要解释指令在提供符号操作数时如何影响符号内存状态。不幸的是,解释每条机器指令是一项非常庞大的任务。例如,最流行的Intel 64位指令集架构(即amd64 ISA)包含了1795条指令【13】,这些指令的描述被记录在一个2000页的手册【14】中。此外,amd64 ISA 不是机器可解释的,因此需要人工来解释每条指令的符号语义。

表1:KLEE和angr的仿真开销与本地执行相比的对比,这两个分别是S2E和Driller的底层符号执行器。我们使用了coreutils中的chksum、md5sum和sha1sum来测试KLEE,使用md5sum (mosml)【12】来测试angr,因为angr不支持coreutils应用程序中使用的fadvise系统调用。

    为了减少实现的巨大复杂性,现有的仿真器采用了中间表示(IR)。例如,KLEE使用LLVM IR,而angr使用VEX IR。这些IR的指令集要小得多(例如,LLVM IR有62条指令【15】),并且比本地机器指令更简单。因此,使用IR显著减少了实现的复杂性,因为仿真器所需的解释处理程序数量会比直接处理机器指令时少得多(例如,1795条机器指令与62条IR指令的对比)。

    为什么不使用IR:IR会引入额外的开销。尽管使用IR使实现变得容易,但在符号仿真中使用IR会引入额外的开销。首先,IR的翻译本身就增加了开销。因为amd64架构是一个复杂指令集计算机(CISC),而IR模型则是简化指令集计算机(RISC),所以在大多数情况下,将一条机器指令翻译为多条IR指令。例如,根据我们的评估,angr使用的VEX IR【16】在CGC二进制文件中,平均将指令数量增加了4.69倍(相对于机器指令),从而导致了更多的符号仿真处理。

    为什么不使用IR:IR阻碍了进一步的优化。其次,使用IR限制了进一步优化的机会。例如,现有的符号仿真器有一种优化策略,旨在最小化仿真使用量,因为仿真速度较慢。具体来说,如果一个基本块不涉及任何符号变量,仿真器就不会在其内部执行该基本块。虽然这种方法有效地减少了开销,但仍有进一步优化的空间。根据我们对实际软件(如libjpeg、libpng、libtiff和file)的测量(如图2所示),符号基本块中的指令中,只有30%需要符号执行。这意味着,基于指令级的处理方法有机会减少不必要的符号执行数量。然而,由于IR缓存的存在,目前的符号执行器难以轻松采用这种方法。为了使用IR,符号执行器需要将本地指令转换为IR,这会引入显著的开销。为了避免重复的开销,它们通常将整个基本块转换并缓存为IR,而不是单独的指令,这样可以节省缓存管理的空间和时间。这种缓存机制迫使现有的符号仿真器在基本块级别执行指令,阻碍了进一步的优化

图2:符号基本块中的指令数量与热门开源软件中符号指令的数量。在基本块中的指令中,有超过一半的指令不是符号指令,可以直接进行本地执行。

我们的方法是:移除IR翻译层,并承担实现复杂性的代价,以减少执行开销,并进一步优化,尽量减少符号仿真的使用。

2.2 P2. 无效的快照

    为什么使用快照:消除重新执行的开销。传统的符号执行引擎使用快照技术来减少在探索多个路径时重新执行目标程序所带来的开销。对于混合模糊测试来说,快照机制也是必不可少的,因为其中的符号重新执行非常慢,例如Driller。举个例子,我们在Driller中关闭快照机制,并使用126个CGC二进制文件和作为初始种子文件的漏洞证明(PoVs),测量代码覆盖率。结果显示,启用快照的Driller在76个二进制文件中达到了更高的代码覆盖率,而没有启用快照的Driller仅在17个二进制文件中达到了更高的代码覆盖率,其他情况下则相同。

    为什么不使用快照:模糊测试输入不共享公共分支。在混合模糊测试中,快照并不有效,因为混合模糊测试中的符号执行很少共享一个公共分支。特别是对于传统的符号执行引擎,当引擎从一个条件分支(即“已采取”和“未采取”的路径)开始分裂路径探索时,会创建一个快照。创建快照的主要目的是在探索相同分支的两条路径时重用符号程序状态。在这种情况下,引擎会备份程序在某一分支的符号状态,然后探索其中一条路径(例如,“已采取”路径)。当路径探索完毕或卡住时,引擎会将符号状态恢复到之前分支的状态,并转向另一条路径(即“未采取”路径)。这样,引擎可以在不重新执行程序到达该分支的情况下探索路径,避免了重新执行的开销。

    相反,在混合模糊测试中,符号执行引擎从模糊测试器中获取多个测试用例,这些测试用例与程序的不同路径相关联(即不共享公共分支)。这是因为随机变异生成了这样的测试用例。这可能会导致两种情况:1)使程序走向不同的代码路径,或2)在处理符号内存访问时对值进行不同的具体化【17】。因此,从一个测试用例路径拍摄的快照不能在另一个测试用例路径中重复使用,这样就无法优化性能。

    为什么不使用快照:快照无法反映外部状态。更糟的是,快照机制在支持外部环境时会变得问题重重,因为它会打破进程边界。支持外部环境是必要的,因为程序在执行过程中会大量与外部环境交互。这些交互包括文件系统和内存管理系统的使用,它们可能会改变程序的符号状态。当程序正在执行时,它不会考虑外部环境,因为底层内核维护了与之相关的内部状态。不幸的是,快照机制打破了内核的这一假设:当进程通过类似 fork() 的系统调用分叉时,内核不再维护这些状态。因此,符号执行引擎必须自行维护这些状态。

    现有的工具试图通过全系统符号执行或外部环境建模来解决这个问题,但这两种方法分别导致了显著的性能下降和不准确的测试。

    全系统符号执行。 像 S2E 这样的符号测试工具对目标程序和外部环境同时进行符号执行。虽然这种方法能够保证完整性和正确性,但由于传统符号执行器过于缓慢且外部环境的复杂性较高,这些工具无法在合理的时间内测试程序。此外,全系统符号执行需要进行昂贵的状态备份和恢复。虽然在通常情况下可以通过写时复制(copy-on-write)来减轻这种开销,但由于混合模糊测试的不可共享性,这种方法并不适用。

    外部环境建模。 混合模糊测试工具(如 Driller)通过对外部环境中的执行进行建模或仿真来应对外部环境问题。这种方法通过避免符号执行带来了明显的性能优势,但由于几乎不可能在实际操作中完全和正确地建模所有系统调用,它也导致了不准确的模型。例如,Linux 内核 2.6 有 337 个系统调用,但 angr 仅支持其中的 22 个。此外,尽管开发者付出了大量努力,angr 对许多函数的建模仍然是不完整的,比如 mmap()。当前在 angr 中的 mmap() 实现忽略了传递给函数的有效文件描述符,而只是返回空的内存,而非包含文件内容的内存。

我们的方法: 优化重复的符号化测试,去除在混合模糊测试中效率低下的快照机制,并使用具体执行来建模外部环境。

2.3 P3. 缓慢且不灵活的健全性分析

在符号具体化执行中,“健全性”意味着所生成的输入确实能够到达执行过程中所预期的路径。这种方法的好处在于,它确保了每一次路径探索都是有意义的,避免了因不准确的路径假设而浪费时间。传统的符号具体化执行工具通常会采用严格的健全性分析,以避免产生无效的测试用例。

这个分析过程虽然能带来准确性,但在实际应用中可能会面临性能和灵活性方面的挑战。

    为什么要进行健全性分析?Concolic execution (符号具体化执行)尝试通过收集完整的约束来保证健全性(soundness)。这种完整性确保了满足这些约束的输入将引导执行到预期的路径。因此,符号具体化执行可以生成输入,以探索程序的其他路径,而无需担心错误的预期。

    为什么不进行健全性分析?复杂逻辑的无休止分析: 然而,在某些情况下,计算完整的约束可能会非常耗费资源。特别是对于复杂操作(如加密函数或压缩)的约束计算通常会出现问题。比如,在图3上部展示的文件程序的代码片段中,如果符号具体化执行进入 file_zmagic() 函数,它会停留在这里计算 zlib 解压缩的复杂约束,从而无法继续探索其他有趣的代码路径。

图3:第一个例子展示了对于复杂例程(如 file_zmagic())收集完整约束可能会阻碍发现新的路径。在这种情况下,符号执行引擎可能会在计算复杂的约束(例如 zlib 解压缩过程)时停滞不前,从而无法有效探索其他代码路径。

第二个例子展示了如果给定的具体输入跟随 looks_ascii() 的真实路径,它会使得路径过度约束,从而无法找到 file_tryelf() 的真实路径。具体来说,过度约束可能会限制探索其他有效路径的能力,使得某些潜在的执行路径被忽略。

    为什么不使用健全性分析?健全性分析可能导致过度约束完整的约束可能导致路径过度约束,从而限制符号执行发现未来路径的能力。特别是,当插入的约束过于严格时,可能会限制符号执行引擎的探索能力。例如,在图 3 的下部代码中,如果通过给定的具体输入将变量 ch 定义为 ‘A’,则符号执行将插入约束 {ch >= 0x20 ∧ ch < 0x7f} 到 looks_ascii() 函数中,因为原生执行会执行该 if 语句的真实分支。然而,当符号执行到 file_tryelf() 时,最终的约束 {ch >= 0x20 ∧ ch < 0x7f ∧ ch == 0x7f} 变得不可满足,导致无法生成任何测试用例。这里的问题在于,file_tryelf() 的路径依赖于 looks_ascii() 的true分支,这导致了过度约束。如果 file_tryelf() 不依赖于 looks_ascii() 的true分支,那么符号执行引擎在没有考虑路径约束 ch == 0x7f 的情况下生成的输入将能够探索 file_tryelf() 中的路径。

我们的方法: 收集不完整的约束集以提高效率:为了提高效率,我们的方法选择收集不完整的约束集。当遇到路径过度约束的情况时,我们只解决部分约束,从而避免因为过度约束而限制路径探索。这样可以有效减少计算复杂约束所带来的性能开销,同时增加发现新路径的机会。

3设计

    在本节中,我们将解释实现QSYM的设计决策。图4显示了QSYM架构的概述。QSYM的目标是通过减少符号化仿真(符号执行的主要性能瓶颈)的开销来实现快速的混合符号执行。为此,QSYM首先对目标程序进行插桩,然后使用动态二进制翻译(DBT)以及覆盖引导模糊测试器提供的输入测试用例来运行该程序。DBT生成用于原生执行的基本块,并对这些块进行修剪以进行符号化执行,使得我们能够在两种执行模式之间快速切换。接着,QSYM选择性地仿真生成符号约束所需的指令,而不是像现有方法那样仿真受污染的基本块中的所有指令。通过这种方式,QSYM将符号仿真的数量显著减少了(减少了5倍,见§5.3中的图10),从而实现了更快的执行速度。得益于其高效的执行方式,QSYM可以重复执行符号化执行,而不是使用需要外部环境建模的快照机制。特别是,QSYM可以与外部环境进行具体的交互,而不是依赖人为构造的环境模型。为了提高约束求解的性能,QSYM应用了各种启发式方法,在严格的健全性和更好的性能之间进行权衡。这种放宽限制为混合模糊测试器中的符号执行器提供了前所未有的机会,其中,配对的模糊测试器可以快速验证新生成的测试用例——如果它们没有实际意义,则直接丢弃。

图4展示了QSYM作为混合模糊测试器的架构概述。QSYM接受一个测试用例和一个目标二进制文件作为输入,并尝试生成可能探索新路径的新测试用例。它使用动态二进制翻译(DBT)来原生执行输入的二进制文件,同时选择用于符号化执行的基本块。由于QSYM在约束求解中应用了各种启发式方法,在严格的健全性和更好的性能之间进行权衡,因此新生成的测试用例将在稍后由模糊测试器验证。

3.1 控制符号执行器

    我们详细解释了四种优化混合模糊测试器中符号执行器的新技术。

    指令级符号执行。QSYM 仅对生成符号约束所需的一小部分指令进行符号执行。与现有的混合符号执行器不同,现有的执行器采用基于代码块的污点分析,从而对受污指令块中的所有指令进行符号执行,而 QSYM 则在受污指令上采用指令级污点跟踪和符号执行。现有的符号执行器采用这种粗粒度的方法,是因为在原生执行和符号执行之间切换时会产生较高的性能开销。然而,对于 QSYM,高效的动态二进制翻译 (DBT) 使得实现精细粒度的指令级污点跟踪和符号执行成为可能,帮助我们避免了不必要的仿真开销。

    这种方法在实际中显著提高了 QSYM 符号执行的性能。以 memset() 为例(图 5),其中只有其大小参数 (rdx) 是受污的。与像 angr 这样的基于代码块的方法不同,它需要对所有指令进行符号执行,而 QSYM 仅通过执行最后两条指令就可以生成符号约束。在实际问题中,这个问题更加严重,因为现代编译器生成高度优化的代码,以尽量减少控制流的变化(例如,使用条件移动指令 cmov)。例如,在 angr 中,任何传递给 memset() 的符号参数都会阻止其符号执行,因为 memset() 依赖于复杂的指令,比如 punpcklbw。

图 5 展示了指令级符号执行的效果。如果在 __memset_sse2() 中大小参数是符号化的,那么指令级符号执行仅会执行那些符号指令,这些指令位于虚线框中。然而,基于基本块的符号执行则需要执行其他可以以本机方式执行的指令,包括 punpcklwd,正如右侧的 angr 代码所示,这些指令处理起来非常复杂。

    QSYM 通过利用动态二进制翻译 (DBT) 在单个进程中运行本地和符号执行,使得模式切换非常轻量化(即普通的函数调用)。值得注意的是,这种方法与大多数现有的符号执行引擎(如 angr)有着显著的不同。对于这些引擎,两个执行模式之间的切换需要进行复杂的通信,例如更新内存映射。因此,像 angr 这样的引擎会进行多种优化以减少模式切换的次数,例如尽可能长时间地运行一种模式。

    QSYM 仅解决与目标分支相关的约束,并通过将这些解决的约束应用于原始输入来生成新的测试用例。与此不同,其他符号执行器如 S2E 和 Driller 采用增量式解决约束的方法;即,它们通过利用前一次执行中学到的引理,专注于解决当前运行中更新的约束部分。对于没有任何初始输入来进行探索的纯符号执行器来说,这种增量方法在枚举所有可能的输入空间时是有效的【18】。然而,对于混合模糊测试工具来说,这并不是一种理想的设计,原因有以下两个。

    首先,混合模糊测试工具中的增量方法会反复解决其他测试用例已经探索过的约束。例如,图 6 展示了 QSYM 和 Driller 在探索相同代码路径时生成的初始测试用例和新测试用例:红色标记显示了原始输入与生成的测试用例之间的差异。通过仅解决与分支相关的约束(即选择删除消息的菜单),QSYM 通过更新初始输入的一小部分来生成新的测试用例。然而,Driller 生成的新测试用例与原始输入相比显得截然不同。这表明 Driller 在解决模糊测试器反复测试的无关约束(如用户名约束)上浪费了时间。

图 6 展示了 QSYM 和 Driller 在探索相同代码路径时生成的测试用例,这些测试用例均源自相同的种子输入。二者生成的测试用例存在差异,因为 QSYM 使用了不相关约束消除作为其底层优化技术,而 Driller 使用了增量求解。不相关约束消除能够移除不必要的约束,例如对用户名的约束,进而在已有具体输入的情况下,简化测试用例的生成。

    第二,增量求解仅在提供完整约束时才有效。然而,由于仿真开销的原因,现有的符号执行器无法为复杂的现实世界程序生成符号约束。然而,仅关注相关约束使我们更有可能求解这些约束,并生成可能会走不同代码路径的新测试用例。例如,仅与命令菜单相关的测试用例不会受到为用户名生成的不完整约束的影响(图 6)。此外,由于其环境支持(§3.1)或各种启发式策略(§3.2,§3.3),QSYM 倾向于生成更宽松(即不完整)的约束形式,这些约束形式更容易求解。这使得 QSYM 能够扩展到测试现实世界的程序。

    QSYM 的快速符号执行使得在重复的符号测试中,选择重新执行程序比采用快照机制更为优越。传统的快照方法会创建目标进程的镜像,并在稍后重用,以克服符号执行中的性能瓶颈。这是因为重新执行一个程序以达到某个特定执行路径并保持有效状态,通常比恢复对应的快照要耗时得多。然而,随着 QSYM 的符号执行器变得更加高效,快照机制的开销不再比重新执行更小。因此,QSYM 更倾向于通过重新执行来处理重复的符号测试,而不是依赖快照。

    QSYM 通过具体地与外部环境交互,避免了由于外部环境建模不完整或错误所导致的问题。模型的不完整性和不正确性可能使符号执行和原生执行产生偏差,从而误导进一步的探索。因此,QSYM 为了避免这些错误模型,采用“黑箱”方式来处理外部环境,即通过具体值直接执行。这是一种常见的处理无法在符号执行中模拟的函数的方法。然而,这种方法在基于分叉的符号执行中很难应用,因为分叉会破坏进程边界。由于 QSYM 不采用基于分叉的符号执行,因此能够利用这一旧但完整的技术来支持外部环境。

    尽管如此,这种方法可能会导致不准确的测试用例,这些测试用例可能不会产生任何新的覆盖,从而浪费资源。如果 QSYM 盲目相信符号执行的结果,它将浪费资源来探索不会引入新覆盖的路径。为了解决这个问题,QSYM 依赖于模糊测试器来快速检查和丢弃那些无法引入新覆盖的测试用例,从而避免进一步的无效分析。

3.2 优化求解

    符号执行容易出现过度约束的问题,即目标分支与当前执行路径中生成的复杂约束相关联(见图 3)。这个问题在实际应用程序中非常普遍,但现有的求解器通常过早放弃(例如超时),而没有尝试利用生成的约束,而这些约束在执行过程中花费了大部分时间(见图 10)。在混合模糊测试中,符号求解器的作用是帮助模糊测试器克服简单障碍(例如图 3 中的狭窄范围约束 {ch == 0x7f}),并深入程序逻辑。因此,作为一个混合模糊测试器,形成潜在的新测试输入是合理的,无论是通过当前路径还是其他路径来达到未探索的代码。

    QSYM通过乐观地选择和解决部分约束(如果不能整体解决的话),努力从生成的约束中生成有趣的新测试用例。由于在复杂程序中,模拟开销通常超过约束求解的开销,因此利用这种机会是经济上合理的。特别地,QSYM 选择路径中的最后一个约束进行乐观求解,主要有两个原因。首先,它通常具有非常简单的形式,使约束求解更高效。另一个候选项是 unsat_core 的补集,即引入不可满足性的最小约束集。然而,计算 unsat_core 成本非常高,甚至有时会使底层约束求解器崩溃。其次,从解决最后一个约束生成的测试用例更有可能探索目标路径,因为它们至少满足到达目标分支时的局部约束。由于 QSYM 首先消除与最后一个约束无关的约束,因此所有不相关的约束不会影响乐观求解的结果。

3.2 基本块剪枝

    我们观察到,由相同代码重复生成的约束对于在实际软件中发现新的代码覆盖率并没有帮助。特别是,由程序中计算密集型操作生成的约束,即使它们被公式化,通常也难以求解(即非线性)。更糟糕的是,这些约束往往阻碍了探索其他部分的可能性,这些部分虽然不相关,但仍然足够有趣,可以进一步探索。例如,在图3的第二个示例中,即使符号执行生成了zlib解压缩的约束,由于这些约束的复杂性,约束求解器将无法求解这些约束。

    为了缓解这个问题,QSYM尝试检测重复的基本块,然后对这些块进行修剪,减少符号执行并只生成约束的子集。更具体地说,QSYM在运行时测量每个基本块的执行频率,并选择那些重复的块进行修剪。如果一个基本块被执行得过于频繁,QSYM会停止从中生成进一步的约束。唯一的例外是,当一个块包含不引入任何新的符号表达式的常量指令时,例如x86架构中的mov指令和带有常量的移位或掩码指令。

    QSYM决定使用指数退避策略来修剪基本块,因为它可以迅速减少过于频繁的块。它仅执行那些频率为2的幂的块。然而,如果过度修剪基本块,可能会错过一些可解的路径,从而可能无法发现新的路径。为此,QSYM建立了两种启发式方法来防止过度修剪:分组多次执行和上下文敏感性。

    分组多次执行是一种减少过度修剪基本块的策略。当我们计算基本块的执行频率时,会将一组执行视为一次进行频率计数。例如,假设组大小为八。只有在执行块八次之后,我们才会将频率计为一次。这将允许QSYM在决定不修剪时执行该块八次。这有助于避免丢失对发现新路径至关重要的约束,并且对符号执行的影响不大,因为运行这些基本块少量次数不会使约束变得过于复杂。

    上下文敏感性作为区分在不同上下文中运行相同基本块的工具,用于频率计数。如果我们不区分上下文(即基本块在什么点被调用),可能会通过修剪更多的块而丢失重要的约束。例如,当有两个 strcmp() 调用时,如 strcmp(buf, “GOOD”) 和 strcmp(buf, “EVIL”),这两个调用必须被视为不同的基本块执行进行频率计数。否则,程序的另一部分中相同基本块的执行(与当前执行无关)可能会影响修剪。QSYM 维护当前执行的调用栈,并使用它的哈希值来区分不同的上下文

4 实现

    我们从头实现了 concolic 执行器。QSYM 总共有 16,000 行代码(LoC),表 2 总结了其各个组件的复杂性。QSYM 依赖于 Intel Pin [24] 进行动态二进制翻译(DBT),其核心组件作为 Pin 插件用 C++ 实现:12,000 行代码用于 concolic 执行核心,1,900 行代码用于表达式生成,1,500 行代码用于处理系统调用。QSYM 还提供了 Python API(500 行代码),使用户可以轻松扩展 concolic 执行器;混合模糊测试器作为展示用例使用了这些 API。QSYM 在处理系统调用时使用了 libdft [25],并为 64 位环境添加了支持。目前的 QSYM 实现支持部分 Intel 64 位指令,这些指令对于漏洞发现至关重要,例如算术、位运算、逻辑和 AVX 指令。QSYM 将会开源,并计划在未来支持更多类型的指令,包括浮点指令。

表 2:QSYM 的主要组件及其代码行数。

5 评估

    为了评估 QSYM,本节将尝试回答以下问题:

    1. 应对实际程序的扩展性:QSYM 在发现新漏洞和提高复杂真实世界软件代码覆盖率方面的效果如何?(§5.1,§5.2)
    2. 设计决策的合理性:QSYM 所做的设计决策在漏洞发现方面的效果如何?(§5.3,§5.4,§5.5)

1.指令级符号执行。我们的细粒度指令级符号执行在节省指令数量和混合模糊测试器的整体性能方面效果如何?(§5.3)

2.乐观约束求解。QSYM 的乐观约束求解在发现漏洞方面的合理性如何?(§5.4)

     3.基本块剪枝。我们剪枝基本块的方法在整体性能和代码覆盖率方面效果如何?(§5.5)

    实验环境。我们在配备 Intel Xeon E7-4820(具有八个 2.0GHz 核心)和 256 GB RAM 的 Ubuntu 14.04 LTS 系统上运行了所有实验。分别使用三个核心用于主 AFL、从 AFL 和 QSYM 的端到端评估(§5.1、§5.2 和 §5.4),一个核心用于仅测试符号执行(§5.3 和 §5.5)。尽管我们使用了具有多核的服务器,但并没有使用所有核心来运行 QSYM,而是旨在同时运行多个实验。

5.1 应对真实程序

    QSYM 的方法能够扩展到复杂的现实世界软件。为了突出我们符号执行引擎的有效性,我们将 QSYM 应用于一些不仅规模大,而且已经被最先进的模糊测试工具长时间测试过的非简单程序。因此,我们选择了所有由 OSS-Fuzz 测试过的应用程序和库作为 QSYM 的理想候选项:包括 libjpeg、libpng、libtiff、lepton、openjpeg、tcpdump、file、libarchive、audiofile、ffmpeg 和 binutils。在这些程序中,QSYM 在八个程序和库中检测到了 13 个之前未知的漏洞,包括堆栈和堆溢出以及空指针解引用(如表 3 所示)。值得注意的是,Google 的 OSS-Fuzz 在几个月内每天生成了 10 万亿个测试输入来模糊测试这些应用程序,而 QSYM 仅用一台工作站运行了三小时就发现了这些漏洞。换句话说,QSYM 发现的所有漏洞都需要精确的输入形式来触发,展示了我们符号执行器的有效性。第 6 节对 QSYM 发现的一些漏洞进行了深入分析。

表3展示了QSYM发现的漏洞以及之前用于模糊测试这些二进制文件的已知模糊测试工具所未能检测到的漏洞。CVE-2017-11543⋆和CVE-2017-1000249⋆是在漏洞修补前由QSYM并发发现的[26,27]。表中的∗标记了QSYM发现的tcpdump漏洞,虽然模糊测试工具也可以发现该漏洞,但在我们的实验中,QSYM比纯模糊测试提前3小时找到了该漏洞。

    与 QSYM 相比,其他混合模糊测试工具无法扩展以支持这些现实世界的应用程序。我们测试了 Driller,这是一个已知的最先进的混合模糊测试工具,作为对比。为了进行测试,我们修改了 Driller 以接受文件输入,因为这些应用程序从文件接收输入,而原版 Driller 只接受标准输入。我们遵循了 Driller 作者的修改建议。结果,由于其缓慢的仿真速度,Driller 只能生成少量的测试用例。在运行 30 分钟内,Driller 平均生成了不到 10 个测试用例,而 QSYM 在相同时间内生成了数百个(超过 10 倍)的测试用例。此外,Driller 由于缺乏环境建模和系统调用支持,无法支持 11 个应用程序中的 5 个,如表 4 所示。

表4总结了Driller由于不完整或错误的系统调用处理,无法应用于实际软件的情况。Driller在处理mmap()时出现了错误:它忽略了文件描述符。我们通过在每个项目中使用基本测试用例动态检测到这些错误。因此,未探索路径中可能还存在其他不正确或不支持的系统调用。这表明Driller在处理复杂的真实环境时存在系统调用支持不足的问题。

5.2 代码覆盖率有效性

    为了展示我们的符号执行器在帮助模糊测试器发现新代码路径方面的效果,我们测量了在模糊测试过程中,通过使用QSYM(混合模糊测试器)和AFL(模糊测试器)在不同数量的输入种子文件下所实现的代码覆盖率。我们选择了libpng作为模糊测试目标,因为它包含各种难以满足的窄范围检查(例如,用于块识别的4字节魔数检查),这些检查在仅依赖模糊测试的方法中很难被满足。

    作为种子输入,我们从libpng项目中收集了141个高质量(即包含各种类型块)的PNG图像文件,并以20%的递增量应用于模糊测试器。对于0%的情况,我们提供了一个包含256个‘A’字符的ASCII文件作为种子输入,因为两个模糊测试器都需要至少一个输入才能开始。为了公平比较与仅模糊测试方法的效果,我们准备了一个混合模糊测试器,包含一个主AFL实例和一个从AFL实例以及QSYM;同时,准备了一个仅模糊测试器,包含一个主AFL实例和两个从AFL实例,以确保两个模糊测试器在相同的执行时间内利用相同的计算资源。我们运行了这两种模糊测试器六个小时,并测量了所探索的代码覆盖率。

    混合模糊测试方法在没有或仅有有限初始输入的情况下,尤其有效地发现了新代码路径(见图7)。在0%情况下(仅使用一个虚拟输入),AFL进展不大,因为libpng在执行的早期阶段检查了PNG头标识符。相反,QSYM不仅制定并解决了检查PNG魔数头标识符的约束,还探索了libpng的20%以上的代码路径,比使用有效图像进行模糊测试的覆盖率高出3%(即20%AFL情况)。即使提供了足够的种子输入,符号执行器仍然使模糊测试器能够找到更有趣的路径。例如,hIST块未包含在任何141个测试用例中,但QSYM能够通过解决符号约束成功生成新的测试用例。值得注意的是,hIST块需要满足复杂的前置和后置条件才能成为PNG中的有效块:hIST块应在PLTE块之后但在IDAT块之前[29]。这个例子也暗示了构造涵盖软件中所有功能的完整测试用例的困难,我们认为QSYM的方法可以对此提供一些启示。

图7:在提供不同数量的种子输入后,QSYM和AFL(为了公平比较,使用两个AFL实例)对libpng进行六小时运行的代码覆盖率。在0%情况下,我们使用了一个由256个‘A’组成的无效PNG文件作为初始输入。100%情况下包含了libpng项目提供的141个PNG图像文件。这个实验结果突显了符号执行方法对混合模糊测试的代码覆盖率的贡献,特别是依赖于高质量种子输入的情况下。

5.3 快速符号模拟

    为了展示 QSYM 符号模拟的性能优势,我们使用了 DARPA CGC 数据集来比较 QSYM 和 Driller(Driller 在 CGC 竞赛中获得了第三名)。CGC 数据集包括了从简单的登录服务到尝试模拟现实世界协议的复杂程序等各种类型的程序。CGC 发布了 131 个在 CGC 资格赛中使用的挑战程序及其对应的 PoVs(用于触发目标程序漏洞的输入)。在这 131 个挑战程序中,我们忽略了 5 个需要进程间通信(IPC)的程序,因为 QSYM 和 Driller 都不支持这些程序。我们选择 PoVs 作为初始种子输入,因为挑战的编写者故意将漏洞隐藏在代码路径的深处,因此 PoVs 往往具有良好的代码覆盖率。为了简化分析,我们为两个模糊测试工具都选择了第一个 PoV(只有一个)作为种子输入。

    为了展示模糊测试的结果,我们使用了在模糊测试每个 CGC 挑战程序时生成的所有测试用例所测量的代码覆盖率。由于 CGC 程序不支持 libgcov(一个实际标准的代码覆盖率测量工具),我们使用了 AFL 位图(bitmap)来表示代码覆盖率。AFL 位图由 65,536 个条目组成,用来表示代码覆盖率,这对于我们的比较目的来说已经足够合理。

    由于简单的代码覆盖率数字的直接比较可能无法准确指示哪个模糊测试工具探索了更多且不同的代码路径,我们对它们的代码覆盖率进行了相对比较(见下文)。此外,为了公平比较新探索的路径,我们删除了初始 PoVs 已经覆盖的位图条目。在此基础上,我们使用以下公式来相对比较和可视化两者的覆盖率结果。对于代码覆盖率 A(QSYM)和 B(Driller),我们可以通过以下公式量化它们之间的覆盖率差异:

    该公式直观地表示了在所有由 A 或 B 探索的唯一路径中,A 探索了多少更多的路径。例如,如果 QSYM 发现的唯一路径比 Driller 更多,那么𝑑(𝐴,𝐵)将呈现为一个正数;当 QSYM 不仅发现的路径比 Driller 多,而且还覆盖了 Driller 发现的所有路径时,𝑑(𝐴,𝐵)将为 1.0。

    图 8 展示了在五分钟内 CGC 代码覆盖率的结果。每个单元格代表我们测试的每个 CGC 挑战,按照字母顺序排列(从左到右,从上到下)。例如,最上方左边的单元格代表 CROMU_00001,最下方右边的单元格代表 YAN01_00012。蓝色表示 QSYM 在代码覆盖率方面表现更好,红色表示 Driller 表现更好。颜色越深,表示一个模糊测试工具在代码覆盖率上完全压倒了另一个工具。

图 8:此颜色图显示了 QSYM 与 Driller 在五分钟内相对代码覆盖率的比较:蓝色表示 QSYM 发现的代码比 Driller 多,红色则表示相反(具体公式见 §5.3)。每个单元格代表按照字母顺序排列的每个 CGC 挑战(从左到右,从上到下)。QSYM 在发现新代码路径方面表现优于 Driller;在 126 个挑战中,QSYM 在 104 个挑战中(82.5%)取得了更好的代码覆盖率,而 Driller 在 18 个挑战中(14.3%)表现更好。

    QSYM 在代码覆盖率方面优于 Driller;在 126 个挑战中,QSYM 在 104 个挑战(82.5%)中探索了更多的代码路径,而 Driller 仅在 18 个挑战(14.3%)中表现更好。更重要的是,QSYM 在 37 个挑战中完全超越了 Driller,QSYM 不仅发现了更多的路径,还覆盖了 Driller 所探索的所有路径。值得注意的是,即使增加 Driller 的超时时间(例如,给予更多时间来解决约束问题)也无法提高代码覆盖率的结果。为了验证这一点,我们在 QSYM 的超时时间固定为 5 分钟的情况下,运行了 Driller,超时时间从 5 分钟增加到 30 分钟(如图 9 所示)。即使将 Driller 的超时时间延长到 30 分钟,QSYM 仍然在 126 个二进制文件中有 98 个探索了更多路径,而 Driller 的覆盖图在超时 10 分钟后基本达到饱和。

图 9:比较 QSYM(5 分钟超时)和 Driller 在增加约束求解时间(从 5 分钟到 30 分钟)时的表现。结果显示,Driller 无法生成新测试用例的原因并不是因为求解生成约束的时间预算有限。

    指令级符号执行:为了理解 QSYM 如何比 Driller 实现更好的性能,我们对 QSYM 和 Driller 的性能因素进行了分解。从高层次来看,Driller 将 27% 的执行时间用于创建快照,70% 用于符号模拟(见图 10(a))。换句话说,Driller 在 concolic 执行上花费了 QSYM 的 2 倍时间,但大部分时间都花在了模拟和快照上。

    在 QSYM 中实现的指令级符号执行在加速符号模拟方面发挥了重要作用。为了展示这一技术的有效性,我们测量了两个系统中符号执行的指令数量。然而,由于 QSYM 和 Driller 对符号指令的定义不同,直接比较这两者变得困难:QSYM 使用本地 x86 指令,而 Driller 使用 VEX IR 进行符号执行。我们没有直接计算和比较符号执行的指令数量,而是考虑了放大因子(即 4.69),这是将所有 CGC 二进制文件从 x86 转换为 VEX IR 的转换率。即使考虑到这个放大因子(假设一个 amd64 指令相当于 4.69 条指令),QSYM 的符号执行指令数量仅为 Driller 的 1/5。此外,QSYM 的快速模拟器帮助我们消除了低效的快照机制。所有这些改进一起,使得约束求解成为整体性能的另一个重要因素。

    进一步的案例分析中,我们可以发现一些趋势:

    (1)QSYM 在处理大型程序和长 PoVs(即探索更深路径)时,比 Driller 探索更多的路径。例如,在 NRFIN_00039 中,QSYM 覆盖的代码比 Driller 更多,而 NRFIN_00039 是挑战中二进制文件大小最大的程序,约为 12MB。此外,QSYM 能够生成覆盖二进制文件深处代码的测试用例。例如,CROMU_00001 是一个可以在用户之间发送消息的服务。要读取一条消息,攻击者需要经过以下步骤:①创建一个新用户(user1),②创建另一个用户(user2),③以 user1 身份登录,④向 user2 发送消息,⑤注销,⑥以 user2 身份登录,⑦通过发送消息 ID 来读取消息。QSYM 能够到达第七步并生成读取消息的测试用例,而 Driller 无法到达该函数。这表明 QSYM 的高效符号仿真在发现隐藏在程序路径深处的复杂漏洞方面非常有效。

    (2)在有限的时间预算(5到30分钟)内,Driller 在具有多个嵌套分支且路径容易到达的应用程序中能够获得更多的代码覆盖率(即浅层路径),因为它的快照机制在这种情况下得到了优化。由于 Driller 的仿真速度较慢,它只能在有限的时间内(5到30分钟)搜索接近程序起始位置的分支。当 Driller 到达嵌套分支(即包含多个比较指令的块)时,它可以充分利用快照机制快速探索这些分支而无需重新执行。相比之下,QSYM 需要重新执行仿真并使用新生成的输入来到达下一个分支。然而,QSYM 可以通过重新执行逐步找到路径,并且这种探索会更加高效,因为这些分支对于 QSYM 来说也很容易到达。

    不完整的仿真。目前,QSYM 并未完全仿真所有指令(例如,它无法仿真具有符号操作数的浮点运算),因此有人可能会认为其性能提升是由于未仿真的指令造成的。为反驳这一假设,我们测量了 QSYM 未仿真的指令数量(见表 5)。需要注意的是,在 126 个二进制文件中,只有 13 个包含至少一条未被 QSYM 处理的指令。此外,只有其中三个二进制文件中未仿真的指令占总指令数的比例超过 1%。因此,我们可以得出结论,QSYM 的性能提升并非由于指令建模的不完整性,而是由于我们在指令级别上的符号执行优化。

表5:由于QSYM的限制,CGC挑战中的未仿真指令数量:不支持浮点运算。

5.4 乐观求解

    为了评估乐观求解的效果,我们使用LAVA数据集对QSYM进行了比较。LAVA是一个测试套件,通过在Linux工具中注入难以发现的漏洞来评估漏洞检测技术,因此该测试非常适合展示这种技术的适应性。LAVA包含两个数据集:LAVA-1和LAVA-M。我们决定使用LAVA-M数据集,该数据集包含四个有漏洞的程序:file、base64、md5sum和who,这些程序已被用于测试其他系统如VUzzer。我们在LAVA-M数据集上运行了启用和未启用乐观求解的QSYM,测试时间为五小时,这是原始LAVA研究中设定的测试时长【10】。为了识别独特的漏洞,我们使用了LAVA项目提供的内置漏洞标识符。

    乐观求解通过放宽过度约束的变量,帮助QSYM发现更多的漏洞。图11展示了启用或未启用乐观求解的QSYM在运行时发现的累积唯一漏洞数量。在所有测试用例中,启用乐观求解的QSYM在发现漏洞数量上都优于未启用的,即使是在早期阶段(如三分钟内)也能发现更多的漏洞。这个结果支持了我们的设计假设,即放宽过度约束的变量有助于路径探索,而模糊测试则可以很好地辅助这一过程,通过排除由于缺失约束而产生的误报。

    以base64为例,该程序使用表查找(即table[input[0]])对输入字符串进行解码,随后进一步的比较将受限于该具体值。在这种情况下,由于表查找将输入符号过度约束为仅有一个与初始测试用例相同的解,符号执行将所有符号约束具体化为当前输入。因此,在没有乐观求解的情况下,虽然QSYM到达了必须通过的分支以触发崩溃,但约束求解器将返回不可满足。然而,启用乐观求解后,即使约束不可满足,求解器仍将只解决最后一个约束并生成潜在的崩溃输入,这有助于模糊测试向前推进,如果这种乐观的推测是正确的。

图11:显示了在启用或未启用乐观求解的情况下,QSYM在LAVA数据集上随时间发现的累积漏洞数量。

    我们还将QSYM与其他最新的系统进行了比较;QSYM的表现优于它们(表6)。首先,我们在我们的环境中测试了VUzzer [9]。然而,由于我们工作站的核心速度较慢(2.0GHz),我们的结果要么与原始论文的结果相等(在md5sum和uniq上),要么更差(在base64和who上)。因此,我们决定引用原始结果。我们还从LAVA [9]的评估中借用了其他结果,因为其测试系统是匿名的。在表6中,FUZZER代表了覆盖导向的模糊测试工具的结果,SES代表了符号执行的结果。QSYM在LAVA-M数据集中发现的漏洞数量是VUzzer和其他任何先前技术的14倍。

表6:现有技术和QSYM在LAVA-M数据集中发现的漏洞数量。VUzzer (R) 代表在我们的机器设置中由VUzzer发现的漏洞数量,VUzzer (P) 代表在VUzzer论文中报告的漏洞数量。

    为了评估我们在乐观求解中的决策,即在执行路径中的约束中仅使用最后一个约束,我们测量了在LAVA-M数据集中找到漏洞的数量和所花费的时间,同时改变了附加约束的数量。当我们包含附加约束时,我们选择了最近添加的约束。为了限制模糊测试的影响,我们使用了由数据集作者提供的初始测试用例进行单次执行,而非端到端评估。结果如图12所示。使用乐观求解的QSYM始终比不使用乐观求解的QSYM找到更多的漏洞。然而,考虑附加的约束在大多数情况下并没有帮助发现更多的漏洞,反而增加了求解时间。在某些情况下,添加更多的约束可以减少乐观求解所需的时间。这并不令人意外,因为添加更多的约束可能有助于决定不可满足性。

图12:显示了在LAVA数据集中,使用初始测试用例单次执行QSYM时,乐观求解的时间消耗和发现的独特漏洞数量。减号(–)表示没有使用乐观求解,因此其时间消耗在所有情况下为零。Opt表示我们使用的仅在执行路径中使用最后一个约束的乐观求解策略,而加号(+)后的数字代表用于乐观求解的附加约束数量。例如,+1表示QSYM使用一个附加约束,因此总共使用两个约束进行乐观求解:最后一个和一个附加的。图表显示,我们仅使用最后一个约束的决策帮助QSYM在花费较少时间的情况下发现最多的漏洞。

5.5 修建基本块

    为了展示基本块修剪的效果,我们用libjpeg、libpng、libtiff和file四个广泛使用的开源程序来评估这种技术。我们从每个项目中选择了5个种子测试用例,它们展示了最大的代码覆盖率(libjpeg只有4个测试用例,所以只使用了4个)。我们用5分钟的超时运行QSYM来运行每个测试用例的concolic执行(总共19个用例,每个测试用例的5分钟超时,最多95分钟),然后测量执行时间和新发现的代码覆盖率。

    图13显示,基本块修剪不仅减少了执行时间(63.6分钟对比94.2分钟),还帮助发现了更多的代码覆盖率(13.2%对比11.8%)。以libtiff为例,TIFFReadDirectoryFindFieldInfo()函数因为包含一个带有符号分支的循环,不断引入新的约束条件。基本块修剪使得QSYM可以具体执行该函数并专注于其他有趣的代码,而未使用修剪时,模拟执行会卡在该函数中生成约束条件。

    其他设计决策,例如上下文敏感性和分组,对提高代码覆盖率至关重要。图13还显示了在禁用分组和上下文敏感性时的代码覆盖率和执行时间。如果禁用分组并直接使用AFL的算法,修剪会过于细致,从而影响代码覆盖率。当禁用上下文敏感性时,也观察到了类似的结果。在这种情况下,QSYM过于激进地修剪基本块,导致无法生成可解的约束条件。因此,这两个设计决策对于最小化代码覆盖率的损失是必要的。

图13:libjpeg、libpng、libtiff 和 file 的总新发现代码覆盖率和耗时情况。每个项目使用了五个种子文件(libjpeg 除外,它仅有四个种子文件),这些文件在各自项目中具有最大的代码覆盖率。

6 新Bug发现分析

    在QSYM发现的13个新漏洞中,我们从ffmpeg和file中选取了两个有趣的案例,这些案例能够清晰地表达我们的思路。对于每个案例,我们尝试回答以下问题:QSYM如何发现这些漏洞?QSYM的哪些特性帮助了发现这些漏洞?以及最重要的是,为什么OSS-Fuzz未能发现这些漏洞?

6.1 ffmpeg

    图14展示了QSYM发现的ffmpeg漏洞的简化代码,以及QSYM生成的触发该漏洞的测试用例。要触发该漏洞,测试用例需要满足非常复杂的约束条件(第3-10行),这几乎是模糊测试无法实现的。相比之下,QSYM成功地生成了一个新的测试用例,通过修改输入的七个字节,使其能够通过复杂的分支,最终AFL利用该新测试用例通过了分支并触发了漏洞。

图14展示了QSYM发现的ffmpeg漏洞的相关代码以及QSYM生成的触发该漏洞的测试用例。由于第3到第10行的条件过于复杂,AFL单独运行时无法生成能够通过这些条件的输入,因而无法触发漏洞。

6.2 file

    图15展示了QSYM发现的file漏洞的简化代码。该漏洞是由于在解析ELF文件的note部分时,错误使用逻辑或运算符,导致对descsz的检查成为恒真条件。有趣的是,尽管该漏洞是在解析ELF文件时触发的,但我们从file项目的tests目录中提取的初始种子文件中并不包含任何ELF文件。换句话说,QSYM成功生成了一个带有note部分的有效ELF文件,并触发了这个漏洞。由于随机生成一个以“GNU”开头并带有note部分的有效ELF文件几乎是不可能的,因此很难通过模糊测试工具检测到这个漏洞。值得注意的是,同时期的一份漏洞报告[27]使用静态分析工具cppcheck[32]也检测到了该漏洞。

图15:QSYM发现的file漏洞。由于错误使用逻辑或运算符,对descsz的检查总是为真。

7 讨论

    我们讨论了QSYM技术在混合模糊测试之外的潜力、QSYM与其他模糊测试工具结合使用的可能性以及QSYM的局限性。

    超越模糊测试的应用。基本块剪枝(§3.3)可以作为启发式路径探索策略直接应用于其他符号执行器。例如,在测试文件解析器时,这种技术使QSYM能够关注控制数据(即头部信息),从而带来新的代码覆盖率[33],而不是负载,这需要更多的时间进行分析但不会发现新的代码覆盖。我们设想,相同的策略也可以帮助其他符号执行器在测试具有复杂数据处理逻辑的程序(如数据压缩、傅里叶变换和加密逻辑)时发挥作用。通过采用这种策略,符号执行器可以自动截断复杂但无关的逻辑,专注于决定程序控制流的输入字段

    乐观求解(见 §3.2)也可以应用于其他领域,以加速符号执行,但前提是该领域使用了像模糊测试器这样的高效验证器。这不能直接应用于通用的符号执行器,因为乐观求解通过放宽过于严格的约束条件来生成一些潜在正确的输入,这可能会产生大量的假阳性,从而使程序状态偏离预期状态。然而,在像 QSYM 这样的混合模糊测试中,由于模糊测试器可以有效地验证输入是否将程序引导到预期状态(即发现新的代码覆盖),因此我们可以从这些假阳性中快速提取有用的结果。同样,其他领域,例如自动漏洞利用生成,也可以采用这种技术来加速快速到达易受攻击的状态并制作漏洞利用。之后,它还可以通过简单地执行制作的漏洞利用并观察核心转储来有效地验证是否是假阳性。

    与其他模糊测试器的互补。将 QSYM 与其他模糊测试器(比 AFL 更好的模糊测试器)结合使用将会显示出更好的结果。虽然存在其他增强 AFL 的模糊测试器,如 VUzzer [9] 和 AFLFast [34],但在这篇论文中,我们将 QSYM 应用于 AFL,以公平地展示仅通过符号执行的增强效果。QSYM 可以通过快速到达具有狭窄范围和复杂约束的分支,并解决这些约束生成测试用例,从而补充其他模糊测试器。此外,QSYM 也可以通过其他模糊测试器进行补充。AFLFast 中的基于频率的分析步骤和马尔可夫链建模,以及 VUzzer 中的错误处理检测,可以生成更有意义的输入,这将使 QSYM 的符号执行器更高效地使用。

    局限性。尽管 QSYM 运行速度较快,但作为一种符号执行器,其性能仍受到约束求解等理论限制的束缚。目前,QSYM 专门针对运行在 x86_64 架构上的程序进行测试。与采用中间表示(IR)的其他执行器不同,QSYM 不能测试运行在其他架构上的程序。我们计划通过改进 QSYM,使其能够与架构规范而非特定架构实现进行兼容,从而克服这一限制。此外,QSYM 目前仅支持内存、算术、按位和向量指令,这些指令对于漏洞发现至关重要。我们计划支持其他指令,包括浮点运算,以扩展 QSYM 的测试能力。

8 相关工作

8.1覆盖率引导的模糊测试

    基于覆盖的模糊测试 变得越来越受欢迎,尤其是自从 AFL [1] 展示了其有效性以来。AFL 通过在程序执行过程中收集覆盖信息来评估生成的输入,从而优先考虑可能揭示新路径的输入,促进了快速的覆盖扩展。此外,AFLFast [34] 使用马尔可夫链模型来优先考虑低可达性的路径,而 CollAFL [36] 提供准确的覆盖信息以减轻路径碰撞。

    然而,模糊测试有一个根本的局限性:它不能遍历狭窄范围的输入约束之外的路径(例如,magic value[WS1] [WS2] )。为了克服这种限制,VUzzer [9] 开发了应用程序感知的变异技术,通过执行静态和动态程序分析来实现。Steelix [37] 通过在程序执行过程中收集比较进度信息来恢复正确的魔法值。FairFuzz [38] 通过程序分析和启发式方法发现魔法值并防止其变异。Angora [39] 采用污点跟踪、形状和类型推断以及基于梯度下降的搜索策略来有效地解决路径约束。然而,这些方法只能处理某些类型的约束。相比之下,QSYM 依赖符号执行,因此有可能满足任何类型的约束。此外,最近的研究 T-Fuzz [40] 通过改造程序本身来覆盖更多有趣的代码路径,这可以与 QSYM 结合使用,从程序中移除无法解决的约束。

8.2 混合符号执行

    混合符号执行是一种路径探索技术,它沿着具体执行路径执行符号执行,以引导程序走向新的执行路径。混合符号执行已广泛应用于从源代码到二进制代码的自动漏洞发现。

    然而,混合符号执行面临路径爆炸问题,即随着程序规模的增加,需要探索的路径数量呈指数级增长。为缓解这一问题,SAGE 提出了代际搜索,以最大化一次执行中的测试用例数量,并应用无关约束求解。Dowser 通过静态分析和污点分析来引导混合符号执行,并最小化符号表达式的数量,以发现缓冲区溢出漏洞。Mayhem 结合了基于分叉的符号执行和基于重新执行的符号执行,以平衡性能和内存使用。相比之下,QSYM 采用了以下方法:(1)通过模糊测试来探索大部分路径,以避免路径爆炸问题;(2)使用通用启发式方法(如基本块修剪),而不假定任何特定的漏洞类型;(3)基于指令级重新执行的符号执行,以提高性能。

8.3 混合模糊测试

    混合模糊测试的概念最早由 Majumdar 和 Sen 提出。后来,Driller 在 DARPA CGC 中通过改进的实现展示了其有效性。在这两项研究中,大多数路径探索任务由模糊测试工具完成,而混合符号执行则被选择性地用于跨越受限于窄范围约束的路径。Pak 也提出了类似的想法,但其方法仅限于早期执行阶段主要进行魔术值检查的前沿节点。然而,这些混合模糊测试工具使用的一般符号执行器不仅速度慢,而且与混合模糊测试不兼容。与之相反,QSYM 针对混合模糊测试进行了定制,因此能够扩展以检测真实世界软件中的漏洞。

9 结论

    本文介绍了 QSYM,这是一种支持混合模糊测试的快速符号执行引擎。QSYM 使混合模糊测试具有足够的可扩展性,以测试复杂的、实际应用程序。我们的评估结果显示,QSYM 在 DARPA CGC 二进制文件和 LAVA-M 测试集中表现优于 Driller 和 VUzzer。更重要的是,QSYM 在八个非简单的程序中发现了 13 个之前未知的漏洞,如 ffmpeg 和 OpenJPEG,这些程序已经在谷歌的分布式模糊测试基础设施上的最先进模糊测试工具 OSS-Fuzz 中进行了大量测试。


 [WS1]"Magic value"(魔法值)是指在程序中具有特殊意义的硬编码常量。这些值通常在程序的控制流或数据处理过程中扮演关键角色。例如,它们可能用于标识特定的数据格式、协议标头或作为程序逻辑的一部分。

 [WS2]在模糊测试中,"magic value" 常常会限制测试的范围,因为这些值通常是硬编码在程序中的特定位置。如果测试数据未能正确地包含这些魔法值,程序可能无法处理这些数据或产生预期的行为。这使得测试覆盖某些代码路径变得更加困难

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值