堆叠溢出

堆叠溢出

堆叠是一个在计算机科学中经常使用的抽象数据类型。堆叠中的物体具有一个特性: 最后一个放入堆叠中的物体总是被最先拿出来, 这个特性通常称为后进先出(LIFO)伫列。 堆叠中定义了一些操作。 两个最重要的是PUSH和POP。 PUSH操作在堆叠的顶部加入一 个元素。POP操作相反, 在堆叠顶部移去一个元素, 并将堆叠的大小减一。

堆叠溢出的产生是由于过多的函式调用,导致调用堆叠无法容纳这些调用的返回地址,一般在递归中产生。堆叠溢出很可能由无限递归(Infinite recursion)产生,但也可能仅仅是过多的堆叠层级。

    • 中文名:堆叠溢出
    • 套用学科:计算机科学
    • 类别:高级语言
    • 技术:过程和函式
    • 记忆体:连续记忆体
    • 地址:固定地址
    • 领域:计算机安全

堆叠溢出

堆叠溢出就是不顾堆叠中分配的局部数据块大小,向该数据块写入了过多的数据,导致数据越界,结果覆盖了别的数据。 可以理解为 在长字元串中嵌入一段代码,并将过程的返回地址覆盖为这段代码的地址,这样当过程返回时,程式就转而开始执行这段自编的代码了。

比如如下这段程式:

#include

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是下载执行,那我们就可以让目标机在网页上下个档案并执行,只要你想到达的功能,都可以想办法实现。

利用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。

相关词条

相关搜索

其它词条