1989年的模糊测试技术:在2018年仍能发现Linux漏洞

本文通过重现1989年的原始模糊测试方法,在现代Ubuntu系统上发现多个漏洞,包括glibc缺陷、缓冲区溢出和死锁问题,证明即使最基础的模糊测试仍能有效发现当代软件的安全隐患。

模糊测试如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博士仍然提供完整的源代码、脚本和数据以重现他的结果,这是一个值得更多研究人员效仿的可称赞目标。脚本和模糊测试代码老化得惊人地好。脚本按原样工作,模糊测试工具仅需少量更改即可编译和运行。

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将可变长度字符串写入固定大小缓冲区:

1
2
3
4
5
6
7
errstr(const char *s, const char *t)
#endif
{
  char buff[100];
  sprintf(buff, s, t);
  err(buff);
}

这个问题看起来自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崩溃。然而,我没有彻底调查它,因为崩溃早已修复且看起来相当良性。崩溃实际上是在字符转换期间触发的断言(即不应发生的情况发生了):

1
2
3
4
5
6
[Thread 1] pl-fli.c:2495: codeToAtom: Assertion failed: chrcode >= 0
C-stack trace labeled "crash":
  [0] __assert_fail+0x41
  [1] PL_put_term+0x18e
  [2] PL_unify_text+0x1c4

应用程序崩溃从来不是好事,但至少在这种情况下程序能察觉到有问题,并且早期且大声地失败。

结论

模糊测试在过去30年中一直是发现程序中错误的简单可靠方法。虽然模糊测试研究进展迅速,但即使是最简单的尝试,重用30年代码,也能成功识别现代Linux实用程序中的错误。

原始模糊测试论文很好地预测了C语言的危险及其将导致数十年的安全问题。它们令人信服地论证了C使编写不安全代码太容易,应尽可能避免。更直接地,论文显示即使朴素的模糊测试仍能暴露错误,此类测试应作为标准软件开发实践纳入。可悲的是,这个建议数十年来未被遵循。

我希望您喜欢这个30年回顾。请关注本系列的下一部分:2000年的模糊测试,它将调查Windows 10应用程序在面临Windows消息模糊测试器时与Windows NT/2000等效程序的比较。我想您已经能猜到答案。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计