`
gaofen100
  • 浏览: 1180023 次
文章分类
社区版块
存档分类
最新评论

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

 
阅读更多

from:http://blog.csdn.net/ruibird/archive/2007/01/23/1491425.aspx

什么是缓冲区溢出?
  缓冲区以前可能被定义为“包含相同数据类型的实例的一个连续计算机内存块”。在C和C++中,缓冲区通常是使用数组和诸如malloc()和new这样的内存分配例程来实现的。极其常见的缓冲区种类是简单的字符数组。溢出是指数据被添加到分配给该缓冲区的内存块之外。
  
  如果攻击者能够导致缓冲区溢出,那么它就能控制程序中的其他值。虽然存在许多利用缓冲区溢出的方法,不过最常见的方法还是“stack-smashing”攻击。EliasLevy(又名为AlephOne)的一篇经典文章“SmashingtheStackforFunandProfit”解释了stack-smashing攻击,EliasLevy是Bugtraq邮件列表(请参阅参考资料以获得相关链接)的前任主持人。
  
  为了理解stack-smashing攻击(或其他任何缓冲区攻击)是如何进行的,您需要了解一些关于计算机在机器语言级实际如何工作的知识。在类UNIX系统上,每个进程都可以划分为三个主要区域:文本、数据和堆栈。文本区域包括代码和只读数据,通常不能对它执行写入操作。数据区域同时包括静态分配的内存(比如全局和静态数据)和动态分配的内存(通常称为堆)。堆栈区域用于允许函数/方法调用;它用于记录函数完成之后的返回位置,存储函数中使用的本地变量,向函数传递参数,以及从函数返回值。每当调用一个函数,就会使用一个新的堆栈帧来支持该调用。了解这些之后,让我们来考察一个简单的程序。
  
  清单1.一个简单的程序
  
  voidfunction1(inta,intb,intc){
   charbuffer1[5];
   gets(buffer1);/*DON'TDOTHIS*/
  }
  
  voidmain(){
   function(1,2,3);
  }
  
  假设使用gcc来编译清单1中的简单程序,在X86上的Linux中运行,并且紧跟在对gets()的调用之后中止。此时的内存内容看起来像什么样子呢?答案是它看起来类似图1,其中展示了从左边的低位地址到右边的高位地址排序的内存布局。
  
  内存的底部    内存的顶部 
  buffer1 sfp ret a b c 
  <---增长--- [] [] [] [] [] [] ... 
  堆栈的顶部    堆栈的底部 
  
  许多计算机处理器,包括所有x86处理器,都支持从高位地址向低位地址“倒”增长堆栈。因此,每当一个函数调用另一个函数,更多的数据将被添加到左边(低位地址),直至系统的堆栈空间耗尽。在这个例子中,当main()调用function1()时,它将c的值压入堆栈,然后压入b的值,最后压入a的值。之后它压入return(ret)值,这个值在function1()完成时告诉function1()返回到main()中的何处。它还把所谓的“已保存的帧指针(savedframepointer,sfp)”记录到堆栈上;这并不是必须保存的内容,此处我们不需要理解它。在任何情况下,function1()在启动以后,它会为buffer1()预留空间,这在图1中显示为具有一个低地址位置。
  
  现在假设攻击者发送了超过buffer1()所能处理的数据。接下来会发生什么情况呢?当然,C和C++程序员不会自动检查这个问题,因此除非程序员明确地阻止它,否则下一个值将进入内存中的“下一个”位置。那意味着攻击者能够改写sfp(即已保存的帧指针),然后改写ret(返回地址)。之后,当function1()完成时,它将“返回”——不过不是返回到main(),而是返回到攻击者想要运行的任何代码。
  
  通常攻击者会使用它想要运行的恶意代码来使缓冲区溢出,然后攻击者会更改返回值以指向它们已发送的恶意代码。这意味着攻击者本质上能够在一个操作中完成整个攻击!AlephOn的文章(请参阅参考资料)详细介绍了这样的攻击代码是如何创建的。例如,将一个ASCII0字符压入缓冲区通常是很困难的,而该文介绍了攻击者一般如何能够解决这个问题。
  
  除了smashing-stack和更改返回地址外,还存在利用缓冲区溢出缺陷的其他途径。与改写返回地址不同,攻击者可以smashing-stack(使堆栈上的缓冲区溢出),然后改写局部变量以利用缓冲区溢出缺陷。缓冲区根本就不必在堆栈上——它可以是堆中动态分配的内存(也称为“malloc”或“new”区域),或者在某些静态分配的内存中(比如“global”或“static”内存)。基本上,如果攻击者能够溢出缓冲区的边界,麻烦或许就会找上你了。然而,最危险的缓冲区溢出攻击就是stack-smashing攻击,因为如果程序对攻击者很脆弱,攻击者获得整个机器的控制权就特别容易。
  
  为什么缓冲区溢出如此常见?
  在几乎所有计算机语言中,不管是新的语言还是旧的语言,使缓冲区溢出的任何尝试通常都会被该语言本身自动检测并阻止(比如通过引发一个异常或根据需要给缓冲区添加更多空间)。但是有两种语言不是这样:C和C++语言。C和C++语言通常只是让额外的数据乱写到其余内存的任何位置,而这种情况可能被利用从而导致恐怖的结果。更糟糕的是,用C和C++编写正确的代码来始终如一地处理缓冲区溢出则更为困难;很容易就会意外地导致缓冲区溢出。除了C和C++使用得非常广泛外,上述这些可能都是不相关的事实;例如,RedHatLinux7.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选项。
  
  非执行的堆栈防御。这包括SolarDesigner的non-exec补丁(由OpenWall所使用)和execshield(由RedHat/Fedora所使用)。
  
  其他方法。这包括libsafe(由Mandrake所使用)和堆栈分割方法。
  
  遗憾的是,迄今所见的所有方法都具有弱点,因此它们不是万能药,但是它们会提供一些帮助。
  
  基于探测方法的防御
  研究人员CrispenCowan创建了一个称为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处理器(最常见的处理器)的内存保护机制无法容易地支持这点;通常,如果一个内存页是可读的,它就是可执行的。一个名叫SolarDesigner的开发人员想出了一种内核和处理器机制的聪明组合,为Linux内核创建了一个“非执行的堆栈补丁”;有了这个补丁,堆栈上的程序就不再能够像通常的那样在x86上运行。事实证明在有些情况下,可执行程序需要在堆栈上;这包括信号处理和跳板代码(trampoline)处理。trampoline是有时由编译器(比如GNATAda编译器)生成的奇妙结构,用以支持像嵌套子例程之类的结构。SolarDesigner还解决了如何在防止攻击的同时使这些特殊情况不受影响的问题。
  
  Linux中实现这个目的的最初补丁在1998年被LinusTorvalds拒绝,这是因为一个有趣的原因。即使不能将代码放到堆栈上,攻击者也可以利用缓冲区溢出来使程序“返回”某个现有的子例程(比如C库中的某个子例程),从而进行攻击。简而言之,仅只是拥有非可执行的堆栈是不足够的。
  
  一段时间之后,人们又想出了一种防止该问题的新思路:将所有可执行代码转移到一个称为“ASCII保护(ASCIIarmor)”区域的内存区。要理解这是如何工作的,就必须知道攻击者通常不能使用一般的缓冲区溢出攻击来插入ASCIINUL字符(0)这个事实。这意味着攻击者会发现,要使一个程序返回包含0的地址是很困难的。由于这个事实,将所有可执行代码转移到包含0的地址就会使得攻击该程序困难多了。
  
  具有这个属性的最大连续内存范围是从0到0x01010100的一组内存地址,因此它们就被命名为ASCII保护区域(还有具有此属性的其他地址,但它们是分散的)。与非可执行的堆栈相结合,这种方法就相当有价值了:非可执行的堆栈阻止攻击者发送可执行代码,而ASCII保护内存使得攻击者难于通过利用现有代码来绕过非可执行堆栈。这样将保护程序代码避免堆栈、缓冲区和函数指针溢出,而且全都不需重新编译。
  
  然而,ASCII保护内存并不适用于所有程序;大程序也许无法装入ASCII保护内存区域(因此这种保护是不完美的),而且有时攻击者能够将0插入目的地址。此外,有些实现不支持跳板代码,因此可能必须对需要这种保护的程序禁用该特性。RedHat的IngoMolnar在他的“exec-shield”补丁中实现了这种思想,该补丁由Fedora核心(可从RedHat获得它的免费版本)所使用。最新版本的OpenWallGNU/Linux(OWL)使用了SolarDesigner提供的这种方法的实现(请参阅参考资料以获得指向这些版本的链接)。
  
  其他方法
  还有其他许多方法。一种方法就是使标准库对攻击更具抵抗力。LucentTechnologies开发了Libsafe,这是多个标准C库函数的包装,也就是像strcpy()这样已知的对stack-smashing攻击很脆弱的函数。Libsafe是在LGPL下授予许可证的开放源代码软件。那些函数的libsafe版本执行相关的检查,确保数组改写不会超出堆栈桢。然而,这种方法仅保护那些特定的函数,而不是从总体上防止堆栈溢出缺陷,并且它仅保护堆栈,而不保护堆栈中的局部变量。它们的最初实现使用了LD_PRELOAD,而这可能与其他程序产生冲突。Linux的Mandrake发行套件(从7.1版开始)包括了libsafe。
  
  另一种方法称为“分割控制和数据堆栈”——基本的思路是将堆栈分割为两个堆栈,一个用于存储控制信息(比如“返回”地址),另一个用于控制其他所有数据。Xuetal.在gcc中实现了这种方法,StackShield在汇编程序中实现了这种方法。这样使得操纵返回地址困难多了,但它不会阻止改变调用函数的数据的缓冲区溢出攻击。
  
  事实上还有其他方法,包括随机化可执行程序的位置;Crispen的“PointGuard”将这种探测仪思想引申到了堆中,等等。如何保护当今的计算机现在已成了一项活跃的研究任务。
  
  一般保护是不足够的
  如此多不同的方法意味着什么呢?对用户来说,好的一面在于大量创新的方法正在试验之中;长期看来,这种“竞争”会更容易看出哪种方法最好。而且,这种多样性还使得攻击者躲避所有这些方法更加困难。然而,这种多样性也意味着开发人员需要避免编写会干扰其中任何一种方法的代码。这在实践上是很容易的;只要不编写对堆栈桢执行低级操作或对堆栈的布局作假设的代码就行了。即使不存在这些方法,这也是一个很好的建议。
  
  操作系统供应商需要参与进来就相当明显了:至少挑选一种方法,并使用它。缓冲区溢出是第一号的问题,这些方法中最好的方法通常能够减轻发行套件中几乎半数已知缺陷的影响。可以证明,不管是基于探测仪的方法更好,还是基于非可执行堆栈的方法更好,它们都具有各自的优点。可以将它们结合起来使用,但是少数方法不支持这样使用,因为附加的性能损失使得这样做不值得。我并没有其他意思,至少就这些方法本身而言是这样;libsafe和分割控制及数据堆栈的方法在它们所提供的保护方面都具有局限性。当然,最糟糕的解决办法就是根本不对这个第一号的缺陷提供保护。还没有实现一种方法的软件供应商需要立即计划这样做。从2004年开始,用户应该开始避免使用这样的操作系统,即它们至少没有对缓冲区溢出提供某种自动保护机制。
  
  然而,没有哪种方法允许开发人员忽略缓冲区溢出。所有这些方法都能够被攻击者破坏。攻击者也许能够通过改变函数中其他数据的值来利用缓冲区溢出;没有哪种方法能够防止这点。如果能够插入某些难于创建的值(比如NUL字符),那么这其中的许多方法都能被攻击者绕开;随着多媒体和压缩数据变得更加普遍,攻击者绕开这些方法就更容易了。从根本上讲,所有这些方法都能减轻从程序接管攻击到拒绝服务攻击的缓冲区溢出攻击所带来的破坏。遗憾的是,随着计算机系统在更多关键场合的使用,即使拒绝服务通常也是不可接受的。因而,尽管发行套件应该至少包括一种适当的防御方法,并且开发人员应该使用(而不是反对)那些方法,但是开发人员仍然需要最初就编写无缺陷的软件。
  
  C/C++解决方案
  针对缓冲区溢出的一种简单解决办法就是转为使用能够防止缓冲区溢出的语言。毕竟,除了C和C++外,几乎每种高级语言都具有有效防止缓冲区溢出的内置机制。但是许多开发人员因为种种原因还是选择使用C和C++。那么您能做什么呢?
  
  事实证明存在许多防止缓冲区溢出的不同技术,但它们都可划分为以下两种方法:静态分配的缓冲区和动态分配的缓冲区。首先,我们将讲述这两种方法分别是什么。然后,我们将讨论静态方法的两个例子(标准Cstrncpy/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()系列函数具有一个最大宽度值,至少IEEEStandard1003-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_tstrlcpy(char*dst,constchar*src,size_tsize);
  size_tstrlcat(char*dst,constchar*src,size_tsize);
  
  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系统中的特权,以便您能防止自己不可避免的错误所带来安全隐患。

分享到:
评论

相关推荐

    防止缓冲区溢出教程源代码

    c/c++防止c/c++防止缓冲区溢出教程源代码 缓冲区溢出教程源代码

    缓冲区溢出漏洞实验报告(附带程序源码和被攻击程序)

    3.缓冲区溢出演示 4.溢出攻击结果与危害 5.防御手段 适合人群: 具备一定编程基础,作业时间不够的学生,对缓冲区溢出原理不了解的初学者 环境: IDA pro7.6, vc++ ,x32dbg。 阅读建议: 有一点 x32dbg逆向工具...

    缓冲区溢出攻击与防止技术

    缓冲区溢出攻击与防止技术缓冲区溢出攻击与防止技术缓冲区溢出攻击与防止技术

    防止缓冲区溢出攻击成为网络防护的弱点

    在过去的十年中,以缓冲区溢出为类型的安全漏洞占是最为常见的一种形式了。更为严重的是,缓冲区溢出漏洞占了远程网络攻击的绝大多数,这种攻击可以使得一个匿名的Internet用户有机会获得一台主机的部分或全部的控制...

    C语言源程序的缓冲区溢出漏洞分析及解决方案

    C语言源程序的缓冲区溢出漏洞分析及解决方案

    缓冲区溢出教程缓冲区溢出教程

    缓冲区溢出教程缓冲区溢出教程缓冲区溢出教程缓冲区溢出教程

    缓冲区溢出程序代码分析

    缓冲区溢出程序代码分析 缓冲溢出是指一种攻击系统的手段,通过往程序的缓冲区中写入超出其长度的内容造成溢出,从而破坏程序的堆栈,使程序转而执行其它指令,而达到攻击的目的。分布式拒绝服务(ddos)的入侵者...

    windows 缓冲区溢出

    我自己学习缓冲区溢出时在网上找到的一些缓冲区溢出保护与突破技巧的pdf文档

    缓冲区溢出光速入门 缓冲区溢出基基础

    缓冲区溢出通常是向数组中写数据时,写入的数据的长度超出了数组原始定义的大小。 比如前面你定义了int buff[10],那么只有buff[0] - buff[9]的空间是我们定义buff 时 申请的合法空间,但后来往里面写入数据时出现了...

    缓冲区溢出实验

    锐捷网络大学,isec综合实验之缓冲区溢出

    Q版缓冲区溢出教程

    Q版缓冲区溢出教程 写在前面 首先,我要声明,我打的这篇文档,原稿是《黑手缓冲区溢出教程》,而不是作者出的正版书,在 这里向王炜老大道歉!!因为我兜里的那个实在是那什么,外加上我们烟台这里买不到……不找...

    缓冲区溢出详解 PDF

    首先,我要声明,我打的这篇文档,原稿是《黑手缓冲区溢出教程》,而不是作者出的正版书,在这 里向王炜老大道歉!!因为我兜里的那个实在是那什么,外加上我们烟台这里买不到……不找什么借口了, 我会补一个正版书...

    Q版缓冲区溢出教程 学习缓冲区溢出的好教材

    学习缓冲区溢出的好教材!Q版缓冲区溢出教程Q版缓冲区溢出教程 学习缓冲区溢出的好教材!Q版缓冲区溢出教程Q版缓冲区溢出教程

    《网络安全技术》大作业:缓冲区溢出实验报告

    缓冲区溢出是因为在程序执行时数据的长度超出了预先分配的空间大小,导致覆盖了其他数据的分配区域,从而执行非授权指令,获取信息,取得系统特权进而进行各种非法操作导致程序运行失败、系统宕机、重新启动等后果。...

    论文研究-Windows针对缓冲区溢出的安全性改进及其缺陷 .pdf

    Windows针对缓冲区溢出的安全性改进及其缺陷,王硕,郭燕慧,本文简述了缓冲区溢出漏洞的原理,之后列举了一些Windows针对缓冲区溢出漏洞所做出的安全性改进。分析并研究这些改进后,结合前人��

    实验五 缓冲区溢出实验1

    实验五 缓冲区溢出实验1

    安全编程之缓冲区溢出.7z

    缓冲区溢出初步(标准栈溢出) 总结 & 提问 深入了解缓冲区溢出 总结 & 提问 安全编程防止缓冲区溢出(一些实例) 拓展:非x86平台上的缓冲区溢出 总结 & 提问

    谈谈缓冲区溢出的原理.docx

    谈谈缓冲区溢出的原理,避免程序被破解,谈谈缓冲区溢出的原理,避免程序被破解,谈谈缓冲区溢出的原理,避免程序被破解

    一个带有缓冲区溢出漏洞的程序

    一个带有缓冲区溢出漏洞的程序,在接受大于200的字符串时发生溢出。

Global site tag (gtag.js) - Google Analytics