diff --git a/APT-hunting/fn_fuzzy/README.org b/APT-hunting/fn_fuzzy/README.org new file mode 100644 index 0000000..32f26c8 --- /dev/null +++ b/APT-hunting/fn_fuzzy/README.org @@ -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]] + diff --git a/APT-hunting/fn_fuzzy/cli_export.py b/APT-hunting/fn_fuzzy/cli_export.py new file mode 100644 index 0000000..28f50f4 --- /dev/null +++ b/APT-hunting/fn_fuzzy/cli_export.py @@ -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() diff --git a/APT-hunting/fn_fuzzy/dump_types.py b/APT-hunting/fn_fuzzy/dump_types.py new file mode 100644 index 0000000..daa9e0f --- /dev/null +++ b/APT-hunting/fn_fuzzy/dump_types.py @@ -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() diff --git a/APT-hunting/fn_fuzzy/fn_fuzzy.py b/APT-hunting/fn_fuzzy/fn_fuzzy.py new file mode 100644 index 0000000..3d0e895 --- /dev/null +++ b/APT-hunting/fn_fuzzy/fn_fuzzy.py @@ -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 + + + +{cGroup}> + +<##Commands##Export:{rExport}> +{rGroup}> + +Export options + +{cEGroup}> + + +Compare options + +{cCGroup}> + + + + + +""", + { + '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() + + + diff --git a/APT-hunting/fn_fuzzy/fuzzy64.dll b/APT-hunting/fn_fuzzy/fuzzy64.dll new file mode 100644 index 0000000..c559d1c Binary files /dev/null and b/APT-hunting/fn_fuzzy/fuzzy64.dll differ diff --git a/APT-hunting/fn_fuzzy/img/fn_fuzzy.png b/APT-hunting/fn_fuzzy/img/fn_fuzzy.png new file mode 100644 index 0000000..21efcca Binary files /dev/null and b/APT-hunting/fn_fuzzy/img/fn_fuzzy.png differ diff --git a/APT-hunting/fn_fuzzy/img/res_funcs.png b/APT-hunting/fn_fuzzy/img/res_funcs.png new file mode 100644 index 0000000..f3a8bb9 Binary files /dev/null and b/APT-hunting/fn_fuzzy/img/res_funcs.png differ diff --git a/APT-hunting/fn_fuzzy/img/res_summary.png b/APT-hunting/fn_fuzzy/img/res_summary.png new file mode 100644 index 0000000..8a59919 Binary files /dev/null and b/APT-hunting/fn_fuzzy/img/res_summary.png differ diff --git a/APT-hunting/fn_fuzzy/yara_fn.py b/APT-hunting/fn_fuzzy/yara_fn.py new file mode 100644 index 0000000..6bc4be1 --- /dev/null +++ b/APT-hunting/fn_fuzzy/yara_fn.py @@ -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 +''' +# 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() diff --git a/APT28/IOC/2019-04-05-ioc-mark.txt b/APT28/IOC/2019-04-05-ioc-mark.txt new file mode 100644 index 0000000..0a28bac --- /dev/null +++ b/APT28/IOC/2019-04-05-ioc-mark.txt @@ -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 \ No newline at end of file diff --git a/APT28/REALTED_REPORT.md b/APT28/REALTED_REPORT.md new file mode 100644 index 0000000..c166d51 --- /dev/null +++ b/APT28/REALTED_REPORT.md @@ -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 \ No newline at end of file diff --git a/APT28/decode/zebrocy_decrypt_artifact.py b/APT28/decode/zebrocy_decrypt_artifact.py new file mode 100644 index 0000000..b6a1239 --- /dev/null +++ b/APT28/decode/zebrocy_decrypt_artifact.py @@ -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(" 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() diff --git a/APT28/tau_fancybear_downloader_public.yara b/APT28/yara/tau_fancybear_downloader_public.yara similarity index 100% rename from APT28/tau_fancybear_downloader_public.yara rename to APT28/yara/tau_fancybear_downloader_public.yara