模糊测试如1989:比特轨迹博客
快速回顾
对于未阅读原论文的读者(您真的应该阅读),本节提供快速摘要和一些精选引用。
模糊测试程序通过生成随机字符流工作,可选择仅生成可打印、控制或不可打印字符。程序使用种子生成可重现结果,这是现代模糊测试器常缺乏的有用功能。一组脚本执行目标程序并检查核心转储。程序挂起需手动检测。适配器为交互式程序(1990年论文)、网络服务(1995年论文)和图形X程序(1995年论文)提供随机输入。
1990年论文测试四种处理器架构(i386、CVAX、Sparc、68020)和五种操作系统(4.3BSD、SunOS、AIX、Xenix、Dynix)。1995年论文具有类似的平台多样性。首篇论文中,25-33%的实用程序失败,具体取决于平台。1995年后续测试中,数字范围为9%-33%,GNU(在SunOS上)和Linux的崩溃概率最低。
1990年论文结论指出:(1)程序员不检查数组边界或错误代码,(2)宏使代码难以阅读和调试,(3)C语言非常不安全。特别提到极不安全的gets函数和C的类型系统。测试期间,作者在格式字符串漏洞被广泛利用前数年即发现该漏洞(见第15页)。论文以用户调查结束,询问用户修复或报告错误的频率。结果显示报告错误困难且修复兴趣不大。
1995年论文提到开源软件,并讨论其可能具有较少错误的原因。包含以下精选引用:
当我们检查导致失败的错误时,出现了一个令人沮丧的现象:许多在1990年发现并报告的错误(约40%)在1995年仍以完全相同的形式存在。…本研究中使用的技术简单且大部分自动化。难以理解供应商为何不利用免费且简单的可靠性改进来源。
模糊测试成为大型软件开发商店标准实践还需15-20年。
我还发现1990年写的以下声明对未来的预见性:
C编程风格的简洁常被推向极端;形式重于正确功能。溢出输入缓冲区的能力也是潜在的安全漏洞,如最近的互联网蠕虫所示。
测试方法
庆幸的是,30年后,Barton博士仍提供完整源代码、脚本和数据以重现其结果,这是值得称赞的目标,更多研究人员应效仿。脚本和模糊测试代码老化 surprisingly well。脚本按原样工作,模糊工具仅需微小更改即可编译和运行。
30年新软件的更新
显然,过去30年Linux软件包发生了一些变化,尽管许多测试的实用程序仍可追溯数十年。尽可能测试了1995年论文审计的相同软件的现代版本。一些软件不再可用,必须替换。每个替换的理由如下:
- cfe ⇨ cc1:这是C预处理器,等同于1995年论文中使用的。
- dbx ⇨ gdb:这是调试器,等同于1995年论文中使用的。
- ditroff ⇨ groff:ditroff不再可用。
- dtbl ⇨ gtbl:GNU Troff等效于旧dtbl实用程序。
- lisp ⇨ clisp:常见lisp实现。
- more ⇨ less:Less is more!
- prolog ⇨ swipl:Prolog有两个选择:SWI Prolog和GNU Prolog。SWI Prolog胜出,因为它更老且更全面。
- awk ⇨ gawk:GNU版本的awk。
- cc ⇨ gcc:默认C编译器。
- compress ⇨ gzip:GZip是旧Unix compress的精神继承者。
- lint ⇨ splint:GPL许可的lint重写。
- /bin/mail ⇨ /usr/bin/mail:这应是不同路径下的等效实用程序。
- f77 ⇨ fort77:Fortran77编译器有两个可能选择:GNU Fortran和Fort77。GNU Fortran推荐用于Fortran 90,而Fort77推荐用于Fortran77支持。f2c程序积极维护,变更日志记录可追溯至1989年。
结果
1989年的模糊测试方法在2018年仍发现错误。然而,已有进展。
衡量进展需要基线,幸运的是,Linux实用程序有基线。虽然1990年原始模糊测试论文早于Linux,但1995年重新测试使用相同代码在1995年Slackware 2.1.0发行版上模糊测试Linux实用程序。相关结果出现在1995年论文表3(第7-9页)。GNU/Linux在商业竞争对手中表现非常好:
自由分发的Linux版本UNIX上实用程序的失败率第二低,为9%。
让我们使用1989年的模糊测试工具比较2018年Linux实用程序与1995年Linux实用程序:
系统 | 崩溃 | 挂起 | 总测试数 | 崩溃/挂起% |
---|---|---|---|---|
Ubuntu 18.10 (2018) | 1 (f77) | 1 (spell) | 81 | 2% |
Ubuntu 18.04 (2018) | 1 (f77) | 1 (spell) | 81 | 2% |
Ubuntu 16.04 (2016) | 2 (f77, ul) | 1 (spell) | 81 | 4% |
Ubuntu 14.04 (2014) | 2 (swipl, f77) | 2 (spell, units) | 81 | 5% |
Slackware 2.1.0 (1995) | 4 (ul, flex, indent, gdb) | 1 (ctags) | 55 | 9% |
令人惊讶的是,即使是最新Ubuntu版本,Linux崩溃和挂起计数仍不为零。f77调用的f2c程序触发分段错误,spell程序在两个测试输入上挂起。
错误是什么?
错误足够少,我可以手动调查一些问题的根本原因。一些结果,如glibc中的错误,令人惊讶,而其他如sprintf到固定大小缓冲区,则可预测。
ul崩溃
ul中的错误实际上是glibc中的错误。具体来说,是2016年此处和此处(另一个人在ul中触发)报告的问题。根据错误跟踪器,它仍未修复。由于在Ubuntu 18.04及更新版本上无法触发该问题,错误已在发行版级别修复。从错误跟踪器评论看,核心问题可能非常严重。
f77崩溃
f77程序由fort77包提供,它本身是f2c的包装脚本,f2c是Fortran77到C源代码转换器。调试f2c显示崩溃在errstr函数中,当打印过长的错误消息时。f2c源代码显示它使用sprintf将可变长度字符串写入固定大小缓冲区:
|
|
此问题看起来自f2c诞生以来就存在。根据变更日志,f2c程序至少自1989年存在。1995年模糊测试重新测试未在Linux上测试Fortran77编译器,但如果测试了,此问题会更早发现。
spell挂起
这是经典死锁的绝佳例子。spell程序通过管道将拼写检查委托给ispell程序。spell程序逐行读取文本并对ispell发出行大小的阻塞写入。然而,ispell程序每次最多读取BUFSIZ/2字节(我系统上为4096字节),并发出阻塞写入以确保客户端收到迄今处理的拼写数据。两个不同的测试输入导致spell向ispell写入超过4096字符的行,引起死锁:spell等待ispell读取整行,而ispell等待spell确认它读取了初始更正。
units挂起
初步检查这似乎是无限循环条件。挂起看起来在libreadline中而非units,尽管新版本units不受此错误影响。变更日志指示添加了一些输入过滤,可能无意中修复了此问题。虽然彻底调查原因和修正超出本博客范围,但可能仍有方法向libreadline提供挂起输入。
swipl崩溃
为完整性,我想包括swipl崩溃。然而,我没有彻底调查它,因为崩溃早已修复且看起来相当良性。崩溃实际上是字符转换期间触发的断言(即不应发生的事发生了):
|
|
应用程序崩溃从来不好,但至少在这种情况下程序能告诉某事不对,它早期且大声地失败。
结论
模糊测试是过去30年中查找程序中错误的简单可靠方法。虽然模糊测试研究进展迅速,但即使重用30年代码的最简单尝试也能成功识别现代Linux实用程序中的错误。
原始模糊测试论文很好地预言了C语言的危险及其将导致数十年的安全问题。它们令人信服地论证C使编写不安全代码太容易,应尽可能避免。更直接地,论文显示即使幼稚的模糊测试仍暴露错误,此类测试应作为标准软件开发实践纳入。可悲的是,此建议数十年未被遵循。
我希望您喜欢这30年回顾。请期待本系列下一部分:2000年的模糊测试,它将调查Windows 10应用程序在面临Windows消息模糊测试器时与Windows NT/2000等效物的比较。我想您已经能猜到答案。