首页 > 代码库 > 浅谈一下缓冲区溢出

浅谈一下缓冲区溢出

0x01 缓冲出溢出概念

  缓冲区是用户为程序运行时在计算机中申请的一段连续内存,它保存了给定类型的数据。

  缓冲区溢出就是在向缓冲区写入数据时,由于没有做边界检查,导致写入缓冲区的数据超过预先分配的边界,从而使溢出数据覆盖在合法数据上而引起系统异常的一种现象。

  缓冲区溢出包括堆栈溢出和堆溢出。

0x02 进程内存的划分

  要清楚缓冲区溢出的原理,就先要对计算机执行程序的内存结构加以分析。

  根据不同的操作系统,一个进程可能被分配到不同的内存区域去执行,但不管什么样的操作系统、什么样的计算机架构,进程使用的内存都可以按照功能分为下面4个部分:

  1. 代码段:存储着执行程序的二进制机器代码,计算机会到这个区域取指令并执行。

  2. 数据段:用于存储全局变量、静态变量等数据。

  3. 堆区:进程可以在通过malloc等函数动态地在堆区申请一定大小的内存,并在用完之后释放内存。

  4. 堆栈区:用于动态地存储函数之间的调用关系,以保证被调用的函数在返回时恢复到母函数中继续执行。函数调用时的参数和局部变量都保存在堆栈中。由系统自动分配。例如,在函数中声明一个局部变量int b;系统自动在栈中为b开辟空间。

  需要注意的是,Intel x86机器上的堆栈被认为是反向的,即堆栈是由高端地址向下增长的。即当一个信息被压栈时,ESP减少,新元素被写入目标地址,当一个信息被弹出时,则从ESP指针所指向的地址中读出一个元素,ESP 增加,向上边界移动并压缩堆栈。

  在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,总之是一个编译时就确定的常数,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

0x03 函数调用过程

  堆栈(简称栈)是一种先进后出的数据表结构。栈有两种常用操作:压栈和出栈。栈有两个重要属性:栈顶和栈底。

  内存的栈区实际上指的是系统栈。系统栈由系统自动维护,用于实现高级语言的函数调用。

  每一个函数在被调用时都有属于自己的栈帧空间。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中,所以正在运行的函数总是在系统栈的栈顶。当函数返回时,系统栈会弹出该函数所对应的栈帧空间。

  win32系统提供了两个特殊的寄存器来标识系统栈最顶端的栈帧。

  ESP:扩展堆栈指针。该寄存器存放一个指针,它指向系统栈最顶端那个函数栈帧的栈顶。
  EBP:扩展基指针。该寄存器存放一个指针,它指向系统栈最顶端那个函数栈帧的栈底。
  此外,EIP寄存器(扩展指令指针)对于堆栈的操作非常重要, EIP包含将要被执行的下一条指令的地址。

  函数栈帧:ESP和EBP之间的空间为当前栈帧,每一个函数都有属于自己的ESP和EBP指针。ESP表示了当前栈帧的栈顶,EBP标识了当前栈的栈底。

在函数栈帧中,一般包含以下重要的信息:

  1. 局部变量:系统会在该函数栈帧上为该函数运行时的局部变量分配相应的内存空间。

  2. 函数返回地址:存放了本函数执行完后应该返回到调用本函数的母函数(主调函数)中继续执行的指令的位置。

在Win32 操作系统中,当程序里出现函数调用时,系统会自动为这次函数调用分配一个堆栈结构。函数的调用大概包括下面几个步骤:

  1. 参数入栈:一般是将被调函数的参数从右到左依次压入系统栈(即调用该函数的母函数的函数栈帧)中。

  2. 返回地址入栈:把当前EIP的值(当前代码区正在执行指令的下一条指令的地址)压入栈中,作为返回地址。

  3. 代码区跳转:将EIP指向被调用函数的入口处。

  4. 栈帧调整:主要是用来保持堆栈平衡,这个过程可以由被调用函数执行,也可以由母函数执行,具体由编译器决定。首先是将EBP压入栈中(用于调用返回时恢复原堆栈),并把母函数的ESP的值送入寄存器EBP中,作为新的基址(新栈帧的EBP实际上保存的是母函数的ESP),最后,为本地变量留出空间,把ESP减去适当的值(注意:内存分配是以字为单位的)

main()
{
     ……
     sub(arg1,arg2,arg3); //调用sub 子函数,参数为arg1,arg2,arg3
 
     return ;
}
 
sub( int  arg1, int  arg2, int  arg3)
{
     char  a,b[10]; //sub 子函数里面的局部变量
     ……
}

以上面的代码为例。在子函数sub 中,定义了两个局部变量,一个是字符变量a,一个是字符数组变量b[10]。这两个局部变量在栈上的分布情况:

    如果sub子函数中存在对b[10]进行写入操作时,如果没有对写入数据的长度进行检查,系统为b[10]申请的内存空间将不够用,这时就会发生缓冲区溢出。发生缓冲区溢出时,写入的数据会把内存中与b[10]数组相邻的内存空间也覆盖掉(从内存低地址向内存高地址依次覆盖)。首先被覆盖的是变量a,如果变量a会影响程序的处理逻辑(如根据a的内容的不同进行不同的操作),这只是会使程序的处理逻辑出现问题,这还不是最致命的。

  当写入b[10]的数据溢出足够多时,将会覆盖sub函数的EBP栈帧和函数的返回地址。上一节提到的函数的返回过程中,函数在执行完之后会将返回地址的值弹出给EIP,继而转到相应的位置继续执行。如果我们写入的数据覆盖了返回地址的话,那么我们就可以修改EIP的值,使得程序跳转到我们指定的内存地址去执行代码。这就产生了一个堆栈缓冲区溢出漏洞。