# zebrocy_decrypt_artifact.py - script to decode Zebrocy downloader hex strings
# Takahiro Haruyama (@cci_forensics)

# Note: the script was used to decode and AES-decrypt C2 traffic data generated by Zebrocy payload
# I've not seen Zebrocy payload lately (2019/1Q), so commented out the code

import argparse, base64, re
from Crypto.Cipher import AES
from struct import *

g_debug = False
g_delimiter_post = ':'
g_delimiter_conf = '\r\n'
g_AES_KEY_SIZE = 38
#g_pat_hexascii = re.compile(r'[0-9A-F]{6,}')
g_pat_hexascii = re.compile(r'[0-9A-F#\-=@%$]{6,}') # downloader type1 (Delphi)
g_pat_hexascii_go = re.compile(r'(?:[2-7][0-9A-F]){2,}') # downloader type1 (Go)
g_pat_hexunicode = re.compile(ur'(?:[0-9A-F][\x00]){2,}') # downloader type2 (Delphi)
#g_pat_ascii = re.compile(r'[\x20-\x7E]{3,}')
g_pat_hexasciidummy = re.compile(r'[0-9A-Fa-z]{76,150}') # hexascii with dummy small alphabet for payload v10.3
g_MAX_HEXTEXT_SIZE = 0x200

g_aes_key = 'DUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMYDUMMY'

def info(msg):
    print "[*] {}".format(msg)

def success(msg):
    print "[+] {}".format(msg)

def error(msg):
    print "[!] {}".format(msg)

def dprint(msg):
    if g_debug:
        print "[DEBUG] {}".format(msg)

def decode(buf, adjust):
    newbuf = []
    for i in range(0, len(buf), 2):
        if buf[i] and buf[i+1]:
            newbuf.append(chr(int(buf[i] + buf[i+1], 16) + adjust))        
    return "".join(newbuf)

def extract_ascii(pat, data):
    for match in pat.finditer(data):
        yield match.group().decode("ascii"), match.start()

def extract_unicode(pat, data):
    for match in pat.finditer(data):
        yield match.group().decode("utf-16le"), match.start()    

def extract_hexkey(s):
    hexkey = [x for x in s if ord(x) < ord('Z')]
    return ''.join(hexkey)

def decrypt_hextext(hexenc, aes=None, adjust=0):
    try:
        hexdec = decode(hexenc, adjust)
    except (ValueError, IndexError):
        return ''
    dprint('hextext to bin: {}'.format(repr(hexdec)))
    
    if aes and len(hexdec) > 8 and unpack("<Q", hexdec[:8])[0] <= len(hexdec[8:]) and len(hexdec[8:]) % 0x10 == 0:
        size = unpack("<Q", hexdec[:8])[0]
        dprint('plain text size = {:#x}'.format(size))
        enc = hexdec[8:]
        #try:
        dec = aes.decrypt(enc)
        #except ValueError:
        #return ''
        dprint('AES-decrypted with null bytes = {}'.format(repr(dec)))
        plain = dec[:size]
        dprint('AES-decrypted plain text = {}'.format(plain))
        return plain
    else:
        #dprint('plain text size {:#x} is larger than encrypted data size {:#x}. this string is not encrypted'.format(size, len(hexdec[8:])))
        if len(hexdec) < g_MAX_HEXTEXT_SIZE:
            success('decoded hextext: {}'.format(hexdec))
            return hexdec
        else:
            return ''

def parse(buf, post=False):
    dprint('now parsing: {}'.format(buf))
    if post:
        success('AES-decrypted POST file content: {}'.format(buf))
        b64enc, txt = buf.split(g_delimiter_post)
        b64dec = base64.decodestring(b64enc)
        success('base64 decoded = {}'.format(b64dec))
        vid = txt.split('-')[0]
        hexdec = decode(vid, 0)
        vsn = unpack("<I", hexdec[:4])[0]
        phn = hexdec[4:]
        success('victim ID = {} (VolumeSerialNumber = {:#x}, part of hostname = {})'.format(vid, vsn, phn))
    else:
        hexdec = decode(buf, 0)
        dprint('hextext to bin #2: {}'.format(repr(hexdec)))
        hexparams = hexdec.split(g_delimiter_conf)
        try:
            params = [decode(x, 0) for x in hexparams]
        except (ValueError, IndexError):
            params = hexparams
        success('{}'.format(params))

def main():    
    parser = argparse.ArgumentParser()
    parser.add_argument('-k', '--key', default=g_aes_key, help="AES key")
    parser.add_argument('-c', '--choose', action='store_true', help="choose AES key from decoded strings")
    parser.add_argument('-t', '--text', help="encrypted hextext")    
    parser.add_argument('-p', '--post', action='store_true', help="the text is HTTP POST file content")
    parser.add_argument('-f', '--file', help="binary file with encrypted hextexts")
    parser.add_argument('--debug', '-d', action='store_true', help="print debug output")
    parser.add_argument('--uni', '-u', action='store_true', help="unicode hextext mode")
    parser.add_argument('--strict', '-s', action='store_true', help="strict hextext mode")
    args = parser.parse_args()
    global g_debug
    g_debug = args.debug    

    info('start')

    aes = AES.new(args.key[:0x20], AES.MODE_ECB)
    if args.text:
            plain = decrypt_hextext(args.text, aes)
            parse(plain, args.post)        
    if args.file:
        with open(args.file, 'rb') as f:
            data = f.read()
            stored = '' # for divided hextext
            if args.uni:
                for s,p in extract_unicode(g_pat_hexunicode, data):
                    dprint('{:#x}: hextext found'.format(p))
                    plain = decrypt_hextext(s, None, 1)
            else:
                ''' # for AES decryption of payload strings
                if args.choose: # for payload 10.3
                    for s,p in extract_ascii(g_pat_hexasciidummy, data):
                        dprint('{:#x}: possible hexkey with dummy small chars found (payload v10.3)'.format(p))
                        hexkey = extract_hexkey(s)
                        if len(hexkey) == g_AES_KEY_SIZE * 2:
                            key = decode(hexkey)
                            success('possible AES key acquired: {}'.format(key))
                            aes = AES.new(key[:0x20], AES.MODE_ECB)
                '''
                pat = g_pat_hexascii_go if args.strict else g_pat_hexascii
                for s,p in extract_ascii(pat, data):
                    dprint('{:#x}: hextext found {}'.format(p, s))
                    s = re.sub(r"[#\-=@%$]", "", s) # delete dummy characters
                    plain = decrypt_hextext(s, aes)
                    dprint('len(s)={:#x}, len(plain)={:#x}'.format(len(s), len(plain)))
                    ''' # for AES decryption of payload strings
                    if len(s) > g_MAX_HEXTEXT_SIZE and plain == '':
                        dprint('{:#x}: possible divided config block'.format(p))
                        stored += s
                        plain = decrypt_hextext(stored, aes)
                        if plain != '':
                            stored = ''
                    if args.choose and len(plain) == g_AES_KEY_SIZE:
                        success('possible AES key acquired: {}'.format(plain))
                        aes = AES.new(plain[:0x20], AES.MODE_ECB)                
                    if g_pat_hexascii.match(plain) and len(plain) % 2 == 0:  
                        parse(plain)
                    '''
        
    info('done')    

if __name__ == '__main__':
    main()
