3.3 缓冲区溢出
缓冲区溢出的漏洞随着冯·诺依曼 1 构架的出 现就已经开始出现了。 在1988年随着莫里斯互联网蠕虫的广泛传播他们开始声名狼藉。不幸的是, 同样的这种攻击一直持续到今天。 到目前为止,大部分的缓冲区溢出的攻击都是基于摧毁栈的方式。
大部分现代计算机系统使用栈来给进程传递参数并且存储局部变量。 栈是一种在进程映象内存的高地址内的后进先出(LIFO)的缓冲区。 当程序调用一个函数时一个新的“栈帧”会被创建。这个栈帧包含着 传递给函数的各种参数和一些动态的局部变量空间。“栈指针”记录着当前 栈顶的位置。 由于栈指针的值会因为新变量的压入栈顶而经常的变化,许多实现也提供了 一种"帧指针"来定位在栈帧的起始位置,以便局部变量可以更容易的被访问。 1调用函数的返回地址也同样存储在栈中, 由于在函数中的局部变量覆盖了函数的返回地址成为了栈溢出的一个原因, 这就潜在的准许了一个恶意用户可以执行他(她)所想运行的任何代码。
虽然基于栈的攻击是目前最广泛的,这也可以使基于堆的攻击(malloc/ free)变成可能。
C程序语言并不像其他一些编程语言一样自动的做数组或者指针的边 界检查。另外,C标准库还具有相当一些非常危险的操作函数。
strcpy (char *dest, const char *src) | 可导致dest缓冲区溢出 |
strcat (char *dest, const char *src) | 可导致dest缓冲区溢出 |
getwd (char *buf) | 可导致buf缓冲区溢出 |
gets (char *s) | 可导致s缓冲区溢出 |
[vf]scanf (const char *format, ...) | 可导致参数溢出 |
realpath (char *path, char resolved_path[]) | 可导致path缓冲区溢出 |
[v]sprintf (char *str, const char *format, ...) | 可导致str缓冲区溢出 |
3.3.1 缓冲区溢出示例
下面的示例代码包含了一个缓冲区溢出的情况,它会覆盖函数的返回地址并且 立即跳过了紧随此函数之后调用。(授权于5)
#include <stdio.h> void manipulate(char *buffer) { char newbuffer[80]; strcpy(newbuffer,buffer); } int main() { char ch,buffer[4096]; int i=0; while ((buffer[i++] = getchar()) != '\n') {}; i=1; manipulate(buffer); i=2; printf("The value of i is : %d\n",i); return 0; }
让我们来查看一下如果在输入回车之前输入160个空格后这个小程序 的内存映象是个什么样子。
[XXX figure here!]
很明显更多的恶意输入能被设计出执行实际的编译指令(例如 exec(/bin/sh))。
3.3.2 避免缓冲区溢出
对于栈溢出的最直接的解决方法就是总是使用长度有限的内存和 字符串复制函数。strncpy
和strncat
是C标准库的一部分。 这些函数接收一个不大于目标缓冲区长度的值作为参数。这些函数会从 源地址复制此值长的字节数到目标地址。然而这些函数还是有一些问题。 如果输入缓冲区的长度和目标缓冲区的一样长则函数不保证两者都以NUL 作为结束符。 长度参数在strncpy和strncat函数中同样的不一致很容易导致程序员在 正常使用时感到困惑。同时当复制一个较短的字符串到一个很大的缓冲 区中时相对于strcpy
也有很重大的性能损失, 因为strncpy
会用NUL填充所指定的长度。
在OpenBSD中,另一个内存复制的实现已经规避了这些问题。 函数strlcpy
和strlcat
保证了当指定了非零的长度参数时目标字符串总是以NUL作为结束符。 关于这些函数的更多信息请参考7。OpenBSD 的strlcpy
和strlcat
自从FreeBSD3.3的版本已经被引入了。
3.3.2.1 基于编译器运行时边界检查
不幸的是扔然有相当数量的代码在广泛使用盲目的内存复制功能 而不是我们所提及到的任何有限制的复制例程。 幸运的是有一种方法能帮助防止此类攻击 ── 一些 C/C++ 编译器实现了运行时边界检查。
ProPolice 就是一种这样的编译器特性, 而且被集成在 gcc(1) 4.1 及以后的版本中。 它替代并扩展了早期的 gcc(1) 中的 StackGuard 扩展。
ProPolice 有助于保护基于栈的缓冲区溢出和其他一些攻击, 比如调用任何函数之前在栈关键地方设置了伪随机数。 当一个函数返回时,就会检查这些 “canaries”, 如果发现他们被改变了就会立即停止运行。 因此任何企图修改返回地址或者修改存于栈上的变量以尝试运行恶意代码都多半不能成功, 因为攻击者还不得不设法防止伪随机的 canaries 不被改动。
使用 ProPolice 重新编译你的程序可以有效的防止大部分的缓冲 区溢出的攻击,但是这仍然是个折衷的办法。
3.3.2.2 基于库运行时边界检查
基于编译器的机制对于不能重新编译的只有二进制的软件完 全无用。对于这些情况仍还是有很多库可以对C库中的不安全的函数 (strcpy
, fscanf
, getwd
等)重新实现并确保这些函数决不回写 栈指针。
libsafe
libverify
libparanoia
不幸的是这些基于库的防护有一些缺点。这些库仅仅保护和安全 相关的一小部分集合,他们忽略了实际的问题。如果程序使用参数 -fomit-frame-pointer进行编译的话这些防护也许会失败。同样,环境 变量LD_PRELOAD和LD_LIBRARY_PATH也可以被用户取消或者重置。