前言:栈
- 栈是一种LIFO的数据结构。
- 应用程序有一到多个用户态栈。
- 栈自底向上增长,由指令PUSH和POP引起其动态变化。
- 局部变量布局在栈中。
- 调用函数时参数由栈传递,返回地址也存储于栈中。
- 函数调用上下文与局部变量共同组成了栈帧——Stack Frame.
栈帧 = 局部变量 + 函数调用上下文
栈帧实际上只是一个通俗的说法,关于栈帧的上下界历来有两种说法,一曰以EBP和ESP之间的栈空间视为栈帧,这也是主流说法;一曰以调用参数和ESP之间的栈空间视为栈帧,我个人更倾向于这种说法,因为它便于理解。
什么是栈溢出?
- 缓冲区溢出是由于C语言系列设有内置检查机制来确保复制到缓冲区的数据不得大于缓冲区的大小,因此当这个数据足够大的时候,将会溢出缓冲区的范围。
- 栈溢出就是缓冲区溢出的一种。 由于缓冲区溢出而使得有用的存储单元被改写, 往往会引发不可预料的后果。程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区中写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就称为缓冲区溢出。缓冲区长度一般与用户自己定义的缓冲变量的类型有关。
- 由于缓冲区溢出而使得有用的存储单元被改写,往往会引发不可预料的后果。向这些单元写入任意的数据,一般只会导致程序崩溃之类的事故,对这种情况我们也至多说这个程序有Bug。但如果向这些单元写入的是精心准备好的数据,就可能使得程序流程被劫持,致使不希望的代码被执行,落入攻击者的掌控之中,这就不仅仅是bug,而是漏洞(exploit)了。
- 对每个程序来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误,程序就崩溃了
什么情况下才会发生栈溢出呢?
最常见的就是递归。每次递归就相当于调用一个函数,函数每次被调用时都会将局部数据(在函数内部定义的变量、参数、数组、对象等)放入栈中。
递归500次,就会将500份这样的数据放入栈中。这些数据占用的内存直到整个递归结束才会被释放,在递归过程中只会累加,不会释放。如果递归次数过多,并且局部数据也多,那么会使用大量的栈内存,很容易就导致栈溢出了。
栈溢出的解决方法
- 减少栈空间的需求,
不要定义占用内存较多的auto变量
,应该将此类变量修改成指针,从堆空间分配内存
。 - 函数参数中不要传递大型结构/联合/对象,应该使用
引用或指针作为函数参数
。 - 减少函数调用层次,
慎用递归函数
,例如A->B->C->A环式调用。
常发生栈溢出的危险函数
- 输入:gets(),直接读取一行,到换行符’\n’为止,同时’\n’被转换为’\x00’;scanf(),格式化字符串中的%s不会检查长度;vscanf(),同上。gets()是C中的危险函数之一,它不进行边界检查。在我们的例子中,a是int型只有4字节大小的空间,所以当输入的字符大于4字节时,就会发生溢出
- 输出:sprintf(),将格式化后的内容写入缓冲区中,但是不检查缓冲区长度
- 字符串:strcpy(),遇到’\x00’停止,不会检查长度,经常容易出现单字节写0(off by one)溢出;strcat(),同上。
堆溢出原理
堆溢出是指程序向某个堆块中写入的字节数超过了堆块本身可使用的字节数(之所以是可使用而不是用户申请的字节数,是因为堆管理器会对用户所申请的字节数进行调整,这也导致可利用的字节数都不小于用户申请的字节数),因而导致了数据溢出,并覆盖到物理相邻的高地址的下一个堆块。
不难发现,堆溢出漏洞发生的基本前提是
1、程序向堆上写入数据。
2、写入的数据大小没有被良好地控制。
动态申请空间使用之后没有释放。由于C语言中没有垃圾资源自动回收机制,因此,需要程序主动释放已经不再使用的动态地址空间。申请的动态空间使用的是堆空间,动态空间使用不会造成堆溢出。比如malloc要用free释放