第3章“程序的机器级表示”:存储器的越界引用和缓冲区溢出

已经看到,C 对于数组引用不进行任何边界检查,而且局部变量和状态信息(如寄存器值和返回指针)都存放在栈中。这两种情况结合到一起就能导致严重的程序错误,一个对越界的数组元素的写操作破坏了存储在栈中的状态信息。然后,当程序使用这个被破坏的状态,试图重新加载寄存器或执行 ret 指令时,就会出现很严重的错误。

一种特别常见的状态破坏称为缓冲区溢出(buffer overflow)。通常,在栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。下面这个程序示例就说明了这个问题:

/* Implementation of library function gets() */
char *gets(char *s)
{
	int c;
	char *dest = s;
	while ((c = getchar()) != '\n' && c != EOF)
		*dest++ = c;
	*dest++ = '\0'; /* Ternimate String */
	if (c == EOF) 
		return NULL;
	return s;
}

/* Read input line and write it back */
void echo()
{
	char buf[4]; /* Way to small! */
	gets(buf);
	puts(buf);
}

该代码是库函数 gets 的实现,用来说明这个函数的严重问题。它从标准输入读入一行,在遇到一个 “\n” 字符或某个错误情况时停止。它将这个字符串拷贝到参数 s 指明的位置,并在字符串结尾加上 null 字符。在函数 echo 中,使用了 gets,这个函数只是简单地从标准输入中读入,再回送到标准输出。

gets 的问题是它没有办法确定是否为保存整个字符串分配了足够的空间。在 echo 示例中,故意将缓冲区设得非常小——只有4字节长。任何长度超过 3 个字符的字符串都会导致写越界。

研究 echo 汇编代码的这一部分,看看栈是如何组织的:

第3章“程序的机器级表示”:存储器的越界引用和缓冲区溢出_第1张图片
该例子中,可以看到,程序总共为局部存储(storage)分配了 32 个字节(第4行和第6行)。不过,字符数组 buf 的位置在 %ebp 下方四个字节处(第7行)。

下图给出了得到的栈结构:
第3章“程序的机器级表示”:存储器的越界引用和缓冲区溢出_第2张图片
正如看到的那样,所有对 buf[4] ~ buf[7] 的写都是会导致 %ebp 的保存值被破坏。当程序随后试图以它为栈指针进行恢复时,所有后来的栈引用都会是非法的。所有对 buf[8] ~ buf[11] 的写都会导致返回地址被破坏。当在函数结尾执行 ret 指令时,程序会 “返回” 到错误的地址。像这个示例说明的那样,缓冲区溢出可能导致程序出现严重的错误。

echo 代码很简单,但是有点太随意了。更好一点的版本是使用 fgets 函数,它包括一个参数,限制待读入的最大字节数。通常,使用 gets 或其他能导致存储溢出的函数,都是不好的编程习惯。当编译一个含有调用 gets 的文件时,C 编译器甚至会产生这样的出错信息:“the gets function is dangerous and should not be used(gets 函数很危险,不应使用)。”

缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。这是一种最常见的通过计算机网络攻击系统安全的方法。通常,输入给程序一个字符串,这个字符串包含一些可执行代码的字节编码,称为 exploit code,另外,还有一些字节会用一个指向缓冲区中那些可执行代码的指针覆盖掉返回指针。所以,执行 ret 指令的效果就是跳转到 exploit code。

在一种攻击形式中,exploit code 会使用系统调用启动一个 shell 程序,提供给攻击者一组操作系统的函数。在另一种攻击形式中,exploit code 执行一些未授权的任务,修复对栈的破坏,然后第二次执行 ret 指令,(看上去好像)正常返回给调用者。

来看个例子,著名的 Internet 蠕虫病毒在 1988 年 11 月通过 Internet 以四种不同的方法获取对许多计算的访问。一种是对 finger 守护进程 fingerd 的缓冲区溢出攻击,fingerd 是通过 FINGER 命令来服务请求的。通过以一个适当的字符串调用 FINGER,蠕虫可以使远程的守护进程缓冲区溢出并只执行一段代码,该代码能让蠕虫访问远程系统。一旦蠕虫获得了对系统的访问,它就能自我复制,几乎完全地消耗掉机器上所有的计算资源。因此,在安全专家抓住如何消除这种蠕虫的方法之前,成百上千的机器实际上都瘫痪了。这种蠕虫的始作俑者最后被抓住并被起诉。他被判处三年徒刑(缓期执行)、400个小时的社区服务以及 10500 美元的罚款。不过,即使到今天,人们还是在不断地发现使他们容易遭受缓冲区溢出攻击的系统安全漏洞,这更加突显了小心仔细编写程序的必要性。任何到外部环境的接口都应该是“防弹的”,这样,外部 agent 的行为才不会导致系统出现错误。

蠕虫和病毒

蠕虫和病毒都是试图在计算机从中传播它们自己的代码。正如 Spafford[73] 讲述的那样,蠕虫(worm)是这样一种程序,它可以自己运行,并且能够将一个完全有效的自己传播到其他机器。与此相应地,病毒(virus)是这样一段代码,它能将自己添加到包括操作系统在内的其他程序中,但它不能独立运行。在一些大众媒体中,术语 “病毒” 用来指各种不同的在系统间传播攻击代码的策略,所以你可能会听到人们把本来应该叫做“蠕虫”的东西称为了“病毒”。

你可能感兴趣的:(#,深入理解计算机系统,缓冲区溢出)