当前位置: 首页 >> 程序设计 >> 防止如今最常见的程序缺陷: 缓冲区溢出
 

防止如今最常见的程序缺陷: 缓冲区溢出

作者:      来源:zz     发表时间:2006-06-06     浏览次数:      字号:    


另一种方法称为“分割控制和数据堆栈”—— 基本的思路是将堆栈分割为两个堆栈,一个用于存储控制信息(比如“返回”地址),另一个用于控制其他所有数据。Xu et al. 在 gcc 中实现了这种方法,StackShield 在汇编程序中实现了这种方法。这样使得操纵返回地址困难多了,但它不会阻止改变调用函数的数据的缓冲区溢出攻击。

事实上还有其他方法,包括随机化可执行程序的位置;Crispen 的“PointGuard”将这种探测仪思想引申到了堆中,等等。如何保护当今的计算机现在已成了一项活跃的研究任务。

一般保护是不足够的
如此多不同的方法意味着什么呢?对用户来说,好的一面在于大量创新的方法正在试验之中;长期看来,这种“竞争”会更容易看出哪种方法最好。而且,这种多样性还使得攻击者躲避所有这些方法更加困难。然而,这种多样性也意味着开发人员需要避免 编写会干扰其中任何一种方法的代码。这在实践上是很容易的;只要不编写对堆栈桢执行低级操作或对堆栈的布局作假设的代码就行了。即使不存在这些方法,这也是一个很好的建议。

操作系统供应商需要参与进来就相当明显了:至少挑选一种方法,并使用它。缓冲区溢出是第一号的问题,这些方法中最好的方法通常能够减轻发行套件中几乎半数已知缺陷的影响。可以证明,不管是基于探测仪的方法更好,还是基于非可执行堆栈的方法更好,它们都具有各自的优点。可以将它们结合起来使用,但是少数方法不支持这样使用,因为附加的性能损失使得这样做不值得。我并没有其他意思,至少就这些方法本身而言是这样;libsafe 和分割控制及数据堆栈的方法在它们所提供的保护方面都具有局限性。当然,最糟糕的解决办法就是根本不对这个第一号的缺陷提供保护。还没有实现一种方法的软件供应商需要立即计划这样做。从 2004 年开始,用户应该开始避免使用这样的操作系统,即它们至少没有对缓冲区溢出提供某种自动保护机制。

然而,没有哪种方法允许开发人员忽略缓冲区溢出。所有这些方法都能够被攻击者破坏。攻击者也许能够通过改变函数中其他数据的值来利用缓冲区溢出;没有哪种方法能够防止这点。如果能够插入某些难于创建的值(比如 NUL 字符),那么这其中的许多方法都能被攻击者绕开;随着多媒体和压缩数据变得更加普遍,攻击者绕开这些方法就更容易了。从根本上讲,所有这些方法都能减轻从程序接管攻击到拒绝服务攻击的缓冲区溢出攻击所带来的破坏。遗憾的是,随着计算机系统在更多关键场合的使用,即使拒绝服务通常也是不可接受的。因而,尽管发行套件应该至少包括一种适当的防御方法,并且开发人员应该使用(而不是反对)那些方法,但是开发人员仍然需要最初就编写无缺陷的软件。

C/C++ 解决方案
针对缓冲区溢出的一种简单解决办法就是转为使用能够防止缓冲区溢出的语言。毕竟,除了 C 和 C++ 外,几乎每种高级语言都具有有效防止缓冲区溢出的内置机制。但是许多开发人员因为种种原因还是选择使用 C 和 C++。那么您能做什么呢?

事实证明存在许多防止缓冲区溢出的不同技术,但它们都可划分为以下两种方法:静态分配的缓冲区和动态分配的缓冲区。首先,我们将讲述这两种方法分别是什么。然后,我们将讨论静态方法的两个例子(标准 C strncpy/strncat 和 OpenBSD 的 strlcpy/strlcat),接着讨论动态方法的两个例子(SafeStr 和 C++ 的 std::string)。

重要选择:静态和动态分配的缓冲区
缓冲区具有有限的空间。因此实际上存在处理缓冲区空间不足的两种可能方式。

* “静态分配的缓冲区”方法:也就是当缓冲区用完时,您抱怨并拒绝为缓冲区增加任何空间。
* “动态分配的缓冲区”方法:也就是当缓冲区用完时,动态地将缓冲区大小调整到更大的尺寸,直至用完所有内存。

