2.1缓冲区溢出攻击的产生
C编程语言中,静态变量分配在数据段中,动态变量分配在堆栈段中,C语言允许程序员在运行时在内存的两个不同部分(堆栈和堆)中创建存储器。通常,分配到堆的数据是那些malloc()或新建时获得的数据,而分配到堆栈的数据一般包括非静态的局部变量和所有按值传递的参数。大部分其它信息存储在全局静态存储器中。一个程序在内存中通常分为程序段、数据段和堆栈三个部分。程序段里为程序的机器码和只读数据,这个段通常是只读代码,故禁止对程序段进行写操作。数据段放的是程序中的静态数据。
存储器主要分为三个部分,一是文本区域,即程序区,用来存储程序指令,只读属性;二是数据区域,它的大小可以由brk()系统调用来改变;三是堆栈,其特点是LIFO(last in, first out)。当C程序调用函数的时候,首先将参数压入堆栈,然后保存指令寄存器(IP)中的
内容作为返回地址(RET),放入堆栈的是地址寄存器(FP),然后把当前的栈指针(SP)拷贝到FP,作为新的基地址,并为本地变量留出一定的空间,把SP减去适当的数值。
计算机执行一条指令,并保留指向下一条指令的指针(IP)。当函数或过程被调用的时候,在堆栈中被保留下来的指令指针将被作为返回地址(RET)。执行完成后,RET替换IP,程序接着继续执行本来的流程。
这里有一个直观的缓冲区溢出的小例子:
void function(char *str)
{
char buffer[16];
strcpy(buffer, str);
}
Void main()
{
int I;
char buffer[128];
for(I=0; I<127; I++)
buffer[I]=A;
buffer[127]=0;
function(buffer);
printf(“This is a test.
”);
}
在函数function中,将一个128字节长度的字符串拷贝到只有16字节长的局部缓冲区中。在使用strcpy()函数前,没有进行缓冲区边界检查,导致从buffer开始的256个字节都将被*str的
内容A覆盖,包括堆栈指针和返回地址,甚至*str都将被A覆盖。
再看看堆栈的结构,由于栈式内存分配具有一条指令即可为子程序分配全部局部变量的存储空间的特点,分配和去配的开销极低,高级语言通常在堆栈上分配局部存储空间。同时,堆栈也被用来存放子程序的返回地址。对C语言来说,调用函数的语句f(arg1,arg2,…,argn)被翻译为如下指令:
push argn
…….
push arg1
push n
call f
而函数的入口则翻译为如下入口指令(在Intel X86上)
pushl ebp
mov esp,ebp
sub esp,m #m为f的局部变量的空间大小
在Intel X86体系结构上,堆栈是从上向下生长的,因此调用以上函数时的堆栈结构如图1所示:
|
arg1 |
|
…… |
|
argn |
|
n |
|
返回地址 |
|
ebp |
|
局部变量 |
高地址
低地址
图1 堆栈结构图
例如,调用以下函数时
Void f(char *src)
{ char dest[4];
memcpy(dest, src,12);
}
堆栈及变量的位置如图2所示:
|
src |
|
l |
|
返回地址 |
|
ebp |
|
dest[3] |
|
dest[2] |
|
dest[1] |
|
dest[0] |
高地址
低地址
图2 堆栈及位置的变量图
从堆栈结构可以看到,当用精心准备好的地址改写返回地址时,即可把控制流程引向自己的代码。C2级操作系统提供了进程空间的隔离机制,因此,利用缓冲区溢出攻击可以在别的进程上下文中执行自己的代码,从而绕过操作系统的安全机制,下面是一个例子:
Void main()
{
char *str[2]={”/bin/sh”,0};
exec (“/bin/sh”,str,0);
}
编译后反编译,并加以整理,得到与以上程序等价的机器码:
“xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00”
“x00xb8x0bx00x00x00x89xf3x8dx4ex08x8dx56x0cxcdx80”
“xb8x01x00x00x00xbbx00x00x00x00xcdx80xe8xdlxffxff”
“xffx2fx62x69x6ex2fx73x68x00x89xecx5dxc3”
事例程序如下:
/ test /
char shellcode[]=
{“xebx2ax5ex89x76x08xc6x46x07x00xc7x46x0cx00x00x00”