缓冲区溢出攻击纵览

作者:Eddie Harari

最好的系统管理员有时也不足以照顾站点安全。有时,像 mount 这样优秀的程序可能会被用户利用,以获得更高的系统权限或远程访问万维网上未经授权的位置。

本文解释了一种流行的黑客攻击背后的逻辑,该攻击利用程序的代码来执行与预期不同的代码。这种黑客攻击被称为缓冲区溢出攻击,可用于利用设置了 suid 的程序,以在 Linux 机器上获得更好的权限——有时甚至是 root 或远程访问。(这些示例取自 “aleph-one”,并已获得他的许可,并被我进行了一些修改。)

A Look at the Buffer-Overflow Hack

图 1. 虚拟内存布局

首先,让我们看一下图 1,了解进程如何组织其虚拟内存。TEXT 区域是程序实际代码所在的位置。DATA 区域是程序已初始化和未初始化数据所在的位置。

STACK 区域是一个动态区域,它随着数据的压入而变大,随着数据的弹出而变小。它之所以被称为堆栈,是因为它以 LIFO 方式(后进先出)工作。堆栈用于保存进程的临时数据,并帮助处理器实现高级功能编程。要准确理解处理器如何使用堆栈,请看下面的例子

void func(int a, int b)
{
  /* This function does nothing */
}
main()
{
 int num1;
 int num2;
 func(num1,num2);
 printf("This is the next instruction after " .
   "the function ...");
}

在处理器需要“中断”程序的正常流程并转到 func 指令之前,main 函数的指令会被执行。当执行“跳转”到 func 的步骤时,func 的参数 num1num2 会在堆栈的帮助下进行传输。也就是说,它们被压入堆栈,func 可以从堆栈中弹出并使用它们。在将这些值压入堆栈后,main 应该立即压入 func 完成后将返回的地址。(在我们的例子中,这是 printf 指令的地址。)当 func 完成时,它知道从堆栈中读取此返回地址并返回到程序的“正常”流程。

堆栈上的另一个值称为帧指针,因为处理器通过它们相对于堆栈指针 (SP) 的偏移量来引用堆栈上的值。每当 SP 值发生变化时,处理器都会将当前值保存在堆栈上。(Intel 没有专用的帧指针 (FP),因此它借助 ebp 寄存器来完成。)帧指针在返回地址之后被压入堆栈。

为了澄清这一点,让我们看另一个例子

void func(int a, int b)
{
 int *p;
}
main()
{
 int num;
 num = 0;
 func(num);
 num = 1;
 printf("num is now %d \n",num);
}

让我们使用 gcc 命令使用 -S 选项编译它以获得汇编输出

gcc -S -o ex2.S ex2.c
我们看到 main 的代码实际上是
main:
pushl %ebp
movl %esp,%ebp    /* Save the SP before changing
                   * its value */
subl $4,%esp      /* SP should subtract 4 so it
                   * points to num on the stack */
movl $0,-4(%ebp)  /* Push num on the stack with
                   * value 0*/
pushl $2          /* Push 2 on the stack*/
pushl $1          /* Push 1 on the stack*/
call func         /* Push return address on the
                   * stack and jump to the first
                   * instruction of func*/
...
main 代码压入 func 的参数,然后调用它。call 指令将返回地址放在堆栈上,然后继续执行 func 代码。func 将四字节的帧指针紧随返回地址之后,然后将 p 指针压入堆栈。因此,如果我们现在转储堆栈的状态,我们得到如图 2 所示的结构。
A Look at the Buffer-Overflow Hack

图 2. 堆栈结构

我们可以使用 func 以十六进制格式打印 ab 的地址;为此,我们只需添加 printf 指令

void func(int a, int b)
{
 int *p;
 printf("The address of a on the stack is %x\n",
   &a);
 printf("The address of b on the stack is %x\n",
   &b);
}

当我们运行修改后的程序时,我们得到以下输出

The address of a on the stack is bffff7ac
The address of b on the stack is bffff7b0
整数 b 与整数 a 相差四个字节。查看图 2,我们看到整数 b 之后是四字节的帧指针,然后是四字节的返回地址。

我们可以使用 gdbdisassemble 选项来查看返回地址。(参见 清单 1。)<main+17> 中的 call 指令位于地址 0x80484b1,这意味着 0x80484b6 中的下一个指令是返回地址。正如我们刚刚计算的那样,当此地址被压入堆栈时,它与 b 相差八个字节,与 a 相差 12 个字节。

