APT_REPORT/APT28/decode/zebrocy_decrypt_artifact.py

169 lines
6.8 KiB
Python
Raw Normal View History

2019-04-08 15:46:31 +08:00
# 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()