现在的位置: 首页 > 综合 > 正文

缓冲区溢出攻击原理、方法及防范(一)

2012年06月03日 ⁄ 综合 ⁄ 共 1984字 ⁄ 字号 评论关闭

由于C/C++语言本身没有数组越界检查机制,当向缓冲区里写入的数据超过了为其分配的大小时,就会发生缓冲区溢出。

攻击者可以利用缓冲区溢出来窜改进程运行时栈,从而改变程序的正常流向。在分析缓冲区溢出攻击的原理之前,我们先来

复习一下进程在内存中的结构。

进程在内存中的组织形式

       

Text段主要包含程序代码(一系列可执行的指令),另外还有一些只读的数据。通常操作系统标记该段为只读段,

如果有进程试图修改该段,会引发段错误。

Data段包括已初始化和未初始化的全局变量,静态变量也放于此处。该段的大小是在编译时期计算的。

紧接着Data段的一块内存是由堆和栈共享的区域。堆向下(高地址)增长,栈向上(低地址)增长。

堆中保存动态分配的数据,调用malloc函数或者calloc函数或者使用new操作符会在堆上分配内存。

栈中保存的是函数调用时传递的参数,局部变量的值和一些记录进程运行状态的寄存器的值。

堆和栈所占内存都是在运行期间分配的。如果进程的堆或栈的增长超出了为进程分配的内存大小,

那么该进程会被阻塞,操作系统重新为其分派一块更大的内存。

栈的作用

栈这种数据结构,我们再熟悉不过。它的特点是后进先出(LIFO),在它之上有两个著名的函数,push和pop。

函数是结构化程序设计最重要的技术。程序的执行过程可以看作是连续的函数调用。当一个函数执行完毕时,程序要回到调用

指令的下一条指令(紧接call指令)处继续执行,而栈的特点刚好适合这个条件。

在介绍函数调用的细节之前,我们先来看一下栈帧的概念。

栈帧是栈中的逻辑片段。每个函数调用时,都会push一个栈帧,当函数返回时,pop。

栈帧里面存放着函数的参数,函数局部变量以及为了恢复前一栈帧所需要的数据。

 我们知道,栈是实现相关的,不同架构的计算机,栈的增长方向和栈顶指针(ESP)的指向是不一样的。这篇文章里所讲的栈,

都是向低地址增长,栈顶指针是指向栈顶的。 由于ESP的值在程序执行过程中会频繁的改变,用到它的偏移量来定位变量会

很麻烦。因此,编译器使用另外一个寄存器EBP来记录局部基地址。EBP的值对于当前函数来说是不变的。函数局部变量和参数

都可以用到EBP的偏移量来定位。当进入一个新函数执行时,首先把旧的EBP值压栈,然后把当前ESP值作为新的EBP值,接着

改变ESP值来为函数局部变量预留出空间。

函数调用

先看一段代码:

1 void func(int a, int b)
2 {
3     char buffer[5];
4 }
5 int main(void)
6 {   
7     func(8, 10);
8     return 0;
9 }

我们把它编译成汇编代码: $ gcc -S function_call.c

可以看到func(8, 10)被译成:

1 movl    $10, 4(%esp)
2 movl    $8, (%esp)
3 call    _func

参数10和8依次压栈,然后执行call指令。call指令会把指令指针寄存器EIP的值压栈,作为函数调用完成后的返回地址ret。

1 _func:
2     pushl    %ebp
3     movl    %esp, %ebp
4     subl    $16, %esp

_func函数先把旧的ebp值压栈,然后把esp的值赋给ebp,作为新的ebp值,最后把esp减去16为局部变量buffer留出空间。

注意虽然buffer只要5个字节,但是由于栈是以16字节边界对齐的,所以实际上分配了16个字节的空间。

func函数被调用时,栈的结构:

好,现在让我们来看一下缓冲区溢出会发生什么?

01 #include <string.h>
02 void func(char *pStr)
03 {
04     char buffer[5];
05     strcpy(buffer, pStr);
06 }
07 int main(void)
08 {
09     char str[32];
10     for (int i = 0; i < 31; ++i)
11         str[i] = 'A';
12     func(str);
13     return 0;
14 }

上面代码存在典型的缓冲区溢出问题。编译,运行,发生了什么?

嗯,弹出一个我们很熟悉的内存访问错误消息框

在上面的代码中,我们向buffer写入一个长字符串,由于strcpy函数没有越界检查,它会一直工作,直到遇见NULL字符。

首先旧的ebp值会被覆盖,接着函数返回地址ret也被改写!甚至连str自身的部分值也被重写了!

'A'的ascii码是0x41,ret的值此时变为0x41414141,这已经不属于该进程的寻址范围内了。

当func函数返回,ret的值送到EIP寄存器,再取指令时便会发生内存访问错误了。

我们发现通过缓冲区溢出的确可以修改函数的返回地址,从而改变程序的正常执行流向。

理论上,我们可以利用这一点来执行任意代码。(未完,待续......)

抱歉!评论已关闭.