为什么程序可以在被破坏的堆栈中”继续存活”?

为什么程序可以在被破坏的堆栈中”继续存活”?

作者:BlogUpdater |  时间:2020-10-18 |  浏览:37 |  评论已关闭 条评论

本文接上一篇文章

在x86架构,传统上会使用EBP寄存器来建立堆栈帧。一个典型的栈帧如下图所示:

上述汇编代码,会建立如下图所示的一个带有两个参数的__stdcall的函数栈帧:

函数的参数会基于EBP的正向偏移来进行访问。在上面的例子中,第一个参数为[ebp + 8],而本地变量则会基于EBP的负向偏移进行访问,例如:local2的值为[ebp – 8]。

现在假设:调用约定或函数声明不匹配,并且在堆栈上留下了额外的垃圾。

这个函数不会真的感受到任何的堆栈破坏,它仍然可以以相同的正偏移量访问参数,而以相同的负偏移量仍然可以访问本地局部变量。

真正的堆栈破坏发生在堆栈清理阶段,我们来看看函数的结尾:

在普通的堆栈中,三个pop指令与堆栈上的实际值匹配,一切都是正常的。
但是在垃圾堆栈上,”pop edi”实际上将垃圾数据加载到EDI寄存器中,”pop esi”也是如此。而” pop ebx”(可以认为它正在恢复EBX的原始值)实际上是将EDI寄存器的原始值加载到EBX中。但是随后,”mov esp, ebp”指令修复了堆栈的备份,因此”pop ebp”和”retd”在修复后的堆栈上执行。

这里会发生什么呢?
ESI,EDI和EBX寄存器的数据遭到了破坏。如果幸运的话,ESI, EDI和EBX中的值并不重要,并且可以幸免于数据破坏。或者,重要的是值是否为零,而你很幸运,将一个非零值替换为另一个。无论出于何种原因,这三个寄存器的破坏都不会立即显现出来,并且最终你也永远都不会意识到自己做错了什么。

堆栈破坏可能会产生微妙的影响(例如,将值从零更改为非零,从而导致调用者使用错误的代码路径),但是这种微妙的作用,你可能根本就不会注意到,程序就发布了。

如果此时来了一个新的编译器,它带有FPO优化功能,就是下面的另一个故事了。

FPO是”Frame Pointer Omission”的缩写。该函数无需将EBP寄存器作为栈帧寄存器使用,而是像其他任何寄存器一样使用它。在x86的寄存器相对较少的情况下,额外的算术寄存器显得尤为稀缺。

有了FPO之后,函数的汇编代码看起来像下面这样:

函数的栈帧会变成下图所示的样子:

现在,可以基于ESP寄存器的偏移里访问所有内容。例如,local-nn变量是[esp + 0x10]。

在这种情况下,堆栈上的垃圾数据更加致命。函数结尾是这样的:

如果堆栈上有垃圾数据,则四个”pop”指令将像以前一样恢复错误的值,但是这次,清除局部变量将无法解决任何问题。”add esp, nn * 4″将根据函数认为正确的数量来调整堆栈,但是由于堆栈上存在垃圾数据,堆栈指针将指向错误的地方。

现在,”retd 8″指令尝试返回到调用方,但它返回的是local2中的任何内容,这可能是无效的代码。
因此,这个优化代码的例子,恰好可以揭示其他人的代码错误。

总结
如何避免上面这些劳什子:老老实实把函数原型写对,别抱有侥幸心理。
计算机是直男:不会搞暧昧。

最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《How can a program survive a corrupted stack?》

评论已关闭。