-
-
[翻译]利用覆盖率分析开始模糊流程自动化以改进测试和模糊开发
-
发表于: 2019-11-6 23:21 6338
-
翻译:看雪翻译小组 - Nxe
校对:看雪翻译小组 - Green奇
在我的前一篇文章中,我们讨论到了使用 bncov 来执行开放式的覆盖率分析。这次我们将研究如何以框架程序(也称为模糊测试驱动,即用于执行部分特定代码的程序)的形式编写更好的测试用例进行模糊测试。
在单元测试和模糊测试中,基本的想法是我们能完整地测试代码提高自信,在遇到意外的输入或场景时,代码仍能按预期执行。如果想要提高自信,我们可以添加步骤来检验更多的代码。使用单元测试的方法的话,就意味着写更多的单元测试用例;使用模糊测试的方法的话,就意味着模糊测试器要运行更长的时间,制作一个更好的初始语料库,或是写更多的框架程序。通过利用覆盖率分析,我们可以自动显示模糊测试的结果,帮助我们使用一个粗略的迭代方法,并从模糊测试中获得更多收获:
现在,我们将重点介绍这些选项中的最后一个:如何编写更多/更好的框架程序,以及如何提高覆盖率。在本文的其余部分,我们将重点讨论块覆盖率,因为标准行或语句的覆盖率指标直接对应于已编译代码中的块覆盖率(有关代码覆盖率基础的更多信息,请参见https://en.wikipedia.org/wiki/Code_coverage)。
接着之前的文章,我们来看看 TinyXML2,这是一个开源的C++ XML 库,其中含有与 Google OSS-Fuzz 项目集成的模糊测试框架。我们希望提高在我们第一篇文章中找到的覆盖率,所以我们将按照上面概述的过程进行。让我们先来看看现有的模糊测试框架,它实际上只是 LibFuzzer 模糊引擎中的一个函数, 叫做 LLVMFuzzerTestOneInput
。这个函数指定了通过 data 参数来将任意的模糊测试数据发送给该函数,长度由 size
指定:
如果你熟悉单元测试,你会发现这看起来很像单元测试,除了输入是一个变量。为了理解这个框架测试了什么代码,我们将快速介绍几种传统工具,然后再使用诸如 bncov 之类的灵活工具来探讨各种可能性。开发人员理解测试代码的最常见方法是通过 gcov/SanitizerCoverage 使用覆盖率工具的特殊构建查看行覆盖率,然后再使用 lcov, llvm-cov, 或者是 IDE中集成的 工具执行后处理过程来查看行覆盖率。
使用图形工具非常好,因为它可以让你随意浏览源代码并搜索未涉及的部分,尽管每个开发者的工具包中都应包含此类工具,但它们并不适合构建自定义分析。bncov 能以 drcov 格式提取覆盖数据,这使我们能通过动态二进制插桩(Dynamic Binary Instrumentation)记录块覆盖,而无需像 gcov 那样使用工具来编译特殊的插桩二进制文件。所有这些工具实际上都在做同样的事情:在代码执行时记录块或边界覆盖,然后解释并渲染该数据。DynamoRIO 甚至提供了 drcov2lcov
,该工具允许开发者使用 drcov 文件 生成 lcov HTML 报告(http://dynamorio.org/docs/page_drcov.html)。
使用 drcov2lcov 来生成 LCOV 报告
第一步是使用现有的 oss-fuzz 框架进行模糊测试,然后生成行覆盖报告,从而查看我们遗漏覆盖的地方。这个想法与单元测试相同,并且可以洞悉潜在的未经测试的选项或功能。在这种情况下,我们要寻找可以添加功能以增加覆盖的代码量的地方,这通常意味着添加对其他函数的调用或将选项更改为更宽松或更具包容性的设置。改进第一个框架的目标是严格添加功能,而不是重复或删除任何功能,如果我们以老式的方式进行操作,则必须深入研究源代码或文档以找到潜在的候选目标。
As soon as you have inputs generated from fuzzing, you can start to analyze coverage with bncov and get feedback automatically. Using the methods previously discussed to gather block coverage information, you can either visualize the information via block coloring in Binary Ninja or produce reports with the Python API as with the script summarize_coverage.py
in the repo (if you have a Personal/Student license for Binary Ninja you can still use script and use bncov with the built-in python console or the snippets editor).
一旦有了由模糊测试产生的输入,就可以开始使用 bncov 分析覆盖率并自动获得反馈。使用前面讨论的方法来收集块覆盖率信息后,你可以通过 Binary Ninja 中的块着色来可视化该信息,也可以使用 Python API,比如仓库中的脚本summarize_coverage.py
生成报告。(如果你有 Binary Ninja 的个人/学生许可证,你还可以使用脚本,并通过内置的 python 控制台或代码片段编辑器(snippets editor)使用 bncov)。
使用 bncov 生成的基本覆盖率概述信息
像这样自动化获取信息很酷,但是它就像 lcov 一样,只是提供大多数未经优先处理的数据供我们探索。相反,我们希望做的是明白在阅读行覆盖报告时通常会问的问题,然后编写脚本来回答这些问题。然后,我们可以使用脚本在每次停止模糊测试时自动向我们展示答案。一个示例性的初始问题是“什么功能没有得到覆盖?”,你可以从多个角度进行探讨,但是我们会使用所谓的“覆盖边界”这一概念:永远不会出现所有条件都满足的情况,一个条件被满足,就会有其他条件不被满足(例如“if”或“else”)。覆盖边界可能会很大,可能有太多信息与测试代码的大小有关,但是如果你要查看它,有一个 bncov 的功能可以图像化展示给你(以下命令在Binary Ninja的Python控制台中输入)。
__
5秒钟的边界浏览
虽然这是一个不错的自动化的展示,但它不一定能帮助我们得出任何结论。相反,让我们问一个更需要关注的问题:“哪些我们没有使用的分支会导致一个函数调用?”编写此问题脚本的一种方法是查看我们有一定覆盖率的函数,并通过检查未覆盖的基本块中的每个汇编指令来查找对其他函数的调用,如下所示。如果有大量结果,我们可以进一步扩展为仅查看只从覆盖边界去除了一两个步骤的基本块,但在这个示例中不需要。
现在我们可以找到我们遗漏的函数调用,但是我们可以更好地完成这一步:通过自动调用 binutils 工具集中的 addr2line
实用程序,我们可以获取这些地址并查找该地址所对应的源代码行,只要我们使用 -g 标志进行编译。结果是:所有未使用分支的源代码片段指向的函数调用不费吹灰之力地呈现给了我们(你可以查看仓库中的 find_uncovered_calls.py 的最终版本以获取更多信息)。对于像 TinyXML2 这样很小的目标,以及我在第一个模糊测试框架之后的覆盖率,它产生了五个源位置,其中一个包含了一个可选参数的提示,该参数可以扩展覆盖率,如下所示。现在,我们已迈出了一步,减少了手动操作,以及自动精炼模糊测试的结果使我们能更快速地理解。
使用 bncov 脚本找到未覆盖的函数调用
因此,为了回到我们原本的目标,即寻找额外的功能添加到模糊测试框架中,我们应该调查为什么在模糊测试运行期间未发生这些函数调用,然后决定是否对框架做出更改,使它能覆盖更多的代码路径。在这种情况下,通过查看对 CollapseWhitespace() 的未覆盖调用,我们知道了可以通过在 XMLDocument 对象的构造函数中添加 COLLAPSE_WHITESPACE 参数来覆盖XMLDocument::Parse() 下的更多代码。
为了执行更多的代码,我们要做的另一步是查看其他的一些函数,这些函数与我们在模糊测试框架中已经做的工作有关。这通常意味着要查找 API 中暴露的其他顶层函数,使用其他正在使用的结构和类的成员变量/函数,或者在当前的模糊测试工具正在执行的工作流程中添加可选步骤。我们需要避免一些陷阱:重复的工作只会减慢模糊测试器的速度,调用当前情况下对目标而言不正确的功能,以及限制代码覆盖的选择(我们需要一个严格的超集,否则我们会必须做出权衡,我们将在后面讨论)。通常,这需要深入阅读研究文档,直到我们做出选择为止。
由于我们具有覆盖率数据,所以我们可以使用 bncov 帮助我们识别候选函数以添加到我们的模糊框架中。我们可以只列出当前覆盖率为零的所有函数,但这会返回很多结果(在我的示例中有243个)。如果我们仅将结果限制为 XMLDocument 中的零覆盖率函数,即我们框架中使用的主要类?使用下面的代码片段,我们可以得到21个候选函数,我们可以将这些候选函数缩小为几个函数,这几个函数明显执行了在当前目标下无法实现的相关功能(例如:tinyxml2::XMLDocument::Print()
)。
现在,我们可以根据是否暴露和值得模糊测试来过滤这21个结果,这仍然是手动操作,而且完全由你决定。该示例中的大多数函数都可以通过它们的名称或通过观察它们的大小而被快速过滤(您也可以基于 len(func.basic_blocks)
过滤掉函数)。因此,有了几个使用新功能的候选目标,我们可以继续并将其添加到我们的框架中。通过我们的改进,我们现在可以对其进行构建,进行一些模糊测试,然后重新进行覆盖率分析。
比较框架的覆盖率可能很棘手,因为编译器和选项有差异,同样的源代码可能会产生不同的二进制文件。也就是说,如果你在同一系统上使用相同选项进行编译,则块覆盖率比较一般会有效,否则,你可以使用源代码行覆盖率,这也是一个非常安全的选择。我们希望看到的是:1)在第二个版本中,没有任何函数(除了可能是我们主函数 LLVMFuzzerTestOneInput
)具有更低的块覆盖率; 2)没有函数在其包含的块数方面发生了重大变化, 3)整体覆盖率呈总体上升趋势,如下面的覆盖率摘要所示。
不同框架之间覆盖率摘要的比较
最近有一些讨论,关于如何预测模糊测试器何时找到其寻找的最大覆盖率,但是所有讨论的结果归结为一个大概的想法:覆盖率发现遵循对数级数,并且运行时间越长,收益递减。对于度量的标准有很多意见,但最重要的因素是你愿意在一个目标上计算多长时间。我认为与其争论多花一两天来运行模糊测试器的预期价值,不如采用以下思路更为有效:
当覆盖率达到稳定水平时,我会停留在某个模糊测试器上。但这也意味着是时候为目标编写更好的模糊测试器了
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!