mirror of
https://github.com/cemu-project/idapython.git
synced 2024-11-30 21:14:20 +01:00
368 lines
11 KiB
Python
368 lines
11 KiB
Python
# -----------------------------------------------------------------------
|
|
# 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 = os.path.join(idaapi.get_user_idadir(), "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()
|
|
|
|
|