1、一个程序在内存中通常分为程序段,数据端和堆栈三部分。程序段里放着程序的机器码和只读数据。数据段放的是程序中的静态数据。动态数据则通过堆栈来存放。在内存中,它们的位置是:+-+内存低端|程序段|-|数据段|堆栈|+-+内存高端当程序中发生函数调用时,计算机做如下操作:首先把参数压入堆栈;然后保存指令寄存器(IP)中的内容做为返回地址(RET);第三个放入堆栈的是基址寄存器(FP);然后把当前的栈指针(SP)拷贝到FP,做为新的基地址;最后为本地变量留出一定空间,把SP减去适当的数值。3.通过缓冲区溢出获得用户SHELL如果在溢出的缓冲区中写入我们想执行的代码,再覆盖返回地址(ret)的内容,使它
2、指向缓冲区的开头,就可以达到运行其它指令的目的。低内存端buffer sfp ret*str高内存端-栈顶|栈底|_|通常,我们想运行的是一个用户shell。4.利用缓冲区溢出进行的系统攻击如果已知某个程序有缓冲区溢出的缺陷,如何知道缓冲区的地址,在那儿放入shell代码呢?由于每个程序的堆栈起始地址是固定的,所以理论上可以通过反复重试缓冲区相对于堆栈起始位置的距离来得到。但这样的盲目猜测可能要进行数百上千次,实际上是不现实的。解决的办法是利用空指令NOP。在shell代码前面放一长串的NOP,返回地址可以指向这一串NOP中任一位置,执行完NOP指令后程序将激活shell进程。这样就大大增加了
3、猜中的可能性。三.缓冲区溢出的保护方法目前有四种基本的方法保护缓冲区免受缓冲区溢出的攻击和影响。在3.1中介绍了强制写正确的代码的方法。在3.2中介绍了通过操作系统使得缓冲区不可执行,从而阻止攻击者殖入攻击代码。这种方法有效地阻止了很多缓冲区溢出的攻击,但是攻击者并不一定要殖入攻击代码来实现缓冲区溢出的攻击(参见2.1节),所以这种方法还是存在很弱点的。在3.3中,我们介绍了利用编译器的边界检查来实现缓冲区的保护。这个方法使得缓冲区溢出不可能出现,从而完全消除了缓冲区溢出的威胁,但是相对而言代价比较大。在3.4中我们介绍一种间接的方法,这个方法在程序指针失效前进行完整性检查。这样虽然这种方法不
4、能使得所有的缓冲区溢出失效,但它的的确确阻止了绝大多数的缓冲区溢出攻击,而能够逃脱这种方法保护的缓冲区溢出也很难实现。然后在3.5,我们要分析这种保护方法的兼容性和性能优势(与数组边界检查)。3.1编写正确的代码编写正确的代码是一件非常有意义但耗时的工作,特别象编写C语言那种具有容易出错倾向的程序(如:字符串的零结尾),这种风格是由于追求性能而忽视正确性的传统引起的。尽管花了很长的时间使得人们知道了如何编写安全的程序,具有安全漏洞的程序依旧出现。因此人们开发了一些工具和技术来帮助经验不足的程序员编写安全正确的程序。最简单的方法就是用grep来搜索源代码中容易产生漏洞的库的调用,比如对strcp
5、y和sprintf的调用,这两个函数都没有检查输入参数的长度。事实上,各个版本C的标准库均有这样的问题存在。为了寻找一些常见的诸如缓冲区溢出和操作系统竞争条件等漏洞,代码检查小组检查了很多的代码。然而依然有漏网之鱼存在。尽管采用了strncpy和snprintf这些替代函数来防止缓冲区溢出的发生,但是由于编写代码的问题,仍旧会有这种情况发生。比如lprm程序就是最好的例子,虽然它通过了代码的安全检查,但仍然有缓冲区溢出的问题存在。为了对付这些问题,人们开发了一些高级的查错工具,如fault injection等。这些工具的目的在于通过人为随机地产生一些缓冲区溢出来寻找代码的安全漏洞。还有一些静
6、态分析工具用于侦测缓冲区溢出的存在。虽然这些工具帮助程序员开发更安全的程序,但是由于C语言的特点,这些工具不可能找出所有的缓冲区溢出漏洞。所以,侦错技术只能用来减少缓冲区溢出的可能,并不能完全地消除它的存在。除非程序员能保证他的程序万无一失,否则还是要用到以下3.2到3.4部分的内容来保证程序的可靠性能。3.2非执行的缓冲区通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被殖入被攻击程序输入缓冲区的代码,这种技术被称为非执行的缓冲区技术。事实上,很多老的Unix系统都是这样设计的,但是近来的Unix和MS Windows系统由于实现更好的性能和功能,往往在在数据段中动态地放入
7、可执行的代码。所以为了保持程序的兼容性不可能使得所有程序的数据段不可执行。但是我们可以设定堆栈数据段不可执行,这样就可以最大限度地保证了程序的兼容性。Linux和Solaris都发布了有关这方面的内核补丁。因为几乎没有任何合法的程序会在堆栈中存放代码,这种做法几乎不产生任何兼容性问题,除了在Linux中的两个特例,这时可执行的代码必须被放入堆栈中:信号传递:Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码来实现向进程发送Unix信号。非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的。GCC的在线重用:研究发现gcc在堆栈区里放置了可执行的代码作为在线重用之用。然而,关闭这
8、个功能并不产生任何问题,只有部分功能似乎不能使用。非执行堆栈的保护可以有效地对付把代码殖入自动变量的缓冲区溢出攻击,而对于其他形式的攻击则没有效果(参见2.1)。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其他的攻击可以采用把代码殖入堆或者静态数据段中来跳过保护。3.3数组边界检查殖入代码引起缓冲区溢出是一个方面,扰乱程序的执行流程是另一个方面。不象非执行缓冲区保护,数组边界检查完全放置了缓冲区溢出的产生和攻击。这样,只要数组不能被溢出,溢出攻击也就无从谈起。为了实现数组边界检查,则所有的对数组的读写操作都应当被检查以确保对数组的操作在正确的范围内。最直接的方法是检查所有的数组操作,
9、但是通常可以采用一些优化的技术来减少检查的次数。目前有以下的几种检查方法:3.3.1 Compaq C编译器Compaq公司为Alpha CPU开发的C编译器(在Tru64的Unix平台上是cc,在Alpha Linux平台上是ccc)支持有限度的边界检查(使用-check_bounds参数)。这些限制是:只有显示的数组引用才被检查,比如a3会被检查,而*(a+3)则不会。由于所有的C数组在传送的时候是指针传递的,所以传递给函数的的数组不会被检查。带有危险性的库函数如strcpy不会在编译的时候进行边界检查,即便是指定了边界检查。由于在C语言中利用指针进行数组操作和传递是如此的频繁,因此这种局
10、限性是非常严重的。通常这种边界检查用来程序的查错,而且不能保证不发生缓冲区溢出的漏洞。3.3.2 Jones&Kelly:C的数组边界检查Richard Jones和Paul Kelly开发了一个gcc的补丁,用来实现对C程序完全的数组边界检查。由于没有改变指针的含义,所以被编译的程序和其他的gcc模块具有很好的兼容性。更进一步的是,他们由此从没有指针的表达式中导出了一个基指针,然后通过检查这个基指针来侦测表达式的结果是否在容许的范围之内。当然,这样付出的性能上的代价是巨大的:对于一个频繁使用指针的程序如向量乘法,将由于指针的频繁使用而使速度比本来慢30倍。这个编译器目前还很不成熟;一些复杂的
11、程序(如elm)还不能在这个上面编译,执行通过。然而在它的一个更新版本之下,它至少能编译执行ssh软件的加密软件包。其实现的性能要下降12倍。3.3.3 Purify:存储器存取检查Purify是C程序调试时查看存储器使用的工具而不是专用的安全工具。Purify使用目标代码插入技术来检查所有的存储器存取。通过用Purify连接工具连接,可执行代码在执行的时候数组的所有引用来保证其合法性。这样带来的性能上的损失要下降3-5倍。3.3.4类型-安全语言所有的缓冲区溢出漏洞都源于C语言缺乏类型安全。如果只有类型-安全的操作才可以被允许执行,这样就不可能出现对变量的强制操作。如果作为新手,可以推荐使用
12、具有类型-安全的语言如Java和ML。但是作为Java执行平台的Java虚拟机是C程序,因此通过攻击JVM的一条途径是使JVM的缓冲区溢出。因此在系统中采用缓冲区溢出防卫技术来使用强制类型-安全的语言可以收到意想不到的效果。3.4程序指针完整性检查程序指针完整性检查和边界检查由略微的不同。与防止程序指针被改变不同,程序指针完整性检查在程序指针被引用之前检测到它的改变。因此,即便一个攻击者成功地改变了程序的指针,由于系统事先检测到了指针的改变,因此这个指针将不会被使用。与数组边界检查相比,这种方法不能解决所有的缓冲区溢出问题;采用其他的缓冲区溢出方法就可以避免这种检测。但是这种方法在性能上有很大
13、的优势,而且在兼容性也很好。程序完整性检查大体上有三个研究方向。在3.4.1中会介绍Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。在3.4.2中会介绍我们自己的堆栈保护方法所开发的一个编译器,它能够在函数调用的时候自动生成完整性检测代码。最后在3.4.3,我们介绍正在开发中的指针保护方法,这种方法类似于堆栈保护,它提供对所有程序指针的完整性的保护。3.4.1手写的堆栈监测Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。这个应用完全用手工汇编写的,而且只保护libc中的当前有效纪录函数。这个应用达到
14、了设计要求,对于基于libc库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。3.4.2堆栈保护:编译器生成的有效纪录完整性检测堆栈保护是一种提供程序指针完整性检查的编译器技术,通过检查函数活动纪录中的返回地址来实现。堆栈保护作为gcc的一个小的补丁,在每个函数中,加入了函数建立和销毁的代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节。而在函数返回时,首先检查这个附加的字节是否被改动过。如果发生过缓冲区溢出的攻击,那么这种攻击很容易在函数返回前被检测到。但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们,那么他就能成功地跳过堆栈保护的检测。通常,我们有如下的两种方案对
copyright@ 2008-2022 冰豆网网站版权所有
经营许可证编号:鄂ICP备2022015515号-1