格式化字符串攻击实验
章节大纲
-
C 语言中的 printf() 函数被用来根据格式打印字符串。它的第一个参数被称为格式化字符串,它定义了字符串应该如何被格式化。格式化字符串使用占位符,这些占位符由 % 字符标记,供 printf() 函数在打印时填充数据。格式化字符串的使用不仅限于printf()函数,许多其他函数,如 sprintf()、fprintf() 和 scanf() 也使用格式化字符串。一些程序允许用户提供格式化字符串的全部或部分内容。如果这些内容未经过滤,恶意用户可以利用这个机会使程序运行任意代码。这样的问题被称为格式化字符串漏洞。
本实验的目标是让大家通过将课堂上学到的关于该漏洞的知识付诸实践,亲身体验格式化字符串漏洞。你将获得一个带有格式化字符串漏洞的程序,你的任务是利用这个漏洞以实现以下目的:
- 使程序崩溃
- 读取程序的内部内存
- 修改程序的内部内存
- 最严重的,使用受害者程序的权限注入并执行恶意代码
本实验涵盖以下主题:
-
格式化字符串漏洞和代码注入
-
栈布局
-
Shellcode
-
反向 Shell
-
本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可根据你的芯片类型从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
-
当我们使用所包含的 docker-compose.yml 文件启动容器时,将启动两个容器,每个容器都运行了一个易受攻击的服务器。对于此任务,我们将使用在 10.9.0.5 上运行的服务器,它运行了一个带有格式化字符串漏洞的 32 位程序。对于 Apple Silicon 机器,两个容器都是一样的,并且它们都运行一个 64 位服务器程序(学生可以在此实验中使用任何一个)。让我们先给这个服务器发送一个消息。我们将看到目标容器打印出的以下消息(你看到的实际消息可能不同)。
$ echo hello | nc 10.9.0.5 9090 Press Ctrl+C // 容器控制台上的打印输出 server-10.9.0.5 | Got a connection from 10.9.0.1 server-10.9.0.5 | Starting format server-10.9.0.5 | Input buffer (address): 0xffffd2d0 server-10.9.0.5 | The secret message's address: 0x080b4008 server-10.9.0.5 | The target variable's address: 0x080e5068 server-10.9.0.5 | Input size: 6 server-10.9.0.5 | Frame Pointer inside myprintf() = 0xffffd1f8 server-10.9.0.5 | The target variable's value (before): 0x11223344 server-10.9.0.5 | hello server-10.9.0.5 | (^_^)(^_^) Returned properly (^_^)(^_^) server-10.9.0.5 | The target variable's value (after): 0x11223344
服务器接受最多 1500 字节的数据。你的主要工作是构建不同的有效载荷来实现每个任务中指定的目标。你可以将有效 Payload 保存在文件中,然后使用以下命令将它发送到服务器。$ cat <file> | nc 10.9.0.5 9090 Press Ctrl+C if it does not exit.
你的任务是为服务器提供输入,使得当服务器程序在 myprintf() 函数中打印用户输入时,它会崩溃。你可以通过查看容器的打印输出来判断 format 程序是否崩溃。如果 myprintf() 返回,它会打印出 "Returned properly" 和笑脸。如果你看不到,format 程序可能已经崩溃了。然而,服务器程序是不会崩溃的,崩溃的 format 程序运行在服务器程序创建的子进程中。由于在这个实验中构建的大多数格式化字符串可能相当长,最好使用程序来完成。如果你不熟悉 Python 语言,在 attack-code 目录中,我们为你准备了一份名为 build_string.py 的示例代码。它展示了如何将各种类型的数据放入一个字符串中。 -
本任务的目标是让服务器打印出其内存中的一些数据(我们将继续使用 10.9.0.5)。数据只是在服务器端打印出来,攻击者无法看到它。因此,这不是一个有意义的攻击,但是在此任务中使用的技术对于后续任务至关重要。
-
本任务的目标是修改服务器程序中的 target 变量的值(我们将继续使用 10.9.0.5)。target 的原始值是 0x11223344。假设这个变量保存了一个重要的值,可以影响程序的控制流。如果远程攻击者可以更改其值,他们可以改变这个程序的行为。我们有三个子任务。
-
现在我们准备去实现这次攻击的终极目标,代码注入。我们希望能够注入一段二进制格式的恶意代码到服务器的内存中,然后使用格式化字符串漏洞修改函数的返回地址字段。当函数返回时,它可以跳转到我们注入的代码。此任务中用的技术与前一个任务相似:它们都修改了内存中的一个4字节数字。前一个任务修改了 target 变量,而这个任务修改了函数的返回地址字段。学生需要根据服务器打印出的信息计算出返回地址字段的地址。
-
注意:对于 Apple Silicon 机器,任务 1-4 已经使用了 64 位服务器程序,所以这个任务与任务 4 相同,不需要重复。但是,你可以在这个部分找到有关 64 位机器的有用信息。
在前面的任务中,我们的目标服务器是 32 位程序。在这个任务中,我们攻击一个 64 位服务器程序。我们的新目标是 10.9.0.6,它运行 format 程序的 64 位版本。让我们先给这个服务器发送一个 hello 消息。我们将看到目标容器打印出的以下消息。$ echo hello | nc 10.9.0.6 9090 Press Ctrl+C // 容器控制台上的打印输出 server-10.9.0.6 | Got a connection from 10.9.0.1 server-10.9.0.6 | Starting format server-10.9.0.6 | Input buffer (address): 0x00007fffffffe200 server-10.9.0.6 | The secret message's address: 0x0000555555556008 server-10.9.0.6 | The target variable's address: 0x0000555555558010 server-10.9.0.6 | Input size: 6 server-10.9.0.6 | Frame Pointer (inside myprintf): 0x00007fffffffe140 server-10.9.0.6 | The target variable's value (before): 0x1122334455667788 server-10.9.0.6 | hello server-10.9.0.6 | (^_^)(^_^) Returned from printf() (^_^)(^_^) server-10.9.0.6 | The target variable's value (after): 0x1122334455667788
你可以看到框架指针和缓冲区的地址的值变为 8 字节长(而不是 32 位程序中的 4 字节)。你的工作是构建你的有效载荷来利用服务器的格式化字符串漏洞。你的最终目标是获得目标服务器上的root shell。你需要使用 64 位版本的 shellcode。 -
还记得 gcc 编译器生成的警告消息吗?请解释它的含义。请修复服务器程序中的漏洞,并重新编译它。编译器警告消失了吗?你的攻击还会有效吗?你只需要尝试你的一个攻击来看看它是否仍然有效。