静态方法具有一些缺点。事实上,静态方法有时可能会带来不同的缺陷。静态方法基本上就是丢弃“过多的”数据。如果程序无论如何还是使用了结果数据,那么攻击者会尝试填满缓冲区,以便在数据被截断时使用他希望的任何内容来填充缓冲区。如果使用静态方法,应该确保攻击者能够做的最糟糕的事情不会使得预先的假设无效,而且检查最终结果也是一个好主意。

动态方法具有许多优点:它们能够向上适用于更大的问题(而不是带来任意的限制),而且它们没有导致安全问题的字符数组截断问题。但它们也具有自身的问题:在接受任意大小的数据时,可能会遇到内存不足的情况 —— 而这在输入时也许不会发生。任何内存分配都可能会失败,而编写真正很好地处理该问题的 C 或 C++ 程序是很困难的。甚至在内存真正用完之前,也可能导致计算机变得太忙而不可用。简而言之,动态方法通常使得攻击者发起拒绝服务攻击变得更加容易。因此仍然需要限制输入。此外,必须小心设计程序来处理任意位置的内存耗尽问题,而这不是一件容易的事情。

标准 C 库方法
最简单的方法之一是简单地使用那些设计用于防止缓冲区溢出的标准 C 库函数(即使在使用 C ++,这也是可行的),特别是 strncpy(3) 和 strncat(3)。这些标准 C 库函数一般支持静态分配方法,也就是在数据无法装入缓冲区时丢弃它。这种方法的最大优点在于,您可以肯定这些函数在任何机器上都可用,并且任何 C/C++ 开发人员都会了解它们。许许多多的程序都是以这种方式编写的,并且确实可行。

遗憾的是,要正确地做到这点却是令人吃惊的困难。下面是其中的一些问题:

* strncpy(3) 和 strncat(3) 都要求您给出 剩余的 空间,而不是给出缓冲区的总大小。这之所以会成为问题是因为,虽然缓冲区的大小一经分配就不会变化,但是缓冲区中剩余的空间量会在每次添加或删除数据时发生变化。这意味着程序员必须始终跟踪或重新计算剩余的空间。这种跟踪或重新计算很容易出错,而任何错误都可能给缓冲区攻击打开方便之门。

* 在发生了溢出(和数据丢失)时,两个函数都不会给出简单的报告,因此如果要检测缓冲区溢出,程序员就必须做更多的工作。

* 如果源字符串至少和目标一样长,那么函数 strncpy(3) 还不会使用 NUL 来结束字符串;这可能会在以后导致严重破坏。因而,在运行 strncpy(3)之后,您通常需要重新结束目标字符串。

* 函数 strncpy(3)还可以用来仅把源字符串的一部分复制到目标中。 在执行这个操作时,要复制的字符的数目通常是基于源字符串的相关信息来计算的。 这样的危险之处在于,如果忘了考虑可用的缓冲区空间,那么 即使在使用 strncpy(3) 时也可能会留下缓冲区攻击隐患。这个函数也不会复制 NUL 字符,这可能也是一个问题。

* 可以通过一种防止缓冲区溢出的方式使用 sprintf(),但是意外地留下缓冲区溢出攻击隐患是非常容易的。sprintf() 函数使用一个控制字符串来指定输出格式,该控制字符串通常包括“%s”(字符串输出)。如果指定字符串输出的精确指定符(比如 %.10s),那么您就能够通过指定输出的最大长度来防止缓冲区溢出。甚至可以使用“*”作为精确指定符(比如“%.*s”),这样您就可以传入一个最大长度值,而不是在控制字符串中嵌入最大长度值。这样的问题在于,很容易就会不正确地使用 sprintf()。一个“字段宽度”(比如“%10s”)仅指定了最小长度 —— 而不是最大长度。“字段宽度”指定符会留下缓冲区溢出隐患,而字段宽度和精确宽度指定符看起来几乎完全相同 —— 唯一的区别在于安全的版本具有一个点号。另一个问题在于,精确字段仅指定一个参数的最大长度,但是缓冲区需要针对组合起来的数据的最大尺寸调整大小。

