1989年模糊测试技术重现:在现代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博士仍提供完整源代码、脚本和数据以重现其结果,这是值得称赞的目标,更多研究人员应效仿。脚本和模糊测试代码老化 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将可变长度字符串写入固定大小缓冲区:

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 设计