From 50b66d026434c0a330f862d478e0e516cb1915eb Mon Sep 17 00:00:00 2001 From: "elias.bachaalany" Date: Mon, 9 May 2011 10:21:32 +0000 Subject: [PATCH] - added VirusTotal script. Check http://www.hexblog.com/?p=324 --- Scripts/3rd/BboeVt.py | 156 ++++++++++++++++++ Scripts/VirusTotal.py | 367 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 523 insertions(+) create mode 100644 Scripts/3rd/BboeVt.py create mode 100644 Scripts/VirusTotal.py diff --git a/Scripts/3rd/BboeVt.py b/Scripts/3rd/BboeVt.py new file mode 100644 index 0000000..8ae9f5a --- /dev/null +++ b/Scripts/3rd/BboeVt.py @@ -0,0 +1,156 @@ +""" +Original code by Bryce Boe: http://www.bryceboe.com/2010/09/01/submitting-binaries-to-virustotal/ + +Modified by Elias Bachaalany + +""" + +import hashlib, httplib, mimetypes, os, pprint, simplejson, sys, urlparse + +# ----------------------------------------------------------------------- +DEFAULT_TYPE = 'application/octet-stream' +FILE_REPORT_URL = 'https://www.virustotal.com/api/get_file_report.json' +SCAN_URL = 'https://www.virustotal.com/api/scan_file.json' +API_KEY = "" # Put API key here. Register an account in VT Community + + +# ----------------------------------------------------------------------- +# The following function is modified from the snippet at: +# http://code.activestate.com/recipes/146306/ +def _encode_multipart_formdata(fields, files=()): + """ + fields is a dictionary of name to value for regular form fields. + files is a sequence of (name, filename, value) elements for data to be + uploaded as files. + Return (content_type, body) ready for httplib.HTTP instance + """ + BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$' + CRLF = '\r\n' + L = [] + for key, value in fields.items(): + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"' % key) + L.append('') + L.append(value) + for (key, filename, value) in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % + (key, filename)) + content_type = mimetypes.guess_type(filename)[0] or DEFAULT_TYPE + L.append('Content-Type: %s' % content_type) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = CRLF.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + +# ----------------------------------------------------------------------- +def _post_multipart(url, fields, files=()): + """ + url is the full to send the post request to. + fields is a dictionary of name to value for regular form fields. + files is a sequence of (name, filename, value) elements for data to be + uploaded as files. + Return body of http response. + """ + content_type, data = _encode_multipart_formdata(fields, files) + url_parts = urlparse.urlparse(url) + if url_parts.scheme == 'http': + h = httplib.HTTPConnection(url_parts.netloc) + elif url_parts.scheme == 'https': + h = httplib.HTTPSConnection(url_parts.netloc) + else: + raise Exception('Unsupported URL scheme') + path = urlparse.urlunparse(('', '') + url_parts[2:]) + h.request('POST', path, data, {'content-type':content_type}) + return h.getresponse().read() + + +# ----------------------------------------------------------------------- +def set_apikey(key, dbg = False): + """ + Set the VT API key + """ + global API_KEY + API_KEY = key + if dbg: + httplib.HTTPConnection.debuglevel = 1 + + + +# ----------------------------------------------------------------------- +def scan_file(filename): + """ + Uploads a file for scanning. + + @param filename: The filename to upload + + @return: - None if upload failed + - scan_id value if upload succeeds + - raises an exception on IO failures + """ + files = [('file', filename, open(filename, 'rb').read())] + json = _post_multipart(SCAN_URL, {'key':API_KEY}, files) + data = simplejson.loads(json) + return str(data['scan_id']) if data['result'] == 1 else None + + +# ----------------------------------------------------------------------- +def get_file_md5_hash(filename): + f = open(filename, 'rb') + r = hashlib.md5(f.read()).hexdigest() + f.close() + return r + + +# ----------------------------------------------------------------------- +def get_file_report(filename=None, md5sum=None): + """ + Returns an report for a file or md5su. + + @param filename: File name to get report. The file is used just + to compute its MD5Sum + @param md5sum: MD5sum string (in case filename was not passed) + + @return: - None: if file was not previously analyzed + - A dictionary if report exists: key=scanner, value=reported name + """ + if filename is None and md5sum is None: + raise Exception('Either filename or md5sum should be passed!') + + # Filename passed? Compute its MD5 + if filename: + global LAST_FILE_HASH + LAST_FILE_HASH = md5sum = get_file_md5_hash(filename) + + # Form the request + json = _post_multipart(FILE_REPORT_URL, {'resource':md5sum, 'key':API_KEY}) + data = simplejson.loads(json) + if data['result'] != 1: + # No results + return None + else: + # date, result_dict = data['report'] + return data['report'][1] + + +# ----------------------------------------------------------------------- +def pretty_print(obj): + pprint.pprint(obj) + + +# ----------------------------------------------------------------------- +if __name__ == '__main__': + if len(sys.argv) != 2: + print('Usage: %s filename' % sys.argv[0]) + sys.exit(1) + + filename = sys.argv[1] + if not os.path.isfile(filename): + print('%s is not a valid file' % filename) + sys.exit(1) + + get_file_report(filename=filename) \ No newline at end of file diff --git a/Scripts/VirusTotal.py b/Scripts/VirusTotal.py new file mode 100644 index 0000000..0b9af4f --- /dev/null +++ b/Scripts/VirusTotal.py @@ -0,0 +1,367 @@ +# ----------------------------------------------------------------------- +# VirusTotal IDA Plugin +# By Elias Bachaalany +# (c) Hex-Rays 2011 +# +# Special thanks: +# - VirusTotal team +# - Bryce Boe for his VirusTotal Python code +# +import idaapi +import idc +from idaapi import Choose2, plugin_t +import BboeVt as vt +import webbrowser +import urllib +import os + + +PLUGIN_TEST = 0 + +# ----------------------------------------------------------------------- +# Configuration file +VT_CFGFILE = idaapi.get_user_idadir() + os.sep + "virustotal.cfg" + +# ----------------------------------------------------------------------- +# VirusTotal Icon in PNG format +VT_ICON = ( + "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D\x49\x48\x44\x52" + "\x00\x00\x00\x10\x00\x00\x00\x10\x04\x03\x00\x00\x00\xED\xDD\xE2" + "\x52\x00\x00\x00\x30\x50\x4C\x54\x45\x03\x8B\xD3\x5C\xB4\xE3\x9C" + "\xD1\xED\xF7\xFB\xFD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\xD3\xF2\x42\x61\x00\x00\x00" + "\x4B\x49\x44\x41\x54\x78\x9C\x2D\xCA\xC1\x0D\x80\x30\x0C\x43\x51" + "\x27\x2C\x50\x89\x05\x40\x2C\x40\xEB\xFD\x77\xC3\x76\xC9\xE9\xEB" + "\xC5\x20\x5F\xE8\x1A\x0F\x97\xA3\xD0\xE4\x1D\xF9\x49\xD1\x59\x29" + "\x4C\x43\x9B\xD0\x15\x01\xB5\x4A\x9C\xE4\x70\x14\x39\xB3\x31\xF8" + "\x15\x70\x04\xF4\xDA\x20\x39\x02\x8A\x0D\xA8\x0F\x94\xA7\x09\x0E" + "\xC5\x16\x2D\x54\x00\x00\x00\x00\x49\x45\x4E\x44\xAE\x42\x60\x82") + + +# ----------------------------------------------------------------------- +class VirusTotalConfig(object): + def __init__(self): + self.Default() + + + def Default(self): + self.md5sum = GetInputMD5() + self.infile = idaapi.dbg_get_input_path() + if not self.infile: + self.infile = "" + + # Persistent options + self.apikey = "" + self.options = 1 | 2 + + + def Read(self): + """ + Read configuration from file + """ + if not os.path.exists(VT_CFGFILE): + return + f = open(VT_CFGFILE, 'r') + lines = f.readlines() + for i, line in enumerate(lines): + line = line.strip() + if i == 0: + self.apikey = line + elif i == 1: + self.options = int(line) + else: + break + + + def Write(self): + """ + Write configuration to file + """ + lines = (self.apikey.strip(), str(self.options)) + try: + f = open(VT_CFGFILE, 'w') + f.write("\n".join(lines)) + f.close() + except: + pass + + +# ----------------------------------------------------------------------- +def VtReport(apikey, filename=None, md5sum=None): + if filename is None and md5sum is None: + return (False, "No parameters passed!") + + # Check filename existance + if filename is not None and not os.path.exists(filename): + return (False, "Input file '%s' does not exist!" % filename) + + #print("fn=%s md5=%s" % (filename, md5sum)) + # Get file report from VirusTotal + try: + vt.set_apikey(apikey) + result = vt.get_file_report(filename=filename, md5sum=md5sum) + except Exception as e: + return (False, "Exception:\n%s" % str(e)) + + # Already analyzed? + if result is not None: + # Transform the results + items = [] + for av, mwname in result.items(): + mwname = str(mwname) if mwname else "n/a" + av = str(av) + items.append([av, mwname]) + result = items + + return (True, result) + + +# ----------------------------------------------------------------------- +class VirusTotalChooser(Choose2): + """ + Chooser class to display results from VT + """ + def __init__(self, title, items, icon, embedded=False): + Choose2.__init__(self, + title, + [ ["Antivirus", 20], ["Result", 40] ], + embedded=embedded) + self.items = items + self.icon = icon + + + def GetItems(self): + return self.items + + + def SetItems(self, items): + self.items = [] if items is None else items + + + def OnClose(self): + pass + + + def OnGetLine(self, n): + return self.items[n] + + + def OnGetSize(self): + return len(self.items) + + + def OnSelectLine(self, n): + # Google search for the malware name and the antivirus name + s = urllib.urlencode({"q" : " ".join(self.items[n])}) + webbrowser.open_new_tab("http://www.google.com/search?%s" % s) + + +# -------------------------------------------------------------------------- +class VirusTotalForm(Form): + def __init__(self, icon): + self.EChooser = VirusTotalChooser("E1", [], icon, embedded=True) + Form.__init__(self, r"""STARTITEM {id:txtInput} +VirusTotal - IDAPython plugin v1.0 (c) Hex-Rays + +{FormChangeCb} +<#API key#~A~pi key:{txtApiKey}> + +Options: +<#Open results in a chooser when form closes#~P~opout results on close:{rOptRemember}> +<#Use MD5 checksum#~M~D5Sum:{rOptMD5}> +<#Use file on disk#~F~ile:{rOptFile}>{grpOptions}> + +<#Type input (file or MD5 string)#~I~nput:{txtInput}> + +<#Get reports from VT#~R~eport:{btnReport}> +""", { + 'FormChangeCb': Form.FormChangeCb(self.OnFormChange), + 'txtApiKey' : Form.StringInput(swidth=80), + 'grpOptions' : Form.ChkGroupControl(("rOptRemember", "rOptMD5", "rOptFile")), + 'txtInput' : Form.FileInput(open=True), + 'btnReport' : Form.ButtonInput(self.OnReportClick), + 'cEChooser' : Form.EmbeddedChooserControl(self.EChooser) + }) + + + + def OnReportClick(self, code=0): + pass + + + def OnFormChange(self, fid): + if fid == self.rOptMD5.id or fid == self.rOptFile.id: + input = (self.cfg.md5sum, self.cfg.infile) + if fid == self.rOptMD5.id: + c1 = self.rOptMD5 + c2 = self.rOptFile + idx = 0 + else: + c1 = self.rOptFile + c2 = self.rOptMD5 + idx = 1 + + v = not self.GetControlValue(c1) + if v: idx = not idx + + # Uncheck the opposite input type + self.SetControlValue(c2, v) + + # Set input field depending on input type + self.SetControlValue(self.txtInput, input[idx]) + # + # Report button + # + elif fid == self.btnReport.id: + input = self.GetControlValue(self.txtInput) + as_file = self.GetControlValue(self.rOptFile) + apikey = self.GetControlValue(self.txtApiKey) + + ok, r = VtReport(self.cfg.apikey, + filename=input if as_file else None, + md5sum=None if as_file else input) + + # Error? + if not ok: + idc.Warning(r) + return 1 + + # Pass the result + self.EChooser.SetItems(r) + + # We have results and it was a file? Print its MD5 + if r and as_file: + print("%s: %s" % (vt.LAST_FILE_HASH, input)) + + # Refresh the embedded chooser control + # (Could also clear previous results if not were retrieved during this run) + self.RefreshField(self.cEChooser) + + # Store the input for the caller + self.cfg.input = input + + # No results and file as input was supplied? + if r is None: + if as_file: + # Propose to upload + if idc.AskYN(0, "HIDECANCEL\nNo previous results. Do you want to submit the file:\n\n'%s'\n\nto VirusTotal?" % input) == 0: + return 1 + + try: + r = vt.scan_file(input) + except Exception as e: + idc.Warning("Exceptio during upload: %s" % str(e)) + else: + if r is None: + idc.Warning("Failed to upload the file!") + else: + idc.Warning("File uploaded. Check again later to get the analysis report. Scan id: %s" % r) + else: + idc.Warning("No results found for hash: %s" % input) + + return 1 + + + def Show(self, cfg): + # Compile the form once + if not self.Compiled(): + _, args = self.Compile() + #print args[0] + + # Populate the form + self.txtApiKey.value = cfg.apikey + self.grpOptions.value = cfg.options + self.txtInput.value = cfg.infile if self.rOptFile.checked else cfg.md5sum + + # Remember the config + self.cfg = cfg + + # Execute the form + ok = self.Execute() + + # Forget the cfg + del self.cfg + + # Success? + if ok != 0: + # Update config + cfg.options = self.grpOptions.value + cfg.apikey = self.txtApiKey.value + + # Popup results? + if self.rOptRemember.checked: + ok = 2 + + return ok + + +# ----------------------------------------------------------------------- +class VirusTotalPlugin_t(plugin_t): + flags = idaapi.PLUGIN_UNL + comment = "VirusTotal plugin for IDA" + help = "" + wanted_name = "VirusTotal report" + wanted_hotkey = "Alt-F8" + + + def init(self): + # Some initialization + self.icon_id = 0 + return idaapi.PLUGIN_OK + + + def run(self, arg=0): + # Load icon from the memory and save its id + self.icon_id = idaapi.load_custom_icon(data=VT_ICON, format="png") + if self.icon_id == 0: + raise RuntimeError("Failed to load icon data!") + + # Create config object + cfg = VirusTotalConfig() + + # Read previous config + cfg.Read() + + # Create form + f = VirusTotalForm(self.icon_id) + + # Show the form + ok = f.Show(cfg) + if ok == 0: + f.Free() + return + + # Save configuration + cfg.Write() + + # Spawn a non-modal chooser w/ the results if any + if ok == 2 and f.EChooser.GetItems(): + VirusTotalChooser( + "VirusTotal results [%s]" % cfg.input, + f.EChooser.GetItems(), + self.icon_id).Show() + + f.Free() + return + + + def term(self): + # Free the custom icon + if self.icon_id != 0: + idaapi.free_custom_icon(self.icon_id) + + +# ----------------------------------------------------------------------- +def PLUGIN_ENTRY(): + return VirusTotalPlugin_t() + +# -------------------------------------------------------------------------- +if PLUGIN_TEST: + # Create form + f = PLUGIN_ENTRY() + f.init() + f.run() + f.term() + +