首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 操作系统 > UNIXLINUX >

Linux上缓冲区溢出攻击的原理及对策

2012-11-08 
Linux下缓冲区溢出攻击的原理及对策首先,execve(2)会负责为进程代码段和数据段建立映射,真正将代码段和数

Linux下缓冲区溢出攻击的原理及对策


首先,execve(2)会负责为进程代码段和数据段建立映射,真正将代码段和数据段的内容读入内存是由系统的缺页异常处理程序按需完成的。另外,execve(2)还会将bss段清零,这就是为什么未赋初值的全局变量以及static变量其初值为零的原因。进程用户空间的最高位置是用来存放程序运行时的命令行参数及环境变量的,在这段地址空间的下方和bss段的上方还留有一个很大的空洞,而作为进程动态运行环境的堆栈和堆就栖身其中,其中堆栈向下伸展,堆向上伸展。

知道了堆栈在进程地址空间中的位置,我们再来看一看堆栈中都存放了什么。相信读者对C语言中的函数这样的概念都已经很熟悉了,实际上堆栈中存放的就是与每个函数对应的堆栈帧。当函数调用发生时,新的堆栈帧被压入堆栈;当函数返回时,相应的堆栈帧从堆栈中弹出。典型的堆栈帧结构如图4所示。

堆栈帧的顶部为函数的实参,下面是函数的返回地址以及前一个堆栈帧的指针,最下面是分配给函数的局部变量使用的空间。一个堆栈帧通常都有两个指针,其中一个称为堆栈帧指针,另一个称为栈顶指针。前者所指向的位置是固定的,而后者所指向的位置在函数的运行过程中可变。因此,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移。对照图4可知,实参的偏移为正,局部变量的偏移为负。


图4 典型的堆栈帧结构
Linux上缓冲区溢出攻击的原理及对策

介绍了堆栈帧的结构,我们再来看一下在Intel i386体系结构上堆栈帧是如何实现的。图5和图6分别是一个简单的C程序及其编译后生成的汇编程序。


图5 一个简单的C程序example1.c

读者不妨回过头去与图4对比一下。这里有几点需要说明。首先,在Intel i386体系结构下,堆栈帧指针的角色是由ebp扮演的,而栈顶指针的角色是由esp扮演的。另外,函数function的局部变量buffer[14]由14个字符组成,其大小按说应为14字节,但是在堆栈帧中却为其分配了16个字节。这是时间效率和空间效率之间的一种折衷,因为Intel i386是32位的处理器,其每次内存访问都必须是4字节对齐的,而高30位地址相同的4个字节就构成了一个机器字。因此,如果为了填补buffer[14]留下的两个字节而将sum分配在两个不同的机器字中,那么每次访问sum就需要两次内存操作,这显然是无法接受的。还有一点需要说明的是,正如我们在本文前言中所指出的,如果读者使用的是较高版本的gcc的话,您所看到的函数function对应的堆栈帧可能和图7所示有所不同。上面已经讲过,为函数function的局部变量buffer[14]和sum在堆栈中分配空间是通过在图6中第11行对esp进行减法操作完成的,而sub指令中的20正是这里两个局部变量所需的存储空间大小。但是在较高版本的gcc中,sub指令中出现的数字可能不是20,而是一个更大的数字。应该说这与优化编译技术有关,在较高版本的gcc中为了有效运用目前流行的各种优化编译技术,通常需要在每个函数的堆栈帧中留出一定额外的空间。

下面我们再来看一下在函数function中是如何将a、b、c的和赋给sum的。前面已经提过,在函数中访问实参和局部变量时都是以堆栈帧指针为基址,再加上一个偏移,而Intel i386体系结构下的堆栈帧指针就是ebp,为了清楚起见,我们在图7中标出了堆栈帧中所有成分相对于堆栈帧指针ebp的偏移。这下图6中12至16的计算就一目了然了,8(%ebp)、12(%ebp)、16(%ebp)和-20(%ebp)分别是实参a、b、c和局部变量sum的地址,几个简单的add指令和mov指令执行后sum中便是a、b、c三者之和了。另外,在gcc编译生成的汇编程序中函数的返回结果是通过eax传递的,因此在图6中第17行将sum的值拷贝到eax中。

最后,我们再来看一下函数function执行完之后与其对应的堆栈帧是如何弹出堆栈的。图6中第21行的leave指令将堆栈帧指针ebp拷贝到esp中,于是在堆栈帧中为局部变量buffer[14]和sum分配的空间就被释放了;除此之外,leave指令还有一个功能,就是从堆栈中弹出一个机器字并将其存放到ebp中,这样ebp就被恢复为main函数的堆栈帧指针了。第22行的ret指令再次从堆栈中弹出一个机器字并将其存放到指令指针eip中,这样控制就返回到了第36行main函数中的addl指令处。addl指令将栈顶指针esp加上12,于是当初调用函数function之前压入堆栈的三个实参所占用的堆栈空间也被释放掉了。至此,函数function的堆栈帧就被完全销毁了。前面刚刚提到过,在gcc编译生成的汇编程序中通过eax传递函数的返回结果,因此图6中第38行将函数function的返回结果保存在了main函数的局部变量i中。


