网络通信 频道

黑客中级技术--缓冲区溢出攻击(下)

四.3 数组边界检查

  数组边界检查能防止所有的缓冲区溢出的产生和攻击。这是因为只要数组不能被溢出,溢出攻击也就无从谈起。为了实现数组边界检查,则所有的对数组的读写操作都应当被检查以确保对数组的操作在正确的范围内。最直接的方法是检查所有的数组操作,但是通常可以采用一些优化的技术来减少检查的次数。目前有以下的几种检查方法:

1、 Jones & Kelly: C的数组边界检查

  Richard Jones和Paul Kelly开发了一个gcc的补丁,用来实现对C程序完全的数组边界检查。由于没有改变指针的含义,所以被编译的程序和其它的gcc模块具有很好的兼容性。更进一步的是,他们由此从没有指针的表达式中导出了一个“基”指针,然后通过检查这个基指针来侦测表达式的结果是否在容许的范围之内。

  当然,这样付出的性能上的代价是巨大的:对于一个频繁使用指针的程序,比如向量乘法,将由于指针的频繁使用而使速度比本来慢30倍。这个编译器目前还很不成熟;一些复杂的程序还不能在这个上面编译,执行通过。

2、 Compaq C 编译器

  Compaq公司为Alpha CPU开发的C编译器支持有限度的边界检查
(使用check_bounds参数)。这些限制是:

只有显式的数组引用才被检查,比如“a[3]”会被检查,而“*(a+3)”则不会。

  由于所有的C数组在传送的时候是指针传递的,所以传递给函数的的数组不会被检查。带有危险性的库函数如strcpy不会在编译的时候进行边界检查,即便是指定了边界检查。

  由于在C语言中利用指针进行数组操作和传递是如此的频繁,因此这种局限性是非常严重的。通常这种边界检查用来程序的查错,而且不能保证不发生缓冲区溢出的漏洞。

3、 Purify:内存存取检查
Purify是C程序调试时查看内存使用的工具。Purify使用“目标代码插入”技术来检查所有的内存存取。通过用Purify连接工具连接,可执行代码在执行的时候数组的所有引用来保证其合法性。这样带来的性能上的损失要下降3-5倍。

4、 类型-安全语言

  所有的缓冲区溢出漏洞都源于C语言缺乏类型安全。如果只有类型-安全的操作才可以被允许执行,这样就不可能出现对变量的强制操作。如果作为新手,可以推荐使用具有类型-安全的语言如Java。但是作为Java执行平台的Java虚拟机是C程序,因此通过攻击JVM的一条途径是使JVM的缓冲区溢出。

四.4 程序指针完整性检查

  程序指针完整性检查和边界检查有略微的不同,程序指针完整性检查在程序指针被引用之前检测到它的改变。因此,即使一个攻击者成功地改变了程序的指针,由于系统事先检测到了指针的改变,因此这个指针将不会被使用。

  与数组边界检查相比,这种方法不能解决所有的缓冲区溢出问题;采用其它的缓冲区溢出攻击方法就可以避免这种检测。但是这种方法在性能上有很大的优势,而且在兼容性也很好。

程序完整性检查大体上有三个研究方向:

Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。堆栈保护方法所开发的一个编译器,它能够在函数调用的时候自动生成完整性检测代码。正在开发中的指针保护方法,这种方法类似于堆栈保护,它提供对所有程序指针的完整性的保护。

1、堆栈监测

  Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。这个应用完全用手工汇编写的,而且只保护libc中的当前有效纪录函数。这个应用达到了设计要求,对于基于libc库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。

2、堆栈保护:编译器生成的有效纪录完整性检测
   堆栈保护是一种提供程序指针完整性检查的编译器技术,通过检查函数活动纪录中的返回地址来实现。堆栈保护作为gcc的一个小的补丁,在每个函数中,加入了函数建立和销毁的代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节。而在函数返回时,首先检查这个附加的字节是否被改动过。如果发生过缓冲区溢出的攻击,那么这种攻击很容易在函数返回前被检测到。

   但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们,那么他就能成功地跳过堆栈保护的检测。通常,有如下的两种方案对付这种欺骗:

(1)终止符号:
  利用在C语言中的终止符号如0(null),CR,LF,-1(EOF)等不能在常用的字符串函数中使用,因为这些函数一旦遇到这些终止符号,就结束函数过程了。