由于堆栈是可写的,我们可以使用指向返回地址的指针,然后更改其值。通过这样做,我们操纵程序的正常流程,以便例如,我们可以跳过一些指令。在 清单 2 中,我们更改了返回地址,以便我们的程序跳过一个指令。编译并执行

gcc -o ex4 ex4.c
ex4

返回此输出

The return address is 80484d2
The new return address is 80484dc
Num is now 0
在清单 2 代码中,我们借助指针 p 指向整数 b 的地址,然后从 p 向下减去八个字节,使其指向第一个输出行中打印的返回地址。接下来,我们在返回地址上加十个字节,以便它跳过 num=1; 汇编代码。(disassemble main 显示指令的确切偏移量,所以我用它来知道要跳过多少字节。)

通过这种方式,程序员可以从内部调节程序的正常流程。最大的问题是,是否有人可以从外部更改此返回地址?答案是 有时。不仅可以更改此地址,而且还可以将其更改为指向程序之外的代码。

清单 3 是一个非常简单的程序,可以从外部利用。首次执行时,输出如下所示

bash# ex5
Please enter your input string:
short
This is the next instruction

第二次执行时,输出是

bash# ex5
Please enter your input string:
long string
This is the next instruction
Segmentation fault (core dumped)
由于 strcpy 不检查它复制的字符串的长度,我们将 12 字节的字符串 long string\n 插入到一个长度为 8 字节的缓冲区中。我的输入中的前八个字符完全填满了缓冲区,然后剩余的四个字符溢出了缓冲区。也就是说,这四个字符覆盖了缓冲区中的相邻地址——返回地址。因此,当 func 尝试返回 main 时,发生了段错误,因为返回地址包含四字符字符串 ing\n,很可能是一个非法内存地址。

strcpy 函数是缓冲区溢出的经典示例,因为它不检查复制的字符串大小以确保它在缓冲区限制内。请注意,strcpy 不是利用缓冲区溢出攻击程序的唯一方法。

实际的缓冲区溢出攻击是这样工作的

  • 查找具有溢出潜力的代码。

  • 将要执行的代码放在缓冲区中,即堆栈上。

  • 将返回地址指向您刚刚放在堆栈上的同一代码。

由于这不是 Linux “hack.HOWTO”,因此我不会详细介绍这三个阶段。

第一阶段非常容易,尤其是在 Linux 系统中,因为 Linux 上有大量开源代码应用程序可用。其中一些应用程序几乎在每个 Linux 系统上都在使用。此类程序的典型示例是 mount 和一些早期版本的 inndmount 不检查用户输入的命令行参数的长度,并且其权限设置为 4555。innd 不检查所有新闻消息标头,因此通过发送特定的标头,用户可以在服务器上获得远程 shell。

第二阶段分为两个部分。第一部分是找到如何表示要执行的代码;这可以使用简单的反汇编程序来完成。第二部分取决于程序在何处读取缓冲区:在某些情况下,是邮件标头;在其他情况下,是长度未检查的环境变量;在另一些情况下,是一些替代方法。

第三阶段并不那么简单,因为人们无法知道要执行的代码的确切地址。基本上,它是通过猜测地址直到找到正确的地址来完成的。可以使用几种方法来提高猜测效率;因此,在仅进行几次猜测之后,我们就可以指定正确的地址,并且代码将被执行。

结论

应用程序在 Web 上被广泛使用并不意味着它是安全的,因此在您的机器上安装新应用程序时要小心。事实上,WWW 应用程序更有可能被怀有恶意的黑客深入搜索安全漏洞。系统管理员应阅读安全新闻组和相关网页,以便将已知存在安全漏洞的应用程序从系统中移除,并在补丁可用时升级它们。应用程序程序员应注意编写严谨的代码,其中包含对数组和变量长度的适当检查,以阻止此类黑客攻击。

最后,我想简要提及其他三件事。第一,有一个内核补丁可用,该补丁使堆栈内存区域成为不可执行区域。我从未测试过它,因为确实存在一些应用程序依赖于堆栈是可执行的事实,并且这些应用程序很可能在使用此修补内核时会遇到问题。第二,Intel 处理器提供了一种特殊模式,该模式使堆栈从较低的内存地址增长到较高的内存地址,从而使缓冲区溢出几乎不可能发生。第三,某些系统上提供的一组库可以帮助程序员编写没有此类错误的代码。程序员所要做的就是告诉库函数关于变量的假设,这些函数将验证变量是否满足指定的标准。

A Look at the Buffer-Overflow Hack
Eddie Harari 在以色列的 Sela Systems 工作,担任讲师和顾问。他目前参与网络安全项目,可以通过电子邮件 eddie@sela.co.il 联系到他。
加载 Disqus 评论