1989年的模糊测试:穿越30年的漏洞挖掘之旅
快速回顾
对于没有阅读原始论文的读者(你真的应该读一读),本节提供快速摘要和一些精选引用。
模糊测试程序通过生成随机字符流工作,可选择生成仅可打印、控制或不可打印字符。程序使用种子生成可重现结果,这是现代模糊测试工具经常缺乏的有用功能。一组脚本执行目标程序并检查核心转储。程序挂起需要手动检测。适配器为交互式程序(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博士仍然提供完整的源代码、脚本和数据来重现他的结果,这是一个值得称赞的目标,更多研究人员应该效仿。脚本和模糊测试代码老化得惊人地好。脚本按原样工作,模糊测试工具只需要少量更改即可编译和运行。
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等效程序的比较。我想你已经可以猜到答案了。