逆向工程/栈溢出
我们经常听到恶意代码导致一个非常模糊的问题,称为栈溢出。本页将讨论什么是栈溢出以及如何防止它。
基于栈的溢出攻击是指向缓冲区写入过多信息以覆盖返回地址并劫持控制流的行为。覆盖的返回地址在大多数情况下会指向程序地址空间中的某个函数。此函数可能已在应用程序中定义,或者黑客可以通过将代码注入到栈中轻松定义它。
如果我们记得关于栈的那一章,我们知道当我们进入一个新函数时关于栈的一些基本事实
- 栈“向下”增长。
- 局部数据被推送到栈顶。
- bp的旧值存储在局部数据下方
- 返回地址存储在旧bp值下方
考虑以下有错误的C代码片段
void MyFunction(void) { int a[100]; int i; for(i = 0; i <= 100; i++) { a[i] = 0; } ...
当i达到100时会发生什么?如前所述,我们知道局部数组是在栈上创建的。如果我们尝试写入“a”的上界以上,我们将覆盖栈上的先前值:a[100]覆盖bp,a[101]覆盖返回地址。
程序流程随后将重定向到我们放置的新地址。这是一个栈溢出漏洞,它源于程序员在向数组写入数据之前没有检查数组边界的错误编程。
逆向工程师如何发现栈溢出漏洞?让我们看一些示例ASM代码
push ebp mov ebp, esp sub esp, 100
这是一个标准的入口序列,我们可以看到此函数在栈上分配了100字节的数据。要么是25个整数值得数据,要么是某种数组。我们检查函数的其余部分,并查看它是什么类型的数据
call _gets push eax push esp call _strcpy ...
显然,我们正在访问栈上的数据作为数组,特别是字符数组。上面的汇编代码片段从控制台获取文本字符串,并将该数据复制到栈上的局部变量中。
不幸的是,我们正在使用的标准C库字符串函数存在一个众所周知的漏洞:它们不检查输入参数的边界。事实上,<string.h>函数很少甚至要求程序员提供数组的大小或最大可用内存大小!
一些最常见的栈漏洞源于此事实。需要注意的违规者是strcpy、strcat和sprintf,这些函数的输出字符串参数可能大于提供的缓冲区以容纳它们。
局部变量只有100个字符(1个字符=1个字节)宽。如果我们输入一个100个字符长的字符串会发生什么?请记住,ASCIIZ字符串以空字符(00h)结尾,这需要数组中的一个额外槽。这意味着第101个字符将为空字节,并且ebp的保存值将丢失。现在想象一下,如果我们输入104个字符甚至108个字符(足以覆盖返回地址)会发生什么。输入正确值的攻击者可以将程序执行重定向到可能帮助接管计算机的恶意函数。
"为了乐趣和利益而粉碎栈",Aleph One,Phrack,7(49),1996年11月。