Shellcode 开发实验
章节大纲
-
Shellcode 在涉及代码注入的许多攻击中被广泛使用。编写 Shellcode 是一项具有挑战性的任务。虽然我们可以从互联网上找到现成的 Shellcode,但在有些情况下我们还是需要编写满足特定要求的 Shellcode。此外,从头开始编写自己的 Shellcode 能学到一些有趣的技术。本实验旨在理解这些技术,从而能够编写自己的Shellcode。
编写 Shellcode 有几个挑战,一个是确保二进制代码中没有零,另一个是找到命令中使用的数据的地址。第一个挑战并不难解决,有几种方法。针对第二个挑战的解决方案导致了两种典型的方法来编写 Shellcode。在第一种方法中,数据在执行期间被推入栈中,因此可以从栈指针获取它们的地址。在第二种方法中,数据存储在代码区域,就在 call 指令之后。当执行 call 指令时,数据的地址被当作返回地址并推入栈中。这两种解决方案都非常有意思,我们希望大家能学习这两种技术。本实验涵盖以下主题:
-
Shellcode
-
汇编代码
-
反汇编
-
-
本实验在 SEEDUbuntu20.04 VM 中测试可行。你可以在本页面右端选择虚拟机版本为 SEEDUbuntu20.04,点击“创建虚拟机”来获取虚拟机平台的临时用户名与密码,登录虚拟机平台即可获得一台预先构建好的 SEEDUbuntu20.04 VM,该虚拟机以及用户名密码将在开启 24 小时后自动销毁。你也可以在其他 VM、物理机器以及云端 VM 上自行配置环境进行实验,但我们不保证实验能在其他 VM 下成功。实验所需的文件可从下方下载,解压后会得到一个名为 Labsetup 的文件夹,该文件夹内包含了完成本实验所需的所有文件。
-
为了直接控制 Shellcode 中要使用的指令,编写 Shellcode 的最佳方式是使用汇编语言。在本任务中,我们将使用一个示例程序来熟悉开发环境。你的任务是完成整个开发过程:编译并运行示例代码,然后从二进制文件中提取机器代码。
不同计算机架构的汇编语言有所不同。在本任务中,示例代码 hello.s 针对的是 amd64(64 位)架构。代码包含在 Labsetup 文件夹中。使用 Apple 芯片的学生可以在 Labsetup/arm 文件夹中找到适用于 arm 架构的示例代码。
;; 一个 amd64 架构的汇编程序示例 hello.s global _start section .text _start: mov rdi, 1 ; 标准输出 mov rsi, msg ; 消息的地址 mov rdx, 15 ; 消息的长度 mov rax, 1 ; write() 系统调用的编号 syscall ; 调用 write(1, msg, 15) mov rdi, 0 ; mov rax, 60 ; exit() 系统调用的编号 syscall ; 调用 exit(0) section .rodata msg: db "Hello, world!", 10
-
Shellcode 的主要目的是运行一个 shell 程序,例如 /bin/sh。在 Ubuntu 操作系统中,这可以通过调用 execve() 系统调用来实现。
execve("/bin/sh", argv[], 0)
我们需要为该系统调用传递三个参数:在 amd64 架构中,它们通过 rdi, rsi, rdx 寄存器传递。在 arm64 架构中,它们通过 x0, x1, x2 寄存器传递。伪代码如下所示:// 针对 amd64 架构 Let rdi = address of the "/bin/sh" string Let rsi = address of the argv[] array Let rdx = 0 Let rax = 59 // 59 是 execve 的系统调用号 syscall // 调用 execve() // 针对 arm64 架构 Let x0 = address of the "/bin/sh" string Let x1 = address of the argv[] array Let x2 = 0 Let x8 = 221 // 221 是 execve 的系统调用号 svc 0x1337 // 调用 execve()
编写 shellcode 的主要挑战是如何获取 "/bin/sh" 字符串的地址以及 argv[] 数组的地址。通常有两种典型的方法:- 方法 1:将字符串和数组存储在代码段中,然后使用指向代码段的 PC 寄存器获取它们的地址。本任务中我们关注这种方法。
- 方法 2:在栈上动态构造字符串和数组,然后使用栈指针寄存器获取它们的地址。我们将在下一任务中关注这种方法。
-
另一种获取 shell 字符串和 argv[] 数组的方法是动态地在栈上构造它们,然后使用栈指针寄存器获取它们的地址。以下是使用这种方法的一个 amd64 示例。amd64 和 arm64 的代码都可以从 Labsetup 文件夹中找到,代码中的注释给出了简要的解释。
section .text global _start _start: xor rdx, rdx ; rdx = 0 push rdx ; 将 0 压入栈中(用于字符串终止) mov rax,'/bin//sh' push rax ; 将字符串压入栈中 mov rdi, rsp ; rdi = 命令字符串的地址 push rdx ; 将 argv[1]=0 压入栈中 push rdi ; 将 argv[0] 压入栈中 mov rsi, rsp ; rsi = argv[] 数组的地址 xor rax, rax mov al, 59 ; execve() syscall
我们可以使用以下命令将汇编代码编译成 64 位二进制代码:// 对于 amd64 $ nasm -f elf64 mysh_64.s -o mysh_64.o $ ld mysh_64.o -o mysh_64 // 对于 arm64 $ as mysh_64.s -o mysh_64.o $ ld mysh_64.o -o mysh_64