作者 | 黄杉 华东师范大学软件工程学院博士
苏亭 华东师范大学软件工程学院教授
首发 | 鉴源论坛 · 观模
01
什么是随机测试
(Random Testing)
随机测试是一种使用随机、相互独立的程序输入来对计算机程序进行测试的黑盒软件测试(在完全忽略程序内部实现细节的情况下进行测试)技术。在处理完随机且独立的程序输入后,程序输出的结果将会和软件规格说明(software specifications)中所描述的软件行为进行比对来判断该测试是否通过。
随机测试的核心思想接近于无限猴子定理[1](The Infinite Monkey Theorem),所以随机测试也被称为Monkey Testing。无限猴子定理是来自Émile Borel的一本出版于1909年的概率相关的书籍,其中描述了这样一个场景:当一只猴子在打字机键盘上不限时间地随机敲击,它可能输出任何给定文章,例如莎士比亚的完整作品,并且这种可能性随着时间的增长会不断地接近100% 。换用测试的语言来描述则是测试程序通过不断地生成测试输入,其能完整测试整个程序并寻找到程序中异常的可能性会不断增大。
02
早期的随机测试
最早对随机测试技术的使用可以追溯到上个世纪五十年代,那时的数据还存储在穿孔卡片(Punched Card)上。程序员会将扔进垃圾桶中的卡片或者标记有随机数字的卡组作为计算机程序的输入来进行随机测试。
当随机测试被广泛认定为测试程序的最差方式时,Duran和Ntafos两人在1981年正式地对使用随机测试技术对程序进行测试的有效性进行了调研,结果表明相对于系统化的测试技术,随机测试是一个成本低收益高的替代品。
后来在1983年,苹果公司的Steve Capps开发了一款名为“The Monkey”的随机输入生成工具用来对传统的MacOS应用进行测试。他对工具的命名就是化用了无限猴子定理。
1991年,一款名叫 “crashme” 的工具发布。这款工具意在通过随机执行带有随机参数的系统调用来测试Unix以及类Unix操作系统的鲁棒性。
03
随机测试的优缺点
3.1 优点
(1)容易实现和使用。随机测试并不需要知晓程序细节,并且输入也通过随机生成。
(2)对程序不存在偏见。由于随机测试的输入都是随机生成的,不存在人为因素影响,也就不会因为对程序某一部分信任而忽略掉潜在的漏洞。
(3)能快速查找漏洞。随机测试的测试速度快,通过快速和大量的测试,能够在短时间内找到大量的候选漏洞(对漏洞的确认还需要人工参与)。
3.2 缺点
(1)寻找漏洞的精度不高。由于随机测试的完全随机性,寻找到的漏洞很可能是一些无关紧要的错误。
(2)过于随机导致对程序的代码覆盖率不高。大部分人认为对程序的测试过于依赖随机,不如通过人工白盒测试的方式来更精确地测试程序。
04
随机测试进阶
模糊测试(Fuzzing)
在1988年的一个风雨交加的夜晚,威斯康星大学的Barton Miller教授在自己的公寓中通过一条电话线连接他在学校中的计算机。暴风雨引发了电话线中的信号错乱,以至于所连接的Unix终端不断接收到糟糕的命令输入,最终导致了系统崩溃。频发的崩溃使这位讲授操作系统课程的教授感到惊讶,因此他脑海中浮现了一个对Unix系统进行鲁棒性测试的念头。于是他在给学生的课程作业中写道:
The goal of this project is to evaluate the robustness of various UNIX utility programs, given an unpredictable input stream. This project has two parts. First, you will build a fuzz generator... Second, you will take the fuzz generator and use it to attack as many UNIX utilities as possible, with the goal of trying to break them... [2]
教授在作业中要求学生开发一个模糊生成器(fuzz generator),这个生成器可以产生不可预测的输入流,然后将这些杂乱的输入给到Unix系统设施,然后试图攻陷这些设施并找到和分析引发错误的随机输入和原因。这就是模糊测试的诞生,教授在作业中使用的fuzz一词也就被用来命名这一技术。
从上文可以看到模糊测试在诞生之初和随机测试十分相似,都是通过随机的输入来对计算机程序进行功能行为测试。下面的Python代码片段给出了一个最简单的模糊器(fuzzer)例子。这个模糊测试器接收三个参数:最大长度(max_length)、字符的ASCII码起始值(char_start)以及字符的ASCII码从起始到结束的范围(char_range)。该模糊器将生成一个长度为max_length的包含ASCII码在[char_start, char_start + char_range)范围内的字符串。
图 1
所以最原始的模糊测试和随机测试拥有相同的优缺点。其中最显著的问题就是由于其完全的随机性,寻找到的程序错误过于刁钻。可能找到的程序错误只是因为错误的输入导致而和程序本身实现无关,或者程序使用人员根本就不会使用像模糊测试器生成的输入,所以不会在软件设计的考虑范畴之内,甚至模糊测试根本就没有测试到程序的主要功能。
无论如何模糊测试还是有其可取之处的(尤其是测试速度快、易于实现),所以后来的研究者们不断想方设法地,尤其是针对如何更好地生成测试输入方面去提高模糊测试的精度(找到的错误确实是和软件规范说明或预期设想行为不符)和深度(能更多地去测试程序的主要功能)来完善模糊测试的实用性。实现这些目标的主要方法就是通过一些静态(基于覆盖率、基于变异)或动态(基于搜索、基于语法)的方式给模糊器提供额外的辅助信息来帮助模糊器更高效地生成、更有效地测试输入,而不再是完全随机,这也促使模糊测试从黑盒测试向灰盒测试进行转变。下文会进行具体介绍。
现在的模糊测试技术可以有几种划分方式(不限于此):
· 一种模糊测试技术可以是基于生成(generation-based)的或者基于变异(mutation-based),取决于它是在没有任何参考的情况下生成随机输入,还是在现有输入的基础上进行修改,也就是所谓的变异;
· 一种模糊测试技术可以是愚笨的(dumb)或者聪明的(smart),取决于它是否对测试输入的结构敏感;
· 一种测试技术可以是白盒、灰盒或者黑盒(基本上不再使用),取决于它是否对程序结构信息或者程序运行信息敏感。
目前最流行的模糊测试工具主要有AFL以及AFL的扩展系列,如AFLFast、AFL++、AFLGo等。
4.1 基于覆盖率的模糊测试(Coverage-Based Fuzzing)
对于如何提高模糊测试精度和深度的探索,其中一种是利用了代码覆盖率[3](code coverage)这一概念。代码覆盖率包括函数覆盖率(function coverage)、语句覆盖率(statement coverage)、边覆盖率(edge coverage)以及条件覆盖(condition coverage)率等多种覆盖准则,这些覆盖率都在一定程度上反映了测试用例对于程序代码的测试程度,即代码覆盖率越高,测试用例对代码的测试程度越高。近期的一项工作[4]也观察到模糊测试技术的代码覆盖率和能找到程序中的错误具有强相关性。
下面的Python代码片段将给出简单的例子。当输入inp为一个正数的时候,程序执行过程中将会执行第 2 行和第 3 行的代码语句,则语句覆盖率为 2 / 6 = 33.3%。另外对于条件覆盖率来说,整个函数中函数有三个 if 条件语句,每一个 if 条件语句都有真假两种情况,则根据组合原理该函数共有 6 个条件分支。在上述情况下,程序执行过程中只有第一个 if 条件语句为真的分支被执行,则条件覆盖为 1 / 6 = 16.7%。对于输入 inp 为零和负数的情况,语句覆盖为 3 / 6 = 50% 和 4 / 6 = 66.7%,条件覆盖率为 2 / 6 = 33.3% 和 3 / 6 = 50%。
图 2
在模糊测试中对于覆盖率的利用,主要是在测试用例的生成过程中。模糊器首先生成一定个数的随机输入,这些输入被称为种子(seed)。接着模糊器将这些种子输入程序,回收程序执行的结果和预先选定的覆盖率指标。接下来模糊器会根据覆盖率高低,将覆盖率低的种子丢弃,将覆盖率高的种子保留并对这些种子进行操作生成一批新种子再作为输入运行程序。重复上述过程到覆盖率不能够更进一步提高时,终止测试。
4.2 基于变异的模糊测试(Mutation-Based Fuzzing)
由于最初的模糊器完全靠随机生成程序输入,模糊测试很难生成出符合程序要求的合法输入,以至于很难测试到程序的核心功能,同时这也是代码覆盖率极低的一个原因。于是基于变异的模糊测试被提出来解决这个问题。
变异的核心思想是对现有的种子(种子可以合法也可以不合法,但大多数情况下会使用合法的种子,这样通过变异得到的种子后代质量较高)以及通过变异得到种子后代进行操作来生成测试输入。具体的变异操作需要测试人员执行制定。假如针对一个计算机程序的一个合法的程序输入为“3+0”,那么在指定变异操作为随机增加、删除和替换字符串中的某一个字符这三种操作时,经过变异之后的输入就有多种可能,如“3 + - 0”、“3 +”、“3 / 0”。将这些通过变异得到的种子后代扔入程序中运行,这就是基于变异的模糊测试。
变异操作可以被设计为更加复杂精细的过程,例如结合其他种子质量评估指标进行变异或者进行某种针对性变异操作。上一小节末尾描述的将变异操作和覆盖率指标进行结合来反复对程序进行测试,这样可以很好地利用这两种方法的优势,从而增强模糊测试的有效性。著名的模糊测试工具AFL就是基于这样的思路开发的。
4.3 基于搜索的模糊测试(Search-Based Fuzzing)
基于搜索的模糊测试主要依赖于搜索算法。搜索算法也被称为启发式算法(heuristic algorithm),其核心思想是通过某些程序信息来启发和引导算法执行。这些算法的灵感大多是来自自然界的现象,如模拟退火算法和本小节会介绍的遗传算法。而它们之所以被称为搜索算法,是因为执行这些算法可以在较大的搜索空间中比随机算法或遍历算法更高效。同样以上一小节的计算机程序输入为例,模糊器对该程序进行模糊测试本质上就是在不断地探索其输入空间,该输入空间中包含了任意长度的合法或不合法的表示四则运算表达式的字符串。想要通过测试来挖掘出程序中隐藏的缺陷,就是在输入空间中搜索到特定的输入使得该程序在运行时出现异常行为;想要满足更广的程序覆盖率,就是在输入空间中搜索到特定的输入使得让该程序运行时执行更多的程序语句。
因此搜索算法也就自然而然地与模糊测试结合在了一起,来使模糊器生成更优质的程序测试输入。下面将对基于结合代码覆盖率的遗传算法的模糊测试进行简单的介绍。
我们构建一个简单函数 fun2,该函数将判断特定字符是否存在于输入字符串中,然后对应分支返回一个整型数字。
图 3
在这个例子中我们给定模糊器的初始种子,为“axxx”,“xxxb”、“xxxc”和“xxxd”。我们首先将这些种子作为输入来运行这个函数,得到每个种子的语句覆盖数量,分别是 3 、2 、2、2 。输入“axxx”是可以让程序执行第 2、3 和 8 行语句,而后三个输入只能执行第 2 和 8 行语句。第一轮测试已经结束,这时我们根据种子的语句覆盖数量来生成下一次测试输入集合,具体过程为从种子中选取语句覆盖数量最多的两个作为亲代来进行交叉变异(模拟自然界的遗传现象)。由于后三个种子的数据覆盖数量都为 2 ,则在选择时会随机选取。假设“axxx”和“xxxb”得到了选择,则继续对这两个种子进行交叉变异,此处交叉变异定义为交换两个种子的右半部分子串,即得到的子代为“axxb”和“xxxx”。重复这样的选择并交叉变异的过程直到子代数量等于初始种子数量。假定经过上述过程我们最后得到了“axxb”、“xxxx”、“axxc”和“xxxx”这四个子代,然后就继续重复整个执行过程,也就是将这些子代输入程序继续运行遗传算法来生成更加优质的测试输入,最终提高代码覆盖率和异常检测能力。
从上述例子中可以看到在子代中的“axxb”不仅将原本“axxx”的语句覆盖数量从 3 提升到了 4,并且能更深入地探索整个函数中的条件分支。但可以看到对于上述例子中假设得到子代整个算法已经无法再有进步了,因为这些子代不论怎么交叉变异也无法得到一个包含abc三个字母的字符串。在实际工业生产环境中算法本身会更加复杂,测试种子会经过精心生成和挑选,同时纳入更多指标进行算法引导,对交叉变异的过程也不会只是简单地字符串交换。
4.4 通过文法进行模糊测试(Fuzzing with Grammars)
当程序的输入具有一定规范和结构的(比如数据库或者API的输入),人们开始尝试通过文法(Grammar)来帮助模糊器生成合法规范的测试输入,属于前面提到的基于生成(Generation-based)的模糊测试技术。
下面展示了一个简单的文法,该文法描述的是含有小数或整数的加减乘除的四则运算表达式,如 (1 + 2) * (3.4 / 5.6 - 789) 。使用该文法,从 <start> 非终止符(non-terminal)开始展开,最终就可以得到一个可用于测试计算程序的合法的四则运算表达式。
图 4
对于表达式 (1 + 2) * 3 的部分推导过程如下,过程中遵循最左推导。推导中剩下的部分读者可以自行尝试完成。
图 5
通过这样来产生测试输入相比于纯随机的方式不仅产生的种子质量高,并且符合程序输入规范,能够节省无效测试用例的时间开销从而提高测试效率。
05
总结
随机测试为软件测试提供了一个在黑盒情况下快速和大量地测试程序的全新思路,其进阶版模糊测试更是在经过包括上述基于覆盖率、基于变异、基于搜索以及文法辅助在内的多种方法的增强之后,成为了当前工业环境下软件测试的主流选择,被广泛应用于人工智能测试、自动驾驶系统测试、数据库系统测试、API测试等各种测试场景。虽然克服了随机测试和模糊测试诞生之初的缺陷和问题,当下的模糊测试仍然有待提高进步,例如对模糊测试过程中对触发的程序错误的类型进行识别、整理和分类,以及对引发错误的根源诱因的分析等。学界和工业界也对传统静态分析工具如符号执行技术和模糊测试技术相结合的道路在不断地探索。
参考资料:
[1] “Infinite Monkey Theorem.” In Wikipedia, September 1, 2022. https://en.wikipedia.org/w/index.php?title=Infinite_monkey_theorem&oldid=1107846001.
[2] Bart Miller. 1988. https://pages.cs.wisc.edu/~bart/fuzz/CS736-Projects-f1988.pdf
[3] “Code Coverage.” In Wikipedia, July 7, 2022. https://en.wikipedia.org/w/index.php?title=Code_coverage&oldid=1096923322.
[4] Marcel Böhme, László Szekeres, and Jonathan Metzman. 2022. On the reliability of coverage-based fuzzer benchmarking. In Proceedings of the 44th International Conference on Software Engineering (ICSE '22). Association for Computing Machinery, New York, NY, USA, 1621–1633. https://doi.org/10.1145/3510003.3510230
[5] “The Fuzzing Book.” Accessed November 5, 2022. https://www.fuzzingbook.org/.