mirror of
https://github.com/cemu-project/idapython.git
synced 2024-11-30 21:14:20 +01:00
- added VirusTotal script. Check http://www.hexblog.com/?p=324
This commit is contained in:
parent
47f7f2e48b
commit
50b66d0264
156
Scripts/3rd/BboeVt.py
Normal file
156
Scripts/3rd/BboeVt.py
Normal 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
367
Scripts/VirusTotal.py
Normal 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()
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user