缓冲区溢出攻击实验-SetUID 版
章节大纲
-
缓冲区溢出被定义为程序试图将数据写入缓冲区边界之外的情况。这种漏洞可以被恶意用户利用来改变程序的流程控制,导致执行恶意代码。 本实验的目标是让大家获得对这种类型的漏洞的实际见解,并学习如何在攻击中利用此漏洞。
在这个实验中,你们将给定一个具有缓冲区溢出漏洞的程序;你们需要想出一种方法来利用该漏洞并最终获取 root 权限。除了攻击外,你们还将了解操作系统实施以防止缓冲区溢出攻击的各种保护机制。你们需要评估这些机制是否有效,并解释原因。本实验涵盖了以下主题:
-
缓冲区溢出漏洞和攻击
-
栈布局
-
地址随机化、不可执行栈以及 StackGuard
-
32 位和 64 位 shellcode
-
return-to-libc 攻击,旨在击败非执行栈防御措施
-
-
本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
-
缓冲区溢出攻击的最终目标是在目标程序中注入恶意代码,从而利用目标程序的权限执行此代码。Shellcode 在大多数代码注入攻击中被广泛使用。我们在本任务中将对其有所了解。
-
我们已从上述汇编代码中生成了二进制代码,并将其放在名为 call_shellcode.c 的 C 程序文件中的 shellcode 文件夹内。在本任务中,我们将测试 shellcode。
#include <stdlib.h> #include <stdio.h> #include <string.h> const char shellcode[] = #if __x86_64__ "\x48\x31\xd2\x52\x48\xb8\x2f\x62\x69\x6e" "\x2f\x2f\x73\x68\x50\x48\x89\xe7\x52\x57" "\x48\x89\xe6\x48\x31\xc0\xb0\x3b\x0f\x05" #else "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f" "\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31" "\xd2\x31\xc0\xb0\x0b\xcd\x80" #endif ; int main(int argc, char **argv) { char code[500]; strcpy(code, shellcode); // 将 shellcode 复制到栈上 int (*func)() = (int(*)())code; func(); // 从栈上调用 shellcode return 1; }
上述代码包括两个 shellcode 的副本,一个是 32 位的,另一个是 64 位的。当我们使用 -m32 标志编译程序时将使用 32 位版本;如果不使用此标志,则将使用 64 位版本。 使用提供的 Makefile 通过键入 make 编译代码。两个二进制文件将被创建,a32.out (32位) 和 a64.out (64位)。运行它们并描述你的观察结果。 请注意编译时使用了 execstack 选项,这允许代码从堆栈执行;如果没有此选项,则程序将失败。
-
本实验中使用的易受攻击的程序名为 stack.c,位于 code 文件夹内。 此程序存在缓冲区溢出漏洞,并且您的任务是利用该漏洞并获得 root 权限。代码已去除一些非本质的信息,所以它与您从 Labsetup 文件中获取的内容略有不同。
#include <stdlib.h> #include <stdio.h> #include <string.h> /* 更改此大小将更改堆栈布局 */ #ifndef BUF_SIZE #define BUF_SIZE 100 #endif int bof(char *str) { char buffer[BUF_SIZE]; /* 下面的语句具有缓冲区溢出问题 */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); bof(str); printf("Returned Properly\n"); return 1; }
上述程序存在缓冲区溢出漏洞。它首先从名为 badfile 的文件读取输入,然后将此输入传递给函数中的另一个缓冲区 bof()。原始输入的最大长度为 517 字节,但 bof() 中的缓冲区大小仅为 BUF_SIZE 字节,且小于 517。 因为 strcpy() 不检查边界,所以会发生缓冲区溢出。由于此程序是 root 所拥有的 setuid 程序,如果普通用户能够利用这个缓冲区溢出漏洞,则可能能够获得一个 root shell。 请注意,该程序从名为 badfile 的文件中获取输入。此文件由用户控制。现在我们的目标是创建 badfile 文件的内容,当易受攻击的程序将这些内容复制到其缓冲区时,可以启动一个 root shell。
要编译上述易受攻击的程序,请不要忘记使用 -fno-stack-protector 和 "-z execstack" 选项来禁用 StackGuard 和不可执行栈保护。 编译完成后,我们需要将程序设置为 root 所拥有的 setuid 程序。我们可以通过首先更改程序的所有权(行 ①)到根,然后将权限更改为 4755 以启用 setuid 比特(行 ②)。请注意,在设置 setuid 比特之前必须更改所有权,因为所有权更改会关闭此位。
$ gcc -DBUF_SIZE=100 -m32 -o stack -z execstack -fno-stack-protector stack.c $ sudo chown root stack ① $ sudo chmod 4755 stack ②
编译和设置命令已包含在 Makefile 中,所以我们只需键入 make 执行这些命令。变量 L1, ..., L4 在 Makefile 中被设置;它们将在编译过程中使用
-
在任务 3 的攻击中,我们通过使用 gdb 知道了缓冲区的大小。但是在现实世界中,这可能很难得到。例如,如果目标是一个运行在远程机器上的服务器程序,我们无法获取二进制或源代码副本。在这个任务中我们将增加一个约束:你仍可以使用 gdb,但不允许通过你的观察直接得到缓冲区大小。事实上,缓冲区大小已经在 Makefile 中提供,但是你不允许在攻击中使用这些信息。
你的任务是通过这种限制条件将易受攻击的程序运行你的 shellcode。我们假设你知道缓冲区大小的范围为 100 到 200 字节。另一个可能对你有用的事实是,由于内存对齐的关系,框架指针的值总是 4 的倍数(32位程序)。请注意,你只能构建一个适用于此范围内所有缓冲区大小的有效 Payload。如果你使用暴力破解的方法(即尝试每个缓冲区大小)那么将不会获得全部分数。尝试的次数越多,被受害者发现的概率更高,这也是为什么减少尝试次数对于一个攻击非常重要。在你的实验报告中,你需要描述你的方法,并提供证据。 -
在这个任务中我们将将易受攻击的程序编译成一个名为 stack-L3 的 64 位二进制文件。 我们将在该程序上启动攻击。编译和设置命令已包含在 Makefile 中,与前一任务相似,在报告中需要详细说明你的攻击。
使用 gdb 在 64 位程序上进行调查与在 32 位程序上的方式相同。 唯一的区别是框架指针寄存器的名称不同。在 x86 架构中,框架指针为 ebp 而在 x64 架构中,则是 rbp。
相比 32 位机器上的缓冲区溢出攻击,在 64 位机器上发起攻击更加困难。最困难的部分在于地址。虽然 x64 架构支持 64 位地址空间,但只有从 0x00 到 0x00007FFFFFFFFFFF 的地址是允许的。这意味着对于每个地址(8字节),最高两位总是为零。 这导致了问题。
在我们的缓冲区溢出攻击中,我们需要在一个 payload 中至少存储一个地址,并通过 strcpy() 将其复制到堆栈中。我们知道 strcpy() 在遇到零时会停止复制。因此,如果 payload 中的中间位置出现了零,则该位置之后的内容将无法被复制到堆栈中。 如何解决这个问题是这个攻击中最困难的挑战。
-
本次任务的目标程序(stack-L4)与任务 4 类似,不同之处在于缓冲区大小非常小。我们将其设置为 10 个字节,而在任务 4 中,缓冲区大小要大得多。 您的目标是通过攻击此 setuid 程序来获得 root shell。由于缓冲区很小可能会遇到额外的挑战。 如果这种情况发生,请解释你如何解决这些挑战。
-
Ubuntu 操作系统中的 dash shell 在检测到有效 UID 不等于真实 UID 时(这在 setuid 程序中是这种情况),会通过将有效 UID 变为真实 UID 来放弃权限。我们之前让 /bin/sh 指向另一个名为 zsh 的 shell,它没有这样的防御措施。在本任务中我们将将其变回,并看看如何击败该防御措施。请执行以下操作以使 /bin/sh 指向 /bin/dash。
$ sudo ln -sf /bin/dash /bin/sh
为了击败缓冲区溢出攻击中的 dash 防御措施,我们需要做的就是更改真实 UID,使其等于有效 UID。当 root 所拥有的 setuid 程序运行时,有效 UID 为零,在执行 shell 程序之前,我们只需将真实 UID 更改为零即可。 这可以通过在 shellcode 中调用 setuid(0) 来实现。
以下汇编代码演示了如何调用 setuid(0)。二进制代码已置于 call_shellcode.c 内,你只需将其添加到 shellcode 的开头即可。
; 调用 setuid(0): 32位 xor ebx, ebx ; ebx = 0: setuid() 的参数 xor eax, eax mov al, 0xd5 ; setuid() 的系统调用号 int 0x80 ; 调用 setuid(0): 64位 xor rdi, rdi ; rdi = 0: setuid() 的参数 xor rax, rax mov al, 0x69 ; setuid() 的系统调用号 syscall
编译 call_shellcode.c 成一个 root 所拥有的二进制文件(键入 "make setuid")。运行 shellcode a32.out 和 a64.out,并带有或不带 setuid(0) 系统调用。请描述和解释你的观察结果。
现在,使用更新的 shellcode 你可以再次针对易受攻击的程序发起攻击,并且这一次,shell 的防御机制已开启。 重复任务 3 的攻击,看看你是否能够获得 root shell。获取 root shell 后,请运行以下命令来验证此防御措施已启用。虽然不需要在任务 2 和任务 3 上重复进行相同的攻击,但你可以自由尝试并查看它们是否会起作用。
-
在 32 位 Linux 机器上,堆栈只有 19 比特的熵,这意味着堆栈基地址可以有
种可能性。这个数字不算高,并且很容易通过暴力破解方法被穷尽。 在这个任务中,我们将使用这样的方法来击败我们的32位虚拟机上的地址随机化防御机制。 首先,我们使用以下命令开启 Ubuntu 的地址随机化功能。然后我们再次针对 stack-L1 运行相同的攻击,请描述和解释你的观察结果。
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
接着,使用暴力破解方法反复地攻击易受攻击的程序,希望我们放在 badfile 中的内容最终是正确的。我们将仅尝试针对 32 位 的 stack-L1 程序进行攻击。 你可以使用以下 Shell 脚本在无限循环中运行该漏洞程序。如果你的攻击成功,脚本会停止;否则它将继续运行。请耐心等待,因为这可能需要几分钟时间,但如果你非常不幸运,则可能需要更长时间。请描述你的观察结果。
#!/bin/bash SECONDS=0 value=0 while true; 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-L1 done
在 64 位程序上使用暴力破解攻击会更难,因为熵更大。尽管这不在要求范围内,但可以自由尝试一下,看看是否能幸运地成功。