* scanf() 系列函数具有一个最大宽度值,至少 IEEE Standard 1003-2001 清楚地规定这些函数一定不能读取超过最大宽度的数据。遗憾的是,并非所有规范都清楚地规定了这一点,我们不清楚是否所有实现都正确地实现了这些限制(这在如今的 GNU/Linux 系统上就 不能 正确地工作)。如果您依赖它,那么在安装或初始化期间运行小测试来确保它能正确工作,这样做将是明智的。

strncpy(3) 还存在一个恼人的性能问题。从理论上讲,strncpy(3) 是 strcpy(3)的安全替代者,但是 strncpy(3) 还会在源字符串结束时使用 NUL 来填充整个目标空间。 这是很奇怪的,因为实际上并不存在这样做的很好理由,但是它从一开始就是这样,并且有些程序还依赖这个特性。这意味着从 strcpy(3) 切换到 strncpy(3) 会降低性能 —— 这在如今的计算机上通常不是一个严重的问题,但它仍然是有害的。

那么可以使用标准 C 库的例程来防止缓冲区溢出吗?是的,不过并不容易。如果计划沿着这条路线走,您需要理解上述的所有要点。或者,您可以使用下面几节将要讲述的一种替代方法。

OpenBSD 的 strlcpy/strlcat
OpenBSD 开发人员开发了一种不同的静态方法,这种方法基于他们开发的新函数 strlcpy(3) 和 strlcat(3)。这些函数执行字符串复制和拼接,不过更不容易出错。这些函数的原型如下:

size_t strlcpy (char *dst, const char *src, size_t size);
size_t strlcat (char *dst, const char *src, size_t size);

strlcpy() 函数把以 NUL 结尾的字符串从“src”复制到“dst”(最多 size-1 个字符)。strlcat()函数把以 NUL 结尾的字符串 src 附加到 dst 的结尾(但是目标中的字符数目将不超过 size-1)。

初看起来,它们的原型和标准 C 库函数并没有多大区别。但是事实上,它们之间存在一些显著区别。这些函数都接受目标的总大小(而不是剩余空间)作为参数。这意味着您不必连续地重新计算空间大小,而这是一项易于出错的任务。此外,只要目标的大小至少为 1,两个函数都保证目标将以 NUL 结尾(您不能将任何内容放入零长度的缓冲区)。如果没有发生缓冲区溢出,返回值始终是组合字符串的长度;这使得检测缓冲区溢出真正变得容易了。

遗憾的是,strlcpy(3) 和 strlcat(3) 并不是在类 UNIX 系统的标准库中普遍可用。OpenBSD 和 Solaris 将它们内置在 <string.h> 中,但是 GNU/Linux 系统却不是这样。这并不是一件那么困难的事情;因为当底层系统没有提供它们时,您甚至可以将一些小函数直接包括在自己的程序源代码中。

SafeStr
Messier 和 Viega 开发了“SafeStr”库,这是一种用于 C 的动态方法,它自动根据需要调整字符串的大小。使用 malloc() 实现所使用的相同技巧,Safestr 字符串很容易转换为常规的 C“char *” 字符串:safestr 在传递指针“之前”的地址处存储重要信息。这种技术的优点在于,在现有程序中使用 SafeStr 将会很容易。SafeStr 还支持“只读”和“受信任”的字符串,这也可能是有用的。这种方法的一个问题在于它需要 XXL(这是一个给 C 添加异常处理和资源管理支持的库),因此您实际上要仅为了处理字符串而引入一个重要的库。Safestr 是在开放源代码的 BSD 风格的许可证下发布的。

C++ std::string
针对 C++ 用户的另一种解决方案是标准的 std::string类,这是一种动态的方法(缓冲区根据需要而增长)。它几乎是不需要伤脑筋的,因为 C++ 语言直接支持该类,因此不需要做特殊的工作就可使用它,并且其他库也可能会使用它。就其本身而言,std::string 通常会防止缓冲区溢出,但是如果通过它提取一个普通 C 字符串(比如使用 data() 或 c_str()),那么上面讨论的所有问题都会重新出现。还要记住 data()并不总是返回以 NUL 结尾的字符串。

由于种种历史原因,许多 C++ 库和预先存在的程序都创建了它们自己的字符串类。这可能使得 std::string 更难于使用,并且在使用那些库或修改那些程序时效率很低,因为不同的字符串类型将不得不连续地来回转换。并非其他所有那些字符串类都会防止缓冲区溢出,并且如果它们对 C 不受保护的 char* 类型执行自动转换,那么缓冲区溢出缺陷很容易引入那些类中。

