Linux 中的简单 32 位 Ret2libc 攻击





5.00/5 (6投票s)
在本文中,我们将探讨一种更高级的缓冲区溢出攻击。
引言
与任何漏洞利用一样,随着时间的推移,新的防护和检测方法不断涌现。在本文中,我们将探讨一种更高级的 缓冲区溢出攻击。 在这个典型的例子中,我们将研究攻击者如何通过将 C 函数的返回地址重定向到 libC(C 标准库)中的函数来绕过 DEP(数据执行保护),而这发生在 32 位二进制文件中。如果需要,我们还可以将 libC 函数链推送到堆栈上,以便一个接一个地执行它们,这就是 ROP(面向返回的编程)。
本文不使用 64 位二进制文件的一个原因是,Ret2libc
最纯粹的形式出现在 32 位二进制时代。攻击的构建基于您在特定函数 $EBP
寄存器值之后创建一个伪堆栈帧。这包括通过溢出将函数参数推送到伪堆栈帧中。64 位二进制文件上的 Ret2libc
攻击依赖于通过 gadget 将函数参数推送到寄存器中,并与 ROP 或面向返回的编程 类似。
如何在不使用直接 shellcode 的情况下生成 shell?或列出一个目录? Ret2libC
攻击允许我们调用 C 函数 system
和名为 exit
的函数,从而生成 shell,然后允许程序干净地退出,不引起任何怀疑。
要求
强烈建议您具备缓冲区溢出经验,或者阅读过我之前关于 缓冲区溢出 的文章,因为本文是它的延伸。在本文中,我们将使用以下工具:
- Ubuntu 8(在我 格式化字符串 漏洞利用文章中有安装指南,我强烈建议您按照该指南操作,因为我们必须从 CD 映像文件中安装额外的组件。您可以在 此处 找到 ISO 映像)。
- GCC
- GDB
- C 语言基础知识
- 对 字节序(大端/小端) 概念的理解。
入门
在本文中,我们将利用一个长度未经验证的函数 gets
。虽然 gets
在现代程序中不被推荐使用,但程序员或新手错误可能会使其绕过检查。gets
函数允许我们从标准输入(STDIN)读取用户输入,并将其存储到字符缓冲区中,而不会进行长度检查,这对于缓冲区溢出来说无疑是灾难的根源。
gets(char* buffer)
让我们开始一个简单的程序
bug.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void buggy_function()
{
char* arg;
char buffer[128];
gets(buffer);
}
int main(int argc, char* argv[])
{
buggy_function();
return 0;
}
此程序允许我们调用包含易受攻击的 gets
函数的 buggy_function
。我们将使用 PERL 编程语言 来创建一个 string
,通过一些简单的 BASH 命令表示法,将其作为用户输入定向到我们的易受攻击程序。
我们在此漏洞利用中的目标是溢出 buggy_function
堆栈帧的返回地址,并将其替换为 system
函数的地址。我们还将传递 exit
函数的地址以及用于执行新 shell 实例的参数 "/bin/sh"
,所有这些都包含在 libC
中。
在我们开始之前,让我们关闭 内存随机化或 ASLR,因为它会干扰我们的漏洞利用。ASLR 通过使用“金丝雀变量”不断移动内存来帮助程序自我保护。这也有助于检查内存是否被篡改,从而提供完整性检查。您可以通过将以下文件的值从 2
修改为 0
来关闭系统上的 ASLR。
/proc/sys/kernel/randomize_va_space
注意:请记住在完成漏洞利用后将 randomize_va_space 文件的值设置回 2。
什么是 libC?
libC
,顾名思义,指的是 C 的标准库,它包含了所有与 C 编程语言相关的函数、类型定义和宏。在这种情况下,它定义了 char
、int
类型。libC
是 C 功能的主要组成部分,库通过以下表示法包含到您的程序中:
#include <library.h>
在这种情况下,exit
和 system
函数包含在 stdlib
库中,该库包含我们 C 程序的标准基本功能。这意味着,当程序运行时,即使我们没有在程序中直接使用它们,我们也可以使用库中包含的函数,因为它们也会被加载到内存中。事实上,您会发现 libC
中的许多函数即使我们没有在程序中包含/使用它们,也仍然可用。如果我们使用 print
命令在 GDB 中调试程序,并使用函数名称设置断点,尤其可以看到这一点。
print system
这种可用性是因为这些库和函数是内置的,允许多个程序指向内存中的特定指针,以便以低开销调用频繁使用的函数。libC
非常复杂且广泛。它定义了内存分配、文件管理等。
Ret2libC 攻击如何在内存中工作
让我们继续了解 Ret2libC
攻击如何在内存中工作。我们知道,函数被组织成堆栈帧(请阅读我关于 缓冲区溢出 的文章以了解更多信息)。一个帧由一个函数序言组成,它有助于设置帧供使用。主体和返回语句。帧的顶部由堆栈指针($ESP
)表示,底部由基址指针($EBP
)表示。返回地址存储在基址指针之后,以指示程序执行完函数后应返回到何处,如下面的示例所示。
为了执行 Ret2libC
攻击,我们将在 buggy_functions
的 $EBP
指针之后创建一个伪堆栈帧,使其看起来像这样:
Example Exploitable Function fnc(char* arg1, char* arg2, n...)
|-------------------------------| ← Start of frame for buggy_function(). Referenced to by $ESP
| //function prologue |
|-------------------------------|
| buffer[128] = 'A' x 128 | ← Our buffer which contains random 'A's
|-------------------------------|
| 'AAAA' | ← $EBP replaced by 4 random bytes
|-------------------------------|
| system() | ← Replaced the return address with the system call
|-------------------------------|
| exit() | ← Allows us to exit cleanely after system()
|-------------------------------|
| address of "/bin/sh" | ← Argument for system()
|-------------------------------|
为了成功运行我们的漏洞利用,我们将不得不溢出我们的缓冲区和 $EBP
指针 4 个随机字节。然后,我们将覆盖返回地址,使其指向 system
的地址,然后是 exit
的地址,最后是 "/bin/sh"
的地址,在本例中,它将包含在一个 环境变量 中。如果您想添加另一个函数而不是 system,您可以这样做。请记住,与允许您执行多个操作的 ROP 不同,这里我们只能调用两个函数,任何后面的地址都不会被执行,很可能会导致错误。
漏洞利用:准备工作
为了执行我们的 Ret2libC
攻击,我们将使用 GDB。作为初学者,这将帮助我们导航 C 二进制文件的复杂世界,当然,随着我们在这些漏洞利用方面变得更好,我们将减少对 GDB 的使用。GDB 被称为 GNU 调试器,它将帮助我们在执行过程中查看内存,创建断点,并进一步了解我们的易受攻击的二进制文件。有各种其他程序可供使用,如 NM 或 hexdump。本文将坚持使用 GDB。
为了包含 GDB 可以在二进制文件中识别的额外符号,我们将使用 -g
标志编译我们的二进制文件,该标志将调试符号包含到我们的二进制文件中。
gcc -g bug.c -o bug.out -fno-stack-protector
-o
标志允许我们定义最终二进制文件的名称。我们将使用 -fno-stack-protector
标志来编译我们的二进制文件。这将禁用我们程序的任何堆栈保护,或者任何有助于检测堆栈破坏(缓冲区溢出)的保护。
现在让我们用 GDB 打开我们的易受攻击程序
gdb -q ./bug.out
-q
标志会抑制 GDB 可能显示的任何欢迎消息。让我们通过查找 system
和 exit
的实际地址来做好准备。我们需要在通过 GDB 运行程序时找到它们。我们可以设置一个运行时断点,以便在 main
函数上执行此操作,这将允许我们观察内存中的其他值,包括 system
和 exit
函数的位置。我们可以使用以下命令设置断点:
break main
让我们使用以下命令运行我们的程序
run
我们现在已经遇到了我们在 main
中设置的断点。
现在我们可以在运行时访问我们程序的内存。让我们找出 system
函数的位置以及 exit
函数的位置。
太棒了!既然我们有了 system
和 exit
的地址,我们就可以在漏洞利用中使用它们了。但是,我们仍然需要为 system
设置一个参数。
在这种情况下,我们将创建一个名为 EGG
的环境变量,其中包含 "/bin/sh"
,这将是我们推送到伪堆栈帧作为调用 system
函数参数的地址。我们可以使用 Q
键退出 GDB。现在输入以下命令来导出我们的环境变量:
export EGG='/bin/sh'
太棒了!我们现在一定已经成功导出了环境变量。您可以使用以下命令在环境变量列表中找到它来验证它:
env
如上所示,EGG
环境变量已成功创建。在 GDB 中,我们可以遍历内存来查找任何环境变量,这并不像它们通常位于非常相似的内存区域那么难,但在此实例中,让我们改为创建一个程序,直接获取我们环境变量的位置,并返回我们想要的那个环境变量。我们将使用 C 函数 getenv
,它允许我们通过名称获取环境变量。我们将使用 printf
的 "%8p" 格式参数直接将其打印出来,这意味着 printf
应该将 getenv
的结果,也就是我们的变量 EGG
,作为指针(即 EGG
在十六进制中的地址)打印出来。
get_env.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
printf("%8p\n", getenv(argv[1]));
return 0;
}
我们可以使用 gcc 编译我们的程序,而无需使用 -g
标志或 -fno-stack-protector
标志,因为我们在这个简单的程序中不需要它们。
gcc get_env.c -o get_env.out
我们现在可以使用这个程序来查找我们的目标环境变量。
如我们所见,EGG
的地址是 0xbffff860
。
让我们在 GDB 中运行我们的程序(bug.out)并在 main
函数中创建一个断点。我们将检查 EGG
环境变量是否确实位于 0xbffff860
。
如我们所见,EGG
实际上不在 0xbffff860
。这是因为在 EGG
之前有一个环境变量被推到了堆栈上。这个变量包含了我们可执行文件的完整路径和完整名称,这意味着 EGG
的位置取决于这个变量的长度,因为我们可执行文件的大小会因我们的路径和名称偏好而异。
此时,EGG
向上移动了 27 个字节。我们需要从 getenv
函数获得的地址中减去这个值。因此,EGG
位于 0xbffff845
。
现在让我们尝试我们的漏洞利用,将 EGG
的校正地址插入我们的 payload 中。这将为我们提供一个新的 shell。
运行我们的漏洞利用
为了运行我们的缓冲区溢出漏洞利用,我们必须覆盖我们的返回地址为 system
的地址,这样它就不会指向 main
中的前一个位置,而是会调用 system 函数。然后我们可以推入代表 exit
地址的 4 个字节,然后是 EGG
的地址。
让我们找出 buffer
的位置和基址指针 $EBP
的位置,以确定覆盖返回地址所需的总长度。
在 GDB 中,让我们在调用 gets
函数之前,在 buggy_function
中设置一个断点。为了知道在哪里,我们可以使用 list
命令。
list buggy_function
让我们在第 9 行设置一个断点。GDB 会在第 9 行代码执行之前中断执行。这将允许创建 buffer
数组,以便我们可以确定其位置。
在这里,我们可以看到 buffer 从 0xbffff484
开始。我们现在必须找出基址指针的位置,以获得正确的 payload 长度。我们可以使用以下命令找出基址指针 $EBP
的位置。
info registers $ebp
我们还可以运行以下 GDB examine 命令,以查看 $EBP
之后的 4 个字节,它们代表 buggy_function
的返回地址。
x/2x $ebp
事实上,正如我们所见,$EBP
存储在 0xbffff508
,之后是包含返回地址指向 0x080483f3
的内存位置。0x080483f3
是调用 buggy_function
后在 main
中的执行点。我们可以通过反汇编 main
函数并查找地址 0x080483f3
来查看 main
函数中的返回点。
disass main
正如您所见,0x080483f3
指的是调用 main
中的 buggy_function
之后的下一个执行点。
让我们找出 payload 的确切大小要求,方法是用较大的地址(即基址指针 $EBP
的地址)减去当前较小的地址(即 buffer
的地址)。
正如我们所见,我们需要 132 字节的 payload 才能到达 $EBP
,我们需要另外 4 个字节来覆盖 $EBP
,然后再需要 12 个字节来插入 system
、exit
和 EGG
的地址。总共,我们的 payload 长度应为 148 字节。太棒了,让我们使用 perl 将 payload 打印到我们的程序中,作为 STDIN
,其中包含 132 个垃圾字节 + 4 个用于覆盖 $EBP
的字节,然后是我们 exploit 的关键字节信息。请记住,您必须以大端序(Big Endian)写入地址。
r <<< $(perl -e 'print "A" x 132 . "B" x 4 .
"\x90\xfa\xea\xb7" . "\xe0\x4c\xea\xb7" . "\x45\xf8\xff\xbf")
程序现在应该正常退出并为您提供一个 /bin/sh shell。如果您省略 exit
函数,程序将不会正常退出并会给您一个错误,但仍然会生成一个 shell。为了避免被利用的系统引起怀疑,最好包含 exit
函数。
另一种有趣的玩法是列出当前目录。事实上,您可以运行任何您选择的可执行文件。在这种情况下,让我们在 GDB 之外将 EGG
改为包含 Linux /bin/ 文件夹中的 ls
可执行文件。
export EGG="/bin/ls"
我们现在可以重新进入 GDB 并运行与之前相同的 payload。system
、exit
和 EGG
的地址应该保持不变。
历史
- 2021 年 5 月 27 日:初始版本