缓冲区溢出攻击纵览
最好的系统管理员有时也不足以照顾站点安全。有时,像 mount 这样优秀的程序可能会被用户利用,以获得更高的系统权限或远程访问万维网上未经授权的位置。
本文解释了一种流行的黑客攻击背后的逻辑,该攻击利用程序的代码来执行与预期不同的代码。这种黑客攻击被称为缓冲区溢出攻击,可用于利用设置了 suid 的程序,以在 Linux 机器上获得更好的权限——有时甚至是 root 或远程访问。(这些示例取自 “aleph-one”,并已获得他的许可,并被我进行了一些修改。)
首先,让我们看一下图 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 的参数 num1 和 num2 会在堆栈的帮助下进行传输。也就是说,它们被压入堆栈,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 所示的结构。
我们可以使用 func 以十六进制格式打印 a 和 b 的地址;为此,我们只需添加 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 之后是四字节的帧指针,然后是四字节的返回地址。
我们可以使用 gdb 的 disassemble 选项来查看返回地址。(参见 清单 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 和一些早期版本的 innd。mount 不检查用户输入的命令行参数的长度,并且其权限设置为 4555。innd 不检查所有新闻消息标头,因此通过发送特定的标头,用户可以在服务器上获得远程 shell。
第二阶段分为两个部分。第一部分是找到如何表示要执行的代码;这可以使用简单的反汇编程序来完成。第二部分取决于程序在何处读取缓冲区:在某些情况下,是邮件标头;在其他情况下,是长度未检查的环境变量;在另一些情况下,是一些替代方法。
第三阶段并不那么简单,因为人们无法知道要执行的代码的确切地址。基本上,它是通过猜测地址直到找到正确的地址来完成的。可以使用几种方法来提高猜测效率;因此,在仅进行几次猜测之后,我们就可以指定正确的地址,并且代码将被执行。
应用程序在 Web 上被广泛使用并不意味着它是安全的,因此在您的机器上安装新应用程序时要小心。事实上,WWW 应用程序更有可能被怀有恶意的黑客深入搜索安全漏洞。系统管理员应阅读安全新闻组和相关网页,以便将已知存在安全漏洞的应用程序从系统中移除,并在补丁可用时升级它们。应用程序程序员应注意编写严谨的代码,其中包含对数组和变量长度的适当检查,以阻止此类黑客攻击。
最后,我想简要提及其他三件事。第一,有一个内核补丁可用,该补丁使堆栈内存区域成为不可执行区域。我从未测试过它,因为确实存在一些应用程序依赖于堆栈是可执行的事实,并且这些应用程序很可能在使用此修补内核时会遇到问题。第二,Intel 处理器提供了一种特殊模式,该模式使堆栈从较低的内存地址增长到较高的内存地址,从而使缓冲区溢出几乎不可能发生。第三,某些系统上提供的一组库可以帮助程序员编写没有此类错误的代码。程序员所要做的就是告诉库函数关于变量的假设,这些函数将验证变量是否满足指定的标准。
