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

本文接上一篇文章

在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?》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拓扑梅尔-漫漫开发路

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值