This commit is contained in:
blackorbird 2019-04-08 15:46:31 +08:00
parent 81a71cac01
commit 105c56463c
13 changed files with 1281 additions and 0 deletions

View File

@ -0,0 +1,30 @@
#+OPTIONS: ^:{}
#+TITLE: fn_fuzzy.py - IDAPython script for fast multiple binary diffing triage
* Motivation
See the [[https://conference.hitb.org/hitbsecconf2019ams/sessions/fn_fuzzy-fast-multiple-binary-diffing-triage-with-ida/][conference information]] or blog post (will be linked soon).
* how to use
- fn_fuzzy.py :: IDAPython script to export/compare fuzzy hashes of the sample
- cli_export.py :: python wrapper script to export fuzzy hashes of multiple samples
The typical usage is to run cli_export.py to make a database for large idbs then compare on IDA by executing fn_fuzzy.py.
[[./img/fn_fuzzy.png]]
[[./img/res_summary.png]]
[[./img/res_funcs.png]]
* supported IDB version
IDBs generated by IDA 6.9 or later due to SHA256 API
* required python packages
- mmh3
- [[https://github.com/williballenthin/python-idb%0A][python-idb]]

View File

@ -0,0 +1,152 @@
# cli_export.py - batch export script for fn_fuzzy
# Takahiro Haruyama (@cci_forensics)
import argparse, subprocess, os, sqlite3, time, sys
import idb # python-idb
import logging
logging.basicConfig(level=logging.ERROR) # to suppress python-idb warning
# plz edit the following paths
g_ida_dir = r'C:\work\tool\IDAx64'
g_db_path = r'Z:\haru\analysis\tics\fn_fuzzy.sqlite'
g_fn_fuzzy_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'fn_fuzzy.py')
g_min_bytes = 0x10 # minimum number of extracted code bytes per function
g_analyzed_prefix = r'fn_' # analyzed function name prefix (regex)
class LocalError(Exception): pass
class ProcExportError(LocalError): pass
def info(msg):
print "[*] {}".format(msg)
def success(msg):
print "[+] {}".format(msg)
def error(msg):
print "[!] {}".format(msg)
def init_db(cur):
cur.execute("SELECT * FROM sqlite_master WHERE type='table'")
if cur.fetchone() is None:
info('DB initialized')
cur.execute("CREATE TABLE IF NOT EXISTS sample(sha256 UNIQUE, path)")
#cur.execute("CREATE INDEX sha256_index ON sample(sha256)")
cur.execute("CREATE INDEX path_index ON sample(path)")
cur.execute("CREATE TABLE IF NOT EXISTS function(sha256, fname, fhd, fhm, f_ana, bsize, ptype, UNIQUE(sha256, fname))")
cur.execute("CREATE INDEX f_ana_index ON function(f_ana)")
cur.execute("CREATE INDEX bsize_index ON function(bsize)")
def existed(cur, sha256):
cur.execute("SELECT * FROM sample WHERE sha256 = ?", (sha256,))
if cur.fetchone() is None:
return False
else:
return True
def remove(cur, sha256):
cur.execute("DELETE FROM sample WHERE sha256 = ?", (sha256,))
cur.execute("DELETE FROM function WHERE sha256 = ?", (sha256,))
def export(f_debug, idb_path, outdb, min_, f_ex_libthunk, f_update, f_ana_exp, ana_pre, f_remove):
# check the ext and signature
ext = os.path.splitext(idb_path)[1]
if ext != '.idb' and ext != '.i64':
return 0
with open(idb_path, 'rb') as f:
sig = f.read(4)
if sig != 'IDA1' and sig != 'IDA2':
return 0
# check the database record for the idb
#print idb_path
conn = sqlite3.connect(outdb)
cur = conn.cursor()
init_db(cur)
with idb.from_file(idb_path) as db:
api = idb.IDAPython(db)
try:
sha256 = api.ida_nalt.retrieve_input_file_sha256()
except KeyError:
error('{}: ida_nalt.retrieve_input_file_sha256() failed. The API is supported in 6.9 or later idb version. Check the API on IDA for validation.'.format(idb_path))
return 0
if f_remove:
remove(cur, sha256)
success('{}: the records successfully removed (SHA256={})'.format(idb_path, sha256))
conn.commit()
cur.close()
return 0
if existed(cur, sha256) and not f_update:
info('{}: The sample records are present in DB (SHA256={}). Skipped.'.format(idb_path, sha256))
return 0
conn.commit()
cur.close()
ida = 'ida.exe' if sig == 'IDA1' else 'ida64.exe'
ida_path = os.path.join(g_ida_dir, ida)
#cmd = [ida_path, '-L{}'.format(os.path.join(g_ida_dir, 'debug.log')), '-S{}'.format(g_fn_fuzzy_path), '-Ofn_fuzzy:{}:{}:{}:{}:{}:{}'.format(min_, f_ex_libthunk, f_update, f_ana_exp, ana_pre, outdb), idb_path]
cmd = [ida_path, '-S{}'.format(g_fn_fuzzy_path), '-Ofn_fuzzy:{}:{}:{}:{}:{}:{}'.format(min_, f_ex_libthunk, f_update, f_ana_exp, ana_pre, outdb), idb_path]
if not f_debug:
cmd.insert(1, '-A')
#print cmd
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode == 0:
success('{}: successfully exported'.format(idb_path))
return 1
elif proc.returncode == 2: # skipped
return 0
else: # maybe 1
raise ProcExportError('{}: Something wrong with the IDAPython script (returncode={}). Use -d for debug'.format(idb_path, proc.returncode))
def list_file(d):
for entry in os.listdir(d):
if os.path.isfile(os.path.join(d, entry)):
yield os.path.join(d, entry)
def list_file_recursive(d):
for root, dirs, files in os.walk(d):
for file_ in files:
yield os.path.join(root, file_)
def main():
info('start')
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('target', help="idb file or folder to export")
parser.add_argument('--outdb', '-o', default=g_db_path, help="export DB path")
parser.add_argument('--min_', '-m', type=int, default=g_min_bytes, help="minimum number of extracted code bytes per function")
parser.add_argument('--exclude', '-e', action='store_true', help="exclude library/thunk functions")
parser.add_argument('--update', '-u', action='store_true', help="update the DB records")
parser.add_argument('--ana_exp', '-a', action='store_true', help="check analyzed functions")
parser.add_argument('--ana_pre', '-p', default=g_analyzed_prefix, help="analyzed function name prefix (regex)")
parser.add_argument('--recursively', '-r', action='store_true', help="export idbs recursively")
parser.add_argument('--debug', '-d', action='store_true', help="display IDA dialog for debug")
parser.add_argument('--remove', action='store_true', help="remove records from db")
args = parser.parse_args()
start = time.time()
cnt = 0
if os.path.isfile(args.target):
try:
cnt += export(args.debug, args.target, args.outdb, args.min_, args.exclude, args.update, args.ana_exp, args.ana_pre, args.remove)
except LocalError as e:
error('{} ({})'.format(str(e), type(e)))
return
elif os.path.isdir(args.target):
gen_lf = list_file_recursive if args.recursively else list_file
for t in gen_lf(args.target):
try:
cnt += export(args.debug, t, args.outdb, args.min_, args.exclude, args.update, args.ana_exp, args.ana_pre, args.remove)
except LocalError as e:
error('{} ({})'.format(str(e), type(e)))
return
else:
error('the target is not file/dir')
return
elapsed = time.time() - start
success('totally {} samples exported'.format(cnt))
info('elapsed time = {} sec'.format(elapsed))
info('done')
if __name__ == '__main__':
main()

View File

@ -0,0 +1,9 @@
import os
def main():
path = os.path.splitext(get_idb_path())[0] + '.idc'
gen_file(OFILE_IDC, path, 0, 0, GENFLG_IDCTYPE)
Exit(0)
if ( __name__ == "__main__" ):
main()

View File

@ -0,0 +1,602 @@
# fn_fuzzy.py - IDAPython script for fast multiple binary diffing triage
# Takahiro Haruyama (@cci_forensics)
import os, ctypes, sqlite3, re, time, sys, subprocess
import cProfile
from collections import defaultdict
from pprint import PrettyPrinter
from StringIO import StringIO
from idc import *
import idautils, ida_nalt, ida_kernwin, idaapi, ida_expr
import mmh3
import yara_fn # modified version in the same folder
g_debug = False
g_db_path = r'Z:\haru\analysis\tics\fn_fuzzy.sqlite' # plz edit your path
g_min_bytes = 0x10 # minimum number of extracted code bytes per function
g_analyzed_prefix = r'fn_' # analyzed function name prefix (regex)
g_threshold = 50 # function similarity score threshold without CFG match
g_threshold_cfg = 10 # function similarity score threshold with CFG match
g_max_bytes_for_score = 0x80 # more code bytes are evaluated by only CFG match
g_bsize_ratio = 40 # function binary size correction ratio to compare (40 is enough)
# debug purpose to check one function matching
g_dbg_flag = False
g_dbg_fva = 0xffffffff
g_dbg_fname = ''
g_dbg_sha256 = ''
# initialization for ssdeep
SPAMSUM_LENGTH = 64
FUZZY_MAX_RESULT = (2 * SPAMSUM_LENGTH + 20)
dirpath = os.path.dirname(__file__)
_lib_path = os.path.join(dirpath, 'fuzzy64.dll')
fuzzy_lib = ctypes.cdll.LoadLibrary(_lib_path)
g_dump_types_path = os.path.join(dirpath, 'dump_types.py')
class defaultdictRecurse(defaultdict):
def __init__(self):
self.default_factory = type(self)
class import_handler_t(ida_kernwin.action_handler_t):
def __init__(self, items, idb_path, title):
ida_kernwin.action_handler_t.__init__(self)
self.items = items
self.idb_path = idb_path
self.title = title
def import_types(self):
idc_path = os.path.splitext(self.idb_path)[0] + '.idc'
# dump type information from the 2nd idb
if not (os.path.exists(idc_path)):
with open(self.idb_path, 'rb') as f:
sig = f.read(4)
ida = 'ida.exe' if sig == 'IDA1' else 'ida64.exe'
ida_path = os.path.join(idadir(), ida)
cmd = [ida_path, '-S{}'.format(g_dump_types_path), self.idb_path]
#print cmd
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
if proc.returncode == 0:
success('{}: type information successfully dumped'.format(self.idb_path))
else:
error('{}: type information dumping failed'.format(self.idb_path))
return False
# import the type information
idc_path = os.path.splitext(self.idb_path)[0] + '.idc'
ida_expr.exec_idc_script(None, str(idc_path), "main", None, 0)
return True
def activate(self, ctx):
sel = []
for idx in ctx.chooser_selection:
# rename the function
ea = get_name_ea_simple(self.items[idx][2])
sfname = str(self.items[idx][4])
#set_name(ea, sfname)
idaapi.do_name_anyway(ea, sfname)
success('{:#x}: renamed to {}'.format(ea, sfname))
# set the function prototype
sptype = str(self.items[idx][5])
if sptype != 'None':
tinfo = idaapi.tinfo_t()
idaapi.parse_decl2(idaapi.cvar.idati, sptype, tinfo, 0)
#idaapi.apply_callee_tinfo(ea, tinfo)
if idaapi.apply_tinfo(ea, tinfo, 0):
success('{:#x}: function prototype set to {}'.format(ea, sptype))
else:
error('{:#x}: function prototype set FAILED (maybe you should import the types?)'.format(ea))
if ask_yn(0, 'Do you import types from the secondary idb?') == 1:
if self.import_types():
tinfo = idaapi.tinfo_t()
idaapi.parse_decl2(idaapi.cvar.idati, sptype, tinfo, 0)
if idaapi.apply_tinfo(ea, tinfo, 0):
success('{:#x}: function prototype set to {}'.format(ea, sptype))
else:
error('{:#x}: function prototype set FAILED again'.format(ea))
# insert the comment
score = self.items[idx][0]
mmatch = self.items[idx][1]
cmt = 'fn_fuzzy: ssdeep={}, machoc={}'.format(score, mmatch)
set_func_cmt(ea, cmt, 1)
#set_decomplier_cmt(ea, cmt) # not sure how to avoid orphan comment
# update the Choose rows
ida_kernwin.refresh_chooser(self.title)
def update(self, ctx):
return idaapi.AST_ENABLE_ALWAYS
'''
return ida_kernwin.AST_ENABLE_FOR_WIDGET \
if ida_kernwin.is_chooser_widget(ctx.widget_type) \
else ida_kernwin.AST_DISABLE_FOR_WIDGET
'''
class FnCh(ida_kernwin.Choose):
def __init__(self, title, mfn, idb_path):
self.mfn = mfn
self.idb_path = idb_path
self.title = title
ida_kernwin.Choose.__init__(
self,
title,
[
["ssdeep score", 10 | ida_kernwin.Choose.CHCOL_DEC],
["machoc matched", 10 | ida_kernwin.Choose.CHCOL_PLAIN],
["primary function", 30 | ida_kernwin.Choose.CHCOL_PLAIN],
["primary bsize", 10 | ida_kernwin.Choose.CHCOL_DEC],
["secondary analyzed function", 30 | ida_kernwin.Choose.CHCOL_PLAIN],
["secondary prototype", 40 | ida_kernwin.Choose.CHCOL_PLAIN]
],
flags = ida_kernwin.Choose.CH_MULTI)
def OnInit(self):
self.items = []
for fva,v in sorted(self.mfn.items(), key=lambda x:x[1]['score'], reverse=True):
if v['sfname']:
self.items.append(['{}'.format(v['score']), '{}'.format(v['cfg_match']), get_name(fva), '{}'.format(v['pbsize']), v['sfname'], '{}'.format(v['sptype'])])
return True
def OnPopup(self, form, popup_handle):
actname = "choose:actFnFuzzyImport"
desc = ida_kernwin.action_desc_t(actname, 'Import function name and prototype', import_handler_t(self.items, self.idb_path, self.title))
ida_kernwin.attach_dynamic_action_to_popup(form, popup_handle, desc)
def OnGetSize(self):
return len(self.items)
def OnGetLine(self, n):
return self.items[n]
def OnSelectLine(self, n):
idx = n[0] # due to CH_MULTI
idc.Jump(get_name_ea_simple(self.items[idx][2]))
def OnRefresh(self, n):
self.OnInit()
# try to preserve the cursor
#return [ida_kernwin.Choose.ALL_CHANGED] + self.adjust_last_item(n)
#return n
return None
def OnClose(self):
print "closed ", self.title
class SummaryCh(ida_kernwin.Choose):
def __init__(self, title, res):
self.res = res
ida_kernwin.Choose.__init__(
self,
title,
[ ["SHA256", 20 | ida_kernwin.Choose.CHCOL_PLAIN],
["total similar functions", 20 | ida_kernwin.Choose.CHCOL_DEC],
["analyzed similar functions", 20 | ida_kernwin.Choose.CHCOL_DEC],
["idb path", 80 | ida_kernwin.Choose.CHCOL_PATH] ])
self.items = []
def OnInit(self):
for sha256,v in sorted(self.res.items(), key=lambda x:x[1]['mcnt']['total'], reverse=True):
if v['mcnt']['total'] > 0:
self.items.append([sha256, '{}'.format(v['mcnt']['total']), '{}'.format(v['mcnt']['analyzed']), v['path']])
return True
def OnGetSize(self):
return len(self.items)
def OnGetLine(self, n):
return self.items[n]
def OnSelectLine(self, n):
sha256 = self.items[n][0]
c = FnCh("similarities with {}(snip)".format(sha256[:8]), self.res[sha256]['mfn'], self.res[sha256]['path'])
c.Show()
def OnRefresh(self, n):
return n
def OnClose(self):
print "closed ", self.title
class FnFuzzyForm(ida_kernwin.Form):
def __init__(self):
ida_kernwin.Form.__init__(self,
r"""BUTTON YES* Run
BUTTON CANCEL Cancel
fn_fuzzy
{FormChangeCb}
General Options
<DB file path:{iDBSave}>
<minimum function code size:{iMinBytes}>
<exclude library/thunk functions:{cLibthunk}>
<enable debug messages:{cDebug}>{cGroup}>
<##Commands##Export:{rExport}>
<Compare:{rCompare}>{rGroup}>
Export options
<update the DB records:{cUpdate}>
<store flags as analyzed functions:{cAnaExp}>{cEGroup}>
<analyzed function name prefix (regex):{iPrefix}>
Compare options
<compare with only analyzed functions:{cAnaCmp}>
<compare with only idbs in the specified folder:{cFolCmp}>{cCGroup}>
<the folder path:{iFolder}>
<function code size comparison criteria (0-100):{iRatio}>
<function similarity score threshold (0-100) without CFG match:{iSimilarity}>
<function similarity score threshold (0-100) with CFG match:{iSimilarityCFG}>
<function code size threshold evaluated by only CFG match:{iMaxBytesForScore}>
""",
{
'FormChangeCb': ida_kernwin.Form.FormChangeCb(self.OnFormChange),
'cGroup': ida_kernwin.Form.ChkGroupControl(("cLibthunk", "cDebug")),
'iDBSave': ida_kernwin.Form.FileInput(save=True),
'iMinBytes': ida_kernwin.Form.NumericInput(tp=ida_kernwin.Form.FT_HEX),
'rGroup': ida_kernwin.Form.RadGroupControl(("rCompare", "rExport")),
'cEGroup': ida_kernwin.Form.ChkGroupControl(("cUpdate", "cAnaExp")),
'iPrefix': ida_kernwin.Form.StringInput(),
'cCGroup': ida_kernwin.Form.ChkGroupControl(("cAnaCmp", "cFolCmp")),
'iFolder': ida_kernwin.Form.DirInput(),
'iRatio': ida_kernwin.Form.NumericInput(tp=ida_kernwin.Form.FT_DEC),
'iSimilarity': ida_kernwin.Form.NumericInput(tp=ida_kernwin.Form.FT_DEC),
'iSimilarityCFG': ida_kernwin.Form.NumericInput(tp=ida_kernwin.Form.FT_DEC),
'iMaxBytesForScore': ida_kernwin.Form.NumericInput(tp=ida_kernwin.Form.FT_HEX),
})
def OnFormChange(self, fid):
if fid == -1:
self.SetControlValue(self.cLibthunk, True)
self.SetControlValue(self.cAnaExp, True)
self.SetControlValue(self.cAnaCmp, True)
self.SetControlValue(self.rCompare, True)
self.EnableField(self.cEGroup, False)
self.EnableField(self.iPrefix, False)
self.EnableField(self.cCGroup, True)
self.EnableField(self.iSimilarity, True)
self.EnableField(self.iSimilarityCFG, True)
self.EnableField(self.iMaxBytesForScore, True)
self.EnableField(self.iRatio, True)
if fid == self.rExport.id:
self.EnableField(self.cEGroup, True)
self.EnableField(self.iPrefix, True)
self.EnableField(self.cCGroup, False)
self.EnableField(self.iSimilarity, False)
self.EnableField(self.iSimilarityCFG, False)
self.EnableField(self.iMaxBytesForScore, False)
self.EnableField(self.iRatio, False)
elif fid == self.rCompare.id:
self.EnableField(self.cEGroup, False)
self.EnableField(self.iPrefix, False)
self.EnableField(self.cCGroup, True)
self.EnableField(self.iSimilarity, True)
self.EnableField(self.iSimilarityCFG, True)
self.EnableField(self.iMaxBytesForScore, True)
self.EnableField(self.iRatio, True)
return 1
class FnFuzzy(object):
def __init__(self, f_debug, db_path, min_bytes, f_ex_libthunk, f_update, f_ana_exp, ana_pre, f_ana_cmp = False, f_fol_cmp = False, ana_fol='', threshold = None, threshold_cfg = None, max_bytes_for_score = None, ratio = 0):
self.f_debug = f_debug
self.conn = sqlite3.connect(db_path)
self.cur = self.conn.cursor()
self.init_db()
self.in_memory_db()
self.min_bytes = min_bytes
self.f_ex_libthunk = f_ex_libthunk
# for export
self.f_update = f_update
self.f_ana_exp = f_ana_exp
self.ana_pre = ana_pre
if f_ana_exp:
self.ana_pat = re.compile(self.ana_pre)
# for compare
self.f_ana_cmp = f_ana_cmp
self.f_fol_cmp = f_fol_cmp
self.ana_fol = ana_fol
self.threshold = threshold
self.threshold_cfg = threshold_cfg
self.max_bytes_for_score = max_bytes_for_score
self.ratio = float(ratio)
self.idb_path = get_idb_path()
self.sha256 = ida_nalt.retrieve_input_file_sha256().lower()
self.md5 = ida_nalt.retrieve_input_file_md5().lower()
def debug(self, msg):
if self.f_debug:
print "[D] {}".format(msg)
def init_db(self):
self.cur.execute("SELECT * FROM sqlite_master WHERE type='table'")
if self.cur.fetchone() is None:
info('DB initialized')
self.cur.execute("CREATE TABLE IF NOT EXISTS sample(sha256 UNIQUE, path)")
#self.cur.execute("CREATE INDEX sha256_index ON sample(sha256)")
self.cur.execute("CREATE INDEX path_index ON sample(path)")
self.cur.execute("CREATE TABLE IF NOT EXISTS function(sha256, fname, fhd, fhm, f_ana, bsize, ptype, UNIQUE(sha256, fname))")
self.cur.execute("CREATE INDEX f_ana_index ON function(f_ana)")
self.cur.execute("CREATE INDEX bsize_index ON function(bsize)")
def in_memory_db(self): # for SELECT
tempfile = StringIO()
for line in self.conn.iterdump():
tempfile.write("{}\n".format(line))
tempfile.seek(0)
self.mconn = sqlite3.connect(":memory:")
self.mconn.cursor().executescript(tempfile.read())
self.mconn.commit()
self.mconn.row_factory=sqlite3.Row
self.mcur = self.mconn.cursor()
def calc_fn_machoc(self, fva, fname): # based on Machoc hash implementation (https://github.com/0x00ach/idadiff)
func = idaapi.get_func(fva)
if type(func) == type(None):
self.debug('{}: ignored due to lack of function object'.format(fname))
return None, None
flow = idaapi.FlowChart(f=func)
cur_hash_rev = ""
addrIds = []
cur_id = 1
for c in range(0,flow.size):
cur_basic = flow.__getitem__(c)
cur_hash_rev += shex(cur_basic.startEA)+":"
addrIds.append((shex(cur_basic.startEA),str(cur_id)))
cur_id += 1
addr = cur_basic.startEA
blockEnd = cur_basic.endEA
mnem = GetMnem(addr)
while mnem != "":
if mnem == "call": # should be separated into 2 blocks by call
cur_hash_rev += "c,"
addr = NextHead(addr,blockEnd)
mnem = GetMnem(addr)
if addr != BADADDR:
cur_hash_rev += shex(addr)+";"+shex(addr)+":"
addrIds.append((shex(addr),str(cur_id)))
cur_id += 1
else:
addr = NextHead(addr,blockEnd)
mnem = GetMnem(addr)
refs = []
for suc in cur_basic.succs():
refs.append(suc.startEA)
refs.sort()
refsrev = ""
for ref in refs:
refsrev += shex(ref)+","
if refsrev != "":
refsrev = refsrev[:-1]
cur_hash_rev += refsrev+";"
# change addr to index
for aid in addrIds:
#cur_hash_rev = string.replace(cur_hash_rev,aid[0],aid[1])
cur_hash_rev = cur_hash_rev.replace(aid[0],aid[1])
# calculate machoc hash value
self.debug('{}: CFG = {}'.format(fname, cur_hash_rev))
return mmh3.hash(cur_hash_rev) & 0xFFFFFFFF, cur_id-1
def calc_fn_ssdeep(self, fva, fname):
d2h = ''
for bb in yara_fn.get_basic_blocks(fva):
rule = yara_fn.get_basic_block_rule(bb)
if rule:
chk = rule.cut_bytes_for_hash
if len(chk) < yara_fn.MIN_BB_BYTE_COUNT:
continue
d2h += chk
#self.debug('chunk at {:#x}: {}'.format(bb.va, get_hex_pat(chk)))
#self.debug('total func seq at {:#x}: {}'.format(fva, get_hex_pat(d2h)))
if len(d2h) < self.min_bytes:
self.debug('{}: ignored because of the number of extracted code bytes {}'.format(fname, len(d2h)))
return None, None
result_buffer = ctypes.create_string_buffer(FUZZY_MAX_RESULT)
file_buffer = ctypes.create_string_buffer(d2h)
hash_result = fuzzy_lib.fuzzy_hash_buf(file_buffer, len(file_buffer) - 1, result_buffer)
hash_value = result_buffer.value.decode("ascii")
return hash_value, len(d2h)
def existed(self):
self.mcur.execute("SELECT sha256 FROM sample WHERE sha256 = ?", (self.sha256,))
if self.mcur.fetchone() is None:
return False
else:
return True
def exclude_libthunk(self, fva, fname):
if self.f_ex_libthunk:
flags = get_func_attr(fva, FUNCATTR_FLAGS)
if flags & FUNC_LIB:
self.debug('{}: ignored because of library function'.format(fname))
return True
if flags & FUNC_THUNK:
self.debug('{}: ignored because of thunk function'.format(fname))
return True
return False
def export(self):
if self.existed() and not self.f_update:
info('{}: The sample records are present in DB. skipped.'.format(self.sha256))
return False
self.cur.execute("REPLACE INTO sample values(?, ?)", (self.sha256, self.idb_path))
pnum = tnum = 0
records = []
for fva in idautils.Functions():
fname = get_func_name(fva)
tnum += 1
if self.exclude_libthunk(fva, fname):
continue
fhd, bsize = self.calc_fn_ssdeep(fva, fname)
fhm, cfgnum = self.calc_fn_machoc(fva, fname)
if fhd and fhm:
pnum += 1
f_ana = bool(self.ana_pat.search(fname)) if self.f_ana_exp else False
tinfo = idaapi.tinfo_t()
idaapi.get_tinfo(fva, tinfo)
ptype = idaapi.print_tinfo('', 0, 0, idaapi.PRTYPE_1LINE, tinfo, fname, '')
ptype = ptype + ';' if ptype is not None else ptype
records.append((self.sha256, fname, fhd, fhm, f_ana, bsize, ptype))
self.debug('EXPORT {}: ssdeep={} (size={}), machoc={} (num of CFG={})'.format(fname, fhd, bsize, fhm, cfgnum))
self.cur.executemany("REPLACE INTO function values (?, ?, ?, ?, ?, ?, ?)", records)
success ('{} of {} functions exported'.format(pnum, tnum))
return True
def compare(self):
res = defaultdictRecurse()
if self.f_fol_cmp:
self.mcur.execute("SELECT sha256,path FROM sample WHERE path LIKE ?", (self.ana_fol+'%',))
else:
self.mcur.execute("SELECT sha256,path FROM sample")
frows = self.mcur.fetchall()
num_of_samples = len(frows)
for sha256, path in frows:
res[sha256]['path'] = path
res[sha256]['mcnt'].default_factory = lambda: 0
#sql = "SELECT sha256,fname,fhd,fhm,f_ana,ptype FROM function WHERE f_ana == 1 AND bsize BETWEEN ? AND ?" if self.f_ana_cmp else "SELECT sha256,fname,fhd,fhm,f_ana,ptype FROM function WHERE bsize BETWEEN ? AND ?"
sql = "SELECT function.sha256,fname,fhd,fhm,f_ana,ptype FROM function INNER JOIN sample on function.sha256 == sample.sha256 WHERE path LIKE ? AND " if self.f_fol_cmp else "SELECT sha256,fname,fhd,fhm,f_ana,ptype FROM function WHERE "
sql += "f_ana == 1 AND bsize BETWEEN ? AND ?" if self.f_ana_cmp else "bsize BETWEEN ? AND ?"
for fva in idautils.Functions():
fname = get_func_name(fva)
if self.exclude_libthunk(fva, fname) or not num_of_samples:
continue
pfhd, pbsize = self.calc_fn_ssdeep(fva, fname)
pfhm, pcfgnum = self.calc_fn_machoc(fva, fname)
if pfhd and pfhm:
pbuf = ctypes.create_string_buffer(pfhd)
self.debug('COMPARE {}: ssdeep={} (size={}), machoc={} (num of bb={})'.format(fname, pfhd, pbsize, pfhm, pcfgnum))
min_ = pbsize * (1 - (self.ratio / 100))
max_ = pbsize * (1 + (self.ratio / 100))
self.debug('min={}, max={}'.format(min_, max_))
if self.f_fol_cmp:
self.mcur.execute(sql, (self.ana_fol+'%', min_, max_))
else:
self.mcur.execute(sql, (min_, max_))
frows = self.mcur.fetchall()
self.debug('targeted {} records'.format(len(frows)))
for sha256, sfname, sfhd, sfhm, sf_ana, sptype in frows:
if sha256 == self.sha256: # skip the self
continue
res[sha256]['mfn'][fva].default_factory = lambda: 0
sbuf = ctypes.create_string_buffer(sfhd)
score = fuzzy_lib.fuzzy_compare(pbuf, sbuf)
if g_dbg_flag and fva == g_dbg_fva and sfname == g_dbg_fname and sha256 == g_dbg_sha256:
self.debug('{:#x}: compared with {} in {} score = {} machoc match = {}'.format(fva, sfname, sha256, score, bool(pfhm == sfhm)))
if (score >= self.threshold) or (score >= self.threshold_cfg and pfhm == sfhm) or (pbsize > self.max_bytes_for_score and pfhm == sfhm):
res[sha256]['mcnt']['total'] += 1
if sf_ana:
res[sha256]['mcnt']['analyzed'] += 1
if score > res[sha256]['mfn'][fva]['score'] or (res[sha256]['mfn'][fva]['score'] == 0 and pbsize > self.max_bytes_for_score):
res[sha256]['mfn'][fva]['score'] = score
res[sha256]['mfn'][fva]['cfg_match'] = bool(pfhm == sfhm)
res[sha256]['mfn'][fva]['sfname'] = sfname
res[sha256]['mfn'][fva]['sptype'] = sptype
res[sha256]['mfn'][fva]['pbsize'] = pbsize
c = SummaryCh("fn_fuzzy summary", res)
c.Show()
success('totally {} samples compared'.format(num_of_samples))
def close(self):
self.conn.commit()
self.cur.close()
def info(msg):
print "[*] {}".format(msg)
def success(msg):
print "[+] {}".format(msg)
def error(msg):
print "[!] {}".format(msg)
def get_hex_pat(buf):
# get hex pattern
return ' '.join(['{:02x}'.format(ord(x)) for x in buf])
def shex(a):
return hex(a).rstrip("L")
def set_decomplier_cmt(ea, cmt):
cfunc = idaapi.decompile(ea)
tl = idaapi.treeloc_t()
tl.ea = ea
tl.itp = idaapi.ITP_SEMI
if cfunc:
cfunc.set_user_cmt(tl, cmt)
cfunc.save_user_cmts()
else:
error("Decompile failed: {:#x}".formart(ea))
def main():
info('start')
if idaapi.get_plugin_options("fn_fuzzy"): # CLI (export only)
start = time.time()
options = idaapi.get_plugin_options("fn_fuzzy").split(':')
#print options
min_bytes = int(options[0])
f_ex_libthunk = eval(options[1])
f_update = eval(options[2])
f_ana_exp = eval(options[3])
ana_pre = options[4]
db_path = ':'.join(options[5:])
ff = FnFuzzy(False, db_path, min_bytes, f_ex_libthunk, f_update, f_ana_exp, ana_pre)
res = ff.export()
ff.close()
elapsed = time.time() - start
info('done (CLI)')
if res: # return code 1 is reserved for error
qexit(0)
else:
qexit(2) # already exported (skipped)
else:
f = FnFuzzyForm()
f.Compile()
f.iDBSave.value = g_db_path
f.iMinBytes.value = g_min_bytes
f.iPrefix.value = g_analyzed_prefix
f.iFolder.value = os.path.dirname(get_idb_path())
f.iSimilarity.value = g_threshold
f.iSimilarityCFG.value = g_threshold_cfg
f.iMaxBytesForScore.value = g_max_bytes_for_score
f.iRatio.value = g_bsize_ratio
r = f.Execute()
if r == 1: # Run
start = time.time()
ff = FnFuzzy(f.cDebug.checked, f.iDBSave.value, f.iMinBytes.value, f.cLibthunk.checked, f.cUpdate.checked, f.cAnaExp.checked, f.iPrefix.value, f.cAnaCmp.checked, f.cFolCmp.checked, f.iFolder.value, f.iSimilarity.value, f.iSimilarityCFG.value, f.iMaxBytesForScore.value, f.iRatio.value)
if f.rExport.selected:
ff.export()
#cProfile.runctx('ff.export()', None, locals())
else:
ff.compare()
#cProfile.runctx('ff.compare()', None, locals())
ff.close()
elapsed = time.time() - start
else:
print 'cancel'
return
info('elapsed time = {} sec'.format(elapsed))
info('done')
if __name__ == '__main__':
main()

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

View File

@ -0,0 +1,288 @@
'''
IDAPython script that generates a YARA rule to match against the
basic blocks of the current function. It masks out relocation bytes
and ignores jump instructions (given that we're already trying to
match compiler-specific bytes, this is of arguable benefit).
If python-yara is installed, the IDAPython script also validates that
the generated rule matches at least one segment in the current file.
author: Willi Ballenthin <william.ballenthin@fireeye.com>
'''
# 2018/8/6 Takahiro Haruyama modified to calculate fixup (relocation) size correctly
# and exclude direct memory reference data and other ignorable variable code
import logging
from collections import namedtuple
from idc import *
import idaapi
import idautils
import ida_ua, ida_kernwin
logger = logging.getLogger(__name__)
BasicBlock = namedtuple('BasicBlock', ['va', 'size'])
# each rule must have at least this many non-masked bytes
MIN_BB_BYTE_COUNT = 4
def get_basic_blocks(fva):
'''
return sequence of `BasicBlock` instances for given function.
'''
ret = []
func = idaapi.get_func(fva)
if func is None:
return ret
for bb in idaapi.FlowChart(func):
ret.append(BasicBlock(va=bb.startEA,
size=bb.endEA - bb.startEA))
return ret
def get_function(va):
'''
return va for first instruction in function that contains given va.
'''
return idaapi.get_func(va).startEA
Rule = namedtuple('Rule', ['name', 'bytes', 'masked_bytes', 'cut_bytes_for_hash'])
def is_jump(va):
'''
return True if the instruction at the given address appears to be a jump.
'''
return GetMnem(va).startswith('j')
def get_fixup_va_and_size(va):
fva = idaapi.get_next_fixup_ea(va)
ftype = get_fixup_target_type(fva)
fsize = ida_fixup.calc_fixup_size(ftype)
return fva, fsize
def get_basic_block_rule(bb):
'''
create and format a YARA rule for a single basic block.
The following bytes are ignored:
- relocation bytes
- the last jump instruction
- direct memory references / immediate values and other igorable data
'''
# fetch the instruction start addresses
insns = []
va = bb.va
while va < bb.va + bb.size:
insns.append(va)
va = NextHead(va)
# drop the last instruction if its a jump
if insns and is_jump(insns[-1]):
insns = insns[:-1]
_bytes = []
# `masked_bytes` is the list of formatted bytes,
# not yet join'd for performance.
masked_bytes = []
cut_bytes_for_hash = ''
for va in insns:
insn = ida_ua.insn_t()
size = ida_ua.decode_insn(insn, va)
mnem = insn.get_canon_mnem()
op1 = insn.Op1
op2 = insn.Op2
fixup_byte_addrs = set([])
if idaapi.contains_fixups(va, size): # not work for x64 binaries? (e.g., idaapi.contains_fixups(here(), 0x2d000) -> False)
logging.debug('ea = {:#x}, fixups'.format(va))
# fetch the fixup locations and sizes within this one instruction.
fixups = []
fva, fsize = get_fixup_va_and_size(va)
fixups.append((fva, fsize))
fva += fsize
while fva < va + size:
fva, fsize = get_fixup_va_and_size(fva - 1) # to detect consecutive fixups
fixups.append((fva, fsize))
fva += fsize
logging.debug('fixups: {}'.format(fixups))
# compute the addresses of each component byte.
for fva, fsize in fixups:
for i in range(fva, fva+fsize):
fixup_byte_addrs.add(i)
# fetch and format each byte of the instruction,
# possibly masking it into an unknown byte if its a fixup or several operand types like direct mem ref.
masked_types = [o_mem, o_imm, o_displ, o_near, o_far]
#masked_types = [o_mem, o_imm, o_near, o_far]
bytes_ = get_bytes(va, size)
if bytes_ is None:
return None
for i, byte in enumerate(bytes_):
_bytes.append(ord(byte))
byte_addr = i + va
if byte_addr in fixup_byte_addrs:
logging.debug('{:#x}: fixup byte (masked)'.format(byte_addr))
masked_bytes.append('??')
elif op1.type in masked_types and i >= op1.offb and (i < op2.offb or op2.offb == 0):
logging.debug('{:#x}: Op1 masked byte'.format(byte_addr))
masked_bytes.append('??')
elif op2.type in masked_types and i >= op2.offb:
logging.debug('{:#x}: Op2 masked byte'.format(byte_addr))
masked_bytes.append('??')
else:
masked_bytes.append('%02X' % (ord(byte)))
cut_bytes_for_hash += byte
return Rule('$0x%x' % (bb.va), _bytes, masked_bytes, cut_bytes_for_hash)
def format_rules(fva, rules):
'''
given the address of a function, and the byte signatures for basic blocks in
the function, format a complete YARA rule that matches all of the
basic block signatures.
'''
name = GetFunctionName(fva)
if not rules:
logging.info('no rules for {}'.format(name))
return None
# some characters aren't valid for YARA rule names
safe_name = name
BAD_CHARS = '@ /\\!@#$%^&*()[]{};:\'",./<>?'
for c in BAD_CHARS:
safe_name = safe_name.replace(c, '')
md5 = idautils.GetInputFileMD5()
ret = []
ret.append('rule a_{hash:s}_{name:s} {{'.format(
hash=md5,
name=safe_name))
ret.append(' meta:')
ret.append(' sample_md5 = "{md5:s}"'.format(md5=md5))
ret.append(' function_address = "0x{fva:x}"'.format(fva=fva))
ret.append(' function_name = "{name:s}"'.format(name=name))
ret.append(' strings:')
for rule in rules:
formatted_rule = ' '.join(rule.masked_bytes).rstrip('?? ')
ret.append(' {name:s} = {{ {hex:s} }}'.format(
name=rule.name,
hex=formatted_rule))
ret.append(' condition:')
ret.append(' all of them')
ret.append('}')
return '\n'.join(ret)
def create_yara_rule_for_function(fva):
'''
given the address of a function, generate and format a complete YARA rule
that matches the basic blocks.
'''
rules = []
for bb in get_basic_blocks(fva):
rule = get_basic_block_rule(bb)
if rule:
# ensure there at least MIN_BB_BYTE_COUNT
# non-masked bytes in the rule, or ignore it.
# this will reduce the incidence of many very small matches.
unmasked_count = len(filter(lambda b: b != '??', rule.masked_bytes))
if unmasked_count < MIN_BB_BYTE_COUNT:
continue
rules.append(rule)
return format_rules(fva, rules)
def get_segment_buffer(segstart):
'''
fetch the bytes of the section that starts at the given address.
if the entire section cannot be accessed, try smaller regions until it works.
'''
segend = idaapi.getseg(segstart).endEA
buf = None
segsize = segend - segstart
while buf is None and segsize > 0:
buf = GetManyBytes(segstart, segsize)
if buf is None:
segsize -= 0x1000
return buf
Segment = namedtuple('Segment', ['start', 'size', 'name', 'buf'])
def get_segments():
'''
fetch the segments in the current executable.
'''
for segstart in idautils.Segments():
segend = idaapi.getseg(segstart).endEA
segsize = segend - segstart
segname = str(SegName(segstart)).rstrip('\x00')
segbuf = get_segment_buffer(segstart)
yield Segment(segstart, segend, segname, segbuf)
class TestDidntRunError(Exception):
pass
def test_yara_rule(rule):
'''
try to match the given rule against each segment in the current exectuable.
raise TestDidntRunError if its not possible to import the YARA library.
return True if there's at least one match, False otherwise.
'''
try:
import yara
except ImportError:
logger.warning("can't test rule: failed to import python-yara")
raise TestDidntRunError('python-yara not available')
r = yara.compile(source=rule)
for segment in get_segments():
if segment.buf is not None:
matches = r.match(data=segment.buf)
if len(matches) > 0:
logger.info('generated rule matches section: {:s}'.format(segment.name))
return True
return False
def main():
print 'Start'
ans = ida_kernwin.ask_yn(0, 'define only selected function?')
if ans:
va = ScreenEA()
fva = get_function(va)
print('-' * 80)
rule = create_yara_rule_for_function(fva)
if rule:
print(rule)
if test_yara_rule(rule):
logging.info('success: validated the generated rule')
else:
logging.error('error: failed to validate generated rule')
else:
for fva in idautils.Functions():
print('-' * 80)
rule = create_yara_rule_for_function(fva)
if rule:
print(rule)
print 'Done'
if __name__ == '__main__':
logging.basicConfig(level=logging.INFO)
logging.getLogger().setLevel(logging.INFO)
#logging.basicConfig(level=logging.DEBUG)
#logging.getLogger().setLevel(logging.DEBUG)
main()

View File

@ -0,0 +1,20 @@
IOC 549726b8bfb1919a343ac764d48fdc81 SedUploader payload, compiled on 2018-11-21 ebdc6098c733b23e99daa60e55cf858b SedUploader payload, compiled on 2018-12-07 70213367847c201f65fed99dbe7545d2 SedUploader payload, compiled on 2018-12-07 c4601c1aa03d83ec11333d7400a2bbaf SedUploader payload, compiled on 2019-01-28 a13c864980159cd9bdc94074b2389dda Zebrocy downloader type 1 (.NET), compiled on 2018-11-13 f05a7cc3656c9467d38d54e037c24391 Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-06 7e67122d3a052e4755b02965e2e56a2e Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-15 ed80d716ddea1dca2ef4c464a8cb5810 Zebrocy downloader type 2 (Delphi), compiled on 2018-11-13 ea5722ed66bd75871e24f7f88c5133aa Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-10-18 fdbfceec5b3d2e855feb036c4e96e9aa Zebrocy downloader type 2 (Delphi), compiled on 2018-10-23 f4cab3a393462a57639faa978a75d10a Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-16 5415b299f969c62174a624d236a56f42 Zebrocy downloader type 2 (Delphi), compiled on 2018-11-13 e57a401e8f0943b703d975692fcfc0e8 Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-28 a4d63973c0e60936f72aed3d391fd461 Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-29 1fe6af243760ca287f80eafbb98ba1b0 Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-29 3eaf97b9c6b44f0447f2bd1c7acb8c96 Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-12-10 3e713a838a68259ae2f9ef2eed05a761 Zebrocy downloader, VT 1st seen on 2019-01-07 f1aeaf72995b12d5edd3971ccbc38fac Zebrocy downloader, VT 1st seen on 2019-01-24 b68434af08360e6cf7a51d623195caa1
Zebrocy downloader, VT 1st seen on 2019-01-24 896ed83884181517a002d2cf73548448
Zebrocy downloader, VT 1st seen on 2019-02-02 53ae587757eb9b4afa4c4ca9f238ade6
Zebrocy downloader, VT 1st seen on 2019-02-04 268426b91d3f455ec7ef4558c4a4dfd1
Zebrocy downloader type 1 (.NET), compiled on 2018-10-23 2b16b0f552ea6973fce06862c91ee8a9
Zebrocy downloader type 1 (.NET), compiled on 2018-10-25 9a7d82ba55216defc2d4131b6c453f02
Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-24 02c46f30f4c68a442cf7e13bebe8d3f8
Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-11-30 d6a60c6455f3937735ce2df82ad83627
Zebrocy downloader type 1 (Delphi), VT 1st seen on 2018-12-01 9ae5e57d8c40f72a508475f19c0a42f6
Zebrocy downloader type 1 (Delphi), VT 1st seen on 2019-01-24 333d2b9e99b36fb42f9e79a2833fad9c
Zebrocy downloader type 1 (Go), VT 1st seen on 2018-12-20 602d2901d55c2720f955503456ac2f68
Zebrocy downloader type 1 (Go), VT 1st seen on 2018-12-04 3773150aeee03783a6da0820a8feb752
Zebrocy downloader type 2 (Go), VT 1st seen on 2018-12-04
SedUploader C2
beatguitar.com
photopoststories.com
wmdmediacodecs.com
Zebrocy downloader: hxxp://109.248.148.42/agr-enum/progress-inform/cube.php hxxp://188.241.58.170/local/s3/filters.php hxxps://91.219.238.118/zx-system/core/main-config.php hxxp://185.203.118.198/en_action_device/center_correct_customer/drivers-i7-x86.php hxxps://109.248.148.22/orders/create/new.phpZebrocy downloader C2 hxxp://185.217.92.119/db-module/version_1594/main.php hxxp://93.113.131.155/Verifica-El-Lanzamiento/Ayuda-Del-Sistema/obtenerId.phpZebrocy downloader C2 hxxp://45.124.132.127/action-center/centerforserviceandaction/service-and-action.php hxxp://45.124.132.127/company-device-support/values/correlate-sec.phpZebrocy downloader C2 hxxp://86.106.131.177/SupportA91i/syshelpA774i/viewsupp.php hxxp://89.37.226.148/technet-support/library/online-service-description.php hxxp://145.249.105.165/resource-store/stockroom-center-service/check.php hxxp://89.37.226.148/technet-support/library/online-service-description.php hxxp://89.37.226.123/advance/portable_version/service.php hxxps://190.97.167.186/pkg/image/do.php

12
APT28/REALTED_REPORT.md Normal file
View File

@ -0,0 +1,12 @@
https://www.welivesecurity.com/wp-content/uploads/2016/10/eset-sednit-full.pdf
https://www.welivesecurity.com/2017/05/09/sednit-adds-two-zero-day-exploits-using-trumps-attack-syria-decoy/
https://www.emanueledelucia.net/apt28-targeting-military-institutions/
https://www.emanueledelucia.net/apt28-sofacy-seduploader-under-the-christmas-tree/
https://unit42.paloaltonetworks.com/unit42-sofacy-continues-global-attacks-wheels-new-cannon-trojan/
https://unit42.paloaltonetworks.com/dear-joohn-sofacy-groups-global-campaign/
https://unit42.paloaltonetworks.com/sofacy-creates-new-go-variant-of-zebrocy-tool/
https://blog.trendmicro.co.jp/archives/19829
https://www.welivesecurity.com/2018/11/20/sednit-whats-going-zebrocy/
https://twitter.com/DrunkBinary
https://github.com/williballenthin/idawilli/blob/master/scripts/yara_fn/yara_fn.py
https://twitter.com/r0ny_123

View File

@ -0,0 +1,168 @@
# 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()