mirror of
https://github.com/blackorbird/APT_REPORT.git
synced 2025-05-05 10:17:49 +00:00
11
This commit is contained in:
parent
81a71cac01
commit
105c56463c
30
APT-hunting/fn_fuzzy/README.org
Normal file
30
APT-hunting/fn_fuzzy/README.org
Normal 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]]
|
||||
|
152
APT-hunting/fn_fuzzy/cli_export.py
Normal file
152
APT-hunting/fn_fuzzy/cli_export.py
Normal 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()
|
9
APT-hunting/fn_fuzzy/dump_types.py
Normal file
9
APT-hunting/fn_fuzzy/dump_types.py
Normal 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()
|
602
APT-hunting/fn_fuzzy/fn_fuzzy.py
Normal file
602
APT-hunting/fn_fuzzy/fn_fuzzy.py
Normal 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()
|
||||
|
||||
|
||||
|
BIN
APT-hunting/fn_fuzzy/fuzzy64.dll
Normal file
BIN
APT-hunting/fn_fuzzy/fuzzy64.dll
Normal file
Binary file not shown.
BIN
APT-hunting/fn_fuzzy/img/fn_fuzzy.png
Normal file
BIN
APT-hunting/fn_fuzzy/img/fn_fuzzy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 107 KiB |
BIN
APT-hunting/fn_fuzzy/img/res_funcs.png
Normal file
BIN
APT-hunting/fn_fuzzy/img/res_funcs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 206 KiB |
BIN
APT-hunting/fn_fuzzy/img/res_summary.png
Normal file
BIN
APT-hunting/fn_fuzzy/img/res_summary.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 318 KiB |
288
APT-hunting/fn_fuzzy/yara_fn.py
Normal file
288
APT-hunting/fn_fuzzy/yara_fn.py
Normal 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()
|
20
APT28/IOC/2019-04-05-ioc-mark.txt
Normal file
20
APT28/IOC/2019-04-05-ioc-mark.txt
Normal 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
12
APT28/REALTED_REPORT.md
Normal 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
|
168
APT28/decode/zebrocy_decrypt_artifact.py
Normal file
168
APT28/decode/zebrocy_decrypt_artifact.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user