工具
有许多工具可以在缓冲区溢出缺陷导致问题之前帮助检测它们。 例如,像我的 Flawfinder 和 Viega 的 RATS 这样的工具能够搜索源代码,识别出可能被不正确地使用的函数(基于它们的参数来归类)。这些工具的一个缺点在于,它们不是完美的 —— 它们会遗漏一些缓冲区溢出缺陷,并且它们会识别出一些实际上不是问题的“问题”。但是使用它们仍然是值得的,因为与手工查找相比,它们将帮助您在短得多的时间内识别出代码中的潜在问题。

结束语
借助知识、谨慎和工具,C 和 C++ 中的缓冲区溢出缺陷是可以防止的。不过做起来并没有那么容易,特别是在 C 中。如果使用 C 和 C++ 来编写安全的程序,您需要真正理解缓冲区溢出和如何防止它们。

一种替代方法是使用另一种编程语言,因为如今的几乎其他所有语言都能防止缓冲区溢出。但是使用另一种语言并不会消除所有问题。许多语言依赖 C 库,并且许多语言还具有关闭该保护特性的机制(为速度而牺牲安全性)。但是即便如此,不管您使用哪种语言,开发人员都可能会犯其他许多错误,从而带来引入缺陷。

不管您做什么,开发没有错误的程序都是极其困难的,即使最仔细的复查通常也会遗漏其中一些错误。 开发安全程序的最重要方法之一是 最小化特权。那意味着程序的各个部分应该具有它们需要的唯一特权,一点也不能多。这样,即使程序具有缺陷(谁能无过?),也可能会避免将该缺陷转化为安全事故。但是在实践中如何做到这点呢?下一篇文章将研究如何实际地最小化 Linux/UNIX 系统中的特权,以便您能防止自己不可避免的错误所带来安全隐患。

参考资料

* 阅读 developerWorks 上 David 的 安全编程 专栏系列中的所有文章连载。

* David 的书 Secure Programming for Linux and Unix HOWTO 详细介绍了如何开发安全的软件。

* “The What, Why, and How of the 1988 Internet Worm”更详细地介绍了 1988 年的 Morris 蠕虫事件。

* C. Ian Kyer、Warren J. Sheffer 和 Bruce Salvatore、Fasken Martineau DuMoulin LLP 所著的 New IT Concerns in the Age of Anti-Terrorism: How the Canadian Government has Reacted and How Business Should React 指出,Morris 蠕虫使得当时大约有 88,000 台计算机的 Internet 中的 10% 的计算机崩溃。

* Steve Burnett and Stephen Paine 所著的 RSA Security's Official Guide to Cryptography(McGraw-Hill,2001 年) 在第 11 章(“Doing it Wrong: The break-ins”)中指出,Morris 蠕虫使得大约 10% 的 Internet 崩溃,该书还对安全故障提出了其他有趣的评论。

* CERT(R) Advisory CA-2001-19 "Code Red" Worm Exploiting Buffer Overflow In IIS Indexing Service DLL 更详细地介绍了 Code Red 病毒。

* “Frontline: Cyber War!: The Warnings?”总结了各种攻击及其已知的影响,包括 Code Red 和 Slammer。

* Aleph One (Elias Levy) 撰写的 Smashing The Stack For Fun And Profit 一文(Phrack Magazine,1996 年 11 月 8 日,第 49 期第 14 篇文章)阐述了 stack-smashing 攻击是如何进行的。在该文刊出之前许多年就已经在发生 stack-smashing 攻击,但是该文很好地描述了这些攻击。

* David 的文章“More than a Gigabuck: Estimating GNU/Linux's Size”研究了 Red Hat Linux 7.1 的源代码。 结果发现这个发行套件包括 3 千多万个实际源代码行(source lines of code,SLOC),其中 86% 都是用 C 或者 C++ 编写的。 该文还发现,如果采用美国的传统专有手段,开发这个 Linux 发行套件将需要 10 亿美元和 8,000 个人年的成本(以 2000 年的美元币值计)。

* Crispin Cowan、Perry Wagle、Calton Pu、Steve Beattie 和 Jonathan Walpole 撰写的 Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade 一文讨论了防止 stack-smashing 攻击的 Stackguard 方法;该 Web 站点还包含 Cowan 用于防止攻击的其他方法的参考资料。该文包括 Bugtraq 1999 年非正式调查 的摘要。

