堆栈溢出是说堆区和栈区的溢出,二者同属于
缓冲区溢出
。从上面关于堆区和栈区的解释可以看出,一旦程序确定,堆栈内存空间的大小就是固定的,当数据已经把堆栈的空间占满时,再往里面存放数据就会超出容量,发生上溢;当堆栈中的已经没有数据时,再取数据就无法取到了,发生下溢。需要注意的是,栈分为
顺序栈
和链栈,链栈不会发生溢出,顺序栈会发生溢出。
堆栈尺寸设置过小、
递归
调用过深、函数调用层次过深等程序设计不当之处都可能导致堆栈溢出。
1、 堆栈尺寸设置过小
由堆栈溢出的定义便可知,堆栈尺寸设置过小时,其能储存的内容过小,容易发生溢出。
2、递归层次太深或函数调用层次过深导致堆栈溢出
调用函数时,系统将为调用者构造一个由参数表返回地址组成的活动记录,并将其押入到由系统提供的运行时刻栈的栈顶,然后将程序的控制权转移到被调函数。若被调函数有局部变量,则在运行时刻,在栈的栈顶也要为其分配相应的空间,因此,活动记录和这些局部变量形成了一个可供被调函数使用的活动结构。被调函数执行完毕时,系统将运行时刻栈的栈顶的活动结构退栈,并根据退栈的活动结构中所保存的返回地址将程序的控制权转移给调用者继续执行。由此可见,当递归层次太深时或者函数调用层次过深时会产生大量的活动记录和局部变量,当超过栈的空间长度时,即发生溢出。
例如C/C++语言中的无限递归:
int foo()
return foo() //重复无限的自我调用
(define (foo) (foo)) //这样定义会造成死循环,但不会导致堆栈溢出
(define (foo) (+(foo)1)) //这样的定义会产生堆栈溢出
3、动态申请空间使用之后没有释放
如果是C语言,由于没有垃圾资源自动回收机制,因此,需要程序主动释放已经不再使用的动态地址空间,如果不释放,程序结束后该部分空间依然存在,还可以继续访问,也就是说这部分依然占据着堆空间,剩余的堆空间减少,就可能造成堆区溢出。
而如果是Java语言则因为有专门的垃圾回收器回收则不会有此问题。
从小处看,堆栈溢出会改变临近堆栈的空间中的内容,从而导致程序运行异常,发生故障;
从大处看,堆栈溢出和计算机网络安全密切相关。堆栈溢出攻击是计算机被攻击的最为常见的一种形式,远程网络的攻击绝大多数是针对堆栈溢出的漏洞,这种攻击可以使得一个匿名的Internet用户有机会获得一台主机的部分或全部控制权。
堆栈溢出
安全威胁
堆栈溢出常见的攻击类型有:修改函数的
返回地址
,使其指向攻击代码,当函数调用结束时程序跳转到攻击者设定的地址而不是原先的地址,修改函数
指针
,长跳转
缓冲区
来找到一个可供溢出的缓冲区。
攻击者通过缓冲区溢出来重写存储在
返回地址
内的值从而达到控制程序的执行流程的目的。程序函数就像是一个大程序中的小程序。它是相对独立的,对传给它的数据做相应的处理然后将处理的结果返回给主函数。因为数据在一个函数内进行处理,因此它用栈作为数据的临时存储区域。当一个程序调用函数时,它将所有的数据压栈,包括返回地址,如图所示。当函数被调用时,指令
指针
指向的就是函数的返回地址。这一点很重要,因为当被调用函数执行结束以后,主程序要回到被调用函数的返回地址处,接着执行下一条指令。返回地址存储在RET中,当被调用函数执行结束,该返回地址传递给指令指针,以便主函数能够回到函数调用之前的地址继续执行。如果攻击者能够使缓冲区溢出并且重写存储在RET中的值,将恶意代码的地址赋值给RET,那么指令指针将指向恶意代码,从而执行恶意代码。
利用堆栈溢出攻击计算机的最典型的例子是1988年利用fingerd漏洞进行攻击的
蠕虫
病毒。
通常的代码要设置堆栈缓冲区,最好能检测堆栈运行形况,设置堆栈溢出检测算法。对于递归引起的堆栈溢出,可以采用循环处理。
针对堆栈溢出可能造成的计算机安全问题,通常有以下这些防范措施:
(1) 强制按照正确的规则写代码
(2) 通过
操作系统
使得缓冲区不可执行,从而阻止攻击者植入攻击代码。但由于攻击者并不一定要通过植入代码来实现攻击,同时
linux
在信号传递和
GCC
的在线重用都使用了可执行堆栈的属性,因此该方法依然有一定弱点。
(3) 利用
编译器
的边界检查来实现缓冲区的保护。该方法使得缓冲区溢出不可能出现,完全消除了缓冲区溢出的威胁,但代价较大,如性能速度变慢。
(4) 程序
指针
完整性检查,该方法能阻止绝大多数缓冲区溢出攻击。该方法就是说在程序使用指针之前,检查指针的内容是否发生了变化。