ssp编译
① 大佬 在Xcode中如何开启SSP栈溢出保护编译
在虚拟机下,安装Mac OS X。下载iOS SDK(包含Xcode),在虚拟机环境下安装,安装时候就可以,直接打开Xcode进行编程了。
② 帮忙说一下集成电路中的各种标语
电源电路、音频电路(含MIC输入部分和DAC音频输出部分)、复位电路等
③ 制作伪春菜需要什么软件
针对你的问题。我给出的回答是伪春菜不需要特别的软件。
如果说需要的话,就是对于shell使用的图像处理软件:推荐使用的比较上手的,如photoshop。
以及一些辅助性质的软件:比如纸娃娃机,coordin之类的。tama是伪春菜系统自带的除错软件。
因为伪春菜本身是完全的明文代码加上调用的图片,所以完全不需要编译就可以直接使用。所以调教起来也相对方便。
所以仅仅只是编写伪春菜的话,只需要一个记事本就可以了。
需要的图片就用photoshop之类的解决。
④ 如何解决:编译时提示`__sync_add_and_fetch' undeclared
gcc version 3.4.4 (Gentoo 3.4.4, ssp-3.4.4-1.0, pie-8.7.8)
cc -o Xtest prog-2.cc -lX11
/tmp/ccD1lG9s.o:(.eh_frame+0x11): undefined reference to `__gxx_personality_v0'
collect2: ld returned 1 exit status
解决方法。。
Ok, solved. The problem was that the file extention was cpp, not c. I
could either rename the file or add -lstdc++ as a gcc flag.
http://mail.gnome.org/archives/gtk-app-devel-list/2004-October/msg00207.html
或则直接用c++编译器,比如g++就没有这个问题。。
Folks,
Just in case it isn't in other responses from earlier, a solution to this
(presumably) is to use the C++ compiler as the linker instead of the C
compiler. This can be accomplished by editing the makefile and changing
"LD=gcc" to "LD=c++" or more generally to "LD=$(CXX)". Or just do the
same on the command line "make LD=c++".
Presumably the 3.7 configure scripts will proce a Makefile with this
done if they don't already.
Actually, I don't get this error myself on Red Hat 8, with gcc 3.2. It would
appear to be because the libgdal.1.1.so is already linked against
/usr/lib/libstdc++.so.5 which provides the C++ support.
If anyone running into this problem finds that my suggested change to LD
does not solve their problem, I would encourage them to contact me directly
by email, and we will resolve the problem and I will try to see the fix gets
into 3.7 (and perhaps a 3.6.5) if it has not already.
PS. If you don't need OGR just configure without it and you won't run into
this problem.
Best regards,
http://lists.dmsolutions.ca/pipermail/mapserver-users/2003-March/007921.html
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u/712/showart_50569.html
⑤ GCC编译器局部变量地址分配为什么总是从低
原因:GCC的堆栈保护技术—— canary的使用。
使用的原因是为了防止某些溢出的攻击。但是只是溢出时方向发生了改变,并没有起到太大的作用,可能对于传统的一些攻击方法有用。
GCC 中的堆栈保护实现
Stack Guard 是第一个使用 Canaries 探测的堆栈保护实现,它于 1997 年作为 GCC 的一个扩展发布。最初版本的 Stack Guard 使用 0x00000000 作为 canary word。尽管很多人建议把 Stack Guard 纳入 GCC,作为 GCC 的一部分来提供堆栈保护。但实际上,GCC 3.x 没有实现任何的堆栈保护。直到 GCC 4.1 堆栈保护才被加入,并且 GCC4.1 所采用的堆栈保护实现并非 Stack Guard,而是 Stack-smashing Protection(SSP,又称 ProPolice)。
SSP 在 Stack Guard 的基础上进行了改进和提高。它是由 IBM 的工程师 Hiroaki Rtoh 开发并维护的。与 Stack Guard 相比,SSP 保护函数返回地址的同时还保护了栈中的 EBP 等信息。此外,SSP 还有意将局部变量中的数组放在函数栈的高地址,而将其他变量放在低地址。这样就使得通过溢出一个数组来修改其他变量(比如一个函数指针)变得更为困难。
⑥ (畅想)如何改进编程模式及cpu体系结构防止缓冲区溢出,不要求标准答案,只要想象得有道理
缓冲区溢出。本文首先解释什么是缓冲区溢出,以及它们为何如此常见和如此危险。然后讨论广泛用于解决缓冲区溢出的新 linux 和 UNIX 方法 ―― 以及为什么这些方法还不足够。随后将展示 C/C++ 程序中防止缓冲区溢出的各种方法,同时包括静态调整大小的方法(比如标准的 C 库和 OpenBSD/strlcpy 解决方案)和动态调整大小的解决方案,以及一些将为您提供帮助的工具。最后,本文以一些关于缓冲区溢出缺陷的未来发展形势的预测来结束全文的讨论。
如果希望自己的程序是安全的,您需要知道什么是缓冲区溢出,如何防止它们,可以采用哪些最新的自动化工具来防止它们(以及为什么这些工具还不足够),还有如何在您自己的程序中防止它们。
什么是缓冲区溢出?
缓冲区以前可能被定义为“包含相同数据类型的实例的一个连续计算机内存块”。在 C 和 C++ 中,缓冲区通常是使用数组和诸如 malloc() 和 new 这样的内存分配例程来实现的。极其常见的缓冲区种类是简单的字符数组。 溢出 是指数据被添加到分配给该缓冲区的内存块之外。
如果攻击者能够导致缓冲区溢出,那么它就能控制程序中的其他值。虽然存在许多利用缓冲区溢出的方法,不过最常见的方法还是“stack-smashing”攻击。Elias Levy (又名为 Aleph One)的一篇经典文章“Smashing the Stack for Fun and Profit”解释了 stack-smashing 攻击,Elias Levy 是 Bugtraq 邮件列表(请参阅 参考资料 以获得相关链接)的前任主持人。
清单 1. 一个简单的程序
void function1(int a, int b, int c) {
char buffer1[5];
gets(buffer1); /* DON'T DO THIS */
}
void main() {
function(1,2,3);
}
假设使用 gcc 来编译清单 1 中的简单程序,在 X86 上的 Linux 中运行,并且紧跟在对 gets() 的调用之后中止。此时的内存内容看起来像什么样子呢?答案是它看起来类似图 1,其中展示了从左边的低位地址到右边的高位地址排序的内存布局。
图 1. 堆栈视图
内存的底部 内存的顶部
buffer1 sfp ret a b c
<--- 增长 --- [ ] [ ] [ ] [ ] [ ] [ ] ...
为什么缓冲区溢出如此常见?
在几乎所有计算机语言中,不管是新的语言还是旧的语言,使缓冲区溢出的任何尝试通常都会被该语言本身自动检测并阻止(比如通过引发一个异常或根据需要给缓冲区添加更多空间)。但是有两种语言不是这样:C 和 C++ 语言。C 和 C++ 语言通常只是让额外的数据乱写到其余内存的任何位置,而这种情况可能被利用从而导致恐怖的结果。更糟糕的是,用 C 和 C++ 编写正确的代码来始终如一地处理缓冲区溢出则更为困难;很容易就会意外地导致缓冲区溢出。除了 C 和 C++ 使用得 非常广泛外,上述这些可能都是不相关的事实;例如,Red Hat Linux 7.1 中 86% 的代码行都是用 C 或 C ++ 编写的。因此,大量的代码对这个问题都是脆弱的,因为实现语言无法保护代码避免这个问题。
在 C 和 C++ 语言本身中,这个问题是不容易解决的。该问题基于 C 语言的根本设计决定(特别是 C 语言中指针和数组的处理方式)。由于 C++ 是最兼容的 C 语言超集,它也具有相同的问题。存在一些能防止这个问题的 C/C++ 兼容版本,但是它们存在极其严重的性能问题。而且一旦改变 C 语言来防止这个问题,它就不再是 C 语言了。许多语言(比如 Java 和 C#)在语法上类似 C,但它们实际上是不同的语言,将现有 C 或 C++ 程序改为使用那些语言是一项艰巨的任务。
然而,其他语言的用户也不应该沾沾自喜。有些语言存在允许缓冲区溢出发生的“转义”子句。Ada 一般会检测和防止缓冲区溢出(即针对这样的尝试引发一个异常),但是不同的程序可能会禁用这个特性。C# 一般会检测和防止缓冲区溢出,但是它允许程序员将某些例程定义为“不安全的”,而这样的代码 可能 会导致缓冲区溢出。因此如果您使用那些转义机制,就需要使用 C/C++ 程序所必须使用的相同种类的保护机制。许多语言都是用 C 语言来实现的(至少部分是用 C 语言来实现的 ),并且用任何语言编写的所有程序本质上都依赖用 C 或 C++ 编写的库。因此,所有程序都会继承那些问题,所以了解这些问题是很重要的。
导致缓冲区溢出的常见 C 和 C++ 错误
从根本上讲,在程序将数据读入或复制到缓冲区中的任何时候,它需要在复制 之前检查是否有足够的空间。能够容易看出来的异常就不可能会发生 ―― 但是程序通常会随时间而变更,从而使得不可能成为可能。
遗憾的是,C 和 C++ 附带的大量危险函数(或普遍使用的库)甚至连这点(指检查空间)也无法做到。程序对这些函数的任何使用都是一个警告信号,因为除非慎重地使用它们,否则它们就会成为程序缺陷。您不需要记住这些函数的列表;我的真正目的是说明这个问题是多么普遍。这些函数包括 strcpy(3)、strcat(3)、sprintf(3) (及其同类 vsprintf(3) )和 gets(3) 。 scanf() 函数集( scanf(3)、fscanf(3)、sscanf(3)、vscanf(3)、vsscanf(3) 和 vfscanf(3) )可能会导致问题,因为使用一个没有定义最大长度的格式是很容易的(当读取不受信任的输入时,使用格式“%s”总是一个错误)。
其他危险的函数包括 realpath(3)、getopt(3)、getpass(3)、streadd(3)、strecpy(3) 和 strtrns(3) 。 从理论上讲, snprintf() 应该是相对安全的 ―― 在现代 GNU/Linux 系统中的确是这样。但是非常老的 UNIX 和 Linux 系统没有实现 snprintf() 所应该实现的保护机制。
Microsoft 的库中还有在相应平台上导致同类问题的其他函数(这些函数包括 wcscpy()、_tcscpy()、_mbscpy()、wcscat()、_tcscat()、_mbscat() 和 CopyMemory() )。注意,如果使用 Microsoft 的 MultiByteToWideChar() 函数,还存在一个常见的危险错误 ―― 该函数需要一个最大尺寸作为字符数目,但是程序员经常将该尺寸以字节计(更普遍的需要),结果导致缓冲区溢出缺陷。
另一个问题是 C 和 C++ 对整数具有非常弱的类型检查,一般不会检测操作这些整数的问题。由于它们要求程序员手工做所有的问题检测工作,因此以某种可被利用的方式不正确地操作那些整数是很容易的。特别是,当您需要跟踪缓冲区长度或读取某个内容的长度时,通常就是这种情况。但是如果使用一个有符号的值来存储这个长度值会发生什么情况呢 ―― 攻击者会使它“成为负值”,然后把该数据解释为一个实际上很大的正值吗?当数字值在不同的尺寸之间转换时,攻击者会利用这个操作吗?数值溢出可被利用吗? 有时处理整数的方式会导致程序缺陷。
防止缓冲区溢出的新技术
当然,要让程序员 不犯常见错误是很难的,而让程序(以及程序员)改为使用另一种语言通常更为困难。那么为何不让底层系统自动保护程序避免这些问题呢?最起码,避免 stack-smashing 攻击是一件好事,因为 stack-smashing 攻击是特别容易做到的。
一般来说,更改底层系统以避免常见的安全问题是一个极好的想法,我们在本文后面也会遇到这个主题。事实证明存在许多可用的防御措施,而一些最受欢迎的措施可分组为以下类别:
基于探测方法(canary)的防御。这包括 StackGuard(由 Immunix 所使用)、ProPolice(由 OpenBSD 所使用)和 Microsoft 的 /GS 选项。
非执行的堆栈防御。这包括 Solar Designer 的 non-exec 补丁(由 OpenWall 所使用)和 exec shield(由 Red Hat/Fedora 所使用)。
其他方法。这包括 libsafe(由 Mandrake 所使用)和堆栈分割方法。
遗憾的是,迄今所见的所有方法都具有弱点,因此它们不是万能药,但是它们会提供一些帮助。
基于探测方法的防御
研究人员 Crispen Cowan 创建了一个称为 StackGuard 的有趣方法。Stackguard 修改 C 编译器(gcc),以便将一个“探测”值插入到返回地址的前面。“探测仪”就像煤矿中的探测仪:它在某个地方出故障时发出警告。在任何函数返回之前,它执行检查以确保探测值没有改变。如果攻击者改写返回地址(作为 stack-smashing 攻击的一部分),探测仪的值或许就会改变,系统内就会相应地中止。这是一种有用的方法,不过要注意这种方法无法防止缓冲区溢出改写其他值(攻击者仍然能够利用这些值来攻击系统)。人们也曾扩展这种方法来保护其他值(比如堆上的值)。Stackguard(以及其他防御措施)由 Immunix 所使用。
IBM 的 stack-smashing 保护程序(ssp,起初名为 ProPolice)是 StackGuard 的方法的一种变化形式。像 StackGuard 一样,ssp 使用一个修改过的编译器在函数调用中插入一个探测仪以检测堆栈溢出。然而,它给这种基本的思路添加了一些有趣的变化。 它对存储局部变量的位置进行重新排序,并复制函数参数中的指针,以便它们也在任何数组之前。这样增强了ssp 的保护能力;它意味着缓冲区溢出不会修改指针值(否则能够控制指针的攻击者就能使用指针来控制程序保存数据的位置)。默认情况下,它不会检测所有函数,而只是检测确实需要保护的函数(主要是使用字符数组的函数)。从理论上讲,这样会稍微削弱保护能力,但是这种默认行为改进了性能,同时仍然能够防止大多数问题。考虑到实用的因素,它们以独立于体系结构的方式使用 gcc 来实现它们的方法,从而使其更易于运用。从 2003 年 5 月的发布版本开始,广受赞誉的 OpenBSD(它重点关注安全性)在他们的整个发行套件中使用了 ssp(也称为 ProPolice)。
Microsoft 基于 StackGuard 的成果,添加了一个编译器标记(/GS)来实现其 C 编译器中的探测仪。
非执行的堆栈防御
另一种方法首先使得在堆栈上执行代码变得不可能。 遗憾的是,x86 处理器(最常见的处理器)的内存保护机制无法容易地支持这点;通常,如果一个内存页是可读的,它就是可执行的。一个名叫 Solar Designer 的开发人员想出了一种内核和处理器机制的聪明组合,为 Linux 内核创建了一个“非执行的堆栈补丁”;有了这个补丁,堆栈上的程序就不再能够像通常的那样在 x86 上运行。 事实证明在有些情况下,可执行程序 需要在堆栈上;这包括信号处理和跳板代码(trampoline)处理。trampoline 是有时由编译器(比如 GNAT Ada 编译器)生成的奇妙结构,用以支持像嵌套子例程之类的结构。Solar Designer 还解决了如何在防止攻击的同时使这些特殊情况不受影响的问题。
一段时间之后,人们又想出了一种防止该问题的新思路:将所有可执行代码转移到一个称为“ASCII 保护(ASCII armor)”区域的内存区。要理解这是如何工作的,就必须知道攻击者通常不能使用一般的缓冲区溢出攻击来插入 ASCII NUL 字符(0)这个事实。 这意味着攻击者会发现,要使一个程序返回包含 0 的地址是很困难的。由于这个事实,将所有可执行代码转移到包含 0 的地址就会使得攻击该程序困难多了。
具有这个属性的最大连续内存范围是从 0 到 0x01010100 的一组内存地址,因此它们就被命名为 ASCII 保护区域(还有具有此属性的其他地址,但它们是分散的)。与非可执行的堆栈相结合,这种方法就相当有价值了:非可执行的堆栈阻止攻击者发送可执行代码,而 ASCII 保护内存使得攻击者难于通过利用现有代码来绕过非可执行堆栈。这样将保护程序代码避免堆栈、缓冲区和函数指针溢出,而且全都不需重新编译。
然而,ASCII 保护内存并不适用于所有程序;大程序也许无法装入 ASCII 保护内存区域(因此这种保护是不完美的),而且有时攻击者 能够将 0 插入目的地址。 此外,有些实现不支持跳板代码,因此可能必须对需要这种保护的程序禁用该特性。Red Hat 的 Ingo Molnar 在他的“exec-shield”补丁中实现了这种思想,该补丁由 Fedora 核心(可从 Red Hat 获得它的免费版本)所使用。最新版本的 OpenWall GNU/Linux (OWL)使用了 Solar Designer 提供的这种方法的实现(请参阅 参考资料 以获得指向这些版本的链接)。
其他方法
还有其他许多方法。一种方法就是使标准库对攻击更具抵抗力。Lucent Technologies 开发了 Libsafe,这是多个标准 C 库函数的包装,也就是像 strcpy() 这样已知的对 stack-smashing 攻击很脆弱的函数。Libsafe 是在 LGPL 下授予许可证的开放源代码软件。那些函数的 libsafe 版本执行相关的检查,确保数组改写不会超出堆栈桢。然而,这种方法仅保护那些特定的函数,而不是从总体上防止堆栈溢出缺陷,并且它仅保护堆栈,而不保护堆栈中的局部变量。它们的最初实现使用了 LD_PRELOAD ,而这可能与其他程序产生冲突。Linux 的 Mandrake 发行套件(从 7.1 版开始)包括了 libsafe。
另一种方法称为“分割控制和数据堆栈”―― 基本的思路是将堆栈分割为两个堆栈,一个用于存储控制信息(比如“返回”地址),另一个用于控制其他所有数据。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* 类型执行自动转换,那么缓冲区溢出缺陷很容易引入那些类中。
⑦ 如何构建自己的 Linux 发行版
风格的书籍,深入研究了调度、内存管理、多进程和线程、文件系统,以及用户与内核之间的交互。写作 Linux 书籍的作家相对于 UNIX 作家来说有一个优势:尽管团体发生了剧变,但是 Linux 内核不可能分成几个相互竞争的分支,由于 GNU Public License (GPL)、集中式研究实验室 Open Source Development Lab (OSDL) 和 Linus Torvalds 不可动摇的地位,使得 Linux 有幸成为一个缓慢移动的目标(slow-moving target)。为什么 UNIX 内核很重要除了某些方面具有一定的相似性之外,不同的 Unix 内核并不怎么相同。各种 UNIX 风格也具有一个 Linux 所缺少的优点:所有 UNIX 风格都被假定是完全的操作系统。Linux 通常被描述为“只是一个内核”(如果有这样的定义的话,也是一个武断的定义),它给出公共功能和实现的核心,不管内核是运行在不太强大的 Pentium? II 机器上还是 Symmetric Multiprocessing (SMP) 系统上,这些公共功能和实现都不会发生本质上的改变。为了更加简化,有人可能会说,离 Linux 内核越远,就会发现更多的变化,而 UNIX 系统则趋向于是各种 UNIX/POSIX 标准的离散实现。事情并没有这么简单。检测 Linux 内核和系统级代码可能是一件很费时间的事情,并且在现实世界中会限制使用。LFS 项目旨在解决 Linux 上有限的系统级可理解性问题。关于内核需要大量的库和工具来让 Linux 系统执行最基本的任务这一事实,已经做过讨论了,但是如果一个比较熟练的用户具有一个 slim-line Linux 发行版,他不想下载几吉字节不让他优化系统、也不让他抛开所有这些麻烦且不必要的工具的二进制代码,那该怎么办?如果一个非常熟练的用户拒绝接受各种社团发行版的 苛刻条件(diktat),而想要运行一个来自 CD 的 Linux/Apache/MySQL/PHP (LAMP) 类型的应用程序堆栈,那该怎么办?LFS 可以解决这些问题。回页首Linux From ScratchLFS 项目显然建立于那些对于构成基本的 Linux 系统来说充分而不必要的源文件的基础之上。它已经超越了 Linux 内核和设备驱动程序,因为要产生一个可工作的 Linux 系统,您必须添加一个完整的编译器工具链、许多 Linux 汇编程序实用工具、glibc 系统库、系统配置工具和连接到 userland shell 访问的工具。LFS 建立在这样一个假设的基础之上,即 Linux 或 UNIX 允许具有一些脚本编写知识的用户,了解一个完全有用的系统的工作方式,而不用深入研究内核代码本身。为了了解 Linux 系统的工作方式,LFS 的创建者们确定,通过遵循模块依赖性来编译系统,可能是了解一般操作系统和特定的 Linux 的机制最自然的方式。用户掌握了编译过程之后,就可以开始消除依赖性树的那些连接到与支持操作系统基本目的无关的系统组件的部件。例如,在编译完成之后,消除编译器工具链本身是可行的。在没有全套的命令行实用工具时,可以凑合使用嵌入式 LAMP 堆栈。配置实用工具也可以被丢弃,大多数用户可以凑合着用一个而不是太多 Linux 将会支持的文件系统。Linux 部件LFS 系统的一个重要部件是,可以作为 tar ball 得到的大量源文件。文档是另一个重要部件,并且是最重要的。实际上,很有可能利用一个最新的 LFS 书籍文件并创建一个 LFS 发行版,因为 LFS 书籍中详细描述了每个下载位置和每个源文件及其依赖性的特征。用于从内核到编译器到 shell 编译每组源文件的过程都是已经写好了的,如果可能,您也可以在描述具有不同特征的系统的 LFS 书籍中找到替代的例程。LFS 系统的另一个不太可能出现在一般用户工具箱中的部件是,在基本 LFS 系统组合在一起之后引导系统所需的引导脚本。现在对 LFS 发行版的最大警告是:勇敢的发行版构建者所需的是一个可工作的 Linux 发行版,包括一个完整的编译器工具链和一套文件系统创建实用工具。自然,所有基于源代码的 Linux 发行版都需要使用各个发行版都完全不同的特定编译器版本来引导。LFS 不是该领域的惟一系统,但它是惟一允许您直接处理单个源文件的系统。大多数其他基于源代码的 Linux 系统,比如 Sourcemage 和 MyGeOS,提供一个完整的下载,建议用户使用。LFS 不作这样的假设,并且鼓励拆开 LFS 框架。预先假设起作用的 Linux 发行版已安装在非外来的(nonexotic)硬件上,即使 LFS 可能没有配置工具和脚本那么受关注。要编译 LFS,您需要准备一个分区和一个文件系统,还需要编译一个编译器和系统库。如果用手工完成的话,这是一个相当伤脑筋的过程,但是也的确可以增加您在处理其余安装方面的自信。整个系统的编译要花一小时到四天的时间,具体时间取决于底层硬件的年代和您的命令行技术熟练程度。作一个相当大的假设,如果您愿意很大程度上保留书籍安装,并使对 LFS 书籍中提议的安装的更改保持最小,您也可以使用自动化的安装例程来安装基于 LFS 的发行版。安装例程没有在 LFS 书籍中给出,但是可以在名称 Automated Linux from Scratch (ALFS) 之下作为基于 XML 的发行版得到。活动安装可以作为基于 C 的脚本得到,该脚本使用 ncurses 来模拟图形安装。该安装也叫做 nALFS 并给出一个极为灵活的包安装框架。工作需要一个起作用的 Linux 系统,并带有可以工作的 C 编译器和 XML 解析器。一个可以工作的 LFS 系统就足够了。Automated Linux From ScratchALFS 的目的是超越 LFS 本身。LFS 自己指导基于 Linux 的操作系统的内部工作,但是它不具有单个图形用户界面 (GUI)。LFS 既不允许连接到网络,也不允许连接到 Internet。ALFS 可以简化系统的扩展,例如,通过添加支持 Internet 访问的库,或者通过安装图形桌面所需的 X 库。回页首超越LFSLFS 的创建者们认识到了对其他各种基于源代码的 Linux 系统的需求。为了这些想要超越 LFS 并添加 X Window System、GNOME 和网络支持的人,创建了另一个 LFS 派生物:Beyond Linux From Scratch (BLFS)。 第三版 LFS 书籍(让我们不会忘记是在谈论书籍而非发行版),形成一个以一个角为基础的三角形:对于自动化编译和完全的基于源代码的 Linux 发行版,基本的 LFS 版本是基础。BLFS 将基本的 Linux 系统转化成一个完全的用户广泛接受的 Linux 系统。AFLS 简化基于源代码的 Linux 安装的安装和扩展。整个基于源代码的系统的编译是由一个脚本指导的,您在针对运行脚本的硬件调整脚本之后,就可以让脚本自己运行了。在您(或安装工程师)确定需要运行哪些包,也即特定的办公应用程序套件之后,您就可以容易地扩展安装顺序了。ALFS 也迟早会派上用场,因为它适用于从源代码进行网络范围的安装。回页首Hardened LFSLFS 家族的最后一个成员解决基于源代码的 Linux 的一个特别重要的方面:安全性。对于不想依赖于所选的 Linux 发行版服务器交付的补丁的那些人,安全性的普通方法是,针对所选的核心库和应用程序跟踪安全报告。对于 LFS 实现人员来说,问题有些不同:尽管不是不可能,但是也难以审计 Linux 内核代码,也许还包括集中于基于 Linux 的操作系统的内部机能的很多库和实用工具。代码审计相当费时,并且只有当补丁服务器是由专门人员集中维护的时,添加大量补丁才是明智的。但是可以取代一些为反映安全问题的新方法而重新编写过的库。一个好的例子是,通过从一个适当大的随机数池中随机分配数字,使得猜测进程标识符极为困难。OpenBSD 项目最先采用了该方法,随后,各种 UNIX 风格和 Linux 发行版都采用了该方法。一个相当新的项目叫做 Hardened Linux From Scratch (HLFS),在 Linux 下采用了这一安全方法。该项目假设相当正规地掌握了 LFS 和 BLFS 的一些部件,并使用了不会成为大多数 Linux 系统中的标准的几个实用工具和库。添加到 HLFS 中的最重要的部件可能是 Stack-Smashing Protector (SSP),通过使用 gcc 指令可以启用该功能。SSP 开发来防御 stack-smashing 攻击,这类攻击属于影响 Linux 系统的一类最常见的安全威胁。其他安全措施包括一流的随机数生成器和位置独立的可执行程序的编译,其中通常转化成静态链接对象代码的可执行代码作为共享库出现,而位置独立的可执行库通过将地址随机化可以隐藏这些地址。当然,可从 HLFS Web 站点得到大量补丁,并可查看其源代码。回页首日益壮大的 LFS 家族在许多方面,Linux 版本的 LFS 家族是一个方法,给予黑客以构造基于 Linux 的操作系统的能力。但是对于 LFS 的创建者们来说,最重要的结果似乎是:通过 LFS,所有 Linux 发行版对于所涉及的用户都成了智能的了。通过允许用户一部分一部分地构建 Linux 发行版,并帮助用户将一个基于 Linux 的操作系统看作一个由许多部件组成的系统,还可以用另外的方法构建 Linux 发行版。更一般来说,用户要更改构建 Linux 发行版的方式,不必要是程序员:只要从构建 LFS 系统学到的一点脚本编写能力就足够了。LFS 专家可以改变和扩展 Linux 发行版的完美组合,而不会影响它的基本结构。对于有人员和专家维护 Linux 系统,但没有资金从咨询机构和公司购买商业支持的机构来说,该功能特别重要。已经针对教学目的和大型网络,演示了基于 LFS 的 Linux 系统。它们也有可能用于其他领域。参考资料 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
最新LFS 版本的门户是 LFS 下载站点。它提供从几个方面可用的 LFS 书籍。
LFS 家族的最新成员是 HLFS 或Hardened LFS 风格。
如果愿意,LFS 狂热者们可以创建 自动化的 安装。
如果基本的 LFS 安装还不够,那么可以继续安装更加复杂的 extended Linux From Scratch,也叫做 BLFS。
为了防御 stack-smashing 攻击,SSP 指令使得 gcc 可以编译针对该类型威胁而保护的代码。
位置独立的代码在关于 position-independent executables 的一章中作了简短介绍。
在developerWorks Linux 专区 可以找到更多为 Linux 开发者准备的资源。
访问developerWorks 开放源码专区,获得广泛的 how-to 信息、工具和项目更新,以帮助您利用开放源码技术进行开发,并将它们与 IBM 产品一起使用。
利用IBM 试用软件 革新您的下一个开放源码开发项目。可从下载或 DVD 得到试用软件。
通过参与 developerWorks blogs 加入developerWorks 社区。
关于作者Frank Pohlmann 以前研究的是中东宗教历史,后来各基金会认为研究宗教辩证历史与当今世界相去甚远,从此他便专攻自己热爱的领域 —— 免费软件。他获准成为英国的 LinuxUser and Developer 的技术编辑。您可通过 [email protected] 与他联系。关闭[x]关于报告滥用的帮助报告滥用谢谢! 此内容已经标识给管理员注意。关闭[x]关于报告滥用的帮助报告滥用报告滥用提交失败。 请稍后重试。关闭[x]developerWorks:登录IBM ID:需要一个 IBM ID?忘记IBM ID?密码:忘记密码?更改您的密码 保持登录。单击提交则表示您同意developerWorks 的条款和条件。 使用条款 当您初次登录到 developerWorks 时,将会为您创建一份概要信息。您在developerWorks 概要信息中选择公开的信息将公开显示给其他人,但您可以随时修改这些信息的显示状态。您的姓名(除非选择隐藏)和昵称将和您在 developerWorks 发布的内容一同显示。所有提交的信息确保安全。关闭[x]请选择您的昵称:当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。昵称:(长度在 3 至 31 个字符之间)单击提交则表示您同意developerWorks 的条款和条件。 使用条款. 所有提交的信息确保安全。为本文评分评论回页首
⑧ C# 如何点出结构体字段
们为何如此常见和如此危险。然后讨论广泛用于解决缓冲区溢出的新 Linux 和 UNIX 方法 ―― 以及为什么这些方法还不足够。随后将展示 C/C++ 程序中防止缓冲区溢出的各种方法,同时包括静态调整大小的方法(比如标准的 C 库和 OpenBSD/strlcpy 解决方案)和动态调整大小的解决方案,以及一些将为您提供帮助的工具。最后,本文以一些关于缓冲区溢出缺陷的未来发展形势的预测来结束全文的讨论。
如果希望自己的程序是安全的,您需要知道什么是缓冲区溢出,如何防止它们,可以采用哪些最新的自动化工具来防止它们(以及为什么这些工具还不足够),还有如何在您自己的程序中防止它们。
什么是缓冲区溢出?
缓冲区以前可能被定义为“包含相同数据类型的实例的一个连续计算机内存块”。在 C 和 C++ 中,缓冲区通常是使用数组和诸如 malloc() 和 new 这样的内存分配例程来实现的。极其常见的缓冲区种类是简单的字符数组。 溢出 是指数据被添加到分配给该缓冲区的内存块之外。
如果攻击者能够导致缓冲区溢出,那么它就能控制程序中的其他值。虽然存在许多利用缓冲区溢出的方法,不过最常见的方法还是“stack-smashing”攻击。Elias Levy (又名为 Aleph One)的一篇经典文章“Smashing the Stack for Fun and Profit”解释了 stack-smashing 攻击,Elias Levy 是 Bugtraq 邮件列表(请参阅 参考资料 以获得相关链接)的前任主持人。
清单 1. 一个简单的程序
void function1(int a, int b, int c) {
char buffer1[5];
gets(buffer1); /* DON'T DO THIS */
}
void main() {
function(1,2,3);
}
假设使用 gcc 来编译清单 1 中的简单程序,在 X86 上的 Linux 中运行,并且紧跟在对 gets() 的调用之后中止。此时的内存内容看起来像什么样子呢?答案是它看起来类似图 1,其中展示了从左边的低位地址到右边的高位地址排序的内存布局。
图 1. 堆栈视图
内存的底部 内存的顶部
buffer1 sfp ret a b c
<--- 增长 --- [ ] [ ] [ ] [ ] [ ] [ ] ...
为什么缓冲区溢出如此常见?
在几乎所有计算机语言中,不管是新的语言还是旧的语言,使缓冲区溢出的任何尝试通常都会被该语言本身自动检测并阻止(比如通过引发一个异常或根据需要给缓冲区添加更多空间)。但是有两种语言不是这样:C 和 C++ 语言。C 和 C++ 语言通常只是让额外的数据乱写到其余内存的任何位置,而这种情况可能被利用从而导致恐怖的结果。更糟糕的是,用 C 和 C++ 编写正确的代码来始终如一地处理缓冲区溢出则更为困难;很容易就会意外地导致缓冲区溢出。除了 C 和 C++ 使用得 非常广泛外,上述这些可能都是不相关的事实;例如,Red Hat Linux 7.1 中 86% 的代码行都是用 C 或 C ++ 编写的。因此,大量的代码对这个问题都是脆弱的,因为实现语言无法保护代码避免这个问题。
在 C 和 C++ 语言本身中,这个问题是不容易解决的。该问题基于 C 语言的根本设计决定(特别是 C 语言中指针和数组的处理方式)。由于 C++ 是最兼容的 C 语言超集,它也具有相同的问题。存在一些能防止这个问题的 C/C++ 兼容版本,但是它们存在极其严重的性能问题。而且一旦改变 C 语言来防止这个问题,它就不再是 C 语言了。许多语言(比如 Java 和 C#)在语法上类似 C,但它们实际上是不同的语言,将现有 C 或 C++ 程序改为使用那些语言是一项艰巨的任务。
然而,其他语言的用户也不应该沾沾自喜。有些语言存在允许缓冲区溢出发生的“转义”子句。Ada 一般会检测和防止缓冲区溢出(即针对这样的尝试引发一个异常),但是不同的程序可能会禁用这个特性。C# 一般会检测和防止缓冲区溢出,但是它允许程序员将某些例程定义为“不安全的”,而这样的代码 可能 会导致缓冲区溢出。因此如果您使用那些转义机制,就需要使用 C/C++ 程序所必须使用的相同种类的保护机制。许多语言都是用 C 语言来实现的(至少部分是用 C 语言来实现的 ),并且用任何语言编写的所有程序本质上都依赖用 C 或 C++ 编写的库。因此,所有程序都会继承那些问题,所以了解这些问题是很重要的。
导致缓冲区溢出的常见 C 和 C++ 错误
从根本上讲,在程序将数据读入或复制到缓冲区中的任何时候,它需要在复制 之前检查是否有足够的空间。能够容易看出来的异常就不可能会发生 ―― 但是程序通常会随时间而变更,从而使得不可能成为可能。
遗憾的是,C 和 C++ 附带的大量危险函数(或普遍使用的库)甚至连这点(指检查空间)也无法做到。程序对这些函数的任何使用都是一个警告信号,因为除非慎重地使用它们,否则它们就会成为程序缺陷。您不需要记住这些函数的列表;我的真正目的是说明这个问题是多么普遍。这些函数包括 strcpy(3)、strcat(3)、sprintf(3) (及其同类 vsprintf(3) )和 gets(3) 。 scanf() 函数集( scanf(3)、fscanf(3)、sscanf(3)、vscanf(3)、vsscanf(3) 和 vfscanf(3) )可能会导致问题,因为使用一个没有定义最大长度的格式是很容易的(当读取不受信任的输入时,使用格式“%s”总是一个错误)。
其他危险的函数包括 realpath(3)、getopt(3)、getpass(3)、streadd(3)、strecpy(3) 和 strtrns(3) 。 从理论上讲, snprintf() 应该是相对安全的 ―― 在现代 GNU/Linux 系统中的确是这样。但是非常老的 UNIX 和 Linux 系统没有实现 snprintf() 所应该实现的保护机制。
Microsoft 的库中还有在相应平台上导致同类问题的其他函数(这些函数包括 wcscpy()、_tcscpy()、_mbscpy()、wcscat()、_tcscat()、_mbscat() 和 CopyMemory() )。注意,如果使用 Microsoft 的 MultiByteToWideChar() 函数,还存在一个常见的危险错误 ―― 该函数需要一个最大尺寸作为字符数目,但是程序员经常将该尺寸以字节计(更普遍的需要),结果导致缓冲区溢出缺陷。
另一个问题是 C 和 C++ 对整数具有非常弱的类型检查,一般不会检测操作这些整数的问题。由于它们要求程序员手工做所有的问题检测工作,因此以某种可被利用的方式不正确地操作那些整数是很容易的。特别是,当您需要跟踪缓冲区长度或读取某个内容的长度时,通常就是这种情况。但是如果使用一个有符号的值来存储这个长度值会发生什么情况呢 ―― 攻击者会使它“成为负值”,然后把该数据解释为一个实际上很大的正值吗?当数字值在不同的尺寸之间转换时,攻击者会利用这个操作吗?数值溢出可被利用吗? 有时处理整数的方式会导致程序缺陷。
防止缓冲区溢出的新技术
当然,要让程序员 不犯常见错误是很难的,而让程序(以及程序员)改为使用另一种语言通常更为困难。那么为何不让底层系统自动保护程序避免这些问题呢?最起码,避免 stack-smashing 攻击是一件好事,因为 stack-smashing 攻击是特别容易做到的。
一般来说,更改底层系统以避免常见的安全问题是一个极好的想法,我们在本文后面也会遇到这个主题。事实证明存在许多可用的防御措施,而一些最受欢迎的措施可分组为以下类别:
基于探测方法(canary)的防御。这包括 StackGuard(由 Immunix 所使用)、ProPolice(由 OpenBSD 所使用)和 Microsoft 的 /GS 选项。
非执行的堆栈防御。这包括 Solar Designer 的 non-exec 补丁(由 OpenWall 所使用)和 exec shield(由 Red Hat/Fedora 所使用)。
其他方法。这包括 libsafe(由 Mandrake 所使用)和堆栈分割方法。
遗憾的是,迄今所见的所有方法都具有弱点,因此它们不是万能药,但是它们会提供一些帮助。
基于探测方法的防御
研究人员 Crispen Cowan 创建了一个称为 StackGuard 的有趣方法。Stackguard 修改 C 编译器(gcc),以便将一个“探测”值插入到返回地址的前面。“探测仪”就像煤矿中的探测仪:它在某个地方出故障时发出警告。在任何函数返回之前,它执行检查以确保探测值没有改变。如果攻击者改写返回地址(作为 stack-smashing 攻击的一部分),探测仪的值或许就会改变,系统内就会相应地中止。这是一种有用的方法,不过要注意这种方法无法防止缓冲区溢出改写其他值(攻击者仍然能够利用这些值来攻击系统)。人们也曾扩展这种方法来保护其他值(比如堆上的值)。Stackguard(以及其他防御措施)由 Immunix 所使用。
IBM 的 stack-smashing 保护程序(ssp,起初名为 ProPolice)是 StackGuard 的方法的一种变化形式。像 StackGuard 一样,ssp 使用一个修改过的编译器在函数调用中插入一个探测仪以检测堆栈溢出。然而,它给这种基本的思路添加了一些有趣的变化。 它对存储局部变量的位置进行重新排序,并复制函数参数中的指针,以便它们也在任何数组之前。这样增强了ssp 的保护能力;它意味着缓冲区溢出不会修改指针值(否则能够控制指针的攻击者就能使用指针来控制程序保存数据的位置)。默认情况下,它不会检测所有函数,而只是检测确实需要保护的函数(主要是使用字符数组的函数)。从理论上讲,这样会稍微削弱保护能力,但是这种默认行为改进了性能,同时仍然能够防止大多数问题。考虑到实用的因素,它们以独立于体系结构的方式使用 gcc 来实现它们的方法,从而使其更易于运用。从 2003 年 5 月的发布版本开始,广受赞誉的 OpenBSD(它重点关注安全性)在他们的整个发行套件中使用了 ssp(也称为 ProPolice)。
Microsoft 基于 StackGuard 的成果,添加了一个编译器标记(/GS)来实现其 C 编译器中的探测仪。
非执行的堆栈防御
另一种方法首先使得在堆栈上执行代码变得不可能。 遗憾的是,x86 处理器(最常见的处理器)的内存保护机制无法容易地支持这点;通常,如果一个内存页是可读的,它就是可执行的。一个名叫 Solar Designer 的开发人员想出了一种内核和处理器机制的聪明组合,为 Linux 内核创建了一个“非执行的堆栈补丁”;有了这个补丁,堆栈上的程序就不再能够像通常的那样在 x86 上运行。 事实证明在有些情况下,可执行程序 需要在堆栈上;这包括信号处理和跳板代码(trampoline)处理。trampoline 是有时由编译器(比如 GNAT Ada 编译器)生成的奇妙结构,用以支持像嵌套子例程之类的结构。Solar Designer 还解决了如何在防止攻击的同时使这些特殊情况不受影响的问题。
一段时间之后,人们又想出了一种防止该问题的新思路:将所有可执行代码转移到一个称为“ASCII 保护(ASCII armor)”区域的内存区。要理解这是如何工作的,就必须知道攻击者通常不能使用一般的缓冲区溢出攻击来插入 ASCII NUL 字符(0)这个事实。 这意味着攻击者会发现,要使一个程序返回包含 0 的地址是很困难的。由于这个事实,将所有可执行代码转移到包含 0 的地址就会使得攻击该程序困难多了。
具有这个属性的最大连续内存范围是从 0 到 0x01010100 的一组内存地址,因此它们就被命名为 ASCII 保护区域(还有具有此属性的其他地址,但它们是分散的)。与非可执行的堆栈相结合,这种方法就相当有价值了:非可执行的堆栈阻止攻击者发送可执行代码,而 ASCII 保护内存使得攻击者难于通过利用现有代码来绕过非可执行堆栈。这样将保护程序代码避免堆栈、缓冲区和函数指针溢出,而且全都不需重新编译。
然而,ASCII 保护内存并不适用于所有程序;大程序也许无法装入 ASCII 保护内存区域(因此这种保护是不完美的),而且有时攻击者 能够将 0 插入目的地址。 此外,有些实现不支持跳板代码,因此可能必须对需要这种保护的程序禁用该特性。Red Hat 的 Ingo Molnar 在他的“exec-shield”补丁中实现了这种思想,该补丁由 Fedora 核心(可从 Red Hat 获得它的免费版本)所使用。最新版本的 OpenWall GNU/Linux (OWL)使用了 Solar Designer 提供的这种方法的实现(请参阅 参考资料 以获得指向这些版本的链接)。
其他方法
还有其他许多方法。一种方法就是使标准库对攻击更具抵抗力。Lucent Technologies 开发了 Libsafe,这是多个标准 C 库函数的包装,也就是像 strcpy() 这样已知的对 stack-smashing 攻击很脆弱的函数。Libsafe 是在 LGPL 下授予许可证的开放源代码软件。那些函数的 libsafe 版本执行相关的检查,确保数组改写不会超出堆栈桢。然而,这种方法仅保护那些特定的函数,而不是从总体上防止堆栈溢出缺陷,并且它仅保护堆栈,而不保护堆栈中的局部变量。它们的最初实现使用了 LD_PRELOAD ,而这可能与其他程序产生冲突。Linux 的 Mandrake 发行套件(从 7.1 版开始)包括了 libsafe。
另一种方法称为“分割控制和数据堆栈”―― 基本的思路是将堆栈分割为两个堆栈,一个用于存储控制信息(比如“返回”地址),另一个用于控制其他所有数据。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* 类型执行自动转换,那么缓冲区溢出缺陷很容易引入那些类中。
⑨ propolice是什么
IBM 的 stack-smashing 保护程序(ssp,起初名为 ProPolice)是 StackGuard 的方法的一种变化形式。像 StackGuard 一样,ssp 使用一个修改过的编译器在函数调用中插入一个探测仪以检测堆栈溢出。然而,它给这种基本的思路添加了一些有趣的变化。 它对存储局部变量的位置进行重新排序,并复制函数参数中的指针,以便它们也在任何数组之前。这样增强了ssp 的保护能力;它意味着缓冲区溢出不会修改指针值(否则能够控制指针的攻击者就能使用指针来控制程序保存数据的位置)。默认情况下,它不会检测所有函数,而只是检测确实需要保护的函数(主要是使用字符数组的函数)。从理论上讲,这样会稍微削弱保护能力,但是这种默认行为改进了性能,同时仍然能够防止大多数问题。考虑到实用的因素,它们以独立于体系结构的方式使用 gcc 来实现它们的方法,从而使其更易于运用。从 2003 年 5 月的发布版本开始,广受赞誉的 OpenBSD(它重点关注安全性)在他们的整个发行套件中使用了 ssp(也称为 ProPolice)。