* IBM 的 stack-smashing protector (ssp,又名为 ProPolice) Web 站点提供了关于 ssp 的更多信息。ssp 由 OpenBSD 所使用。

* StackGuard 还是一个基于探测仪的防御系统。StackGuard 3 计划被合并到 GCC 3.x 主流编译器中。StackGuard 由 Immunix 维护并在其中使用。

* “Linux kernel patch from the Openwall Project”讨论了 Solar Designer 的当前 Linux 内核补丁(包括非可执行的堆栈组件)。

* “Linux: Exec Shield Overflow Protection” 讨论了 Ingo Molnar 的 exec-shield 方法。

* OpenWall GNU/Linux (OWL) 使用了 Solar Designer 的非可执行堆栈补丁版本,而 Red Hat Fedora 则使用了 exec shield —— 两种选择都可以实现非可执行的堆栈(大多数时候)。

* Libsafe 是一个用于保护某些标准 C 函数避免 stack-smashing 攻击的库。Libsafe 已整合到较新版本的 Mandrake Linux 发行套件中。

* 由 Jun Xu、Zbigniew Kalbarczyk、Sanjay Patel 和 Ravishankar K. Iyer 撰稿的 “Architecture Support for Defending Against Buffer Overflow Attacks”一文讨论了分割堆栈方法,还简要讨论了缓冲区溢出攻击的频发程度。

* Messier 和 Viega 编写的 安全 C 字符串(Safe C String,SafeStr)库 是一个有趣的库,它提供简单和安全的 C 字符串处理。

* XXL 库(http://www.zork.org/xxl/ ) 是一个用于 C 的线程安全的异常处理和和资源管理库。它在 BSD 许可证下可用。

* Flawfinder 项目主页(http://www.dwheeler.com/flawfinder ) 提供 Flawfinder,这是一个在 GPL 下授予许可证的工具,用于查找 C 和 C++ 程序中的问题。

* John Viega 的 粗略安全审核工具(或称为 RATS)是一个免费的开放源代码工具,用于审核代码和应用程序。

* O'Reilly & Associates 正在以 安全编程技巧 为题,出版 Gene Spafford、Simson Garfinkel 和 Alan Schwartz 所著的 Practical UNIX & Internet Security, 3rd Edition 中的系列文章摘选。

* “自我管理数据缓冲区内存”(developerWorks,2004 年 1 月)讲述了如何在 C 代码中仅当实际数据变得可用时才分配内存 —— 在恰当地使用时,这种方法最大限度地降低了缓冲区溢出的可能。

* 在 developerWorks Linux 专区 可以找到为 Linux 开发人员准备的更多参考资料。

* 在 Developer Bookstore 的 Linux 专区 可以找到各种有关 Linux 的图书。

关于作者
David A. Wheeler 是计算机安全方面的专家,他长期致力于改进大型和高风险软件系统的开发技术。Wheeler 先生是 Secure Programming for Linux and UNIX HOWTO ( http://www.dwheeler.com/secure-programs ) 一书的作者,并且是一位 Common Criteria 的验证者。Wheeler 先生还编写过文章“Why Open Source Software/Free Software? Look at the Numbers!” 和 Springer-Verlag 出版的图书 Ada95: The Lovelace Tutorial,他还是由 IEEE 出版的 Software Inspection: An Industry Best Practice 一书的合著者和首席编辑。本文介绍了作者的看法,不一定代表 Institute for Defense Analyses 的立场。可以通过 dwheelerNOSPAM@dwheeler.com 与 David 联系。

[1] [2]

编辑 webmaster

 
 
 
评论更多>>
 
 
发表
 
姓名: QQ:
性别: MSN:
E-mail: 主页:
评分: 1 2 3 4 5
评论内容:
验证码:
  
  • 请遵守《互联网电子公告服务管理规定》及中华人民共和国其他各项有关法律法规。
  • 严禁发表危害国家安全、损害国家利益、破坏民族团结、破坏国家宗教政策、破坏社会稳定、侮辱、诽谤、教唆、淫秽等内容的评论 。
  • 用户需对自己在使用本站服务过程中的行为承担法律责任(直接或间接导致的)。
  • 本站管理员有权保留或删除评论内容。
  • 评论内容只代表网友个人观点,与本网站立场无关。
  •