(2)随机符号:
   利用一个在函数调用时产生的一个32位的随机数来实现保密,使得攻击者不可能猜测到附加字节的内容。而且,每次调用,附加字节的内容都在改变,也无法预测。

   堆栈保护对于各种系统的缓冲区溢出攻击都有很好的保护作用,并能保持较好的兼容性和系统性能。堆栈保护版本的Red Hat Linux 5.1已经在各种系统上运行了多年,包括个人的笔记本计算机和工作组文件服务器。这个系统和本来的系统工作完全一样,这表明堆栈保护并不对系统的兼容性构成很大的影响。堆栈保护中增加了系统的开销,而在网络的测试中,表明这种开销不是很大,参见表1。

表1      堆栈保护对系统性能影响的测试结果
  通过在所有的代码指针之后放置附加字节来检验指针在被调用之前的合法性。如果检验失败,会发出报警信号和退出程序的执行,就如同在堆栈保护中的行为一样。这种方案有两点需要注意:
(1)附加字节的定位:
   附加字节的空间是在被保护的变量被分配的时候分配的,同时在被保护字节初始化过程中被初始化。这样就带来了问题:为了保持兼容性,不想改变被保护变量的大小,因此不能简单地在变量的结构定义中加入附加字节。还有,对各种类型也有不同附加字节数目。
(2)检查附加字节:
每次程序指针被引用的时候都要检查附加字节的完整性。这也存在问题:因为编译器关心指针的使用,而各种的优化算法倾向于从内存中读入变量。随着不同类型的变量,读入的方法也各自不同。
   目前为止,只有很少一部分使用非指针变量的攻击能逃脱指针保护的检测。但是,可以通过在编译器上强制对某一变量加入附加字节来实现检测,这时需要程序员自己手工加入相应的保护了。

四.5 程序指针完整性检查与数组边界检查的比较

程序指针完整性检查与数组边界检查相比,并不能防止所有的缓冲区溢出问题。然而在执行的性能和兼容性上具有相当的优势:

(1)性能:
   边界检查必须在每个数组元素操作时完成一次检查。相比之下,程序指针检查只在被引用的时候实现检查。无论在C还是在C++中,这种花在程序指针引用上的开销始终比数组的指针引用小。

(2)应用效率:
   边界检查最难实现之处在于在C语言中,很能确定数组的边界。这是由于在C中,数组的概念和通用指针的混用造成的。由于一个指针是一个独立的对象,没有与特定的边界条件关联,只有一个系统的机器字来存储它,而标识边界信息的资料却没有存放。因此需要特殊的方法来恢复这些信息;数组的引用将不再是一个简单的指针,而是一个对缓冲区描述的指针组。

(3)与现有代码的兼容性:
   一些边界检查方法为了与现有的代码保持兼容而在系统的性能上得到了损失。而另一些则用别的方法达到目的。这样就打破的传统的C的转换规则,转而产生了一类新的C编译器,只能编译C的一个子集,有的还不能使用指针或者需要别的改变。

四.6防卫方法的综合分析
   在这里综合研究在本文描述的各种漏洞攻击和防卫方法,以此来确定何种组合能完全消除缓冲区溢出问题。表2列出了关于缓冲区溢出的攻击和防卫的方法。其中,没有把边界检查计算在内,因为它能有效地防止所有的缓冲区溢出,但是所需的开销也是惊人的。在表2中,最上面一行列出了攻击代码植入的内存空间,最左面一列是溢出方法,中间的单元为对应的防卫措施。

   最常见的缓冲区溢出形式是攻击活动纪录然后在堆栈中植入代码。这种类型的攻击在1997年中有很多纪录。而非执行堆栈和堆栈保护的方法都可以有效防卫这种攻击。非执行堆栈可以防卫所有把代码植入堆栈的攻击方法,堆栈保护可以防卫所有改变活动纪录的方法。这两种方法相互兼容,可以同时防卫多种可能的攻击。

表2      缓冲区溢出的攻击和防卫的方法

   其余的攻击基本上可以用指针保护的方法来防卫,但是在某些特殊的场合需要用手工来实现指针保护。全自动的指针保护需要对每个变量加入附加字节,这样使得指针边界检查在某些情况下具有优势。

、文章转载地址:http://www.cnpaf.net/Class/hack/05121820345256508436.htm

0
相关文章