现在你已经很好地掌握了堆栈的操作方式,所以让我们来看看有史以来最臭名昭著的一个BUG:堆栈缓冲区溢出。示例程序如下:
Vulnerable Program - buffer.c
void doRead()
{
char buffer[28];
gets(buffer);
}
int main(int argc)
{
doRead();
}
上面示例程序是从标准输入读取的。gets函数一直读取,直到遇到换行符或文件结束。下面是读取字符串后堆栈的样子:
这里的问题是gets不知道缓冲区的大小:它将愉快地继续读取输入并将数据填充到缓冲区之外的堆栈中,删除保存的ebp值、返回地址和下面的任何内容。为了利用这种漏洞,攻击者制造一个精确的“输入内容”并将其输入程序。下图是堆栈在攻击后的样子:
其基本思想是提供要执行的恶意程序代码,并覆盖堆栈上的返回地址以指向该代码。这有点像病毒入侵细胞,破坏它,并引入一些RNA来实现它的目标。
和病毒一样,这个漏洞的有效“负载”也有许多显著的特征。它从几个nop指令开始,以增加恶意代码被成功执行的几率。这是因为返回地址是绝对的,必须猜测,因为攻击者不知道他们的代码将存储在堆栈的哪个位置。但是,只要它们停留在一个nop上,这个漏洞就会起作用:处理器将执行nop,直到它遇到可以工作的指令为止。
exec /bin/sh表示执行shell的原始汇编指令(例如,假设该漏洞位于一个联网程序中,因此该漏洞可能提供对系统的shell访问)。将汇编指令嵌入到一个程序中,使程序产生一个命令窗口或者用户输入,这种想法是很可怕的,但是,那只是让安全研究如此有趣且“脑洞大开”的一部分而已。有时脆弱的程序会对其输入调用tolower或toupper,迫使攻击者编写不属于大写或小写ascii字母范围的汇编指令。因为改变大小写的话,栈上的值就会改变,跟攻击者的预期不一致。
最后,攻击者将猜测的返回地址重复几次,以获得对他们有利的机会。通过从一个4字节的边界开始并提供多次重复,它们更有可能覆盖堆栈上的原始返回地址。
值得庆幸的是,现代操作系统有许多针对缓冲区溢出的保护措施,包括非可执行堆栈和堆栈金丝雀。“金丝雀”这个名字来自煤矿用语中的金丝雀,是计算机科学丰富词汇中的一个补充。用史蒂夫·麦康奈尔的话来说:
计算机科学拥有任何领域中最丰富多彩的语言。在其他的领域,你能走进一个无菌室,小心翼翼地控制在68°F,并找到病毒、特洛伊木马、蠕虫、昆虫、炸弹、崩溃以及致命错误吗?-- Steve McConnell 《代码大全2》( 就是说在其他领域,你遇不到这么包罗万象的术语)
无论如何,堆栈金丝雀是这样的:
金丝雀是由编译器实现的。例如,GCC的堆栈保护选项会导致在任何可能易受攻击的函数中使用金丝雀。函数序言将一个幻数加载到canary位置,而在函数调用后该值应该还是完整的。如果不是,则可能发生缓冲区溢出(或错误),并通过__stack_chk_fail中止程序。由于它们在堆栈上的战略位置,金丝雀使得利用堆栈缓冲区溢出变得更加困难。
这就完成了我们在堆栈深处的旅程。我们不想贪得无厌地深挖。下周,我们将在抽象上更进一步,深入研究递归等。在结束这段开场白和结束语之前,我想引用一句铭刻在美国国家档案馆纪念碑上的名言:
译者注:
金丝雀的典故:早期地下采煤的时候,矿井中都有金丝雀,因为它对于甲烷和一氧化碳等致命毒气非常敏感,如果有毒气,金丝雀就会死去,从栖木上掉下来。矿工就知道是时候离开了。过一段时间之后,如果新换上的金丝雀安然无恙,矿工们又可以安全地重返矿井。