- added VirusTotal script. Check http://www.hexblog.com/?p=324

This commit is contained in:
elias.bachaalany 2011-05-09 10:21:32 +00:00
parent 47f7f2e48b
commit 50b66d0264
2 changed files with 523 additions and 0 deletions

156
Scripts/3rd/BboeVt.py Normal file
View File

@ -0,0 +1,156 @@
"""
Original code by Bryce Boe: http://www.bryceboe.com/2010/09/01/submitting-binaries-to-virustotal/
Modified by Elias Bachaalany <elias at hex-rays.com>
"""
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)

367
Scripts/VirusTotal.py Normal file
View File

@ -0,0 +1,367 @@
# -----------------------------------------------------------------------
# VirusTotal IDA Plugin
# By Elias Bachaalany <elias at hex-rays.com>
# (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}>
<Results:{cEChooser}>
<#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()