1.没有回收垃圾资源
2.层次太深的递归调用
堆栈溢出
堆栈是一个在计算机科学中经常使用的抽象数据类型。堆栈中的物体具有一个特性: 最后一个放入堆栈中的物体总是被最先拿出来, 这个特性通常称为后进先出(LIFO)队列。 堆栈中定义了一些操作。 两个最重要的是PUSH和POP。 PUSH操作在堆栈的顶部加入一 个元素。POP操作相反, 在堆栈顶部移去一个元素, 并将堆栈的大小减一。
堆栈溢出的产生是由于过多的函数调用,导致调用堆栈无法容纳这些调用的返回地址,一般在递归中产生。堆栈溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆栈层级。
堆栈溢出就是不顾堆栈中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了别的数据。 可以理解为 在长字符串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址,这样当过程返回时,程序就转而开始执行这段自编的代码了。
比如如下这段程序:
#include<stdio.h>
int main()
{
char name[8];
printf("Please type your name:");
gets(name);
printf("Hello.%s!",name);
return 0;
}
编译并且执行,输入ipxodiAAAAAAAAAAAAAAAA,执行完gets(name)之后,堆栈如下:
内存底部 内存顶部
name EBP ret
<-------[ipxodiAA][AAAA][AAAA]............
^&name
堆栈顶部 堆栈底部
由于我们输入的name字符串太长,name
数组容纳不下,只好向内存顶部继续写'A',如果提前申请动态内存就可以避免堆栈溢出。而此例由于堆栈的生长方向与内存的生长方向相反,这些'A’覆盖了堆栈的老的元素。'EBP ret’都被'A'覆盖了。在main返回的时候,就会把'AAAA'的ASCII码:0x41414141作为返回地址,CPU会试图执行0x41414141处的指令,结果出现错误。这就是一次堆栈溢出!
解决措施
能够监视malloc,memset,memcpy,free这四个函数的行为(栈就不检测了,一般栈溢出的情况比较少,也好查。另外new和delete由于水平有限,无法对其监视)。 如果发现越界操作,打印出来,继续执行。也就是说该检测工具不影响程序的行为。
堆栈区域
堆栈是一块保存数据的连续内存。 一个名为堆栈指针(SP)的寄存器指向
堆栈的顶部。 堆栈的底部在一个固定的地址。 堆栈的大小在运行时由内核动态地调整。
CPU
实现指令 PUSH和POP, 向堆栈中添加元素和从中移去元素。 堆栈由逻辑堆栈帧组成。 当调用函数时逻辑堆栈帧被压入栈中, 当函数返回时逻辑堆栈帧被从栈中弹出。 堆栈帧包括函数的参数, 函数地
局部变量
, 以及恢复前一个堆栈帧所需要的数据, 其中包括在
函数调用
时指令
指针
(IP)的值。 堆栈既可以向下增长(向内存低地址)也可以向上增长, 这依赖于具体的实现。 在我们的例子中, 堆栈是向下增长的。 这是很多计算机的实现方式, 包括Intel, Motorola, SPARC和
MIPS处理器
。 堆栈指针(SP)也是依赖于具体实现的。 它可以指向堆栈的最后地址, 或者指向堆栈之后的下一个空闲可用地址。 在我们的讨论当中, SP指向堆栈的最后地址。 除了
堆栈指针
(SP指向堆栈顶部的的低地址)之外, 为了使用方便还有指向帧内固定 地址的指针叫做帧指针(FP)。 有些文章把它叫做局部基
指针
(LB-local base pointer)。 从理论上来说,
局部变量
可以用SP加
偏移量
来引用。 然而, 当有字被压栈和
出栈
后, 这 些
偏移量
就变了。 尽管在某些情况下
编译器
能够跟踪栈中的字操作, 由此可以修正偏移 量, 但是在某些情况下不能。 而且在所有情况下, 要引入可观的管理开销。 而且在有些 机器上, 比如Intel处理器, 由SP加
偏移量
访问一个变量需要多条指令才能实现。 因此, 许多
编译器
使用第二个
寄存器
, FP, 对于
局部变量
和函数参数都可以引用, 因为它们到FP的距离不会受到PUSH和POP操作的影响。 在Intel CPU中, BP(EBP)用于这 个目的。 在Motorola CPU中, 除了A7(
堆栈指针
SP)之外的任何
地址寄存器
都可以做FP。 考虑到我们堆栈的增长方向, 从FP的位置开始计算, 函数参数的
偏移量
是正值, 而局部 变量的偏移量是负值。 当一个例程被调用时所必须做的第一件事是保存前一个FP(这样当例程退出时就可以 恢复)。 然后它把SP复制到FP, 创建新的FP, 把SP向前移动为
局部变量
保留空间。 这称为 例程的序幕(prolog)工作。 当例程退出时, 堆栈必须被清除干净, 这称为例程的收尾 (epilog)工作。 Intel的ENTER和LEAVE指令, Motorola的LINK和UNLINK指令, 都可以用于 有效地序幕和收尾工作。
堆栈溢出攻击
利用JMP ESP的方式
其利用格式是NNNNNNRSSSSS,这里N=NOP,R=RET(jmp esp的地址),S=ShellCode。就是把缓冲区一直覆盖成NOP(空指令,什么都不做),直到原来的EIP位置时,我们填入系统中某个核心dll中的jmp esp的地址,紧跟后面才是我们的ShellCode。 正常情况下,函数返回时,执行RET指令,这等于POP EIP,会把保存的原来程序的EIP的值恢复,从而完成中断的返回。但在这里,我们把保存的EIP的值覆盖了,改写成了jmp esp的地址。这样,POP EIP后,EIP = jmp esp的地址,而堆栈指针ESP会往下走,指向ShellCode的开始。程序继续执行,此时EIP里的内容是jmp esp,系统执行jmp esp,就正好就跳到我们的ShellCode的地方了.
如果ShellCode是开个端口,那我们就可以远程连上去;如果ShellCode是下载执行,那我们就可以让目标机在网页上下个文件并执行,只要你想到达的功能,都可以想办法实现。 [3]
如果ShellCode是开个端口,那我们就可以远程连上去;如果ShellCode是下载执行,那我们就可以让目标机在网页上下个文件并执行,只要你想到达的功能,都可以想办法实现。 [3]
利用JMP EBX的方式
其利用格式是NNNNN JESSSSSS。这里N = NOP, J = Jmp 04,E = jmp ebx的地址,S = ShellCode。 这里的J和E的位置是关键,E是在出错处理的入口位置,而J在其前面。 在第一种方式中,我们知道将返回地址覆盖成另一个地址。但如果是个无效的地址呢?那里指向的数据或许不能读,或许不能执行,那会怎么样呢?其实相信大家都遇到过,那就是系统会弹出个对话框报错,我们点确定,就会终止运行。 这是因为作为一个系统级的程序,内部有健全的出错处理机制。简单的说,如果运行时有错误产生,windows就会跳到一个专门处理错误的地方,对应不同的错误,执行不同的代码。上面执行的代码就是弹出个对话框报错。所以这里我们故意把返回的地址覆盖成一个错误的地址。这样出错时,windows就会跳到处理错误的入口,而ebx指向入口前4个字节的地方!那我们把错误入口处覆盖为jmp ebx的地址,就会跳到前4个字节,怎么跳到ShellCode呢?在这里我们写入jmp 04,哈哈,往后跳4个字节,正好跳过覆盖值,达到我们的ShellCode。