Shellcoding 0x2: Bring Your Own Reverse Shell

Abhinav Thakur
System Weakness
Published in
6 min readJul 8, 2022

--

Communication is the key to success, let’s talk shell x_x

In the previous article, we wrote shellcode to spawn a shell. While this is still useful, it may be more valuable to demonstrate a quick POC exploit that works locally. Till now, we have been dealing with local processes on the target machine but shellcode can be designed to do much more than that. In this article, we will leverage networking API provided by Linux platform to craft shellcode that delivers a shell session (a /bin/sh interface to target machine) to a virtual machine listening over the same network interface (at a certain IP address-port number).

While this might not sound much interesting but the fact that it can connect-back to any machine over internet is what starts to carry attention from adversaries. An attacker after getting its shellcode executed would need a way to control the victim, getting a remote shell starts to sound more like why not!. The payload achieving this effect is loosely called remote shell, also known as connect-back or reverse shell.

getting remote access to compromised machine

Even though the article series is written in the below mentioned chronological order, feel free to skip to whatever interests you more.

C Through Logic

Network communication between devices happen over endpoints known as sockets. A socket is identified by a pair of IP address and port number (representing machine address and service executing on it). Good news is that Linux kernel provides a rich API to create sockets and deal with network operations. Below is how we design a program that connects back to attacker machine listening on socket (IP address 192.168.1.6 & port 19999 ). Here, we perform a sequence of socket() → connect() → dup2() → execve() syscalls.

reverse_shell.c (avoids error checking)
  • socket (2) (Line 28): creates an internet domain socket (AF_INET) of type SOCK_STREAM (which defines the semantics of network communication). This system call returns a file descriptor representing a socket endpoint which will be used to connect to remote machine.
  • connect(2) (Line 3237): before we perform connect(2) , we need to specify the IP address and port number of remote machine to connect to. connect(2) takes in a socket address structure of type struct sockaddr, which holds what IP address and port number (service) to connect to (see /usr/include/netinet/in.h for referencing struct sockaddr). We start with declaring and populating evilAddr with address family (AF_INET), remote port to connect to (encoded from host →network byte order) and IP address of remote host (encoded into network-byte-order). Later, we perform a connect(2) to evilAddr.
  • dup2(2) (Line 4041): we run a loop to perform dup2(sockfd, i) syscall over standard I/O file descriptors — 0 (STDIN_FILENO), 1 (STDOUT_FILENO) and 2 (STDERR_FILENO) such that the file descriptor table entries (0, 1 and 2) points to sockfd’s (socket file descriptor) open file description (within system-wide open file table). This has such an effect that the all I/O performed on FDs 0,1 and 2 will indeed be performed on socket.
  • execve (2) (Line 44): By default, file descriptors accross an execve() syscall (unless file descriptors are marked with close-on-exec, see fcntl(2)) which means all I/O performed by the newly loaded program (on FDs 0, 1 & 2) will be happening on attacker-connected socket.

Below is how the code was tested — attacker machine (running kali-linux-2022.1) is listening on IP address 192.168.16 and port number 19999 ( $ nc -lp 19999). Below, the victim/target machine (running Ubuntu 21.04) executes reverse_shell.elf (compiled by $ gcc -o reverse_shell reverse_shell.c) and attacker gets a shell running as a process on victim machine.

Attacker machine (kali — above) talking to victim machine (ubuntu — below)

Now that we understand the logic at a higher level, nothing stops us from crafting a shellcode for the same.

TCP Reverse shell (100 bytes)

Before performing a syscall, arguments to syscall should be passed in sequence — rdi, rsi, rdx, r10, r8 and r9 while syscall number in rax (as per System V ABI). Let’s write some code.

socket (2) + connect (2)

I hope the comments above explain the program well. Just in case they don’t —

  • socket(2) (Line 613): uses push-pop sequence using stack to initialise registers with appropriate values to perform socket(2). Line 12 uses cdq instruction to zero out rdx (cdq extends the sign/most-significant bit of eax to all bits of rdx).
  • connect(2) (Line 1628): saves the return value from socket(2) in edi and clears rdx (using cdq). Next, we push 2 QWORDS (0x10 bytes) onto the stack that represents struct sockaddr_in (lines 1819). (lines 2022) moves AF_INET (0x2), port number (19999) and remote IP 192.168.1.6. (lines 2324) pushes the address of populated structure and pops it into rsi (2nd argument to connect()). (lines 2528) sets rdx to sizeof struct sockaddr , rax to syscall number and performs a connect(2).
dup2 (2) syscall (setting up standard I/O FDs)
  • (lines 3847): To duplicate standard I/O file descriptors, we iterate from rbx = [0,3)performing dup2(sockfd, rbx);. Since edi is already set to sockfd from above, we just need to set rsi (newfd) to 0, 1 and 2 at each iteration performing dup2(2) syscall.
execve_binsh.s

Above, we perform execve(“/bin/sh”) and exit(7) which have been described in fair depth here.

Testing above payload, we take harness.elf as a vulnerable SUID binary, we supply reverse_shell.raw (raw bytes) as input below.

Where‘s my root shell ?

We got a remote shell, but it isn’t having root privileges. If you read the previous article, it might have already clicked that shell program drops privilege and we therefore need a suid(0) syscall before execve(“/bin/sh”). Adding this syscall and testing again as shown below —

adding setuid(0) to reverse shell
reverse root shell

Finally, we get a reverse shell with root privileges !

Land of Detection & Prevention

Rootkits can make life of a defender tough, but if no rootkit installation took place before initiating connection with attacker C2 infrastructure — such mischief can trivially be detected by network monitoring tools like netstat . To prevent such scenarios, it would be a better idea to run victim software service/application process under a sandbox (under a new mount namespace instance having no network device mount point to bind attacker’s socket address to) and use passing-the-file descriptor via UNIX domain sockets to pass socket file descriptor keeping victim process’s legitimate/intended connection(s) up while denying any other network activity.

Epilogue

This article explains the process of getting remote access to compromised machine over a shell. It starts with writing a C program invoking Linux networking syscalls and later implementing a reverse shell in x86–64 assembly. Next, we proceed towards creating multi-stage shellcode.

DISCLAIMER — Since the attackers are already making use of this knowledge, it’s the defenders who might find any value to the approach mentioned in this paper. This article series is intended for exploit developers, malware researchers, folks indulged in red/blue team operations and independent researchers struggling to find relevant resources into this area. The content is intended to be used solely for educational purposes. Therefore, it doesn’t take responsibility for anyone attracting hell by carrying out malicious intentions. Happy hacking ×_×

Cheers,
Abhinav Thakur
(a.k.a
compilepeace)

--

--