模糊测试之书-一-
模糊测试之书(一)
原文:
exploringjs.com/ts/book/index.html译者:飞龙
模糊测试之书
生成软件测试的工具和技术
由安德烈亚斯·泽勒、拉胡尔·戈皮纳特、马塞尔·博 hme、戈登·弗莱泽和克里斯蒂安·霍勒编写
关于本书
欢迎来到《模糊测试书》! 软件存在缺陷,捕捉缺陷可能需要大量努力。本书通过自动化软件测试来解决这个问题,特别是通过自动生成测试。近年来,新型技术的开发导致了测试生成和软件测试的显著改进。这些技术现在已经足够成熟,可以汇编成书——甚至包含可执行代码。
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo("w4u5gCgPlmg")
适用于纸张、屏幕和键盘的教科书
你可以用四种方式使用这本书:
-
你可以在浏览器中阅读章节。查看菜单上方的章节列表,或立即开始阅读测试介绍或模糊测试介绍。所有代码均可下载。
-
你可以作为 Jupyter 笔记本与章节进行交互(测试版)。这允许你在浏览器中编辑和扩展代码,实时实验。只需在每个章节的顶部选择“资源→作为笔记本编辑”。尝试与模糊测试的介绍进行交互。
-
你可以在自己的项目中使用代码。你可以将代码作为 Python 程序下载;只需选择“资源→下载代码”以获取某一章节或“资源→所有代码”以获取所有章节。这些代码文件可以执行,产生(希望)与笔记本相同的结果。甚至更简单:安装 fuzzingbook Python 包。
-
你可以将章节作为幻灯片进行展示。这允许你在讲座中展示材料。只需在每个章节的顶部选择“资源→查看幻灯片”。尝试查看模糊测试的介绍幻灯片。
本书面向的对象
这本书被设计为一本软件测试或安全测试课程的教科书;作为软件测试、安全测试或软件工程课程的补充材料;以及作为软件开发者的资源。我们涵盖了随机模糊测试、基于变异的模糊测试、基于语法的测试生成、符号测试等,所有技术都用你可以亲自尝试的代码示例进行了说明。
新闻
本书是正在进行的作品。所有计划中的章节都已发布,但我们仍在通过[小版本和大版本发布]对文本和代码进行改进。关注我们在 Mastodon 上的更新。
关于作者
本书由 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser 和 Christian Holler 撰写。我们都是软件测试和测试生成领域的长期专家;我们编写或为地球上一些最重要的测试生成器和模糊器做出了贡献。例如,如果您正在 Firefox、Chrome 或 Edge 网络浏览器中阅读此内容,您可以在我们的帮助下安全地这样做,因为 本书中列出的技术已经在他们的 JavaScript 解释器中发现了超过 2,600 个错误。我们很高兴分享我们的专业知识,并使其对公众可访问。
常见问题
故障排除
为什么启动交互式笔记本需要这么长时间?
交互式笔记本使用 mybinder.org 服务,该服务在其自己的服务器上运行笔记本。通过 mybinder.org 启动 Jupyter 通常需要大约 30 秒,具体取决于您的互联网连接。然而,如果您是在书籍更新后首次调用 binder,binder 将重新创建其环境,这可能需要几分钟。偶尔重新加载页面。
交互式笔记本不工作!
mybinder.org 对存储库施加了 100 个并发用户的限制。此外,如 mybinder.org 状态和可靠性页面中所述,
由于 mybinder.org 是一个研究试点项目,该项目的主要目标是了解使用模式和负载,以便未来项目发展。虽然我们努力实现网站可靠性和可用性,但我们希望用户了解此服务的目的是研究,我们不保证其在关键任务使用中的性能。
有 mybinder.org 的替代品;见下文。
我有交互式笔记本的替代品吗?
如果 mybinder.org 无法工作或不符合您的需求,您有许多替代方案:
-
下载 Python 代码(使用顶部菜单)并在您喜欢的环境中编辑和运行它。这很容易做到,并且不需要很多资源。
-
下载 Jupyter 笔记本(使用顶部菜单)并在 Jupyter 中打开。以下是 如何在您的机器上安装 jupyter notebook 的方法。
有关详细信息,请参阅我们关于 在您的程序中使用 Debuggingbook 代码 的文章。
作为另一种替代方案,您还可以 使用我们的 Docker 镜像(实验性)。安装 Docker 然后运行
$ docker pull zeller24/fuzzingbook
$ docker run -it --rm -p 8888:8888 zeller24/fuzzingbook
然后在您的网络浏览器中,打开控制台输出中给出的 URL (http://127.0.0.1/... 或 http://localhost/...)。这应该会为您提供与 mybinder.org 相同的环境。
如果您想创建自己的 Docker 镜像,请以我们的 Dockerfile 作为起点。
我能在 Windows 机器上运行代码吗?
我们尽量使代码尽可能通用,但偶尔,当我们与操作系统交互时,我们假设是一个类 Unix 环境(因为那是 Binder 提供的)。要在您的 Windows 机器上运行这些示例,您可以安装 Linux 子系统或 Linux 虚拟机。
您不能运行自己的专用云服务吗?
技术上,是的;但这将花费金钱和精力,而我们宁愿把这些精力花在本书上。如果您想为公众托管JupyterHub或BinderHub实例,请这样做并让我们知道。
内容
我可以在自己的程序中使用您的代码吗?
是的!有关详细信息,请参阅安装说明。
哪些内容已经出现?
请参阅发布说明以获取详细信息。
我如何引用您的工作?
感谢您引用我们的工作!一旦本书完成,您将能够以传统方式引用它。在此期间,只需点击每个章节网页底部的“引用”按钮即可获取引用条目。
您可以引用我的论文吗?并且可能写一个关于它的章节?
我们总是很高兴收到建议!如果我们遗漏了重要的参考文献,我们当然会添加。如果您想涵盖特定的材料,最好的方法是自己编写笔记本;请参阅我们的作者指南以获取编码和写作的说明。然后我们可以引用它,甚至托管它。
教学和课程作业
我可以在我的课程中使用您的材料吗?
当然可以!只需尊重许可证(包括归属和共享)。如果您想将材料用于商业目的,请联系我们。
我可以扩展或改编您的材料吗?
是的!再次,请参阅许可证以获取详细信息。
我如何基于本书运行课程?
我们已经在各种课程中成功使用了这些材料。
-
最初,我们在讲座中使用幻灯片和代码,并进行现场编码来展示技术是如何工作的。
-
现在,本书的目标是做到完全自包含;也就是说,它应该在没有额外支持的情况下也能工作。因此,我们现在在翻转课堂环境中向学生提供完整的章节,让学生在空闲时间完成笔记本。我们会在教室里讨论过去笔记本的经验,并讨论未来的笔记本。
-
我们让学生从书中做练习或进行更大的(模糊)项目。我们还有使用本书作为其研究基础的学生;实际上,用 Python 原型化 Python 非常容易。
在运行课程时,不要依赖 mybinder.org – 它不会为更大的学生群体提供足够多的资源。相反,安装并运行您自己的中心。
我可以专注于特定的子集吗?
我们为不同的受众编译了多本书的导览。我们的网站地图列出了各个章节之间的依赖关系。
我如何扩展或修改您的幻灯片?
下载 Jupyter Notebooks(使用顶部菜单),并在您方便的时候修改笔记本(见上文),包括“幻灯片类型”设置。然后,
-
从 Jupyter Notebook 下载幻灯片;或者
-
使用 RISE 扩展程序(说明)直接从 Jupyter notebook 中展示您的幻灯片。
您提供材料的 PDF 版本吗?
到目前为止,我们不提供 PDF 版本的支援。在完成书籍后,我们将制作 PDF 和印刷版本。
其他问题
我有一个问题、评论或建议。我该怎么做?
您可以在 Mastodon 上的@TheFuzzingBook@mastodon.social上发布,让读者社区参与讨论。如果您希望修复的错误,请在开发页面上报告问题。
我两周前报告了一个问题。何时会得到解决?
我们按照以下顺序优先处理问题:
-
在 fuzzingbook.org 上发布的代码中的错误
-
在 fuzzingbook.org 上发布的文本中的错误
-
编写缺失的章节
-
尚未发布的代码或文本中的问题
-
与开发或构建相关的问题
-
标记为“beta”的事项
-
其他所有事项
我如何自己解决问题?
我们很高兴您提出这个问题。开发页面有所有源代码和一些补充材料。欢迎提交修复问题的拉取请求。
我如何贡献?
再次,我们很高兴您在这里!我们很高兴接受
-
代码修复和改进。请将任何代码放在 MIT 许可下,这样我们就可以轻松地将其包含在内。
-
关于特定主题的额外文本、章节和笔记本。我们计划为第三方贡献设置一个专门的文件夹。
请参阅我们的作者指南,了解编码和写作的说明。
本项目的内容根据Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议授权。内容的一部分源代码,以及用于格式化和显示该内容的源代码,根据MIT 许可协议授权。最后更改:2024-07-01 16:50:18+02:00 • 引用 • 版权信息
如何引用这篇作品?
安德烈亚斯·泽勒、拉胡尔·戈皮纳特、马塞尔·博 hme、戈登·弗拉瑟和克里斯蒂安·霍勒:"模糊测试书"。检索日期:2024-07-01 16:50:18+02:00。
@book{fuzzingbook2024,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
title = {The Fuzzing Book},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/}},
note = {Retrieved 2024-07-01 16:50:18+02:00},
url = {https://www.fuzzingbook.org/},
urldate = {2024-07-01 16:50:18+02:00}
}
模糊测试书籍
网站地图
虽然这本书的章节可以依次阅读,但书中有许多可能的路径。在这个图中,箭头 A → B 表示章节 A 是章节 B 的先决条件。你可以选择任意路径来获取你最感兴趣的主题:
在本章中,我们将从最简单的测试生成技术开始。随机文本生成,也称为模糊测试,的关键思想是将一串随机字符输入到程序中,希望揭露故障。《模糊测试:破坏》
在上一章中,我们介绍了基本的模糊测试——即生成随机输入来测试程序。 我们如何衡量这些测试的有效性? 一种方法就是检查找到的(数量和严重性)错误;但如果错误很少,我们需要一个代理来衡量测试发现错误的概率。 在本章中,我们引入了代码覆盖的概念,测量在测试运行期间程序的实际执行部分。 对于试图覆盖尽可能多代码的测试生成器来说,测量这种覆盖率也是至关重要的。《代码覆盖率
有时我们不仅对模糊测试尽可能多的不同程序输入感兴趣,还希望推导出能够实现某些目标的特定测试输入,例如到达程序中的特定语句。当我们有了我们想要寻找的东西的概念时,我们就可以去寻找它。搜索算法是计算机科学的核心,但将经典搜索算法如广度优先搜索或深度优先搜索应用于测试搜索是不切实际的,因为这些算法可能需要我们查看所有可能的输入。然而,领域知识可以用来克服这个问题。例如,如果我们能够估计几个程序输入中哪一个更接近我们正在寻找的,那么这些信息可以引导我们更快地达到目标——这种信息被称为启发式。启发式系统地应用的方式被元启发式搜索算法所捕捉。"元"表示这些算法是通用的,并且可以根据不同的问题实例化。元启发式通常从自然界观察到的过程中获得灵感。例如,有一些算法模仿进化过程、群体智能或化学反应。总的来说,它们比穷举搜索方法更有效率,因此可以应用于广阔的搜索空间——对于它们来说,程序输入领域的广阔搜索空间不是问题。《基于搜索的模糊测试》
在“基于变异的模糊测试”章节中,我们看到了如何使用额外的提示——例如样本输入文件——来加速测试生成。在本章中,我们将这一想法进一步发展,通过提供程序合法输入的规范。通过语法指定输入可以实现非常系统和高效的测试生成,特别是在复杂的输入格式中。语法还作为配置模糊测试、API 模糊测试、GUI 模糊测试等的基础。《使用语法进行模糊测试》
传统模糊测试方法的一个问题是,它们无法测试系统可能具有的所有可能行为,尤其是在输入空间很大时。很多时候,特定执行分支的执行可能只发生在非常特定的输入下,这可能只占输入空间的一小部分。传统的模糊测试方法依赖于偶然来产生所需的输入。然而,当要探索的空间很大时,依赖于随机生成我们想要的值是一个坏主意。例如,一个接受字符串的函数,即使只考虑前 10 个字符,也有\(2^{80}\)种可能的输入。如果寻找特定的字符串,即使在超级计算机上,随机生成值也需要几千年。《符号模糊测试
在前面的章节中,我们总是只关注几秒钟内在一台机器上进行的模糊测试。然而,在现实世界中,模糊测试通常在数十甚至数千台机器上运行;持续数小时、数天甚至数周;针对一个或数十个程序。在这种情况下,需要一个基础设施来收集单个模糊测试运行中的故障数据,并将这些数据汇总到中央存储库中。在本章中,我们将探讨这样一个基础设施,即来自 Mozilla 的 FuzzManager 框架。《大规模模糊测试
大多数随机生成的输入在语法上都是无效的,因此很快就会被处理程序拒绝。为了在输入处理之外锻炼功能,我们必须增加获得有效输入的机会。其中一种方法就是所谓的突变模糊测试——也就是说,对现有输入进行微小修改,这些修改可能仍然保持输入的有效性,同时测试新的行为。我们展示了如何创建这样的突变,以及如何引导它们指向尚未发现的代码,应用了流行的 AFL 模糊器中的核心概念。《基于突变的模糊测试》
在关于覆盖率的章节中,我们展示了如何确定程序中哪些部分被执行,从而了解一组测试用例在覆盖程序结构方面的有效性。然而,覆盖率本身可能不是衡量测试有效性的最佳指标,因为即使没有检查结果是否正确,也可能有很高的覆盖率。在本章中,我们介绍了一种评估测试套件有效性的另一种方法:在代码中注入突变——人工错误后,我们检查测试套件是否能够检测到这些人工错误。其理念是,如果它未能检测到这样的突变,它也会错过真实的错误。《突变分析》
从语法中生成输入使得一个规则的每个可能的扩展都有相同的可能性。然而,为了生成一个全面的测试套件,最大化多样性更有意义——例如,通过避免重复相同的扩展。在本章中,我们探讨了如何系统地覆盖语法的元素,以最大化多样性并确保不遗漏任何单个元素。《语法覆盖率》(Grammar Coverage)
让我们通过为单个扩展分配概率来赋予语法更多的能力。这使我们能够控制每种元素应该产生多少,从而允许我们将生成的测试针对特定的功能。我们还展示了如何从给定的样本输入中学习这样的概率,并特别将我们的测试针对这些样本中不常见的输入特征。《概率语法模糊测试》(Probabilistic Grammar Fuzzing)
在信息流章节中,我们看到了如何使用动态污点来生成比仅仅寻找程序崩溃更智能的测试用例。我们还看到了如何使用污点来更新语法,从而更专注于危险的方法。《动态不变量》(DynamicInvariants)
在测试一个程序时,不仅需要覆盖其多种行为;还需要检查结果是否符合预期。在本章中,我们介绍了一种技术,使我们能够从一组给定的执行中挖掘函数规范,从而得到函数期望和提供的形式化和抽象描述。">
在本章中,我们将利用语法和基于语法的测试来系统地生成程序代码——例如,用于测试编译器或解释器。不出所料,我们使用 Python 和 Python 解释器作为我们的领域。">
在前面的章节中,我们讨论了几种模糊测试技术。知道该做什么很重要,但知道何时停止做某事也同样重要。在本章中,我们将学习何时停止模糊测试——并为此目的使用一个突出的例子:第二次世界大战中纳粹德国海军使用的恩尼格玛机来加密通信,以及艾伦·图灵和 I.J.古德如何使用模糊测试技术破解海军恩尼格玛机的密码。">
在关于语法的章节中,我们看到了如何使用语法进行非常有效和高效的测试。在本章中,我们将之前基于字符串的算法精炼为基于树的算法,这要快得多,并且允许对模糊输入的生产有更多的控制。">
在我们进入本书的核心部分之前,让我们介绍软件测试的基本概念。为什么需要测试软件?如何测试软件?如何判断测试是否成功?如何知道是否测试得足够?在本章中,让我们回顾最重要的概念,同时熟悉 Python 和交互式笔记本。">
在上一章中,我们介绍了基于变异的模糊测试技术,这是一种通过在给定输入上应用小变异来生成模糊输入的技术。在本章中,我们展示了如何引导这些变异以实现特定目标,例如覆盖率。本章中的算法源自流行的美国模糊跳蚤(AFL)模糊器,特别是其 AFLFast 和 AFLGo 版本。我们将探索 AFL 背后的灰盒模糊测试算法,以及我们如何利用它来解决自动化漏洞检测的各种问题。《灰盒模糊测试》(Greybox Fuzzing)
到目前为止,我们所看到的语法大多是手动指定的——也就是说,你必须(或者知道输入格式的人)首先设计和编写一个语法。虽然我们迄今为止看到的语法相对简单,但为复杂输入创建语法可能需要相当多的努力。因此,在本章中,我们介绍了从程序中自动挖掘语法的技巧——通过执行程序并观察它们如何处理输入的哪些部分。与语法模糊器结合使用,这使我们能够
-
选择一个程序,
-
提取其输入语法,
使用本书中的概念以高效率和效果进行模糊处理。">
程序的行为不仅受其数据控制。程序配置——即控制程序在其(常规)输入数据上执行设置的选项或配置文件——同样会影响行为,因此可以也应该进行测试。在本章中,我们探讨了如何系统地测试和覆盖软件配置。通过自动推断配置选项,我们可以直接应用这些技术,无需编写语法。最后,我们展示了如何系统地覆盖配置选项的组合,快速检测不希望出现的干扰。">
到目前为止,我们一直生成系统输入,即程序整体通过其输入通道获得的数据。如果我们只对测试一小组函数感兴趣,必须通过系统进行测试可能会非常低效。本章介绍了一种称为雕刻的技术,给定一个系统测试,自动提取一组单元测试,这些测试复制了系统测试期间看到的调用。关键思想是记录这些调用,以便我们可以在以后重新播放它们——整体或选择性地。此外,我们还探讨了如何从雕刻的单元测试中合成 API 语法;这意味着我们可以合成 API 测试,而无需编写任何语法。">
在本章中,我们探讨如何为图形用户界面(GUI)生成测试,从我们之前的 Web 测试示例中抽象出来。基于提取用户界面元素和激活它们的一般方法,我们的技术可以推广到任意图形用户界面,从富 Web 应用到移动应用,并通过表单和导航元素系统地探索用户界面。">
到目前为止,我们一直生成系统输入,即程序整体通过其输入通道获得的数据。然而,我们也可以生成直接进入单个函数的输入,在这个过程中获得灵活性和速度。在本章中,我们探讨使用语法来合成函数调用代码,这使得你可以生成非常高效地直接调用函数的程序代码。">
在本章中,我们介绍了对我们句法模糊技术的重要扩展,所有这些扩展都利用了现有输入的句法部分。">
在关于语法的章节中,我们讨论了语法如何被
用于表示各种语言。我们还看到了语法如何被用来
生成相应语言的字符串。语法还可以执行
反向操作。也就是说,给定一个字符串,可以将字符串分解为其组成部分
与生成它的语法中使用的语法部分相对应的组成部分
– 该字符串的推导树。这些部分(以及来自其他类似
字符串)可以在以后使用相同的语法重新组合以生成新的字符串。">
在本章中,我们展示了如何通过函数扩展语法 – 在语法扩展期间执行的代码片段,可以生成、检查或更改生成的元素。 向语法中添加函数允许进行非常灵活的测试生成,将语法生成和编程的最佳之处结合起来。">
通过构造,模糊器生成的输入可能难以阅读。 这会在调试期间造成问题,当人类需要分析失败的确切原因时。 在本章中,我们介绍了自动将导致失败的输入减少和简化的技术,以便于调试。">
在前面的章节中,我们看到了基于语法的模糊测试如何使我们能够高效地生成大量的语法有效输入。
然而,有一些语义输入特征无法用上下文无关文法表达,例如">
在本章中,我们探讨了如何为图形用户界面(GUIs),特别是网络界面生成测试。 我们设置了一个(有漏洞的)网络服务器,并展示了如何系统地探索其行为—— 首先使用手写的语法,然后使用从用户界面自动推断出的语法。 我们还展示了如何对这些服务器进行系统性的攻击,特别是使用代码和 SQL 注入。">
我们已经探讨了如何生成更好的输入,这些输入可以深入到所讨论的程序中。在这样做的时候,我们依赖于程序崩溃来告诉我们我们已经成功地在程序中找到了问题。然而,这相当简单。如果程序的行为只是不正确,但不会导致崩溃呢?能否做得更好?">
目录
第一部分:激发你的兴趣
在这部分,我们介绍了本书的主题。
本书之旅
这本书非常庞大。拥有超过 20,000 行代码和 150,000 字的文本,印刷版将覆盖超过 1,200 页的文本。显然,我们并不假设每个人都想阅读所有内容。
软件测试简介
在我们进入本书的核心部分之前,让我们介绍软件测试的基本概念。为什么需要测试软件?一个人如何测试软件?一个人如何知道测试是否成功?一个人如何知道是否测试得足够?在本章中,让我们回顾最重要的概念,同时熟悉 Python 和交互式笔记本。
第二部分:词汇模糊测试
这一部分介绍了词汇级别的测试生成,即字符序列的组成。
模糊测试:使用随机输入破坏事物
在本章中,我们将从最简单的测试生成技术开始。随机文本生成的关键思想,也称为模糊测试,是将一串随机字符输入到程序中,希望揭示出故障。
代码覆盖率
在上一章中,我们介绍了基本模糊测试——即生成随机输入以测试程序。我们如何衡量这些测试的有效性呢?一种方法就是检查找到的(数量和严重性)错误;但如果错误很少,我们需要一个测试发现错误的可能性的代理。在本章中,我们引入了代码覆盖率的概念,测量在测试运行期间程序的实际执行部分。测量这种覆盖率对于试图覆盖尽可能多代码的测试生成器来说也是至关重要的。
基于突变的模糊测试
大多数随机生成的输入在语法上是无效的,因此很快就会被处理程序拒绝。为了测试输入处理之外的功能,我们必须增加获得有效输入的机会。其中一种方法就是所谓的突变模糊测试——即对现有输入进行微小更改,这些更改可能仍然保持输入有效,但可以测试新的行为。我们展示了如何创建这样的突变,以及如何引导它们指向尚未发现的代码,应用来自流行的 AFL 模糊测试器的核心概念。
灰盒模糊测试
在上一章中,我们介绍了基于突变的模糊测试,这是一种通过在给定输入上应用小突变来生成模糊输入的技术。在本章中,我们展示了如何引导这些突变指向特定的目标,如覆盖率。本章中的算法源自流行的American Fuzzy Lop(AFL)模糊测试器,特别是其AFLFast和AFLGo版本。我们将探索 AFL 背后的灰盒模糊测试算法以及我们如何利用它来解决自动化漏洞检测的各种问题。
基于搜索的模糊测试
有时候,我们不仅对模糊测试尽可能多的不同程序输入感兴趣,还希望推导出特定的测试输入,以达到某些目标,例如到达程序中的特定语句。当我们有了我们想要寻找的东西的概念时,我们就可以搜索它了。搜索算法是计算机科学的核心,但将经典搜索算法如广度优先搜索或深度优先搜索应用于测试搜索是不切实际的,因为这些算法可能需要我们查看所有可能的输入。然而,领域知识可以用来克服这个问题。例如,如果我们能估计几个程序输入中哪一个更接近我们想要找的,那么这个信息可以引导我们更快地达到目标——这个信息被称为启发式。启发式系统地应用的方式被捕获在元启发式搜索算法中。"Meta"表示这些算法是通用的,并且可以根据不同的问题实例化。元启发式通常从自然界观察到的过程中获得灵感。例如,有一些算法模仿进化过程、群体智能或化学反应。总的来说,它们比穷举搜索方法更有效率,因此可以应用于广阔的搜索空间——对于它们来说,程序输入领域的广阔搜索空间不是问题。
突变分析
在关于覆盖率(Coverage)的章节中,我们展示了如何确定程序中哪些部分被执行,从而对一组测试用例在覆盖程序结构方面的有效性有一个概念。然而,覆盖率本身可能不是衡量测试有效性的最佳指标,因为一个人可以有很高的覆盖率,但从未检查结果是否正确。在这一章中,我们介绍了一种评估测试套件有效性的另一种方法:在代码中注入突变——人工故障后,我们检查测试套件是否可以检测到这些人工故障。这个想法是,如果它未能检测到这样的突变,它也会错过真实的错误。
第三部分:语法模糊测试
这一部分介绍了语法级别的测试生成,即从语言结构中组合输入。
使用语法进行模糊测试
在"基于突变的模糊测试"(Mutation-Based Fuzzing)这一章中,我们看到了如何使用额外的提示——例如样本输入文件——来加速测试生成。在这一章中,我们更进一步,通过提供程序合法输入的规范。通过语法指定输入允许非常系统和高效的测试生成,特别是对于复杂的输入格式。语法还作为配置模糊测试、API 模糊测试、GUI 模糊测试等的基础。
高效的语法模糊测试
在语法章节中,我们看到了如何使用语法进行非常有效和高效的测试。在本章中,我们将之前的基于字符串的算法精炼为基于树的算法,这要快得多,并允许对模糊输入的生产有更多的控制。
语法覆盖率
从语法生成输入为规则的每个可能扩展赋予相同的可能性。然而,为了生成全面的测试套件,最大化多样性更有意义——例如,不要反复重复相同的扩展。在本章中,我们探讨了如何系统地覆盖语法的元素,以最大化多样性,并确保不遗漏单个元素。
解析输入
在语法章节中,我们讨论了如何使用语法来表示各种语言。我们还看到了如何使用语法来生成对应语言的字符串。语法还可以执行相反的操作。也就是说,给定一个字符串,可以将该字符串分解为其组成部分,这些组成部分对应于用于生成它的语法的部分——该字符串的推导树。这些部分(以及来自其他类似字符串的部分)可以稍后使用相同的语法重新组合,以生成新的字符串。
概率语法模糊
让我们通过为单个扩展分配概率来赋予语法更多的能力。这允许我们控制应该生成多少个每个元素,从而允许我们将生成的测试针对特定的功能。我们还展示了如何从给定的样本输入中学习这样的概率,并具体地将测试指向这些样本中不常见的输入特征。
使用生成器进行模糊测试
在本章中,我们展示了如何通过函数扩展语法——这些代码在语法扩展期间执行,可以生成、检查或更改生成的元素。向语法添加函数允许进行非常灵活的测试生成,结合了语法生成和编程的最佳之处。
使用语法进行灰盒模糊测试
在本章中,我们介绍了对我们语法模糊技术的重要扩展,所有这些扩展都利用了现有输入的语法部分。
减少导致失败的输入
通过构造,模糊器创建的输入可能难以阅读。这会在调试期间造成问题,当人类需要分析失败的确切原因时。在本章中,我们介绍了将导致失败输入自动减少和简化的技术,以简化调试过程。
第四部分:语义模糊测试
本部分介绍了考虑输入语义的测试生成技术,特别是处理输入的程序的行为。
约束模糊测试
在前面的章节中,我们看到了基于语法的模糊测试如何使我们能够高效地生成大量的语法有效输入。然而,有一些语义输入特征无法在上下文无关语法中表达,例如
挖掘输入语法
到目前为止,我们所看到的语法大多是手动指定的——也就是说,你必须(或知道输入格式的人)首先设计和编写一个语法。虽然我们迄今为止看到的语法相当简单,但为复杂输入创建语法可能需要相当多的努力。因此,在这一章中,我们介绍了从程序中自动挖掘语法的技术——通过执行程序并观察它们如何处理输入的哪些部分。与语法模糊测试器结合使用,这使我们能够
-
取一个程序,
-
提取其输入语法,并且
-
使用本书中的概念,以高效率和有效性进行模糊测试。#### 跟踪信息流
我们已经探讨了如何生成更好的输入,这些输入可以深入到所讨论的程序中。在这样做的时候,我们依赖于程序崩溃来告诉我们我们已经成功地在程序中找到了问题。然而,这相当简单。如果程序的行为只是不正确,但不会导致崩溃呢?能否做得更好?
约束模糊测试
在信息流章节中,我们看到了如何使用动态污点来生成比仅仅寻找程序崩溃更智能的测试用例。我们也看到了如何使用污点来更新语法,从而更专注于危险的方法。
符号模糊测试
模糊测试的传统方法中存在的问题是,它们无法锻炼系统可能具有的所有可能行为,尤其是在输入空间很大时。很多时候,特定执行分支的执行可能只发生在非常特定的输入上,这可能只占输入空间的一小部分。传统的模糊测试方法依赖于机会来生成它们需要的输入。然而,当要探索的空间很大时,依赖于随机性来生成我们想要的值是一个坏主意。例如,一个接受字符串的函数,即使只考虑前 10 个字符,也有\(2^{80}\)种可能的输入。如果寻找特定的字符串,即使在超级计算机上,随机生成值也需要几千年。
挖掘函数规范
在测试程序时,不仅需要覆盖其多种行为;还需要检查结果是否符合预期。在本章中,我们介绍了一种技术,允许我们从一组给定的执行中挖掘函数规范,从而得到函数期望和提供的抽象和正式描述。
第五部分:特定领域模糊测试
这一部分讨论了针对多个特定领域的测试生成。对于所有这些领域,我们引入了模糊器,用于生成输入,以及挖掘器,用于分析输入结构。
测试配置
程序的行为不仅受其数据控制。程序配置——即通过选项或配置文件设置的,控制程序在其(常规)输入数据上执行设置的设置——同样会影响行为,因此可以也应该进行测试。在本章中,我们探讨了如何系统地测试和覆盖软件配置。通过自动推断配置选项,我们可以直接应用这些技术,无需编写语法。最后,我们展示了如何系统地覆盖配置选项的组合,快速检测不希望出现的干扰。
模糊测试 API
到目前为止,我们始终生成系统输入,即程序整体通过其输入通道获得的数据。然而,我们也可以生成直接进入单个函数的输入,从而在过程中获得灵活性和速度。在本章中,我们探讨了使用语法合成函数调用代码的使用,这允许你生成非常高效地直接调用函数的程序代码。
雕刻单元测试
到目前为止,我们始终生成系统输入,即程序整体通过其输入通道获得的数据。如果我们只对测试一小组函数感兴趣,通过系统进行测试可能非常低效。本章介绍了一种称为雕刻的技术,它给定一个系统测试,自动提取一组单元测试,这些测试复制了系统测试期间看到的调用。关键思想是记录这些调用,以便我们可以在以后回放它们——整体或选择性地。此外,我们还探讨了如何从雕刻的单元测试中合成 API 语法;这意味着我们可以合成 API 测试,而无需编写任何语法。
测试编译器
在本章中,我们将利用语法和基于语法的测试系统地生成程序代码——例如,测试编译器或解释器。不出所料,我们使用Python和Python 解释器作为我们的领域。
测试 Web 应用程序
在本章中,我们探讨如何为图形用户界面(GUI)生成测试,特别是在 Web 界面。我们设置了一个(有漏洞的)Web 服务器,并演示了如何系统地探索其行为——首先使用手写的语法,然后使用从用户界面自动推断出的语法。我们还展示了如何对这些服务器进行系统性的攻击,特别是使用代码和 SQL 注入。
测试图形用户界面
在本章中,我们探讨如何为图形用户界面(GUI)生成测试,从我们之前关于 Web 测试的示例中抽象出来。基于提取用户界面元素和激活它们的一般方法,我们的技术可以推广到任意图形用户界面,从富 Web 应用到移动应用,并通过表单和导航元素系统地探索用户界面。
第六部分:管理模糊测试
这一部分讨论了如何管理大规模的模糊测试。
大规模模糊测试
在过去的章节中,我们总是关注仅在一台机器上持续几秒钟的模糊测试。然而,在现实世界中,模糊测试是在数十台甚至数千台机器上运行的;持续数小时、数天甚至数周;针对一个程序或数十个程序。在这种情况下,需要一个基础设施来收集单个模糊测试运行中的失败数据,并将这些数据聚合到一个中央存储库中。在本章中,我们将检查这样一个基础设施,即 Mozilla 的FuzzManager框架。
何时停止模糊测试
在过去的章节中,我们讨论了几种模糊测试技术。知道做什么很重要,但知道何时停止做事情也同样重要。在本章中,我们将学习何时停止模糊测试——并使用一个突出的例子来说明这一点:在第二次世界大战中,纳粹德国海军使用的用于加密通信的恩尼格玛机器,以及 Alan Turing 和 I.J. Good 如何使用模糊测试技术来破解海军恩尼格玛机的密码。
附录
这一部分包含支持其他笔记本的笔记本和模块。
学术原型设计
这是 Andreas Zeller 在 ESEC/FSE 2022 会议上发表的“学术原型设计”教程的手稿。
使用 Python 进行原型设计
这是 Andreas Zeller 在 TAIC PART 2020 会议上发表的“几分钟内编写有效的测试工具”主题演讲的手稿。
错误处理
这个笔记本中的代码有助于处理错误。通常,笔记本代码中的错误会导致代码执行停止;而笔记本代码中的无限循环会导致笔记本无限运行。这个笔记本提供了两个类来帮助解决这些问题。
计时器
这个笔记本中的代码有助于测量时间。
超时
本笔记本中的代码有助于在给定时间后中断执行。
类图
这是一个简单的类图查看器。针对本书进行了定制。
铁路图
本笔记本中的代码有助于绘制语法图。它是Tab Atkins Jr.的优秀库的一个(略有定制)副本,不幸的是,它不是一个 Python 包。
控制流图
本笔记本中的代码有助于获取 Python 函数的控制流图。
本项目的内文内容根据Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议授权。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,根据MIT 许可协议授权。最后更改:2024-07-01 12:05:22+02:00 • 引用 • 版权信息
如何引用这篇作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler: "Fuzzing Book". In Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, and Christian Holler, "Fuzzing Book", www.fuzzingbook.org/html/00_Table_of_Contents.html. Retrieved 2024-07-01 12:05:22+02:00.
@incollection{fuzzingbook2024:00_Table_of_Contents,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {The Fuzzing Book},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/00_Table_of_Contents.html}},
note = {Retrieved 2024-07-01 12:05:22+02:00},
url = {https://www.fuzzingbook.org/html/00_Table_of_Contents.html},
urldate = {2024-07-01 12:05:22+02:00}
}
第一部分:激发你的兴趣
在这部分,我们介绍了本书的主题。
-
本书导览展示了本书的多个导览,介绍了讨论的不同技术——从最简单的随机输入形式到高端的符号推理。
-
软件测试入门介绍了软件测试的基础知识,并使你熟悉 Python 和交互式笔记本。
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。内容中包含的源代码,以及用于格式化和显示该内容的源代码,受MIT License许可。最后修改时间:2022-08-06 14:51:00+02:00。引用 • 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "第一部分:激发你的兴趣". 在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 的"模糊测试书"中,www.fuzzingbook.org/html/01_Intro.html。检索时间:2022-08-06 14:51:00+02:00。
@incollection{fuzzingbook2022:01_Intro,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Part I: Whetting Your Appetite},
year = {2022},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/01_Intro.html}},
note = {Retrieved 2022-08-06 14:51:00+02:00},
url = {https://www.fuzzingbook.org/html/01_Intro.html},
urldate = {2022-08-06 14:51:00+02:00}
}
书籍之旅
这本书非常庞大。拥有超过 20,000 行代码和 150,000 字的文本,印刷版将覆盖超过 1,200 页的文本。显然,我们不假设每个人都想阅读所有内容。
虽然这本书的章节可以依次阅读,但有许多可能的阅读路径。在这个图中,箭头 \(A \rightarrow B\) 表示章节 \(A\) 是章节 \(B\) 的先决条件。你可以选择任意路径来获取你最感兴趣的主题:
在本章中,我们将从最简单的测试生成技术开始。随机文本生成,也称为模糊测试,的关键思想是将一串随机字符输入到程序中,希望揭露故障。《模糊测试:破坏事物的方法》
在上一章中,我们介绍了基本的模糊测试——即生成随机输入来测试程序。如何衡量这些测试的有效性?一种方法就是检查找到的(数量和严重性)错误;但如果错误很少,我们需要一个代理来衡量测试揭露错误的概率。在本章中,我们引入了代码覆盖的概念,测量在测试运行期间程序的实际执行部分。测量这种覆盖率对于试图覆盖尽可能多代码的测试生成器来说也非常关键。《代码覆盖率》
有时我们不仅对模糊测试尽可能多的不同程序输入感兴趣,还希望推导出特定的测试输入,以实现某些目标,例如到达程序中的特定语句。当我们对我们要找的东西有一个想法时,我们就可以去寻找它。搜索算法是计算机科学的核心,但将经典搜索算法如广度优先搜索或深度优先搜索应用于测试搜索是不切实际的,因为这些算法可能需要我们查看所有可能的输入。然而,领域知识可以用来克服这个问题。例如,如果我们能估计几个程序输入中哪一个更接近我们正在寻找的,那么这些信息可以引导我们更快地达到目标——这种信息被称为启发式。启发式系统地应用的方式被元启发式搜索算法所捕捉。这里的“元”表示这些算法是通用的,并且可以根据不同的问题实例化。元启发式通常从自然界观察到的过程中获得灵感。例如,有一些算法模仿进化过程、群体智能或化学反应。总的来说,它们比穷举搜索方法更有效率,因此可以应用于广阔的搜索空间——对于它们来说,程序输入领域的广阔搜索空间不是问题。《基于搜索的模糊测试》(
在“基于变异的模糊测试”章节中,我们看到了如何使用额外的提示——例如样本输入文件——来加速测试生成。在本章中,我们将这一想法进一步发展,通过提供程序合法输入的规范。通过语法指定输入可以实现非常系统和高效的测试生成,尤其是在复杂的输入格式中。语法还作为配置模糊测试、API 模糊测试、GUI 模糊测试以及更多测试的基础。《使用语法进行模糊测试》(
传统模糊测试方法的一个问题是,它们无法测试系统可能具有的所有可能行为,尤其是在输入空间很大时。很多时候,特定执行分支的执行可能只发生在非常特定的输入下,这可能只占输入空间的一小部分。传统的模糊测试方法依赖于偶然来产生所需的输入。然而,当要探索的空间很大时,依赖于随机生成我们想要的值是一个坏主意。例如,一个接受字符串的函数,即使只考虑前 10 个字符,也有\(2^{80}\)种可能的输入。如果寻找特定的字符串,即使在超级计算机上,随机生成值也需要几千年。《符号模糊测试
在前面的章节中,我们总是只关注几秒钟内在一台机器上进行的模糊测试。然而,在现实世界中,模糊测试通常在数十台甚至数千台机器上运行;持续数小时、数天甚至数周;针对一个程序或数十个程序。在这种情况下,需要一个基础设施来收集单个模糊测试运行中的失败数据,并将这些数据汇总到一个中央存储库中。在本章中,我们将探讨这样一个基础设施,即来自 Mozilla 的 FuzzManager 框架。《大规模模糊测试
大多数随机生成的输入在语法上是无效的,因此很快就会被处理程序拒绝。为了在输入处理之外测试功能,我们必须增加获得有效输入的机会。其中一种方法就是所谓的突变模糊测试——即对现有输入进行微小修改,这些修改可能仍然保持输入的有效性,但会测试新的行为。我们展示了如何创建这样的突变,以及如何引导它们向尚未发现的代码,应用了流行的 AFL 模糊器中的核心概念。《基于突变的模糊测试》(Mutation-Based Fuzzing)
在关于覆盖率的一章中,我们展示了如何识别程序中哪些部分被执行,从而对一组测试用例覆盖程序结构的有效性有一个感性的认识。然而,覆盖率本身可能并不是衡量测试有效性的最佳指标,因为即使没有检查结果是否正确,也可能有很高的覆盖率。在这一章中,我们介绍了一种评估测试套件有效性的另一种方法:在代码中注入突变——人工错误后,我们检查测试套件是否能够检测到这些人工错误。其理念是,如果它未能检测到这样的突变,它也可能错过真实的错误。《突变分析》(Mutation Analysis)
从语法中生成输入使得一个规则的每个可能的扩展都有相同的可能性。然而,为了生成一个全面的测试套件,最大化多样性更有意义——例如,通过避免重复相同的扩展。在本章中,我们探讨了如何系统地覆盖语法的元素,以最大化多样性并确保不遗漏任何单个元素。《语法覆盖率》(Grammar Coverage)
让我们通过为单个扩展分配概率来赋予语法更多的能力。这使我们能够控制每种元素应该产生多少,从而允许我们将生成的测试针对特定的功能。我们还展示了如何从给定的样本输入中学习这样的概率,并特别将我们的测试针对这些样本中不常见的输入特征。《概率语法模糊测试》(Probabilistic Grammar Fuzzing)
在信息流章节中,我们看到了如何使用动态污点来生成比仅仅寻找程序崩溃更智能的测试用例。我们还看到了如何使用污点来更新语法,从而更专注于危险的方法。《动态不变量》(DynamicInvariants)
在测试一个程序时,不仅需要覆盖其多种行为;还需要检查结果是否符合预期。 在本章中,我们介绍了一种技术,使我们能够从一组给定的执行中挖掘函数规范,从而得到函数期望和提供的内容的抽象和形式描述。">
在本章中,我们将利用语法和基于语法的测试来系统地生成程序代码—— 例如,测试编译器或解释器。不出所料,我们使用 Python 和 Python 解释器作为我们的领域。">
在过去的章节中,我们讨论了几种模糊测试技术。 知道该做什么很重要,但知道何时停止做某事也同样重要。 在本章中,我们将学习何时停止模糊测试—— 并使用一个突出的例子来说明这一点:第二次世界大战期间纳粹德国海军使用的恩尼格玛机来加密通信,以及艾伦·图灵和 I.J.古德如何使用模糊测试技术破解海军恩尼格玛机的密码。">
在关于语法的章节中,我们看到了如何使用语法进行非常有效和高效的测试。 在本章中,我们将之前基于字符串的算法精炼为基于树的算法,这要快得多,并且允许对模糊输入的生产有更多的控制。《高效语法》模糊测试
在我们进入本书的核心部分之前,让我们介绍软件测试的基本概念。 为什么需要测试软件? 一个人如何测试软件? 一个人如何判断测试是否成功? 一个人如何知道是否测试得足够? 在本章中,让我们回顾最重要的概念,同时熟悉 Python 和交互式笔记本。《软件测试入门》
在上一章中,我们介绍了基于变异的模糊测试,这是一种通过在给定输入上应用小变异来生成模糊输入的技术。在本章中,我们展示了如何引导这些变异以实现特定的目标,如覆盖率。本章中的算法源自流行的美国模糊跳蚤(AFL)模糊器,特别是其 AFLFast 和 AFLGo 版本。我们将探索 AFL 背后的灰盒模糊测试算法,以及我们如何利用它来解决自动化漏洞检测的各种问题。《灰盒模糊测试》
到目前为止,我们看到的语法大多是手动指定的——也就是说,你必须(或者知道输入格式的人)首先设计和编写一个语法。 虽然我们迄今为止看到的语法相对简单,但为复杂输入创建语法可能需要相当多的努力。 因此,在本章中,我们介绍了从程序中自动挖掘语法的技巧——通过执行程序并观察它们如何处理输入的哪些部分。 结合语法模糊器,这使我们能够
-
选择一个程序,
-
提取其输入语法,
使用本书中的概念以高效率和效果进行模糊处理。">
程序的行为不仅受其数据控制。 程序的配置——即通过选项或配置文件设置的,控制程序在其(常规)输入数据上执行设置的设置——同样会影响行为,因此可以也应该进行测试。 在本章中,我们探讨如何系统地测试和覆盖软件配置。 通过自动推断配置选项,我们可以直接应用这些技术,无需编写语法。 最后,我们展示如何系统地覆盖配置选项的组合,快速检测不希望出现的干扰。">
到目前为止,我们总是生成系统输入,即程序整体通过其输入通道获得的数据。如果我们只对测试一小组函数感兴趣,必须通过系统进行测试可能会非常低效。本章介绍了一种称为雕刻的技术,它给定一个系统测试,自动提取一组单元测试,这些测试复制了系统测试期间看到的调用。关键思想是记录这些调用,以便我们可以在以后重新播放它们——整体或选择性地。此外,我们还探讨了如何从雕刻的单元测试中合成 API 语法;这意味着我们可以合成 API 测试而无需编写任何语法。《
在本章中,我们探讨如何为图形用户界面(GUI)生成测试,从我们之前关于 Web 测试的示例中抽象出来。基于提取用户界面元素和激活它们的一般方法,我们的技术可以推广到任意图形用户界面,从富 Web 应用到移动应用,并通过表单和导航元素系统地探索用户界面。《
到目前为止,我们总是生成系统输入,即程序整体通过其输入通道获得的数据。然而,我们也可以生成直接进入单个函数的输入,在这个过程中获得灵活性和速度。在本章中,我们探讨使用语法来合成函数调用代码的使用,这使得您可以生成非常高效地直接调用函数的程序代码。《
在本章中,我们介绍了对我们句法模糊测试技术的重要扩展,所有这些扩展都利用了现有输入的句法部分。《使用语法进行灰盒模糊测试》
在关于语法的章节中,我们讨论了语法如何
用于表示各种语言。我们还看到了语法如何
生成对应语言的字符串。语法还可以执行
反向。也就是说,给定一个字符串,可以将字符串分解为其
与生成它的语法中使用的部分相对应的组成部分
– 该字符串的推导树。这些部分(以及来自其他类似
字符串的部分)可以稍后使用相同的语法重新组合,以生成新的字符串。">
在本章中,我们展示了如何通过函数扩展语法——这些代码片段在语法扩展期间执行,可以生成、检查或更改生成的元素。向语法添加函数允许进行非常灵活的测试生成,将语法生成和编程的最佳之处结合起来。《使用生成器模糊测试》
通过构建,模糊器生成的输入可能难以阅读。这会在调试期间引起问题,当人类需要分析失败的确切原因时。在本章中,我们介绍了自动将导致失败的输入简化到最小以简化调试的技术。《减少失败-诱导输入》
在前面的章节中,我们看到了基于语法的模糊测试如何使我们能够高效地生成大量的语法有效输入。
然而,有一些语义输入特征无法在上下文无关语法中表达,例如《使用约束进行模糊测试》
在本章中,我们探讨了如何为图形用户界面(GUIs),特别是 Web 界面生成测试。我们设置了一个(有漏洞的)Web 服务器,并展示了如何系统地探索其行为——首先使用手写的语法,然后使用从用户界面自动推断出的语法。我们还展示了如何对这些服务器进行系统性的攻击,特别是使用代码和 SQL 注入。《测试 Web》
我们已经探讨了如何生成更好的输入,这些输入可以深入到要测试的程序中。在这样做的时候,我们依赖于程序崩溃来告诉我们我们已经成功地在程序中找到了问题。然而,这相当简单。如果程序的行为只是不正确,但不会导致崩溃呢?能否做得更好?">
但是,由于即使是这张地图也可能让人感到不知所措,这里有一些导览来帮助你开始。这些导览允许你根据你是程序员、学生还是研究人员,专注于特定的视图。
实用主义程序员导览
你有一个要测试的程序。你希望尽可能快地生成尽可能彻底的测试。你不太关心如何实现,但应该能完成任务。你想要学习如何使用事物。
-
从测试简介开始以获取基本概念。(你本来就会知道这些,但快速提醒一下也无妨)。
-
使用模糊器章节中的简单模糊器来测试你的程序与第一个随机输入。
-
从你的程序中获取覆盖率并使用覆盖率信息来引导测试生成以实现代码覆盖率。
-
为你的程序定义一个输入语法并使用这个语法彻底模糊测试你的程序,使用语法正确的输入。作为模糊器,我们推荐使用语法覆盖率模糊器,因为这确保了输入元素的覆盖率。
-
如果你想要对生成的输入有更多控制,考虑概率模糊测试和使用生成函数的模糊测试。
-
如果你想要部署大量模糊器,学习如何管理大量模糊器。
在每一章中,从“概述”部分开始;这些将快速介绍如何使用事物,并指向相关的使用示例。就这样吧。回去工作并享受吧!
按页导览
这些之旅是本书的组织方式。在阅读了测试简介以获得基本概念之后,你可以通过这些部分进行阅读:
-
词法之旅专注于词法测试生成技术,即逐字符和逐字节组合输入的技术。非常快速且健壮的技术,具有最小的偏差。
-
语法之旅专注于语法作为指定输入语法的手段。生成的测试生成器产生语法正确的输入,使测试更快,并为测试者提供大量控制机制。
-
语义之旅利用代码语义来塑造和引导测试生成。高级技术包括提取输入语法、挖掘函数规范和符号约束求解,以覆盖尽可能多的代码路径。
-
应用之旅将早期部分定义的技术应用于 Web 服务器、用户界面、API 或配置等领域。
-
管理之旅最终关注如何处理和组织大量测试生成器,以及何时停止模糊测试。
大多数这些章节都从“概要”部分开始,解释如何使用最重要的概念。你可以选择是否想要一个“使用”视角(那么只需阅读概要)或一个“理解”视角(那么继续阅读)。
本科生之旅
你是计算机科学和/或软件工程的学生。你想要了解测试和相关领域的基础知识。除了仅仅使用技术,你还想深入挖掘算法和实现。以下是我们为你推荐的:
-
从测试简介和覆盖率开始,以获得基本概念。(你可能已经了解其中的一些,但嘿,你是个学生,对吧?)
-
从模糊器章节学习简单的模糊器是如何工作的。这已经为你提供了在 90 年代摧毁了 30%的 UNIX 工具的工具。如果你测试一个从未被模糊测试过的工具会发生什么?
-
基于变异的模糊测试是当今模糊测试的标准:取一组种子,并对其进行变异,直到找到错误。
-
学习如何使用语法生成语法正确的输入。这使得测试生成更加高效,但首先你必须编写(或挖掘)一个语法。
-
学习如何 fuzz API 和图形用户界面。这两个都是软件测试生成的重要领域。
-
学习如何自动将导致失败的输入减少到最小。这对于调试来说是一个巨大的节省时间,尤其是在与自动化测试结合使用时。
对于所有这些章节,请尝试实验实现,以理解其概念。请随意进行实验。
如果你是一名教师,上述章节可以在编程和/或软件工程课程中派上用场。利用幻灯片和/或现场编程,让学生们完成练习。
研究生之旅
在“本科生”之旅的基础上,你希望更深入地了解测试生成技术,包括更复杂的技术。
-
基于搜索的测试 允许你引导测试生成向特定目标发展,例如代码覆盖率。稳健且高效。
-
了解配置测试的介绍。如何测试和覆盖带有多个配置选项的系统?
-
变异分析 将合成缺陷(变异)种入程序代码,以检查测试是否能够找到它们。如果测试没有找到变异,它们可能也不会找到真正的错误。
-
学习如何解析输入 使用语法。如果你想分析、分解、变异现有输入,你需要一个解析器。
-
Concolic 和 符号 模糊测试 通过解决程序路径上的约束来达到难以测试的代码。在可靠性至关重要的地方使用;也是一个热门的研究课题。
-
学习如何估计何时停止模糊测试。总得在某处停下来,对吧?
对于所有这些章节,请尝试实验代码;自由地创建你自己的变体和扩展。这就是我们如何进行研究的!
如果你是一名教师,上述章节可以在软件工程和测试的高级课程中派上用场。再次强调,你可以利用幻灯片和/或现场编程,让学生们完成练习。
黑盒之旅
这次之旅专注于 黑盒模糊测试 ——也就是说,不需要来自被测试程序的反馈的技术。看看
-
基本模糊测试。这已经为你提供了在 90 年代摧毁了 30%的 UNIX 工具的工具。如果你测试一个以前从未进行过模糊测试的工具会发生什么呢?
-
语法模糊测试 专注于 语法 作为指定输入语法的手段。生成的测试生成器产生语法正确的输入,使测试更快,并为测试者提供大量的控制机制。
-
语义模糊测试 将 约束 附加到语法上,使得输入不仅语法有效,而且 语义 也有效——并赋予你塑造测试输入的能力,就像你希望的那样,
-
特定领域模糊测试 展示了这些技术的多种应用,从配置到图形用户界面。
-
如果你想部署大量模糊测试器,学习如何管理大量模糊测试器。
白盒之旅
这次游览专注于白盒模糊测试——即利用被测试程序反馈的技术。看看
-
覆盖率 了解覆盖率的基本概念以及如何为 Python 测量它。
-
基于变异的模糊测试 在今天的模糊测试中几乎是标准:取一组种子,并变异它们,直到找到错误。
-
灰盒模糊测试 使用来自流行的美国模糊跳蚤(AFL)模糊测试器的算法。
-
信息流 和 约束性模糊测试 展示了如何在 Python 程序中捕获信息流以及如何利用它来生成更智能的测试用例。
-
符号模糊测试,在不执行程序的情况下推理程序的行为。
研究者游览
在“研究生”游览之上,你正在寻找介于实验室阶段和广泛应用之间的技术——特别是,仍有大量改进空间的技术。如果你在寻找研究想法,就选择这些主题。
-
挖掘函数规范 是研究中的一个热门话题:给定一个函数,我们如何推断一个描述其行为的抽象模型?与测试生成相结合在这里提供了几个机会,特别是对于动态规范挖掘。
-
挖掘输入语法 承诺将词汇模糊测试的鲁棒性和易用性与语法模糊测试的效率和速度相结合。想法是从程序中自动挖掘输入语法,然后作为语法模糊测试的基础。仍处于早期阶段,但潜力巨大。
-
概率语法模糊测试 为程序员提供了对哪些元素应该生成的很多控制。在本章概述的概率模糊测试和从给定测试中挖掘数据交叉处有许多研究可能性。
-
使用生成器的模糊测试 和 使用约束的模糊测试 为程序员提供了对输入生成的最终控制权,即通过允许他们定义自己的生成器函数或定义自己的输入约束。最大的挑战是:如何在最小的上下文约束下最大限度地利用语法描述的威力?
-
切割单元测试 通过从仅重放单个函数调用(可能带有新生成的参数)的程序执行中提取单元测试,承诺可以显著加快测试执行(和生成)。在 Python 中,切割简单易行;这里有很多可以玩的空间。
-
测试 Web 服务器和图形用户界面是一个热门的研究领域,由实践者测试和保障其接口的需求(以及其他实践者突破这些接口的需求)推动。同样,这里还有许多未探索的潜力。
-
灰盒模糊测试和基于语法的灰盒模糊测试 引入了统计估计器,以引导测试生成向最有可能发现新漏洞的输入和输入属性。测试、程序分析和统计学的交汇处为未来的研究提供了许多可能性。
对于所有这些主题,拥有实现并展示这些概念的 Python 源代码是一个重要的资产。你可以轻松地用你自己的想法扩展实现,并在笔记本中直接运行评估。一旦你的方法稳定,考虑将其移植到具有更广泛可用主题的语言(例如 C 语言)。
作者之旅
这是一次终极之旅——你已经学到了所有内容,并希望为这本书做出贡献。那么,你应该再读两章:
-
作者指南介绍了如何为这本书做出贡献(编码风格、写作风格、约定等)。
-
模板章节是您章节的蓝图。
如果你想做出贡献,请随时联系我们——最好在写作之前,但写作之后也可以。我们将很高兴将你的材料纳入其中。
经验教训
-
您可以从头到尾阅读这本书...
-
...但根据你的需求和资源,遵循一个特定的路线可能更合适。
-
现在去探索生成软件测试!
本项目的内容根据Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议授权。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,根据MIT 许可协议授权。最后修改:2024-06-30 18:32:41+02:00 • 引用 • 版权信息
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "书中的之旅". 在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 的 "模糊测试书", www.fuzzingbook.org/html/Tours.html. Retrieved 2024-06-30 18:32:41+02:00.
@incollection{fuzzingbook2024:Tours,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Tours through the Book},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Tours.html}},
note = {Retrieved 2024-06-30 18:32:41+02:00},
url = {https://www.fuzzingbook.org/html/Tours.html},
urldate = {2024-06-30 18:32:41+02:00}
}
软件测试简介
在我们进入本书的核心部分之前,让我们先介绍软件测试的基本概念。为什么需要测试软件呢?一个人该如何测试软件?一个人如何判断一个测试是否成功?一个人如何知道是否测试得足够了?在这一章中,让我们回顾最重要的概念,同时熟悉 Python 和交互式笔记本。
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('C8_pjdl7pK0')
本章(以及本书)并不打算取代测试方面的教科书;请参阅结尾处的背景,以获取推荐的阅读材料。
简单测试
让我们从简单的例子开始。你的同事被要求实现一个平方根函数 \(\sqrt{x}\)。(让我们暂时假设环境中还没有这样的函数。)在研究了牛顿-拉夫森方法之后,她提出了以下 Python 代码,声称实际上这个my_sqrt()函数可以计算平方根。
def my_sqrt(x):
"""Computes the square root of x, using the Newton-Raphson method"""
approx = None
guess = x / 2
while approx != guess:
approx = guess
guess = (approx + x / approx) / 2
return approx
你的任务是找出这个函数是否真的做了它声称要做的事情。
理解 Python 程序
如果你刚开始接触 Python,你可能首先需要理解上述代码的功能。我们非常推荐阅读Python 教程,以了解 Python 是如何工作的。理解上述代码最重要的三件事是这些:
-
Python 通过缩进来构建程序结构,因此函数和
while循环体是通过缩进来定义的; -
Python 是动态类型化的,这意味着变量如
x、approx或guess的类型是在运行时确定的。 -
Python 的大多数语法特性都受到了其他常见语言的影响,例如控制结构(
while、if)、赋值(=)或比较(==、!=、<)。
有了这些,你就可以理解上述代码的功能了:从一个guess值x / 2开始,它在approx中计算越来越好的近似值,直到approx的值不再改变。这就是最终返回的值。
运行一个函数
为了找出my_sqrt()函数是否工作正确,我们可以用几个值来测试它。例如,对于x = 4,它会产生正确的值:
my_sqrt(4)
2.0
上面的my_sqrt(4)部分(所谓的单元)是 Python 解释器的输入,默认情况下会评估它。下面的部分(2.0)是它的输出。我们可以看到my_sqrt(4)产生了正确的值。
对于x = 2.0,情况似乎也是一样:
my_sqrt(2)
1.414213562373095
与笔记本交互
如果你在这个交互式笔记本中阅读,你可以尝试用其他值来测试my_sqrt()。点击上面带有my_sqrt()调用的其中一个单元格,并更改其值——比如说,改为my_sqrt(1)。按Shift+Enter(或点击播放符号)来执行它并查看结果。如果你得到一个错误信息,请转到上面定义my_sqrt()的单元格并首先执行这个操作。你也可以一次性运行所有单元格;有关详细信息,请查看笔记本菜单。(实际上,你也可以通过点击来更改文本,并纠正如这句话中的错误。)
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import quiz
习题
my_sqrt(16)会产生什么结果?
通过取消以下行的注释并执行它来亲自尝试:
# my_sqrt(16)
执行单个单元格不会执行其他单元格,所以如果你的单元格依赖于另一个单元格中的定义,而你尚未执行该定义,你会得到一个错误。你可以从菜单中选择运行所有单元格以上来确保所有定义都已设置。
还要注意,除非被覆盖,否则所有定义都会在执行之间保留。有时,因此重启内核(即从头开始启动 Python 解释器)以消除旧的、多余的定义是有帮助的。
调试一个函数
要了解my_sqrt()是如何工作的,一个简单的策略是在关键位置插入print()语句。例如,你可以记录approx的值,以查看每次循环迭代如何逐渐接近实际值:
def my_sqrt_with_log(x):
"""Computes the square root of x, using the Newton–Raphson method"""
approx = None
guess = x / 2
while approx != guess:
print("approx =", approx) # <-- New
approx = guess
guess = (approx + x / approx) / 2
return approx
my_sqrt_with_log(9)
approx = None
approx = 4.5
approx = 3.25
approx = 3.0096153846153846
approx = 3.000015360039322
approx = 3.0000000000393214
3.0
交互式笔记本还允许启动一个交互式调试器——在单元格顶部插入一个“魔法行”%%debug并查看会发生什么。不幸的是,交互式调试器会干扰我们的动态分析技术,所以我们主要使用日志和断言进行调试。
检查一个函数
让我们回到测试。我们可以读取并运行代码,但上述my_sqrt(2)的值实际上是否正确?我们可以通过利用\(\sqrt{x}\)平方再次必须是\(x\)来轻松验证,换句话说,\(\sqrt{x} \times \sqrt{x} = x\)。让我们看看:
my_sqrt(2) * my_sqrt(2)
1.9999999999999996
好吧,我们确实有一些舍入误差,但除此之外,这似乎很正常。
我们现在所做的是测试上述程序:我们在给定的输入上执行它,并检查其结果是否正确。这种测试是在程序投入生产前的质量保证的最基本要求。
自动化测试执行
到目前为止,我们都是手动测试上述程序,即手动运行它并手动检查其结果。这是一种非常灵活的测试方式,但从长远来看,它相当低效:
-
手动地,你只能检查非常有限数量的执行及其结果
-
在对程序进行任何更改后,你必须重复测试过程
这就是为什么自动化测试非常有用。一个简单的方法是让计算机首先进行计算,然后让它检查结果。
例如,这段代码会自动测试\(\sqrt{4} = 2\)是否成立:
result = my_sqrt(4)
expected_result = 2.0
if result == expected_result:
print("Test passed")
else:
print("Test failed")
Test passed
这个测试的好处是我们可以反复运行它,从而确保至少 4 的平方根被正确计算。但仍然有一些问题:
-
我们需要一个测试的单行代码
-
我们并不关心舍入误差
-
我们只检查单个输入(和单个结果)
让我们逐一解决这些问题。首先,让我们使测试更加紧凑。几乎所有的编程语言都有一种方法来自动检查条件是否成立,如果不成立则停止执行。这被称为 断言,它在测试中非常有用。
在 Python 中,assert 语句接受一个条件,如果条件为真,则不发生任何操作。(如果一切按预期进行,你就不应该被打扰。)但是,如果条件评估为假,则 assert 会引发异常,表明测试刚刚失败。
在我们的例子中,我们可以使用 assert 来轻松检查 my_sqrt() 是否产生如上所述的预期结果:
assert my_sqrt(4) == 2
当你执行这一行代码时,没有任何操作:我们只是展示了(或断言)我们的实现确实产生了 \(\sqrt{4} = 2\)。
但是,记住,浮点数计算可能会引入舍入误差。因此,我们不能简单地比较两个浮点数的相等性;相反,我们需要确保它们之间的绝对差值保持在某个特定的阈值以下,通常表示为 \(\epsilon\) 或 epsilon。这就是我们如何做到这一点:
EPSILON = 1e-8
assert abs(my_sqrt(4) - 2) < EPSILON
我们还可以为此引入一个特殊函数,并现在对具体值进行更多测试:
def assertEquals(x, y, epsilon=1e-8):
assert abs(x - y) < epsilon
assertEquals(my_sqrt(4), 2)
assertEquals(my_sqrt(9), 3)
assertEquals(my_sqrt(100), 10)
看起来是有效的,对吧?如果我们知道计算的预期结果,我们可以反复使用这样的断言来确保我们的程序正确工作。
(提示:真正的 Python 程序员会使用函数 math.isclose() 来代替。)
生成测试
记住,性质 \(\sqrt{x} \times \sqrt{x} = x\) 在普遍情况下是成立的?我们也可以用几个值显式地测试这一点:
assertEquals(my_sqrt(2) * my_sqrt(2), 2)
assertEquals(my_sqrt(3) * my_sqrt(3), 3)
assertEquals(my_sqrt(42.11) * my_sqrt(42.11), 42.11)
仍然看起来是有效的,对吧?最重要的是,\(\sqrt{x} \times \sqrt{x} = x\) 是我们可以很容易地测试成千上万个值的东西:
for n in range(1, 1000):
assertEquals(my_sqrt(n) * my_sqrt(n), n)
使用 100 个值测试 my_sqrt() 需要多少时间?让我们看看。
我们使用自己的 Timer 模块来测量经过的时间。为了能够使用 Timer,我们首先导入我们的实用模块,这允许我们导入其他笔记本。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
from Timer import Timer
with Timer() as t:
for n in range(1, 10000):
assertEquals(my_sqrt(n) * my_sqrt(n), n)
print(t.elapsed_time())
0.013280000013764948
10,000 个值大约需要百分之一秒,所以 my_sqrt() 的单次执行需要 1/1000000 秒,或者说大约 1 微秒。
让我们用随机选择的 10,000 个值重复这个过程。Python 的 random.random() 函数返回一个介于 0.0 和 1.0 之间的随机值:
import [random](https://docs.python.org/3/library/random.html)
with Timer() as t:
for i in range(10000):
x = 1 + random.random() * 1000000
assertEquals(my_sqrt(x) * my_sqrt(x), x)
print(t.elapsed_time())
0.015275916957762092
在一秒钟内,我们已经测试了 10,000 个随机值,每次计算平方根都是正确的。我们可以对my_sqrt()的每一次更改重复这个测试,每次都增强我们对my_sqrt()按预期工作的信心。不过,请注意,虽然随机函数在产生随机值时是无偏的,但它不太可能生成会剧烈改变程序行为的特殊值。我们将在下面进一步讨论这个问题。
运行时验证
我们不仅可以为my_sqrt()编写和运行测试,还可以将检查直接集成到实现中。这样,每次调用my_sqrt()都将自动进行检查。
这样的自动运行时检查非常容易实现:
def my_sqrt_checked(x):
root = my_sqrt(x)
assertEquals(root * root, x)
return root
现在,每次我们用my_sqrt_checked()计算根时\(\dots\)
my_sqrt_checked(2.0)
1.414213562373095
...我们已经知道结果是正确的,并且对于每一次新的成功计算也将如此。
自动运行时检查,如上所述,假设了两件事:
-
必须能够制定这样的运行时检查。总是应该有具体的值来检查,但以抽象的方式制定所需属性可能非常复杂。在实践中,你需要决定哪些属性最为关键,并为它们设计适当的检查。此外,运行时检查可能不仅取决于局部属性,还取决于程序状态的多个属性,所有这些属性都必须被识别。
-
必须能够承担这样的运行时检查。在
my_sqrt()的情况下,检查并不昂贵;但如果我们必须检查,比如说,在简单操作之后的大型数据结构,检查的成本可能会很快变得难以承受。在实践中,运行时检查通常在生产过程中被禁用,以效率换取可靠性。另一方面,一套全面的运行时检查是发现错误并快速调试它们的极好方式;你需要决定在生产过程中你仍然需要多少这样的功能。
问答
运行时检查能保证总是会有正确的结果吗?
运行时检查的一个重要限制是,它们只能确保如果存在结果需要检查时的正确性——也就是说,它们不能保证总是会有一个结果。与符号验证技术和程序证明相比,这是一个重要的限制,后者也可以保证存在结果——尽管需要付出更高的(通常是手动)努力。
系统输入与函数输入
在这一点上,我们可以将my_sqrt()提供给其他程序员,他们可以将它嵌入到他们的代码中。在某个时候,它将不得不处理来自第三方的输入,即程序员无法控制的输入。
让我们通过假设一个程序 sqrt_program() 来模拟这个系统输入,其输入是由第三方控制的字符串:
def sqrt_program(arg: str) -> None:
x = int(arg)
print('The root of', x, 'is', my_sqrt(x))
我们假设sqrt_program是一个程序,它从命令行接受系统输入,如下所示:
$ sqrt_program 4
2
我们可以很容易地用一些系统输入调用sqrt_program():
sqrt_program("4")
The root of 4 is 2.0
问题是什么?问题是我们没有检查外部输入的有效性。尝试调用sqrt_program(-1),例如。会发生什么?
事实上,如果你用一个负数调用my_sqrt(),它会进入一个无限循环。由于技术原因,我们在这个章节中不能有无限循环(除非我们想让代码永远运行);所以我们使用一个特殊的with ExpectTimeOut(1)结构在一秒后中断执行。
from ExpectError import ExpectTimeout
with ExpectTimeout(1):
sqrt_program("-1")
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/1288144681.py", line 2, in <module>
sqrt_program("-1")
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/449782637.py", line 3, in sqrt_program
print('The root of', x, 'is', my_sqrt(x))
^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/2661069967.py", line 5, in my_sqrt
while approx != guess:
^^^^^^^^^^^^^^^
File "Timeout.ipynb", line 43, in timeout_handler
raise TimeoutError()
TimeoutError (expected)
上述信息是一个错误信息,表示出现了问题。它列出了在错误发生时活跃的函数和行号的调用栈。最底部的行是最后执行的行;上面的行代表函数调用——在我们的例子中,直到my_sqrt(x)。
我们不希望我们的代码因为异常而终止。因此,在接收外部输入时,我们必须确保它得到了适当的验证。例如,我们可以这样写:
def sqrt_program(arg: str) -> None:
x = int(arg)
if x < 0:
print("Illegal Input")
else:
print('The root of', x, 'is', my_sqrt(x))
然后我们可以确信my_sqrt()是按照其规范调用的。
sqrt_program("-1")
Illegal Input
但等等!如果sqrt_program()没有用数字调用会发生什么?
习题
sqrt_program('xyzzy')的结果是什么?
让我们试试看!当我们尝试转换一个非数字字符串时,这也会导致运行时错误:
from ExpectError import ExpectError
with ExpectError():
sqrt_program("xyzzy")
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/1336991207.py", line 2, in <module>
sqrt_program("xyzzy")
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/3211514011.py", line 2, in sqrt_program
x = int(arg)
^^^^^^^^
ValueError: invalid literal for int() with base 10: 'xyzzy' (expected)
这里有一个版本,它也检查了不良输入:
def sqrt_program(arg: str) -> None:
try:
x = float(arg)
except ValueError:
print("Illegal Input")
else:
if x < 0:
print("Illegal Number")
else:
print('The root of', x, 'is', my_sqrt(x))
sqrt_program("4")
The root of 4.0 is 2.0
sqrt_program("-1")
Illegal Number
sqrt_program("xyzzy")
Illegal Input
我们现在已经看到,在系统层面,程序必须能够优雅地处理任何类型的输入,而不会进入不受控制的状态。这当然给程序员带来了负担,他们必须努力使他们的程序在各种情况下都健壮。然而,当生成软件测试时,这种负担却变成了好处:如果一个程序可以处理任何类型的输入(可能带有定义良好的错误信息),我们也可以发送任何类型的输入。但是,在用生成的值调用函数时,我们必须知道其精确的先决条件。
测试的局限性
尽管我们在测试中尽了最大努力,但请记住,你总是在检查一个有限的输入集的功能。因此,可能始终存在未测试的输入,对于这些输入,函数可能仍然会失败。
在my_sqrt()的情况下,例如,计算\(\sqrt{0}\)会导致除以零:
with ExpectError():
root = my_sqrt(0)
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/820411145.py", line 2, in <module>
root = my_sqrt(0)
^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/2661069967.py", line 7, in my_sqrt
guess = (approx + x / approx) / 2
~~^~~~~~~~
ZeroDivisionError: float division by zero (expected)
在我们迄今为止的测试中,我们没有检查这个条件,这意味着基于\(\sqrt{0} = 0\)的程序会意外地失败。但即使我们将随机生成器的输入范围设置为 0–1000000 而不是 1–1000000,随机产生零值的概率仍然是一百万分之一。如果一个函数对少数几个个别值的行为有根本性的不同,普通的随机测试很少有机会产生这些值。
当然,我们可以相应地修复函数,记录x接受的值,并处理特殊情况x = 0:
def my_sqrt_fixed(x):
assert 0 <= x
if x == 0:
return 0
return my_sqrt(x)
这样,我们现在可以正确地计算\(\sqrt{0} = 0\):
assert my_sqrt_fixed(0) == 0
非法值现在会导致异常:
with ExpectError():
root = my_sqrt_fixed(-1)
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/305965227.py", line 2, in <module>
root = my_sqrt_fixed(-1)
^^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1231/3001478627.py", line 2, in my_sqrt_fixed
assert 0 <= x
^^^^^^
AssertionError (expected)
然而,我们必须记住,尽管广泛的测试可能会让我们对程序的正确性有很高的信心,但它并不能保证所有未来的执行都将正确。即使是运行时验证,它检查每一个结果,也只能保证如果它产生了结果,那么结果将是正确的;但无法保证未来的执行不会导致失败的检查。当我写这篇文章的时候,我相信my_sqrt_fixed(x)是所有有限数字\(x\)的\(\sqrt{x}\)的正确实现,但我不能确定。
使用牛顿-拉夫森方法,我们仍然有很好的机会实际上证明实现是正确的:实现简单,数学理解得很好。遗憾的是,这种情况只适用于少数领域。如果我们不想进行完整的正确性证明,我们在测试中最好的机会是
-
在几个精心选择的输入上测试程序;并且
-
详尽且自动地检查结果。
这就是我们在这门课程的剩余部分要做的:设计帮助我们彻底测试程序的技术,以及帮助我们检查其状态是否正确的技术。享受吧!
经验教训
-
测试的目标是执行一个程序,以便我们发现错误。
-
测试执行、测试生成和检查测试结果可以自动化。
-
测试是不完整的;它不能提供 100%的保证代码没有错误。
下一步
从这里,你可以继续学习如何
- 使用fuzzing测试具有随机输入的程序
享受阅读!
背景
关于软件测试与分析有许多研究。
-
一本全新的现代、全面和在线的测试教科书是"有效的软件测试:开发者指南" [Maurício Aniche,2022]。强烈推荐!
-
对于这本书,我们也很乐意推荐“软件测试与分析”[Pezzè et al,2008]作为该领域的入门书籍;其强大的技术重点非常适合我们的方法。
-
其他一些重要的必读之作,包括心理学和组织,对软件测试有全面的方法,包括“软件测试的艺术”[Myers et al,2004]以及“软件测试技术”[Beizer et al,1990]。
练习
练习 1:熟悉笔记本和 Python
你在这本书中的第一个练习是熟悉笔记本和 Python,这样你就可以运行书中的代码示例——并尝试你自己的。以下是一些帮助你开始的任务。
初级水平:在浏览器中运行笔记本
获取代码的最简单方法是在浏览器中运行它们。
-
从网页中,查看顶部的菜单。选择
资源\(\rightarrow\)作为笔记本编辑。 -
短暂等待后,这将直接在你的浏览器中打开一个 Jupyter Notebook,其中包含当前章节作为笔记本。
-
你可以再次滚动浏览材料,但你可以点击任何代码示例来编辑并运行其代码(通过输入
Shift+Return)。你可以随意编辑示例。 -
注意,代码示例通常依赖于早期代码,所以请确保先运行前面的代码。
-
你所做的任何更改都不会被保存(除非你将笔记本保存到磁盘)。
关于 Jupyter Notebook 的帮助,从网页中,查看帮助菜单。
高级水平:在你的机器上运行 Python 代码
如果你想要做出更大的更改,但不想使用 Jupyter,这将很有用。
-
从网页中,查看顶部的菜单。选择
资源\(\rightarrow\)下载代码。 -
这将下载该章节的 Python 代码作为一个单独的 Python .py 文件,你可以将其保存到你的电脑上。
-
然后,你可以打开文件,在你的首选 Python 环境中编辑并运行它以重新运行示例。
-
最重要的是,你可以导入它到你的代码中并重用函数、类和其他资源。
关于 Python 的帮助,从网页中,查看帮助菜单。
高级水平:在你的机器上运行笔记本
如果你想在你的机器上使用 Jupyter,这将很有用。这将允许你运行更复杂的示例,例如带有图形输出的示例。
-
从网页中,查看顶部的菜单。选择
资源\(\rightarrow\)所有笔记本。 -
这将下载所有 Jupyter Notebook 作为一个
.ipynb文件的集合,你可以将其保存到你的电脑上。 -
然后,你可以在 Jupyter Notebook 或 Jupyter Lab 中打开这些笔记本,编辑并运行它们。要导航到其他笔记本,请打开笔记本
00_ 目录.ipynb。 -
你也可以使用
选择资源\(\rightarrow\)下载笔记本来下载单个笔记本。但是,运行这些笔记本需要你已经下载了其他笔记本。
关于 Jupyter Notebook 的帮助,从网页中,查看帮助菜单。
老板级别:做出贡献!
如果你想要通过补丁或其他材料为本书做出贡献,这将很有用。它还让你可以访问本书的最新版本。
-
从网页中,查看顶部的菜单。选择
资源\(\rightarrow\)GitHub 仓库。 -
这将带你去包含本书所有源代码的 GitHub 仓库,包括最新的笔记本。
-
然后,你可以将此仓库克隆到你的磁盘上,这样你就能获得最新和最好的版本。
-
你可以在 GitHub 页面上报告问题并提出拉取请求。
-
使用
git pull更新仓库将使您获得更新。
如果您想贡献代码或文本,请查看作者指南。
练习 2:测试 Shellsort
考虑以下 Shellsort 函数的实现,它接受一个元素列表并(可能)对其进行排序。
def shellsort(elems):
sorted_elems = elems.copy()
gaps = [701, 301, 132, 57, 23, 10, 4, 1]
for gap in gaps:
for i in range(gap, len(sorted_elems)):
temp = sorted_elems[i]
j = i
while j >= gap and sorted_elems[j - gap] > temp:
sorted_elems[j] = sorted_elems[j - gap]
j -= gap
sorted_elems[j] = temp
return sorted_elems
第一次测试表明 shellsort() 实际上可能工作:
shellsort([3, 2, 1])
[1, 2, 3]
实现使用一个 列表 作为参数 elems(它将其复制到 sorted_elems)以及用于固定列表 gaps。列表在其他语言中像 数组 一样工作:
a = [5, 6, 99, 7]
print("First element:", a[0], "length:", len(a))
First element: 5 length: 4
range() 函数返回一个包含元素的可迭代列表。它通常与 for 循环一起使用,如上述实现所示。
for x in range(1, 5):
print(x)
1
2
3
4
第一部分:手动测试用例
您现在的任务是彻底测试 shellsort() 的各种输入。
首先,设置带有多个手动编写的测试用例的 assert 语句。选择测试用例以确保覆盖极端情况。使用 == 比较两个列表。
使用笔记本来练习习题并查看解决方案。
第二部分:随机输入
第二次,创建随机列表作为 shellsort() 的参数。利用以下辅助谓词来检查结果是否(a)已排序,以及(b)是否是原始列表的排列。
def is_sorted(elems):
return all(elems[i] <= elems[i + 1] for i in range(len(elems) - 1))
is_sorted([3, 5, 9])
True
def is_permutation(a, b):
return len(a) == len(b) and all(a.count(elem) == b.count(elem) for elem in a)
is_permutation([3, 2, 1], [1, 3, 2])
True
从一个随机列表生成器开始,使用 [] 作为空列表,并使用 elems.append(x) 将元素 x 添加到列表 elems 中。使用上述辅助函数来评估结果。生成并测试 1,000 个列表。
使用笔记本来练习习题并查看解决方案。
练习 3:二次方程求解器
给定一个方程 \(ax² + bx + c = 0\),我们希望找到 \(x\) 的解,给定 \(a\)、\(b\) 和 \(c\) 的值。以下代码应该完成这个任务,使用方程 $$ x = \frac{-b \pm \sqrt{b² - 4ac}}{2a} $$
def quadratic_solver(a, b, c):
q = b * b - 4 * a * c
solution_1 = (-b + my_sqrt_fixed(q)) / (2 * a)
solution_2 = (-b - my_sqrt_fixed(q)) / (2 * a)
return (solution_1, solution_2)
quadratic_solver(3, 4, 1)
(-0.3333333333333333, -1.0)
上述实现是不完整的。您可以触发
-
一个除以零的错误;并且
-
违反
my_sqrt_fixed()的先验条件。
如何做到这一点,以及如何防止这种情况发生?
第一部分:寻找触发错误的输入
对于上述两种情况中的每一种,确定 a、b、c 的值以触发错误。
使用笔记本来练习习题并查看解决方案。
第二部分:修复问题
适当地扩展代码以处理这些情况。对于不存在的值返回 None。
使用笔记本来练习习题并查看解决方案。
第三部分:其他事项
随机输入下发现这些条件的机会有多大?假设每秒可以进行十亿次测试,平均需要等待多长时间才能触发一个错误?
使用笔记本进行练习并查看解决方案。
练习 4:无限与更远
当我们说my_sqrt_fixed(x)对所有有限数字\(x\)都有效时:如果你将\(x\)设置为\(\infty\)(无穷大),会发生什么?试一试!
使用笔记本进行练习并查看解决方案。
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议许可。内容的一部分源代码,以及用于格式化和显示该内容的源代码受MIT 许可协议许可。最后更改日期:2023-11-11 18:18:06+01:00。引用 版权信息
如何引用此作品
安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒:"软件测试入门"。在安德烈亚斯·策勒,拉胡尔·戈皮纳特,马塞尔·博 hme,戈登·弗莱泽,以及克里斯蒂安·霍勒的《模糊测试书www.fuzzingbook.org/html/Intro_Testing.html]中。检索日期:2023-11-11 18:18:06+01:00。
@incollection{fuzzingbook2023:Intro_Testing,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Introduction to Software Testing},
year = {2023},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Intro_Testing.html}},
note = {Retrieved 2023-11-11 18:18:06+01:00},
url = {https://www.fuzzingbook.org/html/Intro_Testing.html},
urldate = {2023-11-11 18:18:06+01:00}
}
第二部分:词汇模糊测试
这一部分介绍了在词汇级别的测试生成,即组成字符序列。
-
模糊测试:使用随机输入破坏事物 从最简单的测试生成技术之一开始:模糊测试将一串随机字符输入到程序中,希望揭示失败。
-
在获取覆盖率中,我们通过评估这些测试的代码覆盖率来衡量这些测试的有效性——也就是说,在测试运行期间测量程序的实际执行部分。对于试图覆盖尽可能多代码的测试生成器来说,测量这种覆盖率也是至关重要的。
-
基于变异的模糊测试 展示了如何变异现有输入以测试新的行为。我们展示了如何创建这样的变异,以及如何引导它们向尚未发现的代码,应用了流行的 AFL 模糊测试器的核心概念。
-
灰盒模糊测试进一步扩展了输入变异的概念,使用统计估计器来引导测试生成向可能存在的错误方向。
-
基于搜索的模糊测试将引导的概念进一步发展,引入基于搜索的算法来系统地生成程序的测试数据。
-
变异分析 将合成的缺陷(变异)种入程序代码中,以检查测试是否能够找到它们。如果测试没有找到变异,它们很可能也不会找到真正的错误。
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 国际许可协议许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,受MIT 许可协议许可。 最后更改:2023-01-07 15:27:27+01:00 • 引用 • imprint
如何引用本作品
Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler: "第二部分:词汇模糊测试"。在 Andreas Zeller, Rahul Gopinath, Marcel Böhme, Gordon Fraser, 和 Christian Holler 编著的 "模糊测试书" 中,www.fuzzingbook.org/html/02_Lexical_Fuzzing.html。检索日期:2023-01-07 15:27:27+01:00。
@incollection{fuzzingbook2023:02_Lexical_Fuzzing,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Part II: Lexical Fuzzing},
year = {2023},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/02_Lexical_Fuzzing.html}},
note = {Retrieved 2023-01-07 15:27:27+01:00},
url = {https://www.fuzzingbook.org/html/02_Lexical_Fuzzing.html},
urldate = {2023-01-07 15:27:27+01:00}
}
模糊测试:使用随机输入破坏事物
在本章中,我们将从最简单的测试生成技术开始。随机文本生成的关键思想,也称为 模糊测试,是将 随机字符序列 输入到程序中,希望揭示故障。
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import YouTubeVideo
YouTubeVideo('YjO1pIx7wS4')
先决条件
-
你应该了解软件测试的基础知识;例如,从章节 "软件测试简介"。
-
你应该对 Python 有一定的了解;例如,从 Python 教程。
我们可以明确这些先决条件。首先,我们将导入一个在笔记本中工作的标准包。
import [bookutils.setup](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils)
from [typing](https://docs.python.org/3/library/typing.html) import Dict, Tuple, Union, List, Any
现在,我们显式导入(因此需要)前面的章节。
import Intro_Testing
摘要
要 使用本章提供的代码,请编写
>>> from fuzzingbook.Fuzzer import <identifier>
然后利用以下功能。
本章提供了两个重要的类,在 模糊测试架构 中介绍:
-
Fuzzer作为模糊器的基类;并且 -
Runner作为测试程序的基础类。
模糊器
Fuzzer 是模糊器的基类,RandomFuzzer 是一个简单的实例化。Fuzzer 对象的 fuzz() 方法返回一个包含生成输入的字符串。
>>> random_fuzzer = RandomFuzzer()
>>> random_fuzzer.fuzz()
'%$<1&<%+=!"83?+)9:++9138 42/ "7;0-,)06 "1(2;6>?99$%7!!*#96=>2&-/(5*)=$;0$$+;<12"?30&'
RandomFuzzer() 构造函数允许一系列关键字参数:
>>> print(RandomFuzzer.__init__.__doc__)
Produce strings of `min_length` to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`)
>>> random_fuzzer = RandomFuzzer(min_length=10, max_length=20, char_start=65, char_range=26)
>>> random_fuzzer.fuzz()
'XGZVDDPZOOW'
生成随机输入。">
生成 min_length 到 max_length 个字符的字符串
在范围 [char_start, char_start + char_range)">
返回模糊输入">
模糊器的基类。
构造函数
返回模糊输入
使用模糊输入运行 runner,run() 函数
使用模糊输入运行 runner,trials 次数
运行器
一个 Fuzzer 可以与一个 Runner 配对,该 Runner 将模糊字符串作为输入。其结果是特定类的 状态 和 结果 (PASS, FAIL, 或 UNRESOLVED)。一个 PrintRunner 将简单地打印出给定的输入并返回一个 PASS 结果:
>>> print_runner = PrintRunner()
>>> random_fuzzer.run(print_runner)
EQYGAXPTVPJGTYHXFJ
('EQYGAXPTVPJGTYHXFJ', 'UNRESOLVED')
一个 ProgramRunner 将生成的输入传递给外部程序。其结果是程序状态(CompletedProcess 实例)和 结果 (PASS, FAIL, 或 UNRESOLVED) 的一个对:
>>> cat = ProgramRunner('cat')
>>> random_fuzzer.run(cat)
(CompletedProcess(args='cat', returncode=0, stdout='BZOQTXFBTEOVYX', stderr=''),
'PASS')
使用输入测试程序。">
初始化。
program 是传递给 subprocess.run() 的程序规范">
使用 inp 作为输入运行程序。
根据 subprocess.run() 的结果返回测试结果。">
使用 inp 作为输入运行程序。
返回 subprocess.run() 的结果。">
测试输入的基类。">
初始化">
使用给定输入运行运行者">
简单的运行者,打印输入。">
打印给定输入">
测试作业
模糊测试诞生于“1988 年秋一个黑暗且暴风雨的夜晚”[Takanen 等人,2008]。当时,巴顿·米勒教授坐在威斯康星州麦迪逊的公寓里,通过一条 1200 波特电话线连接到他的大学电脑。雷暴导致线路产生噪音,而这种噪音反过来又导致两端 UNIX 命令接收到了错误的输入——从而导致崩溃。频繁的崩溃让他感到惊讶——难道程序应该比这更健壮吗?作为一名科学家,他想要调查问题的范围及其原因。因此,他为威斯康星大学麦迪逊分校的学生们设计了一个编程练习——一个让学生们创建第一个模糊测试器的练习。
这就是作业的内容:
本项目的目标是评估各种 UNIX 实用程序的鲁棒性,给定一个不可预测的输入流。[...] 首先,你将构建一个模糊生成器。这是一个会输出随机字符流的程序。其次,你将使用模糊生成器攻击尽可能多的 UNIX 实用程序,目标是尝试使它们崩溃。
这项作业捕捉了模糊测试的精髓:创建随机输入,看看它们是否会导致系统崩溃。 只需运行足够长的时间,你就会看到结果。
一个简单的模糊器
让我们尝试完成这个任务并构建一个模糊生成器。想法是产生随机字符,将它们添加到缓冲字符串变量(out)中,最后返回字符串。
此实现使用了以下 Python 特性和函数:
-
random.randrange(start, end)– 返回一个随机数 \([\)start,end\()\) -
range(start, end)– 创建一个整数范围 \([\)start,end\()\) 的迭代器(可以用作列表)。 -
for elem in list: body– 在循环中执行body,其中elem从list中取每个值。 -
for i in range(start, end): body– 在循环中执行body,其中i从start到end\(-\) 1。 -
chr(n)– 返回 ASCII 码为n的字符
要使用随机数,我们必须导入相应的模块。
import [random](https://docs.python.org/3/library/random.html)
现在是实际的 fuzzer() 函数。
def fuzzer(max_length: int = 100, char_start: int = 32, char_range: int = 32) -> str:
"""A string of up to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`)"""
string_length = random.randrange(0, max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(char_start, char_start + char_range))
return out
使用默认参数,fuzzer() 函数返回一个随机字符的字符串:
fuzzer()
'!7#%"*#0=)$;%6*;>638:*>80"=</>(/*:-(2<4 !:5*6856&?""11<7+%<%7,4.8,*+&,,$,."'
巴特·米勒(Bart Miller)将“模糊”一词用作此类随机、无结构数据的名称。现在想象一下,这个“模糊”字符串是期望特定输入格式的程序的输入——比如说,逗号分隔的值列表,或者一个电子邮件地址。程序能否无任何问题地处理这种输入?
如果上述模糊输入已经很有趣,那么考虑一下,模糊可以轻松地设置来产生其他类型的输入。例如,我们也可以让 fuzzer() 产生一系列小写字母。我们使用 ord(c) 来返回字符 c 的 ASCII 码。
fuzzer(1000, ord('a'), 26)
'zskscocrxllosagkvaszlngpysurezehvcqcghygphnhonehczraznkibltfmocxddoxcmrvatcleysksodzlwmzdndoxrjfqigjhqjxkblyrtoaydlwwisrvxtxsejhfbnforvlfisojqaktcxpmjqsfsycisoexjctydzxzzutukdztxvdpqbjuqmsectwjvylvbixzfmqiabdnihqagsvlyxwxxconminadcaqjdzcnzfjlwccyudmdfceiepwvyggepjxoeqaqbjzvmjdlebxqvehkmlevoofjlilegieeihmetjappbisqgrjhglzgffqrdqcwfmmwqecxlqfpvgtvcddvmwkplmwadgiyckrfjddxnegvmxravaunzwhpfpyzuyyavwwtgykwfszasvlbwojetvcygectelwkputfczgsfsbclnkzzcjfywitooygjwqujseflqyvqgyzpvknddzemkegrjjrshbouqxcmixnqhgsgdwgzwzmgzfajymbcfezqxndbmzwnxjeevgtpjtcwgbzptozflrwvuopohbvpmpaifnyyfvbzzdsdlznusarkmmtazptbjbqdkrsnrpgdffemnpehoapiiudokczwrvpsonybfpaeyorrgjdmgvkvupdtkrequicexqkoikygepawmwsdcrhivoegynnhodfhryeqbebtbqnwhogdfrsrksntqjbocvislhgrgchkhpaiugpbdygwkhrtyniufabdnqhtnwreiascfvmuhettfpbowbjadfxnbtzhobnxsnf'
假设一个程序期望接收一个标识符作为其输入。它会期望这么长的标识符吗?
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import quiz
问答
哪个会产生任意长度的十进制数字字符串?
的确!最后一个才是关键:
fuzzer(100, ord('0'), 10)
'905902398493166953126081485047020401153418590518545517740565959745145909835837'
模糊外部程序
让我们看看如果我们实际使用模糊输入调用外部程序会发生什么。为此,让我们分两步进行。首先,我们创建一个带有模糊测试数据的 输入文件;然后我们将这个输入文件喂给一个选择好的程序。
创建输入文件
让我们获取一个临时文件名,这样我们就不至于使文件系统变得杂乱。
import [os](https://docs.python.org/3/library/os.html)
import [tempfile](https://docs.python.org/3/library/tempfile.html)
basename = "input.txt"
tempdir = tempfile.mkdtemp()
FILE = os.path.join(tempdir, basename)
print(FILE)
/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt
我们现在可以打开这个文件进行写入。Python 的 open() 函数打开一个文件,然后我们可以向其中写入任意内容。它通常与 with 语句一起使用,这确保了文件在不再需要时立即关闭。
data = fuzzer()
with open(FILE, "w") as f:
f.write(data)
我们可以通过读取其内容来验证文件是否实际创建:
contents = open(FILE).read()
print(contents)
assert(contents == data)
<?6&" !3'7-5>18%55*,5
调用外部程序
现在我们有了输入文件,我们可以在其上调用一个程序。为了好玩,让我们测试 bc 计算器程序,它接受一个算术表达式并对其进行评估。
要调用 bc,让我们使用 Python 的 subprocess 模块。这是如何工作的:
import [os](https://docs.python.org/3/library/os.html)
import [subprocess](https://docs.python.org/3/library/subprocess.html)
program = "bc"
with open(FILE, "w") as f:
f.write("2 + 2\n")
result = subprocess.run([program, FILE],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True) # Will be "text" in Python 3.7
从 result 中,我们可以检查程序输出。在 bc 的情况下,这是评估算术表达式的结果:
result.stdout
'4\n'
我们还可以检查状态。值为 0 表示程序正确终止。
result.returncode
0
任何错误信息都会在 results.stderr 中可用:
result.stderr
''
你可以用 bc 之外的任何你喜欢的程序。不过,请注意,如果你的程序能够更改或甚至损坏你的系统,那么模糊输入中包含的数据或命令可能会恰好做到这一点。
问答
就为了好玩,想象一下你会测试一个文件删除程序——比如说 rm -fr FILE,其中 FILE 是由 fuzzer() 生成的字符串。fuzzer()(使用默认参数)产生一个导致删除所有文件的 FILE 参数的概率是多少?
实际上的概率可能比你想象的要高。例如,如果你删除了 /(所有文件的根),那么你的整个文件系统都将消失。如果你删除了 .(当前文件夹),当前目录中的所有文件都将消失。
生成一个正好 1 个字符长的字符串的概率是 1/101,这是因为字符串的长度是通过调用 random.randrange(0, max_length + 1) 来确定的,其中 max_length 的默认值是 100。根据 random.randrange 的描述,它应该返回一个在 [0, 99 + 1) 区间内的随机数。因此,我们最终得到一个包含 101 个值的区间 [0, 100]。
要生成 / 或 .,你需要一个长度为 1 的字符串(概率:101 分之一)和这两个字符之一(概率:32 分之二)。
1/101 * 2/32
0.0006188118811881188
上述代码块排除了删除 ~(你的主目录)的可能性,这是因为生成字符 '~' 的概率不是 1/32;它是 0/32。字符是通过调用 chr(random.randrange(char_start, char_start + char_range)) 生成的,其中 char_start 的默认值是 32,char_range 的默认值也是 32。chr 的文档说明,“[r]eturn the string representing a character whose Unicode code point is the integer i。” '~' 的 Unicode 代码点是 126,因此不在区间 [32, 64) 内。
如果代码被修改为 char_range = 95,那么获得字符 '~' 的概率将是 1/94,因此删除所有文件的事件的概率等于 0.000332。
你主目录中的所有文件都将消失。
3/94 * 1/94 * 99/101
0.0003327969736765437
然而,只要第二个字符是空格,我们实际上可以处理任何字符串——毕竟,rm -fr / WHATEVER 将首先处理 /,然后才是随后的任何内容。第一个字符的概率是 32 分之二,因为上面的代码块只允许获得 / 或 . 的概率,但不允许获得 ~。
对于空格,概率是 32 分之一。
我们必须包括获得至少 2 个字符的概率项,这是在获得空格作为第二个字符的场景中所需的。这个概率是 99/101,因为它被计算为(1 - 获得单个字符或没有任何字符的概率),因此等于 1-(2/101)。
因此,在第二个字符有空间的情况下,删除所有文件的概率计算如下:
[获取'/'或'. '后跟空格的概率] = [获取'/'字符或'. '字符的概率] * [获取空格的概率] * [获取至少 2 个字符的概率] = 0.001914
获取至少 2 个字符的概率图。
2/32 * 1/32 * 99/101
0.0019144492574257425
由于模糊测试通常运行数百万次,你真的不希望承担这种风险。请在可以随时重置的安全环境中运行你的模糊器,例如 Docker 容器。
长时间模糊测试
让我们现在向经过测试的程序输入大量输入,看看它是否会在某些输入上崩溃。我们将所有结果存储在runs变量中,作为输入数据和实际结果的配对。(注意:运行此操作可能需要一段时间。)
trials = 100
program = "bc"
runs = []
for i in range(trials):
data = fuzzer()
with open(FILE, "w") as f:
f.write(data)
result = subprocess.run([program, FILE],
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
runs.append((data, result))
我们现在可以查询runs以获取一些统计数据。例如,我们可以查询实际通过多少次运行——也就是说,没有错误消息。这里我们使用列表推导:形式为expression for element in list if condition的列表推导返回一个评估后的expression列表,其中每个element如果条件为真则来自list。实际上,列表推导返回一个列表生成器,但就我们的目的而言,生成器表现得像列表。这里,我们让expression对所有满足条件的元素为 1,并使用sum()对列表中的所有元素进行求和。
sum(1 for (data, result) in runs if result.stderr == "")
9
大多数输入显然是无效的——这并不令人惊讶,因为随机输入包含有效算术表达式的可能性不大。
让我们看看第一条错误消息:
errors = [(data, result) for (data, result) in runs if result.stderr != ""]
(first_data, first_result) = errors[0]
print(repr(first_data))
print(first_result.stderr)
'5&8>"86,?"/7!1%5-**&-$&)$91;"21(\'8"(%$4,("(&!67%89$!.?(*(96(28$=6029:<:$(6 !-+2622(&4'
Parse error: bad character '&'
/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1
有没有除了非法字符、解析错误或语法错误之外的消息的运行?(比如崩溃或你发现了一个致命的错误?)并不多:
[result.stderr for (data, result) in runs if
result.stderr != ""
and "illegal character" not in result.stderr
and "parse error" not in result.stderr
and "syntax error" not in result.stderr]
["\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad assignment: left side must be scale, ibase, obase, seed, last, var, or array element\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '?'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '?'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '?'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad assignment: left side must be scale, ibase, obase, seed, last, var, or array element\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '&'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character '?'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
"\nParse error: bad character ':'\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
"\nParse error: bad character '''\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n",
'\nParse error: bad expression\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n',
'\nParse error: bad token\n /var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/tmp33gvbu2n/input.txt:1\n\n']
也许bc崩溃就能表明崩溃。不幸的是,返回代码永远不会是非零值:
sum(1 for (data, result) in runs if result.returncode != 0)
91
我们让上面的bc测试再运行一段时间怎么样?当它运行时,让我们看看 1989 年的技术水平。
模糊器发现的错误
当米勒和他的学生在 1989 年运行他们的第一个模糊器时,他们发现了一个令人震惊的结果:他们模糊测试的约三分之一的 UNIX 实用程序存在问题——它们在遇到模糊测试输入时崩溃、挂起或以其他方式失败[Miller et al, 1990]。这还包括上面的bc程序。(上面的bc是一个现代重实现,其作者是一位坚定的模糊测试信仰者!)
考虑到许多这些 UNIX 实用程序被用于也会处理网络输入的脚本中,这是一个令人担忧的结果。程序员迅速构建并运行了自己的模糊器,急忙修复报告的错误,并学会了不再信任外部输入。
米勒的模糊实验发现了什么样的问题?结果是,程序员在 1990 年犯的错误今天仍在犯。
缓冲区溢出
许多程序为输入和输入元素内置了最大长度。在 C 语言等语言中,很容易超出这些长度,而程序(或程序员)甚至没有注意到,从而触发所谓的缓冲区溢出。例如,以下代码会愉快地将input字符串复制到weekday字符串中,即使input有超过八个字符:
char weekday[9]; // 8 characters + trailing '\0' terminator
strcpy (weekday, input);
具有讽刺意味的是,如果input是"Wednesday"(9 个字符),这已经失败了;任何多余的字符(在这里是'y'和随后的字符串终止符'\0')都会简单地复制到weekday之后的内存中,从而触发任意行为;也许是一些布尔字符变量,它会被从'n'设置为'y'。使用模糊测试,很容易产生任意长度的输入和输入元素。
我们可以很容易地在 Python 函数中模拟这种缓冲区溢出行为:
def crash_if_too_long(s):
buffer = "Thursday"
if len(s) > len(buffer):
raise ValueError
并且,它很快就崩溃了。
from ExpectError import ExpectError
trials = 100
with ExpectError():
for i in range(trials):
s = fuzzer()
crash_if_too_long(s)
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/292568387.py", line 5, in <module>
crash_if_too_long(s)
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/2784561514.py", line 4, in crash_if_too_long
raise ValueError
ValueError (expected)
上述代码中的with ExpectError()行确保打印错误信息,但执行继续;这是为了将这种“预期”错误与其他代码示例中的“意外”错误区分开来。
缺少错误检查
许多编程语言没有异常,而是在异常情况下通过函数返回特殊的错误代码。例如,C 函数getchar()通常从标准输入返回一个字符;如果没有更多的输入,它返回特殊值EOF(文件结束)。现在假设程序员正在扫描输入以查找下一个字符,使用getchar()读取字符,直到读取到空格字符:
while (getchar() != ' ');
如果输入提前结束,这在模糊测试中是完全可行的,会发生什么?嗯,getchar()返回EOF,并且在再次调用时继续返回EOF;因此,上述代码简单地进入了一个无限循环。
再次,我们可以模拟这种缺少错误检查。以下是一个函数,如果没有空格出现在输入中,它将有效地挂起:
def hang_if_no_space(s):
i = 0
while True:
if i < len(s):
if s[i] == ' ':
break
i += 1
使用我们测试介绍中的超时机制,我们可以在一段时间后中断此函数。是的,它会在几次模糊测试输入后挂起。
from ExpectError import ExpectTimeout
trials = 100
with ExpectTimeout(2):
for i in range(trials):
s = fuzzer()
hang_if_no_space(s)
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/3194687366.py", line 5, in <module>
hang_if_no_space(s)
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/3035466707.py", line 3, in hang_if_no_space
while True:
^^^^
File "Timeout.ipynb", line 43, in timeout_handler
raise TimeoutError()
TimeoutError (expected)
上述代码中的with ExpectTimeout()行确保在两秒后中断封装代码的执行,并打印错误信息。
恶意数字
使用模糊测试,很容易在输入中生成不常见的值,导致各种有趣的行为。考虑以下代码,再次在 C 语言中,它首先从输入中读取缓冲区大小,然后分配给定大小的缓冲区:
char *read_input() {
size_t size = read_buffer_size();
char *buffer = (char *)malloc(size);
// fill buffer
return (buffer);
}
如果size非常大,超过了程序内存,会发生什么?如果size小于后续字符的数量,会发生什么?如果size是负数,会发生什么?通过在这里提供一个随机数,模糊测试可以造成各种损害。
再次,我们可以很容易地在 Python 中模拟这种恶意数字。函数collapse_if_too_large()如果传递的值(一个字符串)在转换为整数后太大,就会失败。
def collapse_if_too_large(s):
if int(s) > 1000:
raise ValueError
我们可以让fuzzer()创建一个数字字符串:
long_number = fuzzer(100, ord('0'), 10)
print(long_number)
7056414967099541967374507745748918952640135045
如果我们将这样的数字输入到collapse_if_too_large()中,它很快就会失败。
with ExpectError():
collapse_if_too_large(long_number)
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/2775103647.py", line 2, in <module>
collapse_if_too_large(long_number)
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/1591744602.py", line 3, in collapse_if_too_large
raise ValueError
ValueError (expected)
如果我们真的想在系统上分配这么多内存,那么像上面那样快速失败实际上是一个更好的选择。在现实中,内存耗尽可能会极大地减慢系统的速度,甚至到它们完全无响应的程度——而重启是唯一的选择。
有些人可能会认为这些都是编程不良或编程语言不良的问题。但是,每天都有成千上万的人开始编程,他们一次又一次地犯同样的错误,即使是在今天。
捕捉错误
当 Miller 和他的学生们构建他们的第一个模糊测试器时,他们可以简单地通过程序崩溃或挂起来识别错误——这两种情况都很容易识别。但如果失败更加微妙,我们就需要想出额外的检查方法。
通用检查器
如上文所述,缓冲区溢出是更一般问题的一个特例:在 C 和 C++等语言中,程序可以访问其内存的任意部分——甚至包括那些未初始化、已释放或根本不属于你试图访问的数据结构的部分。如果你想要编写操作系统,这可能是必要的;如果你想要最大化的性能或控制,这将是很好的;但如果你想要避免错误,这将是相当糟糕的。幸运的是,有一些工具可以帮助在运行时捕捉这类问题,而且当与模糊测试结合使用时,它们是非常棒的。
检查内存访问
为了在测试中捕捉到有问题的内存访问,可以在特殊的内存检查环境中运行 C 程序;在运行时,这些环境会检查每个内存操作是否访问了有效的已初始化内存。一个流行的例子是LLVM Address Sanitizer,它可以检测一系列潜在的内存安全违规。在下面的例子中,我们将使用这个工具编译一个相当简单的 C 程序,并通过读取已分配内存部分之外的数据来引发越界读取。
with open("program.c", "w") as f:
f.write("""
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv) {
/* Create an array with 100 bytes, initialized with 42 */
char *buf = malloc(100);
memset(buf, 42, 100);
/* Read the N-th element, with N being the first command-line argument */
int index = atoi(argv[1]);
char val = buf[index];
/* Clean up memory so we don't leak */
free(buf);
return val;
}
""")
from [bookutils](https://github.com/uds-se/fuzzingbook//tree/master/notebooks/shared/bookutils) import print_file
print_file("program.c")
#include <stdlib.h>
#include <string.h>
int main(int argc, char** argv) {
/* Create an array with 100 bytes, initialized with 42 */
char *buf = malloc(100);
memset(buf, 42, 100);
/* Read the N-th element, with N being the first command-line argument */
int index = atoi(argv[1]);
char val = buf[index];
/* Clean up memory so we don't leak */
free(buf);
return val;
}
我们启用地址清理功能来编译这个 C 程序:
!clang -fsanitize=address -g -o program program.c
如果我们用99作为参数运行程序,它将返回buf[99],其值为 42。
!./program 99; echo $?
program(2097,0x1ff330240) malloc: nano zone abandoned due to inability to reserve vm space.
42
然而,访问buf[110]在 AddressSanitizer 中会导致越界错误。
!./program 110
program(2132,0x1ff330240) malloc: nano zone abandoned due to inability to reserve vm space.
=================================================================
==2132==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60b0000000ae at pc 0x000104927e84 bp 0x00016b4daa50 sp 0x00016b4daa48 READ of size 1 at 0x60b0000000ae thread T0
#0 0x104927e80 in main program.c:12
#1 0x1956d0270 (<unknown module>)
0x60b0000000ae is located 10 bytes after 100-byte region [0x60b000000040,0x60b0000000a4) allocated by thread T0 here:
#0 0x104e58c04 in malloc+0x94 (libclang_rt.asan_osx_dynamic.dylib:arm64e+0x54c04)
#1 0x104927dc8 in main program.c:7
#2 0x1956d0270 (<unknown module>)
SUMMARY: AddressSanitizer: heap-buffer-overflow program.c:12 in main
Shadow bytes around the buggy address:
0x60affffffe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x60affffffe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x60afffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x60afffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x60b000000000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
=>0x60b000000080: 00 00 00 00 04[fa]fa fa fa fa fa fa fa fa fa fa
0x60b000000100: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x60b000000180: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x60b000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x60b000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x60b000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==2132==ABORTING
如果你想要在 C 程序中找到错误,开启模糊测试的这些检查相对容易。这将根据工具的不同而减慢执行速度(对于 AddressSanitizer 来说,通常是 2 倍)并消耗更多的内存,但与找到这些错误所需的人力相比,CPU 周期是非常便宜的。
越界访问内存是一个巨大的安全风险,因为它们可能让攻击者访问或甚至修改本不应该被他们访问的信息。以一个著名的例子,HeartBleed 漏洞是 OpenSSL 库中的一个安全漏洞,该库实现了提供计算机网络上通信安全的加密协议。(如果你在浏览器中阅读这篇文章,它很可能是使用这些协议加密的。)
HeartBleed 漏洞是通过向 SSL 心跳服务发送一个特别定制的命令而被利用的。心跳服务用于检查另一端的服务器是否仍然存活。客户端会向服务发送一个类似
BIRD (4 letters)
服务器会回复BIRD,客户端就会知道服务器是活着的。
不幸的是,这个服务可以通过要求服务器回复比请求的字母集更多的内容来被利用。这一点在这XKCD 漫画中解释得很好:



在 OpenSSL 实现中,这些内存内容可能包括加密证书、私钥等等——更糟糕的是,没有人会注意到这些内存刚刚被访问过。当 HeartBleed 漏洞被发现时,它已经存在了多年,没有人会知道是否以及哪些秘密已经泄露;快速建立的HeartBleed 公告页面说了这一切。
但 HeartBleed 是如何被发现的呢?非常简单。Codenomicon 公司和谷歌的研究人员都使用内存清理器编译了 OpenSSL 库,然后愉快地向它发送了模糊命令。内存清理器会注意到是否发生了越界内存访问——实际上,它会非常快地发现这一点。
内存检查器只是可以在模糊测试期间运行的许多检查器之一。在关于挖掘函数规范的章节中,我们将学习更多关于如何定义通用检查器的方法。
我们已经完成了program,所以我们需要清理:
!rm -fr program program.*
信息泄露
信息泄露不仅可能通过非法内存访问发生;它们也可能在“有效”内存中发生——如果这个“有效”内存包含敏感信息,这些信息不应该泄露出去。让我们用一个 Python 程序来阐述这个问题。首先,让我们创建一些填充了实际数据和随机数据的程序内存:
secrets = ("<space for reply>" + fuzzer(100) +
"<secret-certificate>" + fuzzer(100) +
"<secret-key>" + fuzzer(100) + "<other-secrets>")
我们向secrets中添加更多的“记忆”字符,用"deadbeef"作为未初始化内存的标记:
uninitialized_memory_marker = "deadbeef"
while len(secrets) < 2048:
secrets += uninitialized_memory_marker
我们定义了一个服务(类似于上面讨论的心跳服务),它会接收一个要发送回的回复以及一个长度。它会将待发送的回复存储在内存中,然后以给定的长度发送它回。
def heartbeat(reply: str, length: int, memory: str) -> str:
# Store reply in memory
memory = reply + memory[len(reply):]
# Send back heartbeat
s = ""
for i in range(length):
s += memory[i]
return s
这对于标准字符串工作得很好:
heartbeat("potato", 6, memory=secrets)
'potato'
heartbeat("bird", 4, memory=secrets)
'bird'
然而,如果长度大于回复字符串的长度,内存的额外内容就会溢出。请注意,所有这些仍然发生在常规数组边界内,因此地址清理器不会被触发:
heartbeat("hat", 500, memory=secrets)
'hatace for reply>#,,!3?30>#61)$4--8=<7)4 )03/%,5+! "4)0?.9+?3();<42?=?0<secret-certificate>7(+/+((1)#/0\'4!>/<#=78%6$!!$<-"3"\'-?1?85!05629%/); *)1\'/=9%<secret-key>.(#.4%<other-secrets>deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadb'
如何检测这类问题?思路是识别那些不应该泄露的信息,例如给定的秘密,以及未初始化的内存。我们可以在一个小的 Python 示例中模拟这样的检查:
from ExpectError import ExpectError
with ExpectError():
for i in range(10):
s = heartbeat(fuzzer(), random.randint(1, 500), memory=secrets)
assert not s.find(uninitialized_memory_marker)
assert not s.find("secret")
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/4040656327.py", line 4, in <module>
assert not s.find(uninitialized_memory_marker)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError (expected)
通过这样的检查,我们发现秘密和/或未初始化的内存确实泄露了。在信息流章节中,我们将讨论如何自动执行此操作,“污染”敏感信息和从它们派生出的值,并确保“污染”的值不会泄露。
作为一条经验法则,你应该在模糊测试期间尽可能启用尽可能多的自动检查器。CPU 周期很便宜,而错误很昂贵。如果你只执行程序而没有实际检测错误的选项,你将错过几个机会。
程序特定检查器
除了适用于给定平台或给定语言的所有程序的通用检查器之外,你还可以设计特定的检查器,适用于你的程序或子系统。在测试章节中,我们已暗示了运行时验证的技术,这些技术会在运行时检查函数结果是否正确。
检测错误的一个关键思想是断言的概念——一个检查重要函数的输入(先决条件)和结果(后置条件)的谓词。你程序中的断言越多,你在执行过程中检测到错误的几率就越高,这些错误在通用的检查器中可能无法检测到——尤其是在模糊测试期间。如果你担心断言对性能的影响,请记住,在生产代码中可以关闭断言(尽管保持最关键的检查激活可能是有帮助的)。
断言在查找错误中的一个最重要的用途是检查复杂数据结构的完整性。让我们用一个简单的例子来说明这个概念。假设我们有一个机场代码到机场的映射,如下所示
airport_codes: Dict[str, str] = {
"YVR": "Vancouver",
"JFK": "New York-JFK",
"CDG": "Paris-Charles de Gaulle",
"CAI": "Cairo",
"LED": "St. Petersburg",
"PEK": "Beijing",
"HND": "Tokyo-Haneda",
"AKL": "Auckland"
} # plus many more
airport_codes["YVR"]
'Vancouver'
"AKL" in airport_codes
True
这个机场代码列表可能非常重要:如果我们任何一个机场代码中存在拼写错误,这可能会影响我们拥有的任何应用程序。因此,我们引入了一个检查列表一致性的函数。一致性条件被称为表示不变性,因此检查它的函数(或方法)通常命名为repOK(),表示“表示是正确的”。
首先,让我们有一个用于单个机场代码的检查器。如果代码不一致,检查器就会失败。
def code_repOK(code: str) -> bool:
assert len(code) == 3, "Airport code must have three characters: " + repr(code)
for c in code:
assert c.isalpha(), "Non-letter in airport code: " + repr(code)
assert c.isupper(), "Lowercase letter in airport code: " + repr(code)
return True
assert code_repOK("SEA")
我们现在可以使用code_repOK()来检查列表中的所有元素:
def airport_codes_repOK():
for code in airport_codes:
assert code_repOK(code)
return True
with ExpectError():
assert airport_codes_repOK()
如果我们向列表中添加一个无效元素,我们的检查就会失败:
airport_codes["YMML"] = "Melbourne"
with ExpectError():
assert airport_codes_repOK()
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/2308942452.py", line 2, in <module>
assert airport_codes_repOK()
^^^^^^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/480627665.py", line 3, in airport_codes_repOK
assert code_repOK(code)
^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/865020192.py", line 2, in code_repOK
assert len(code) == 3, "Airport code must have three characters: " + repr(code)
^^^^^^^^^^^^^^
AssertionError: Airport code must have three characters: 'YMML' (expected)
当然,我们不会直接操作列表,而会有一个特殊的函数来添加元素;这可以检查代码是否有效:
def add_new_airport(code: str, city: str) -> None:
assert code_repOK(code)
airport_codes[code] = city
with ExpectError(): # For BER, ExpectTimeout would be more appropriate
add_new_airport("BER", "Berlin")
这个检查还允许我们找出参数列表中的错误:
with ExpectError():
add_new_airport("London-Heathrow", "LHR")
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/1427835309.py", line 2, in <module>
add_new_airport("London-Heathrow", "LHR")
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/2655039924.py", line 2, in add_new_airport
assert code_repOK(code)
^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/865020192.py", line 2, in code_repOK
assert len(code) == 3, "Airport code must have three characters: " + repr(code)
^^^^^^^^^^^^^^
AssertionError: Airport code must have three characters: 'London-Heathrow' (expected)
然而,为了进行最大限度的检查,add_new_airport() 函数还会确保在更改之前和之后机场代码列表的正确表示。
def add_new_airport_2(code: str, city: str) -> None:
assert code_repOK(code)
assert airport_codes_repOK()
airport_codes[code] = city
assert airport_codes_repOK()
这捕捉了之前引入的不一致性:
with ExpectError():
add_new_airport_2("IST", "Istanbul Yeni Havalimanı")
Traceback (most recent call last):
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/1096579022.py", line 2, in <module>
add_new_airport_2("IST", "Istanbul Yeni Havalimanı")
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/2099116665.py", line 3, in add_new_airport_2
assert airport_codes_repOK()
^^^^^^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/480627665.py", line 3, in airport_codes_repOK
assert code_repOK(code)
^^^^^^^^^^^^^^^^
File "/var/folders/n2/xd9445p97rb3xh7m1dfx8_4h0006ts/T/ipykernel_1932/865020192.py", line 2, in code_repOK
assert len(code) == 3, "Airport code must have three characters: " + repr(code)
^^^^^^^^^^^^^^
AssertionError: Airport code must have three characters: 'YMML' (expected)
你的代码中存在的 repOK() 断言越多,你将捕获的错误就越多——甚至那些仅针对你自己的领域和问题的错误。此外,这样的断言记录了你在编程过程中做出的 假设,因此有助于其他程序员理解你的代码并防止错误。
作为最后的例子,让我们考虑一个相当复杂的数据结构——一个 红黑树,一个自平衡的二叉搜索树。实现红黑树并不太难,但即使是经验丰富的程序员,正确实现它也可能需要几个小时。然而,repOK() 方法记录了所有的假设,并对其进行检查:
class RedBlackTree:
def repOK(self):
assert self.rootHasNoParent()
assert self.rootIsBlack()
assert self.rootNodesHaveOnlyBlackChildren()
assert self.treeIsAcyclic()
assert self.parentsAreConsistent()
return True
def rootIsBlack(self):
if self.parent is None:
assert self.color == BLACK
return True
def add_element(self, elem):
assert self.repOK()
... # Add the element
assert self.repOK()
def delete_element(self, elem):
assert self.repOK()
... # Delete the element
assert self.repOK()
在这里,repOK() 是在 RedBlackTree 类的对象上运行的方法。它运行五个不同的检查,所有这些检查都有自己的断言。每当添加或删除元素时,都会自动运行所有这些一致性检查。如果你在这些检查中存在错误,检查器会发现它们——当然,如果你通过足够多的模糊输入运行树的话。
静态代码检查器
许多从 repOK() 断言中获得的益处也可以通过在你的代码上使用 静态类型检查器 来获得。例如,在 Python 中,MyPy 静态检查器可以在正确声明参数类型后立即找到类型错误:
typed_airport_codes: Dict[str, str] = {
"YVR": "Vancouver", # etc
}
如果我们现在添加一个非字符串类型的键,就像
typed_airport_codes[1] = "First"
这会被 MyPy 立即捕获:
$ mypy airports.py
airports.py: error: Invalid index type "int" for "Dict[str, str]"; expected type "str"
然而,静态检查更高级的特性,如由恰好三个大写字母组成的机场代码或一个无环树,很快就会达到静态检查的极限。你的 repOK() 断言仍然需要——最好与一个好的测试生成器结合使用。
模糊测试架构
由于我们希望在下文中重用本章的一些部分,让我们以更容易重用的方式定义事物,特别是更容易 扩展 的方式。为此,我们引入了一系列 类,以可重用的方式封装了上述功能。
运行器类
我们首先引入的是 Runner 的概念——即一个执行具有给定输入的某个对象的对象。一个运行者通常是一些程序或正在测试的函数,但我们也可以有更简单的运行者。
让我们从运行器的基础类开始。运行器本质上提供了一个 run(input) 方法,用于将 input(一个字符串)传递给运行器。run() 返回一个对(result,outcome)。在这里,result 是运行器特定的值,它提供了关于运行的详细信息;outcome 是一个值,将结果分类为三个类别:
-
Runner.PASS– 测试 通过。运行产生了正确的结果。 -
Runner.FAIL– 测试 失败。运行产生了错误的结果。 -
Runner.UNRESOLVED– 测试既未通过也未失败。如果运行无法进行——例如,因为输入无效,这种情况就会发生。
Outcome = str
class Runner:
"""Base class for testing inputs."""
# Test outcomes
PASS = "PASS"
FAIL = "FAIL"
UNRESOLVED = "UNRESOLVED"
def __init__(self) -> None:
"""Initialize"""
pass
def run(self, inp: str) -> Any:
"""Run the runner with the given input"""
return (inp, Runner.UNRESOLVED)
作为基类,Runner 仅提供对更复杂运行器的接口,这些运行器基于它构建。更具体地说,我们引入 子类,它们 继承 自其超类以添加额外的方法或覆盖继承的方法。
这里是一个这样的子类的例子:PrintRunner 简单地打印出它所接受的一切,覆盖了继承的 run() 方法。这在许多情况下是默认的运行器。
class PrintRunner(Runner):
"""Simple runner, printing the input."""
def run(self, inp) -> Any:
"""Print the given input"""
print(inp)
return (inp, Runner.UNRESOLVED)
p = PrintRunner()
(result, outcome) = p.run("Some input")
Some input
结果只是我们传递的输入字符串:
result
'Some input'
然而,到目前为止,我们还没有方法来分类程序行为:
outcome
'UNRESOLVED'
ProgramRunner 类将输入发送到程序的标准输入。程序在创建 ProgramRunner 对象时指定。
class ProgramRunner(Runner):
"""Test a program with inputs."""
def __init__(self, program: Union[str, List[str]]) -> None:
"""Initialize.
`program` is a program spec as passed to `subprocess.run()`"""
self.program = program
def run_process(self, inp: str = "") -> subprocess.CompletedProcess:
"""Run the program with `inp` as input.
Return result of `subprocess.run()`."""
return subprocess.run(self.program,
input=inp,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
def run(self, inp: str = "") -> Tuple[subprocess.CompletedProcess, Outcome]:
"""Run the program with `inp` as input.
Return test outcome based on result of `subprocess.run()`."""
result = self.run_process(inp)
if result.returncode == 0:
outcome = self.PASS
elif result.returncode < 0:
outcome = self.FAIL
else:
outcome = self.UNRESOLVED
return (result, outcome)
这里是一个针对二进制(即非文本)输入和输出的变体。
class BinaryProgramRunner(ProgramRunner):
def run_process(self, inp: str = "") -> subprocess.CompletedProcess:
"""Run the program with `inp` as input.
Return result of `subprocess.run()`."""
return subprocess.run(self.program,
input=inp.encode(),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
让我们用一个 ProgramRunner 来演示 cat 程序——一个将输入复制到输出的程序。我们看到标准的 cat 调用只是简单地完成工作,cat 的输出与它的输入相同:
cat = ProgramRunner(program="cat")
cat.run("hello")
(CompletedProcess(args='cat', returncode=0, stdout='hello', stderr=''), 'PASS')
模糊器类
现在让我们定义一个 模糊器,它实际上将数据喂给消费者。模糊器的基类提供了一个中心方法 fuzz(),用于创建一些输入。然后 run() 函数将 fuzz() 输入发送到运行器,返回结果;runs() 为给定的次数(trials)执行此操作。
class Fuzzer:
"""Base class for fuzzers."""
def __init__(self) -> None:
"""Constructor"""
pass
def fuzz(self) -> str:
"""Return fuzz input"""
return ""
def run(self, runner: Runner = Runner()) \
-> Tuple[subprocess.CompletedProcess, Outcome]:
"""Run `runner` with fuzz input"""
return runner.run(self.fuzz())
def runs(self, runner: Runner = PrintRunner(), trials: int = 10) \
-> List[Tuple[subprocess.CompletedProcess, Outcome]]:
"""Run `runner` with fuzz input, `trials` times"""
return [self.run(runner) for i in range(trials)]
默认情况下,Fuzzer 对象并不做很多事情,因为它们的 fuzz() 函数只是一个抽象占位符。然而,子类 RandomFuzzer 实现了上述 fuzzer() 函数的功能,并添加了一个参数 min_length 来指定最小长度。
class RandomFuzzer(Fuzzer):
"""Produce random inputs."""
def __init__(self, min_length: int = 10, max_length: int = 100,
char_start: int = 32, char_range: int = 32) -> None:
"""Produce strings of `min_length` to `max_length` characters
in the range [`char_start`, `char_start` + `char_range`)"""
self.min_length = min_length
self.max_length = max_length
self.char_start = char_start
self.char_range = char_range
def fuzz(self) -> str:
string_length = random.randrange(self.min_length, self.max_length + 1)
out = ""
for i in range(0, string_length):
out += chr(random.randrange(self.char_start,
self.char_start + self.char_range))
return out
使用 RandomFuzzer,我们现在可以创建一个模糊器,其配置只需在创建模糊器时指定一次。
random_fuzzer = RandomFuzzer(min_length=20, max_length=20)
for i in range(10):
print(random_fuzzer.fuzz())
'>23>33)(&"09.377.*3
*+:5 ? (?1$4<>!?3>.'
4+3/(3 (0%!>!(+9%,#$
/51$2964>;)2417<9"2&
907.. !7:&--"=$7',7*
(5=5'.!*+&>")6%9)=,/
?:&5) ";.0!=6>3+>)=,
6&,?:!#2))- ?:)=63'-
,)9#839%)?&(0<6("*;)
4?!(49+8=-'&499%?< '
我们现在可以将这些生成的输入发送到之前定义的 cat 运行器,验证 cat 是否确实将其(模糊的)输入复制到输出。
for i in range(10):
inp = random_fuzzer.fuzz()
result, outcome = cat.run(inp)
assert result.stdout == inp
assert outcome == Runner.PASS
然而,将 Fuzzer 与 Runner 结合使用是如此普遍,以至于我们可以使用 Fuzzer 类提供的 run() 方法来完成此目的:
random_fuzzer.run(cat)
(CompletedProcess(args='cat', returncode=0, stdout='?:+= % <1<6$:(>=:9)5', stderr=''),
'PASS')
使用 runs(),我们可以多次重复模糊测试运行,获得一系列结果。
random_fuzzer.runs(cat, 10)
[(CompletedProcess(args='cat', returncode=0, stdout='3976%%&+%6=(1)3&3:<9', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='33$#42$ 11=*%$20=<.-', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='"?<\'#8 </:*%9.--\'97!', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="/0-#(03/!#60'+6>&&72", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="=,+:,6'5:950+><3(*()", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout=" 379+0?'%3137=2:4605", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="02>!$</'*81.#</22>+:", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout="=-<'3-#88*%&*9< +1&&", stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='2;;0=3&6=8&30&<-;?*;', stderr=''),
'PASS'),
(CompletedProcess(args='cat', returncode=0, stdout='/#05=*3($>::#7!0=12+', stderr=''),
'PASS')]
现在,我们已经准备好创建模糊器——从本章介绍的简单随机模糊器开始,甚至更高级的模糊器。敬请期待!
经验教训
-
随机生成输入(“模糊测试”)是一种简单、成本效益高的方法,可以快速测试任意程序的鲁棒性。
-
模糊测试器发现的错误主要归因于输入处理中的错误和不足。
-
为了捕捉错误,尽可能多地使用一致性检查器。
我们已经完成了,所以别忘了清理:
os.remove(FILE)
os.removedirs(tempdir)
下一步
从这里,你可以探索如何
-
通过在现有输入上使用变异来获取更多有效输入
-
使用语法来指定输入格式,从而获取更多有效输入
-
减少失败输入以进行高效的调试
享受阅读吧!
背景
关于生成软件测试的一般书籍很少(这也是我们写这本书的原因)。不过,也有一些关于模糊测试的显著书籍,它们也是基于本章介绍的基本模糊测试技术:
-
书籍《模糊测试 - 强制力漏洞发现》涵盖了广泛的模糊测试领域,包括文件、网页、环境变量和网络协议。作者们带来了在微软进行模糊测试的大量经验,并包括了一些为 Windows 和 UNIX 程序准备的现成工具。这些工具可能有些过时,但原则依然适用。
-
书籍《Fuzzing for Software Security Testing and Quality Assurance》[Takanen 等人,2008],现在已进入 2018 年的第二版,涵盖了广泛的模糊测试工具和检测技术;其作者们带来了丰富的安全测试和漏洞发现经验。这可能是该领域最全面和最新的书籍之一。
特别针对本章,关于模糊测试的开创性工作,介绍了该术语和方法,是“An Empirical Study of the Reliability of UNIX Utilities”[Miller 等人,1990]。作为该领域的基础,这是任何对模糊测试和鲁棒性测试感兴趣的人必读的,其观察结果至今依然有效,就像 30 年前一样。
练习
米勒等人[Miller 等人,1990]发现的错误之一涉及troff排版系统。Troff以文本作为输入,文本由行组成;以点(.)开头的行包含排版命令,如下所示:
.NH
Some Heading
.LP
Some paragraph
这将产生(使用nroff -ms)以下文本
1\. Some Heading
Some paragraph
在米勒等人当时,如果其输入包含troff会失败
-
输入序列
\D(反斜杠+D)后面跟着一个不可打印字符 -
ASCII 范围内的一个字符 128-255(即第 8 位被设置),后面跟着一个换行符
-
一个点(
.)后面跟着一个换行符。
练习 1:模拟 Troff
对于上述每一项,编写一个 Python 函数f(s),如果s满足失败标准,则该函数失败。
使用笔记本进行练习并查看解决方案。
练习 2:运行模拟的 Troff
创建一个名为TroffRunner的类,它是Runner的子类,用于检查上述谓词。使用Fuzzer运行它。确保Fuzzer对象产生整个字符范围。计算各个谓词失败的频率。
使用笔记本进行练习并查看解决方案。
练习 3:运行真实的 Troff
使用BinaryProgramRunner,将配置好的 fuzzer 应用到真实的troff程序上。检查是否可以产生输出代码非零的运行,这表明有失败或崩溃。
使用笔记本进行练习并查看解决方案。
本项目的内容受Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License许可。作为内容一部分的源代码,以及用于格式化和显示该内容的源代码,均受MIT License许可。最后修改时间:2024-11-09 17:07:29+01:00。引用 版权信息
如何引用本作品
安德烈亚斯·泽勒(Andreas Zeller)、拉胡尔·戈皮纳特(Rahul Gopinath)、马塞尔·博 hme(Marcel Böhme)、戈登·弗朗西斯(Gordon Fraser)和克里斯蒂安·霍勒(Christian Holler):"Fuzzing: Breaking Things with Random Inputs"。收录于安德烈亚斯·泽勒、拉胡尔·戈皮纳特、马塞尔·博 hme、戈登·弗朗西斯和克里斯蒂安·霍勒的《The Fuzzing Book》中,www.fuzzingbook.org/html/Fuzzer.html。检索时间:2024-11-09 17:07:29+01:00。
@incollection{fuzzingbook2024:Fuzzer,
author = {Andreas Zeller and Rahul Gopinath and Marcel B{\"o}hme and Gordon Fraser and Christian Holler},
booktitle = {The Fuzzing Book},
title = {Fuzzing: Breaking Things with Random Inputs},
year = {2024},
publisher = {CISPA Helmholtz Center for Information Security},
howpublished = {\url{https://www.fuzzingbook.org/html/Fuzzer.html}},
note = {Retrieved 2024-11-09 17:07:29+01:00},
url = {https://www.fuzzingbook.org/html/Fuzzer.html},
urldate = {2024-11-09 17:07:29+01:00}
}


浙公网安备 33010602011771号