解决的办法很简单,既然不能对代码段进行写操作,我们就把图12中的代码挪到可写的数据段或堆栈段中。可是一段可执行的代码在数据段中应该怎么表示呢?其实,内存中存放着的无非是0和1这样的比特,当我们的程序将其用作代码时这些比特就成了代码,而当我们的程序将其用作数据时这些比特又成了数据。我们先来看一下图12中的代码在内存中是如何存放的,通过gdb中的x命令可以很容易的做到这一点,如图14所示。


图14 通过gdb中的x命令查看图12中的代码在内存中对应的数据

下面我们该回头看看本文开头的那个Linux下缓冲区溢出攻击实例了。攻击程序exe.c利用了系统中存在漏洞的程序toto.c,通过以下步骤向系统发动了一次缓冲区溢出攻击:

通过命令行参数argv[2]得到toto.c程序中缓冲区buffer[96]的地址,并将该地址填充到large_string[128]中;将我们已经准备好的shellcode拷贝到large_string[128]的开头;通过环境变量KIRIKA将我们的shellcode注射到buffer[96]中;当toto.c程序中的main函数返回时,buffer[96]中的shellcode得以运行;由于toto的属主为root,并且具有setuid属性,因此我们得到的shell便具有了root权限。

程序exe.c的控制流程与图19所示程序testsc.c的控制流程非常相似,唯一的不同在于这次我们的shellcode是寄宿在toto运行时的堆栈里,而不是在数据段中。之所以不能再将shellcode放在数据段中是因为当我们在程序exe.c中调用execle(3) 运行toto时,进程整个地址空间的映射会根据toto程序头部的描述信息重新设置,而原来的地址空间中数据段的内容已经不能再访问了,因此在程序exe.c中shellcode是通过环境变量来传递的。

怎么样,是不是感觉传说中的黑客不再像你想象的那样神秘了?暂时不要妄下结论,在上面的缓冲区溢出攻击实例中,攻击程序exe之所以能够准确的将shellcode注射到toto的buffer[96]中,关键在于我们在toto程序中打印出了buffer[96]在堆栈中的起始地址。当然,在实际的系统中,不要指望有像toto这样家有丑事还自揭疮疤的事情发生。

$ gcc -static testlibsafe.c -o testlibsafe_static$ env | grep LDLD_PRELOAD=/lib/libsafe.so.2$ ./testlibsafe_staticSegmentation fault (core dumped)

如果在使用gcc编译时加上-static选项,那么链接时使用的便是静态链接库。在系统已经安装了Libsafe的情况下,可以看到testlibsafe_static再次产生了Segmentation fault。

另外,正如我们在本文前言中所指出的那样,如果读者使用的是较高版本的bash的话,那么即使您在运行攻击程序exe之后得到了一个新的shell,您可能会发现并没有得到您所期望的root权限。其实这正是的高版本bash的改进之一。由于近十年来缓冲区溢出攻击屡见不鲜,而且大部分的攻击对象都是系统中属主为root的setuid程序,以借此获得root权限。因此以root权限运行系统中的程序是十分危险的。为此,在新的POSIX.1标准中增加了一个名为seteuid(2)的系统调用,其作用在于改变进程的effective uid。而新版本的bash也都纷纷采用了这一技术,在bash启动运行之初首先通过调用seteuid(getuid())将bash的运行权限恢复为进程属主的权限,这样就出现了我们在高版本bash中运行攻击程序exe所看到的结果。那么高版本的bash就已经无懈可击了吗?其实不然,只要在通过execve(2)创建shell之前先调用setuid(0)将进程的uid也改为0,bash的这一改进也就徒劳无功了。也就是说,你所要做的就是遵照前面所讲的系统调用规则将setuid(0)加入到shellcode中,而新版shellocde的这一改进只需要很少的工作量。附件中的shellcodeasm3.c和exe_pro.c告诉了你该如何去做。

结束语

安全有两种不同的表现形式,一种是如果你所使用的系统在安全上存在漏洞,但是黑客们对此一无所知,那么你可以暂且认为你的系统是安全的;另一种是黑客和你都发现了系统中的安全漏洞,但是你会想方设法将漏洞弥补上,使你的系统真正无懈可击。你想要的是哪一种呢?圣经上的一句话给出了这个问题的答案,而这句话也被刻在了美国中央情报局大厅的墙壁上:“你应当了解真相,真相会使你自由。”


参考资料

Aleph One. Smashing The Stack For Fun And Profit.


Pierre-Alain FAYOLLE, Vincent GLAUME. A Buffer Overflow Study -- Attacks & Defenses.


Taeho Oh. Advanced buffer overflow exploit.


绿盟科技(nsfocus). NSFOCUS 2002年十大安全漏洞, 2002, http://www.nsfocus.net/index.php?act=sec_bug&do=top_ten


王卓威。基于系统行为模式的缓冲区溢出攻击检测技术。


developerWorks上的 《使您的软件运行起来:防止缓冲区溢出》为您列出了标准C库中所有存在安全隐患的函数以及对这些函数的使用建议。


毛德操,胡希明的《Linux内核源代码情景分析》向读者介绍了Linux下嵌入式汇编语言的语法。


W.Richard Stevens的《Advanced Programming in the UNIX Environment》为您详细介绍了uid和effective uid的概念以及setuid(2)和seteuid(2)等相关函数的用法。


Joel Scambray, Stuart McClure, George Kurtz的《Hacking Exposed》向读者介绍了网络安全的方方面面,从而使读者对网络安全有更多的了解,知道如何去加强安全性。


Intel. Intel Architecture Software Developer's Manual. Intel Corporation.

热点排行