引言:栈溢出攻击是通过构造特殊的代码来达到溢出攻击的一种攻击方式,可以造成系统异常甚至获取计算机权限等危害。本文通过对栈的结构分析,探讨了栈溢出的形成原理及防范办法,是提高软件的安全性、网络的安全性的一个重要部分。
程序在开发过程中,出现溢出错误是正常现象,也是一种较严重的程序错误,因为溢出错误不仅仅会造成程序的异常、丢失数据等问题,严重时还会造成操作系统的异常甚至崩溃。
程序在运行过程中,进程会被加载到内存的不同区域中执行,而按功能划分,进程所使用的内存空间可以分成四类:1.数据区,用来存储全局变量、常量等;2.栈区,用来存储函数间的调用关系,从而保证函数调用结束后,返回到调用点继续向下执行;3.堆区,是系统动态分配和回收的一段特殊内存空间,进程可以动态地申请,作为缓冲区来使用,使用完成后,按照不同的堆算法回收;4.代码区,用于存储程序执行过程中的机器指令,CPU会按照程序执行流程逐条取出后依次执行。
上述四类内存空间中,栈区是由操作系统自动维护的,这是保证函数调用的基础,也是简化程序设计的难度和降低程序的复杂度。一般来说,栈的绝大多数操作,如PUSH、POP等,对于C语言等高级设计语言来说都是透明的,操作系统都有丰富、完善的函数、接口等供程序员直接调用。同一个文件的不同函数的代码在内存代码区中的分布的先后顺序也不固定,一般是根据一定的内存分配算法来随机分配的。
当CPU在执行到调用function_A函数时,会从代码区中的main方法所在的区域跳转到function_A函数对应的代码区,并从那里取得指令继续执行。当function_A函数执行完后需要返回时,又会跳转到main方法对应的指令区域并继续向下执行。上述代码区中的跳转都是通过栈来实现的,当函数调用发生时,栈区会为每个函数开辟一个新的栈,并把函数的相关的各寄存器信息等压入栈中,同时该栈在内存中会以独占方式存在,正常情况下,其它函数不会访问到它的。当函数调用完成后,栈中的数据也会被依次POP,恢复到调用函数前的各寄存器数据,代码继续向下执行。
正常情况下,代码区中的跳转都是通过栈区来完成的,当函数调用发生时,栈区会为函数开辟一个新的栈区单元,并将相关数据PUSH后,这一内存区域理论上是独占属性,不会再被分配或占用,只有当函数返回时,栈里数据全部POP后才会调用相应的回收机制回收内存后再分配。
以C语言为例,其函数调用时,Main函数调用function_A函数后,会在系统分配的栈区中PUSH相关的数据,而当Function_A调用Function_B时,同样会先把自己的栈区单元压入函数返回地址,然后为Function_B创建新的栈区单元并压入栈区。在Function_B返回时,Function_B的栈区单元被弹出栈区,而Function_A栈区单元中的返回地址则会位于栈顶,而此时程序会继续跳转到Function_A代码区中执行。在Runction_A返回时,其栈区单元弹出栈区,main方法栈区单元中的返回地址位于栈顶,此时CPU则会按这个地址跳转,回到main方法中继续向下执行。也就是说,每个函数独占自己的栈区单元空间,当前运行的函数的栈区单元总是位于栈顶,而栈顶的单元地址,通常也是由CPU的ESP和EBP两个寄存器来标识,其中ESP为指针寄存器,而EBP则为基址指针寄存器,共同来指向栈区的顶部单元。
栈区中,一般会包含几类较重要的信息,包括局部变量、状态值、返回地址,函数调用的相关指令,一般如下:
上面代码部分,包括了函数调用的几个基本步骤:
1.参数入栈。
2.返回地址入栈:将当前代码区调用的指令下一条地址入栈,供函数返回时继续执行。
3.代码区跳转:CPU从当前代码区跳转到被调用函数入口地址处。
4.栈区单元调整,包括了保存当前栈区单元状态值,EBP入栈后,当前栈区单元切换到新栈区单元,将ESP值装入EBP,更新栈区单元底部,给新栈区单元分配空间,将ESP减去所需要的空间大小。
栈溢出的基本思路是人为构造代码数据,覆盖函数的返回地址,从而改变程序的执行流程,其难点在于如何准确定位,并构造一段数据代码,恰好覆盖返回地址,而又不造成程序的执行错误。
除了上述构造特殊的入口地址达到栈溢出的效果以外,局部静态变量过大也比较常见。对栈溢出原理清楚以后,解决办法主要有两种:
1.增加栈内在的数目,增加办法相对较简单,不同的编译器都有类似的设置,但这种办法也容易造成一些不可预计的问题,例如影响稳定性、数据库ADO连接异常等。
2.使用堆内存,这也是得到多数程序员认可的可行性较高的办法,实现手段也有多种,如可以把数组的定义改为指针,然后申请动态内存,也可以把局部变量设置成全局变量或静态变量,当然定义一个大数组,有时能更好的解决栈溢出问题。