Merge pull request #58 from fireeye/capa-explorer-support-doc-format

Capa explorer support doc format
This commit is contained in:
Willi Ballenthin
2020-07-01 09:50:42 -06:00
committed by GitHub
5 changed files with 846 additions and 656 deletions

View File

@@ -1,4 +1,3 @@
import binascii
import codecs import codecs
import sys import sys
@@ -10,115 +9,116 @@ import idc
import capa.ida.helpers import capa.ida.helpers
def info_to_name(s): def info_to_name(display):
''' ''' """ extract root value from display name
e.g. function(my_function) => my_function
"""
try: try:
return s.split('(')[1].rstrip(')') return display.split('(')[1].rstrip(')')
except IndexError: except IndexError:
return '' return ''
def ea_to_hex_str(ea): def location_to_hex(location):
''' ''' """ convert location to hex for display """
return '%08X' % ea return '%08X' % location
class CapaExplorerDataItem(object): class CapaExplorerDataItem(object):
''' store data for CapaExplorerDataModel """ store data for CapaExplorerDataModel """
TODO
'''
def __init__(self, parent, data): def __init__(self, parent, data):
''' ''' """ """
self._parent = parent self.pred = parent
self._data = data self._data = data
self._children = [] self.children = []
self._checked = False self._checked = False
self.flags = (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsTristate | QtCore.Qt.ItemIsUserCheckable) self.flags = (QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsTristate | QtCore.Qt.ItemIsUserCheckable)
if self._parent: if self.pred:
self._parent.appendChild(self) self.pred.appendChild(self)
def setIsEditable(self, isEditable=False): def setIsEditable(self, isEditable=False):
''' modify item flags to be editable or not ''' """ modify item flags to be editable or not """
if isEditable: if isEditable:
self.flags |= QtCore.Qt.ItemIsEditable self.flags |= QtCore.Qt.ItemIsEditable
else: else:
self.flags &= ~QtCore.Qt.ItemIsEditable self.flags &= ~QtCore.Qt.ItemIsEditable
def setChecked(self, checked): def setChecked(self, checked):
''' set item as checked ''' """ set item as checked """
self._checked = checked self._checked = checked
def isChecked(self): def isChecked(self):
''' get item is checked ''' """ get item is checked """
return self._checked return self._checked
def appendChild(self, item): def appendChild(self, item):
''' add child item """ add child item
@param item: CapaExplorerDataItem* @param item: CapaExplorerDataItem*
''' """
self._children.append(item) self.children.append(item)
def child(self, row): def child(self, row):
''' get child row """ get child row
@param row: TODO @param row: TODO
''' """
return self._children[row] return self.children[row]
def childCount(self): def childCount(self):
''' get child count ''' """ get child count """
return len(self._children) return len(self.children)
def columnCount(self): def columnCount(self):
''' get column count ''' """ get column count """
return len(self._data) return len(self._data)
def data(self, column): def data(self, column):
''' get data at column ''' """ get data at column """
try: try:
return self._data[column] return self._data[column]
except IndexError: except IndexError:
return None return None
def parent(self): def parent(self):
''' get parent ''' """ get parent """
return self._parent return self.pred
def row(self): def row(self):
''' get row location ''' """ get row location """
if self._parent: if self.pred:
return self._parent._children.index(self) return self.pred.children.index(self)
return 0 return 0
def setData(self, column, value): def setData(self, column, value):
''' set data in column ''' """ set data in column """
self._data[column] = value self._data[column] = value
def children(self): def children(self):
''' yield children ''' """ yield children """
for child in self._children: for child in self.children:
yield child yield child
def removeChildren(self): def removeChildren(self):
''' ''' """ remove children from node """
del self._children[:] del self.children[:]
def __str__(self): def __str__(self):
''' get string representation of columns ''' """ get string representation of columns """
return ' '.join([data for data in self._data if data]) return ' '.join([data for data in self._data if data])
@property @property
def info(self): def info(self):
''' ''' """ return data stored in information column """
return self._data[0] return self._data[0]
@property @property
def ea(self): def location(self):
''' ''' """ return data stored in location column """
try: try:
return int(self._data[1], 16) return int(self._data[1], 16)
except ValueError: except ValueError:
@@ -126,107 +126,108 @@ class CapaExplorerDataItem(object):
@property @property
def details(self): def details(self):
''' ''' """ return data stored in details column """
return self._data[2] return self._data[2]
class CapaExplorerRuleItem(CapaExplorerDataItem): class CapaExplorerRuleItem(CapaExplorerDataItem):
''' store data relevant to capa function result ''' """ store data relevant to capa function result """
view_fmt = '%s (%d)' fmt = '%s (%d matches)'
def __init__(self, parent, name, count, definition): def __init__(self, parent, display, count, source):
''' ''' """ """
self._definition = definition display = self.fmt % (display, count) if count > 1 else display
name = CapaExplorerRuleItem.view_fmt % (name, count) if count else name super(CapaExplorerRuleItem, self).__init__(parent, [display, '', ''])
super(CapaExplorerRuleItem, self).__init__(parent, [name, '', '']) self._source = source
@property @property
def definition(self): def source(self):
''' ''' """ return rule contents for display """
return self._definition return self._source
class CapaExplorerRuleMatchItem(CapaExplorerDataItem):
""" store data relevant to capa function match result """
def __init__(self, parent, display, source=''):
""" """
super(CapaExplorerRuleMatchItem, self).__init__(parent, [display, '', ''])
self._source = source
@property
def source(self):
""" return rule contents for display """
return self._source
class CapaExplorerFunctionItem(CapaExplorerDataItem): class CapaExplorerFunctionItem(CapaExplorerDataItem):
''' store data relevant to capa function result ''' """ store data relevant to capa function result """
view_fmt = 'function(%s)' fmt = 'function(%s)'
def __init__(self, parent, name, ea): def __init__(self, parent, location):
''' ''' """ """
address = ea_to_hex_str(ea) super(CapaExplorerFunctionItem, self).__init__(parent, [self.fmt % idaapi.get_name(location),
name = CapaExplorerFunctionItem.view_fmt % name location_to_hex(location), ''])
super(CapaExplorerFunctionItem, self).__init__(parent, [name, address, ''])
@property @property
def info(self): def info(self):
''' ''' """ """
info = super(CapaExplorerFunctionItem, self).info info = super(CapaExplorerFunctionItem, self).info
name = info_to_name(info) display = info_to_name(info)
return name if name else info return display if display else info
@info.setter @info.setter
def info(self, name): def info(self, display):
''' ''' """ """
self._data[0] = CapaExplorerFunctionItem.view_fmt % name self._data[0] = self.fmt % display
class CapaExplorerBlockItem(CapaExplorerDataItem): class CapaExplorerBlockItem(CapaExplorerDataItem):
''' store data relevant to capa basic block results ''' """ store data relevant to capa basic block result """
view_fmt = 'basic block(loc_%s)' fmt = 'basic block(loc_%08X)'
def __init__(self, parent, ea): def __init__(self, parent, location):
''' ''' """ """
address = ea_to_hex_str(ea) super(CapaExplorerBlockItem, self).__init__(parent, [self.fmt % location, location_to_hex(location), ''])
name = CapaExplorerBlockItem.view_fmt % address
super(CapaExplorerBlockItem, self).__init__(parent, [name, address, ''])
class CapaExplorerDefaultItem(CapaExplorerDataItem): class CapaExplorerDefaultItem(CapaExplorerDataItem):
''' store data relevant to capa default result ''' """ store data relevant to capa default result """
def __init__(self, parent, name, ea=None): def __init__(self, parent, display, details='', location=None):
''' ''' """ """
if ea: location = location_to_hex(location) if location else ''
address = ea_to_hex_str(ea) super(CapaExplorerDefaultItem, self).__init__(parent, [display, location, details])
else:
address = ''
super(CapaExplorerDefaultItem, self).__init__(parent, [name, address, ''])
class CapaExplorerFeatureItem(CapaExplorerDataItem): class CapaExplorerFeatureItem(CapaExplorerDataItem):
''' store data relevant to capa feature result ''' """ store data relevant to capa feature result """
def __init__(self, parent, data): def __init__(self, parent, display, location='', details=''):
super(CapaExplorerFeatureItem, self).__init__(parent, data) location = location_to_hex(location) if location else ''
super(CapaExplorerFeatureItem, self).__init__(parent, [display, location, details])
class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem): class CapaExplorerInstructionViewItem(CapaExplorerFeatureItem):
def __init__(self, parent, name, ea): def __init__(self, parent, display, location):
''' ''' """ """
details = capa.ida.helpers.get_disasm_line(ea) details = capa.ida.helpers.get_disasm_line(location)
address = ea_to_hex_str(ea) super(CapaExplorerInstructionViewItem, self).__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
super(CapaExplorerInstructionViewItem, self).__init__(parent, [name, address, details])
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
class CapaExplorerByteViewItem(CapaExplorerFeatureItem): class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
def __init__(self, parent, name, ea): def __init__(self, parent, display, location):
''' ''' """ """
address = ea_to_hex_str(ea) byte_snap = idaapi.get_bytes(location, 32)
byte_snap = idaapi.get_bytes(ea, 32)
if byte_snap: if byte_snap:
byte_snap = codecs.encode(byte_snap, 'hex').upper() byte_snap = codecs.encode(byte_snap, 'hex').upper()
# TODO: better way?
if sys.version_info >= (3, 0): if sys.version_info >= (3, 0):
details = ' '.join([byte_snap[i:i + 2].decode() for i in range(0, len(byte_snap), 2)]) details = ' '.join([byte_snap[i:i + 2].decode() for i in range(0, len(byte_snap), 2)])
else: else:
@@ -234,17 +235,13 @@ class CapaExplorerByteViewItem(CapaExplorerFeatureItem):
else: else:
details = '' details = ''
super(CapaExplorerByteViewItem, self).__init__(parent, [name, address, details]) super(CapaExplorerByteViewItem, self).__init__(parent, display, location=location, details=details)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)
class CapaExplorerStringViewItem(CapaExplorerFeatureItem): class CapaExplorerStringViewItem(CapaExplorerFeatureItem):
def __init__(self, parent, name, ea, value): def __init__(self, parent, display, location):
''' ''' """ """
address = ea_to_hex_str(ea) super(CapaExplorerStringViewItem, self).__init__(parent, display, location=location)
self.ida_highlight = idc.get_color(location, idc.CIC_ITEM)
super(CapaExplorerStringViewItem, self).__init__(parent, [name, address, value])
self.ida_highlight = idc.get_color(ea, idc.CIC_ITEM)

View File

@@ -1,6 +1,7 @@
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui, Qt
from collections import deque from collections import deque
import binascii
import capa.render.utils as rutils
import idaapi import idaapi
import idc import idc
@@ -8,24 +9,24 @@ import idc
from capa.ida.explorer.item import ( from capa.ida.explorer.item import (
CapaExplorerDataItem, CapaExplorerDataItem,
CapaExplorerDefaultItem, CapaExplorerDefaultItem,
CapaExplorerFeatureItem,
CapaExplorerFunctionItem, CapaExplorerFunctionItem,
CapaExplorerRuleItem, CapaExplorerRuleItem,
CapaExplorerStringViewItem, CapaExplorerStringViewItem,
CapaExplorerInstructionViewItem, CapaExplorerInstructionViewItem,
CapaExplorerByteViewItem, CapaExplorerByteViewItem,
CapaExplorerBlockItem CapaExplorerBlockItem,
CapaExplorerRuleMatchItem,
CapaExplorerFeatureItem
) )
import capa.ida.helpers import capa.ida.helpers
# default highlight color used in IDA window # default highlight color used in IDA window
DEFAULT_HIGHLIGHT = 0xD096FF DEFAULT_HIGHLIGHT = 0xD096FF
class CapaExplorerDataModel(QtCore.QAbstractItemModel): class CapaExplorerDataModel(QtCore.QAbstractItemModel):
''' ''' """ """
COLUMN_INDEX_RULE_INFORMATION = 0 COLUMN_INDEX_RULE_INFORMATION = 0
COLUMN_INDEX_VIRTUAL_ADDRESS = 1 COLUMN_INDEX_VIRTUAL_ADDRESS = 1
@@ -34,114 +35,134 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
COLUMN_COUNT = 3 COLUMN_COUNT = 3
def __init__(self, parent=None): def __init__(self, parent=None):
''' ''' """ """
super(CapaExplorerDataModel, self).__init__(parent) super(CapaExplorerDataModel, self).__init__(parent)
self.root_node = CapaExplorerDataItem(None, ['Rule Information', 'Address', 'Details'])
self._root = CapaExplorerDataItem(None, ['Rule Information', 'Address', 'Details'])
def reset(self): def reset(self):
''' ''' """ """
# reset checkboxes and color highlights # reset checkboxes and color highlights
# TODO: make less hacky # TODO: make less hacky
for idx in range(self._root.childCount()): for idx in range(self.root_node.childCount()):
rindex = self.index(idx, 0, QtCore.QModelIndex()) root_index = self.index(idx, 0, QtCore.QModelIndex())
for mindex in self.iterateChildrenIndexFromRootIndex(rindex, ignore_root=False): for model_index in self.iterateChildrenIndexFromRootIndex(root_index, ignore_root=False):
mindex.internalPointer().setChecked(False) model_index.internalPointer().setChecked(False)
self._util_reset_ida_highlighting(mindex.internalPointer(), False) self.util_reset_ida_highlighting(model_index.internalPointer(), False)
self.dataChanged.emit(mindex, mindex) self.dataChanged.emit(model_index, model_index)
def clear(self): def clear(self):
''' ''' """ """
self.beginResetModel() self.beginResetModel()
# TODO: make sure this isn't for memory self.root_node.removeChildren()
self._root.removeChildren()
self.endResetModel() self.endResetModel()
def columnCount(self, mindex): def columnCount(self, model_index):
''' get the number of columns for the children of the given parent """ get the number of columns for the children of the given parent
@param mindex: QModelIndex* @param model_index: QModelIndex*
@retval column count @retval column count
''' """
if mindex.isValid(): if model_index.isValid():
return mindex.internalPointer().columnCount() return model_index.internalPointer().columnCount()
else: else:
return self._root.columnCount() return self.root_node.columnCount()
def data(self, mindex, role): def data(self, model_index, role):
''' get data stored under the given role for the item referred to by the index """ get data stored under the given role for the item referred to by the index
@param mindex: QModelIndex* @param model_index: QModelIndex*
@param role: QtCore.Qt.* @param role: QtCore.Qt.*
@retval data to be displayed @retval data to be displayed
''' """
if not mindex.isValid(): if not model_index.isValid():
return None return None
item = model_index.internalPointer()
column = model_index.column()
if role == QtCore.Qt.DisplayRole: if role == QtCore.Qt.DisplayRole:
# display data in corresponding column # display data in corresponding column
return mindex.internalPointer().data(mindex.column()) return item.data(column)
if role == QtCore.Qt.ToolTipRole and \ if role == QtCore.Qt.ToolTipRole and isinstance(item, (CapaExplorerRuleItem, CapaExplorerRuleMatchItem)) and \
CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == mindex.column() and \ CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column:
isinstance(mindex.internalPointer(), CapaExplorerRuleItem): # show tooltip containing rule source
# show tooltip containing rule definition return item.source
return mindex.internalPointer().definition
if role == QtCore.Qt.CheckStateRole and mindex.column() == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION: if role == QtCore.Qt.CheckStateRole and column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION:
# inform view how to display content of checkbox - un/checked # inform view how to display content of checkbox - un/checked
return QtCore.Qt.Checked if mindex.internalPointer().isChecked() else QtCore.Qt.Unchecked return QtCore.Qt.Checked if item.isChecked() else QtCore.Qt.Unchecked
if role == QtCore.Qt.FontRole and mindex.column() in (CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS, CapaExplorerDataModel.COLUMN_INDEX_DETAILS): if role == QtCore.Qt.FontRole and column in (CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS,
return QtGui.QFont('Courier', weight=QtGui.QFont.Medium) CapaExplorerDataModel.COLUMN_INDEX_DETAILS):
# set font for virtual address and details columns
font = QtGui.QFont('Courier', weight=QtGui.QFont.Medium)
if column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS:
font.setBold(True)
return font
if role == QtCore.Qt.FontRole and mindex.internalPointer() == self._root: if role == QtCore.Qt.FontRole and isinstance(item, (CapaExplorerRuleItem, CapaExplorerRuleMatchItem,
return QtCore.QFont(bold=True) CapaExplorerBlockItem, CapaExplorerFunctionItem,
CapaExplorerFeatureItem)) and \
column == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION:
# set bold font for top-level rules
font = QtGui.QFont()
font.setBold(True)
return font
if role == QtCore.Qt.ForegroundRole and column == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS:
# set color for virtual address column
return QtGui.QColor(88, 139, 174)
if role == QtCore.Qt.ForegroundRole and isinstance(item, CapaExplorerFeatureItem) and column == \
CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION:
# set color for feature items
return QtGui.QColor(79, 121, 66)
return None return None
def flags(self, mindex): def flags(self, model_index):
''' get item flags for given index """ get item flags for given index
@param mindex: QModelIndex* @param model_index: QModelIndex*
@retval QtCore.Qt.ItemFlags @retval QtCore.Qt.ItemFlags
''' """
if not mindex.isValid(): if not model_index.isValid():
return QtCore.Qt.NoItemFlags return QtCore.Qt.NoItemFlags
return mindex.internalPointer().flags return model_index.internalPointer().flags
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
''' get data for the given role and section in the header with the specified orientation """ get data for the given role and section in the header with the specified orientation
@param section: int @param section: int
@param orientation: QtCore.Qt.Orientation @param orientation: QtCore.Qt.Orientation
@param role: QtCore.Qt.DisplayRole @param role: QtCore.Qt.DisplayRole
@retval header data list() @retval header data list()
''' """
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole: if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self._root.data(section) return self.root_node.data(section)
return None return None
def index(self, row, column, parent): def index(self, row, column, parent):
''' get index of the item in the model specified by the given row, column and parent index """ get index of the item in the model specified by the given row, column and parent index
@param row: int @param row: int
@param column: int @param column: int
@param parent: QModelIndex* @param parent: QModelIndex*
@retval QModelIndex* @retval QModelIndex*
''' """
if not self.hasIndex(row, column, parent): if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex() return QtCore.QModelIndex()
if not parent.isValid(): if not parent.isValid():
parent_item = self._root parent_item = self.root_node
else: else:
parent_item = parent.internalPointer() parent_item = parent.internalPointer()
@@ -152,64 +173,66 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
else: else:
return QtCore.QModelIndex() return QtCore.QModelIndex()
def parent(self, mindex): def parent(self, model_index):
''' get parent of the model item with the given index """ get parent of the model item with the given index
if the item has no parent, an invalid QModelIndex* is returned if the item has no parent, an invalid QModelIndex* is returned
@param mindex: QModelIndex* @param model_index: QModelIndex*
@retval QModelIndex* @retval QModelIndex*
''' """
if not mindex.isValid(): if not model_index.isValid():
return QtCore.QModelIndex() return QtCore.QModelIndex()
child = mindex.internalPointer() child = model_index.internalPointer()
parent = child.parent() parent = child.parent()
if parent == self._root: if parent == self.root_node:
return QtCore.QModelIndex() return QtCore.QModelIndex()
return self.createIndex(parent.row(), 0, parent) return self.createIndex(parent.row(), 0, parent)
def iterateChildrenIndexFromRootIndex(self, mindex, ignore_root=True): def iterateChildrenIndexFromRootIndex(self, model_index, ignore_root=True):
''' depth-first traversal of child nodes """ depth-first traversal of child nodes
@param mindex: QModelIndex* @param model_index: QModelIndex*
@param ignore_root: if set, do not return root index
@retval yield QModelIndex* @retval yield QModelIndex*
''' """
visited = set() visited = set()
stack = deque((mindex,)) stack = deque((model_index,))
while True: while True:
try: try:
cmindex = stack.pop() child_index = stack.pop()
except IndexError: except IndexError:
break break
if cmindex not in visited: if child_index not in visited:
if not ignore_root or cmindex is not mindex: if not ignore_root or child_index is not model_index:
# ignore root # ignore root
yield cmindex yield child_index
visited.add(cmindex) visited.add(child_index)
for idx in range(self.rowCount(cmindex)): for idx in range(self.rowCount(child_index)):
stack.append(cmindex.child(idx, 0)) stack.append(child_index.child(idx, 0))
def _util_reset_ida_highlighting(self, item, checked): def util_reset_ida_highlighting(self, item, checked):
''' ''' """ """
if not isinstance(item, (CapaExplorerStringViewItem, CapaExplorerInstructionViewItem, CapaExplorerByteViewItem)): if not isinstance(item, (CapaExplorerStringViewItem, CapaExplorerInstructionViewItem,
CapaExplorerByteViewItem)):
# ignore other item types # ignore other item types
return return
curr_highlight = idc.get_color(item.ea, idc.CIC_ITEM) curr_highlight = idc.get_color(item.location, idc.CIC_ITEM)
if checked: if checked:
# item checked - record current highlight and set to new # item checked - record current highlight and set to new
item.ida_highlight = curr_highlight item.ida_highlight = curr_highlight
idc.set_color(item.ea, idc.CIC_ITEM, DEFAULT_HIGHLIGHT) idc.set_color(item.location, idc.CIC_ITEM, DEFAULT_HIGHLIGHT)
else: else:
# item unchecked - reset highlight # item unchecked - reset highlight
if curr_highlight != DEFAULT_HIGHLIGHT: if curr_highlight != DEFAULT_HIGHLIGHT:
@@ -217,36 +240,37 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
item.ida_highlight = curr_highlight item.ida_highlight = curr_highlight
else: else:
# reset highlight to previous # reset highlight to previous
idc.set_color(item.ea, idc.CIC_ITEM, item.ida_highlight) idc.set_color(item.location, idc.CIC_ITEM, item.ida_highlight)
def setData(self, mindex, value, role): def setData(self, model_index, value, role):
''' set the role data for the item at index to value """ set the role data for the item at index to value
@param mindex: QModelIndex* @param model_index: QModelIndex*
@param value: QVariant* @param value: QVariant*
@param role: QtCore.Qt.EditRole @param role: QtCore.Qt.EditRole
@retval True/False @retval True/False
''' """
if not mindex.isValid(): if not model_index.isValid():
return False return False
if role == QtCore.Qt.CheckStateRole and mindex.column() == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION: if role == QtCore.Qt.CheckStateRole and model_index.column() ==\
CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION:
# user un/checked box - un/check parent and children # user un/checked box - un/check parent and children
for cindex in self.iterateChildrenIndexFromRootIndex(mindex, ignore_root=False): for child_index in self.iterateChildrenIndexFromRootIndex(model_index, ignore_root=False):
cindex.internalPointer().setChecked(value) child_index.internalPointer().setChecked(value)
self._util_reset_ida_highlighting(cindex.internalPointer(), value) self.util_reset_ida_highlighting(child_index.internalPointer(), value)
self.dataChanged.emit(cindex, cindex) self.dataChanged.emit(child_index, child_index)
return True return True
if role == QtCore.Qt.EditRole and value and \ if role == QtCore.Qt.EditRole and value and \
mindex.column() == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION and \ model_index.column() == CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION and \
isinstance(mindex.internalPointer(), CapaExplorerFunctionItem): isinstance(model_index.internalPointer(), CapaExplorerFunctionItem):
# user renamed function - update IDA database and data model # user renamed function - update IDA database and data model
old_name = mindex.internalPointer().info old_name = model_index.internalPointer().info
new_name = str(value) new_name = str(value)
if idaapi.set_name(mindex.internalPointer().ea, new_name): if idaapi.set_name(model_index.internalPointer().location, new_name):
# success update IDA database - update data model # success update IDA database - update data model
self.update_function_name(old_name, new_name) self.update_function_name(old_name, new_name)
return True return True
@@ -254,167 +278,280 @@ class CapaExplorerDataModel(QtCore.QAbstractItemModel):
# no handle # no handle
return False return False
def rowCount(self, mindex): def rowCount(self, model_index):
''' get the number of rows under the given parent """ get the number of rows under the given parent
when the parent is valid it means that is returning the number of when the parent is valid it means that is returning the number of
children of parent children of parent
@param mindex: QModelIndex* @param model_index: QModelIndex*
@retval row count @retval row count
''' """
if mindex.column() > 0: if model_index.column() > 0:
return 0 return 0
if not mindex.isValid(): if not model_index.isValid():
item = self._root item = self.root_node
else: else:
item = mindex.internalPointer() item = model_index.internalPointer()
return item.childCount() return item.childCount()
def render_capa_results(self, rule_set, results): def render_capa_doc_statement_node(self, parent, statement, doc):
''' populate data model with capa results """ render capa statement read from doc
@param rule_set: TODO @param parent: parent to which new child is assigned
@param results: TODO @param statement: statement read from doc
''' @param doc: capa result doc
# prepare data model for changes
self.beginResetModel()
for (rule, ress) in results.items(): "statement": {
if rule_set.rules[rule].meta.get('lib', False): "type": "or"
# skip library rules },
continue """
if statement['type'] in ('and', 'or', 'optional'):
return CapaExplorerDefaultItem(parent, statement['type'])
elif statement['type'] == 'not':
# TODO: do we display 'not'
pass
elif statement['type'] == 'some':
return CapaExplorerDefaultItem(parent, statement['count'] + ' or more')
elif statement['type'] == 'range':
# `range` is a weird node, its almost a hybrid of statement + feature.
# it is a specific feature repeated multiple times.
# there's no additional logic in the feature part, just the existence of a feature.
# so, we have to inline some of the feature rendering here.
display = 'count(%s): ' % self.capa_doc_feature_to_display(statement['child'])
# top level item is rule if statement['max'] == statement['min']:
parent = CapaExplorerRuleItem(self._root, rule, len(ress), rule_set.rules[rule].definition) display += '%d' % (statement['min'])
elif statement['min'] == 0:
display += '%d or fewer' % (statement['max'])
elif statement['max'] == (1 << 64 - 1):
display += '%d or more' % (statement['min'])
else:
display += 'between %d and %d' % (statement['min'], statement['max'])
for (ea, res) in sorted(ress, key=lambda p: p[0]): return CapaExplorerFeatureItem(parent, display=display)
if rule_set.rules[rule].scope == capa.rules.FILE_SCOPE: elif statement['type'] == 'subscope':
# file scope - parent is rule return CapaExplorerFeatureItem(parent, 'subscope(%s)' % statement['subscope'])
parent2 = parent elif statement['type'] == 'regex':
elif rule_set.rules[rule].scope == capa.rules.FUNCTION_SCOPE: # regex is a `Statement` not a `Feature`
parent2 = CapaExplorerFunctionItem(parent, idaapi.get_name(ea), ea) # this is because it doesn't get extracted, but applies to all strings in scope.
elif rule_set.rules[rule].scope == capa.rules.BASIC_BLOCK_SCOPE: # so we have to handle it here
parent2 = CapaExplorerBlockItem(parent, ea) return CapaExplorerFeatureItem(parent, 'regex(%s)' % statement['pattern'], details=statement['match'])
else: else:
# TODO: better way to notify a missed scope? raise RuntimeError('unexpected match statement type: ' + str(statement))
parent2 = CapaExplorerDefaultItem(parent, '', ea)
self._render_result(rule_set, res, parent2) def render_capa_doc_match(self, parent, match, doc):
""" render capa match read from doc
# reset data model after making changes @param parent: parent node to which new child is assigned
self.endResetModel() @param match: match read from doc
@param doc: capa result doc
def _render_result(self, rule_set, result, parent): "matches": {
''' ''' "0": {
if not result.success: "children": [],
# TODO: display failed branches?? "locations": [
4317184
],
"node": {
"feature": {
"section": ".rsrc",
"type": "section"
},
"type": "feature"
},
"success": true
}
},
"""
if not match['success']:
# TODO: display failed branches at some point? Help with debugging rules?
return return
if isinstance(result.statement, capa.engine.Some): # optional statement with no successful children is empty
if result.statement.count == 0: if (match['node'].get('statement', {}).get('type') == 'optional'
if sum(map(lambda c: c.success, result.children)) > 0: and not any(map(lambda m: m['success'], match['children']))):
parent2 = CapaExplorerDefaultItem(parent, 'optional') return
else:
parent2 = parent if match['node']['type'] == 'statement':
else: parent2 = self.render_capa_doc_statement_node(parent, match['node']['statement'], doc)
parent2 = CapaExplorerDefaultItem(parent, '%d or more' % result.statement.count) elif match['node']['type'] == 'feature':
elif not isinstance(result.statement, (capa.features.Feature, capa.engine.Range, capa.engine.Regex)): parent2 = self.render_capa_doc_feature_node(parent, match['node']['feature'], match['locations'], doc)
# when rending a structural node (and/or/not) then we only care about the node name.
if not list(filter(lambda c: bool(c), result.children)):
# ignore structural expressions that do not have any successful children (e.g. not)
return
parent2 = CapaExplorerDefaultItem(parent, result.statement.name.lower())
else: else:
# but when rendering a Feature, want to see any arguments to it raise RuntimeError('unexpected node type: ' + str(match['node']['type']))
if len(result.locations) == 1:
# ea = result.locations.pop()
ea = next(iter(result.locations))
parent2 = self._render_feature(rule_set, parent, result.statement, ea, str(result.statement))
else:
parent2 = CapaExplorerDefaultItem(parent, str(result.statement))
for ea in sorted(result.locations): for child in match['children']:
self._render_feature(rule_set, parent2, result.statement, ea) self.render_capa_doc_match(parent2, child, doc)
for child in result.children: def render_capa_doc(self, doc):
self._render_result(rule_set, child, parent2) """ render capa features specified in doc
def _render_feature(self, rule_set, parent, feature, ea, name='-'): @param doc: capa result doc
''' render a given feature """
self.beginResetModel()
for rule in rutils.capability_rules(doc):
parent = CapaExplorerRuleItem(self.root_node, rule['meta']['name'], len(rule['matches']), rule['source'])
for (location, match) in doc[rule['meta']['name']]['matches'].items():
if rule['meta']['scope'] == capa.rules.FILE_SCOPE:
parent2 = parent
elif rule['meta']['scope'] == capa.rules.FUNCTION_SCOPE:
parent2 = CapaExplorerFunctionItem(parent, location)
elif rule['meta']['scope'] == capa.rules.BASIC_BLOCK_SCOPE:
parent2 = CapaExplorerBlockItem(parent, location)
else:
raise RuntimeError('unexpected rule scope: ' + str(rule['meta']['scope']))
self.render_capa_doc_match(parent2, match, doc)
self.endResetModel()
def capa_doc_feature_to_display(self, feature):
""" convert capa doc feature type string to display string for ui
@param feature: capa feature read from doc
"feature": {
"number": 2147483903,
"type": "number"
},
"""
mapping = {
'string': 'string(%s)',
'bytes': 'bytes(%s)',
'api': 'api(%s)',
'mnemonic': 'mnemonic(%s)',
'export': 'export(%s)',
'import': 'import(%s)',
'section': 'section(%s)',
'number': 'number(0x%X)',
'offset': 'offset(0x%X)',
'characteristic': 'characteristic(%s)',
'match': 'rule match(%s)'
}
@param rule_set: TODO
@param parent: TODO
@param result: TODO
@param ea: virtual address
@param name: TODO
''' '''
"feature": {
"characteristic": [
"loop",
true
],
"type": "characteristic"
},
'''
if feature['type'] == 'characteristic':
return mapping['characteristic'] % feature['characteristic'][0]
# convert bytes feature from "410ab4" to "41 0A B4"
if feature['type'] == 'bytes':
return mapping['bytes'] % ' '.join(feature['bytes'][i:i + 2] for i in
range(0, len(feature['bytes']), 2)).upper()
try:
fmt = mapping[feature['type']]
except KeyError:
raise RuntimeError('unexpected doc type: ' + str(feature['type']))
return fmt % feature[feature['type']]
def render_capa_doc_feature_node(self, parent, feature, locations, doc):
""" """
display = self.capa_doc_feature_to_display(feature)
if len(locations) == 1:
parent2 = self.render_capa_doc_feature(parent, feature, next(iter(locations)), doc, display=display)
else:
# feature has multiple children, nest under one parent feature node
parent2 = CapaExplorerFeatureItem(parent, display)
for location in sorted(locations):
self.render_capa_doc_feature(parent2, feature, location, doc)
return parent2
def render_capa_doc_feature(self, parent, feature, location, doc, display='-'):
""" render capa feature read from doc
@param parent: parent node to which new child is assigned
@param feature: feature read from doc
@param doc: capa feature doc
"node": {
"feature": {
"number": 255,
"type": "number"
},
"type": "feature"
},
@param location: address of feature
@param display: text to display in plugin ui
"""
instruction_view = ( instruction_view = (
capa.features.Bytes, 'bytes',
capa.features.String, 'api',
capa.features.insn.API, 'mnemonic',
capa.features.insn.Mnemonic, 'number',
capa.features.insn.Number, 'offset'
capa.features.insn.Offset
) )
byte_view = ( byte_view = (
capa.features.file.Section, 'section',
) )
string_view = ( string_view = (
capa.engine.Regex, 'string',
)
default_feature_view = (
'import',
'export'
) )
if isinstance(feature, instruction_view): # special handling for characteristic pending type
return CapaExplorerInstructionViewItem(parent, name, ea) if feature['type'] == 'characteristic':
if feature['characteristic'][0] in ('embedded pe',):
return CapaExplorerByteViewItem(parent, display, location)
if isinstance(feature, byte_view): if feature['characteristic'][0] in ('loop', 'recursive call', 'tight loop', 'switch'):
return CapaExplorerByteViewItem(parent, name, ea) return CapaExplorerFeatureItem(parent, display=display)
if isinstance(feature, string_view): # default to instruction view
# TODO: move string collection to item constructor return CapaExplorerInstructionViewItem(parent, display, location)
if isinstance(feature, capa.engine.Regex):
# rstrip "matched="<string>")" because data already displayed in interface
name = name.split(',')[0] + ')'
return CapaExplorerStringViewItem(parent, name, ea, feature.match)
if isinstance(feature, capa.features.Characteristic): if feature['type'] == 'match':
# special rendering for characteristics return CapaExplorerRuleMatchItem(parent, display, source=doc.get(feature['match'], {}).get('source', ''))
if feature.name in ('loop', 'recursive call', 'tight loop', 'switch'):
return CapaExplorerDefaultItem(parent, name)
if feature.name in ('embedded pe',):
return CapaExplorerByteViewItem(parent, name, ea)
return CapaExplorerInstructionViewItem(parent, name, ea)
if isinstance(feature, capa.features.MatchedRule): if feature['type'] in instruction_view:
# render feature as a rule item return CapaExplorerInstructionViewItem(parent, display, location)
return CapaExplorerRuleItem(parent, name, 0, rule_set.rules[feature.rule_name].definition)
if isinstance(feature, capa.engine.Range): if feature['type'] in byte_view:
# render feature based upon type child return CapaExplorerByteViewItem(parent, display, location)
return self._render_feature(rule_set, parent, feature.child, ea, name)
# no handle, default to name and virtual address display if feature['type'] in string_view:
return CapaExplorerDefaultItem(parent, name, ea) return CapaExplorerStringViewItem(parent, display, location)
if feature['type'] in default_feature_view:
return CapaExplorerFeatureItem(parent, display=display)
raise RuntimeError('unexpected feature type: ' + str(feature['type']))
def update_function_name(self, old_name, new_name): def update_function_name(self, old_name, new_name):
''' update all instances of function name """ update all instances of function name
@param old_name: previous function name @param old_name: previous function name
@param new_name: new function name @param new_name: new function name
''' """
rmindex = self.index(0, 0, QtCore.QModelIndex()) root_index = self.index(0, 0, QtCore.QModelIndex())
# convert name to view format for matching # convert name to view format for matching
# TODO: handle this better old_name = CapaExplorerFunctionItem.fmt % old_name
old_name = CapaExplorerFunctionItem.view_fmt % old_name
for mindex in self.match(rmindex, QtCore.Qt.DisplayRole, old_name, hits=-1, flags=QtCore.Qt.MatchRecursive): for model_index in self.match(root_index, QtCore.Qt.DisplayRole, old_name, hits=-1,
if not isinstance(mindex.internalPointer(), CapaExplorerFunctionItem): flags=QtCore.Qt.MatchRecursive):
if not isinstance(model_index.internalPointer(), CapaExplorerFunctionItem):
continue continue
mindex.internalPointer().info = new_name
self.dataChanged.emit(mindex, mindex) model_index.internalPointer().info = new_name
self.dataChanged.emit(model_index, model_index)

View File

@@ -1,25 +1,27 @@
from PyQt5 import QtCore from PyQt5 import QtCore
from capa.ida.explorer.model import CapaExplorerDataModel from capa.ida.explorer.model import CapaExplorerDataModel
class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel): class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel):
def __init__(self, parent=None): def __init__(self, parent=None):
''' ''' """ """
super(CapaExplorerSortFilterProxyModel, self).__init__(parent) super(CapaExplorerSortFilterProxyModel, self).__init__(parent)
def lessThan(self, left, right): def lessThan(self, left, right):
''' true if the value of the left item is less than value of right item """ true if the value of the left item is less than value of right item
@param left: QModelIndex* @param left: QModelIndex*
@param right: QModelIndex* @param right: QModelIndex*
@retval True/False @retval True/False
''' """
ldata = left.internalPointer().data(left.column()) ldata = left.internalPointer().data(left.column())
rdata = right.internalPointer().data(right.column()) rdata = right.internalPointer().data(right.column())
if ldata and rdata and left.column() == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS and left.column() == right.column(): if ldata and rdata and left.column() == CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS and left.column() \
== right.column():
# convert virtual address before compare # convert virtual address before compare
return int(ldata, 16) < int(rdata, 16) return int(ldata, 16) < int(rdata, 16)
else: else:
@@ -27,49 +29,49 @@ class CapaExplorerSortFilterProxyModel(QtCore.QSortFilterProxyModel):
return ldata.lower() < rdata.lower() return ldata.lower() < rdata.lower()
def filterAcceptsRow(self, row, parent): def filterAcceptsRow(self, row, parent):
''' true if the item in the row indicated by the given row and parent """ true if the item in the row indicated by the given row and parent
should be included in the model; otherwise returns false should be included in the model; otherwise returns false
@param row: int @param row: int
@param parent: QModelIndex* @param parent: QModelIndex*
@retval True/False @retval True/False
''' """
if self._filter_accepts_row_self(row, parent): if self.filter_accepts_row_self(row, parent):
return True return True
alpha = parent alpha = parent
while alpha.isValid(): while alpha.isValid():
if self._filter_accepts_row_self(alpha.row(), alpha.parent()): if self.filter_accepts_row_self(alpha.row(), alpha.parent()):
return True return True
alpha = alpha.parent() alpha = alpha.parent()
if self._index_has_accepted_children(row, parent): if self.index_has_accepted_children(row, parent):
return True return True
return False return False
def add_single_string_filter(self, column, string): def add_single_string_filter(self, column, string):
''' add fixed string filter """ add fixed string filter
@param column: key column @param column: key column
@param string: string to sort @param string: string to sort
''' """
self.setFilterKeyColumn(column) self.setFilterKeyColumn(column)
self.setFilterFixedString(string) self.setFilterFixedString(string)
def _index_has_accepted_children(self, row, parent): def index_has_accepted_children(self, row, parent):
''' ''' """ """
mindex = self.sourceModel().index(row, 0, parent) model_index = self.sourceModel().index(row, 0, parent)
if mindex.isValid(): if model_index.isValid():
for idx in range(self.sourceModel().rowCount(mindex)): for idx in range(self.sourceModel().rowCount(model_index)):
if self._filter_accepts_row_self(idx, mindex): if self.filter_accepts_row_self(idx, model_index):
return True return True
if self._index_has_accepted_children(idx, mindex): if self.index_has_accepted_children(idx, model_index):
return True return True
return False return False
def _filter_accepts_row_self(self, row, parent): def filter_accepts_row_self(self, row, parent):
''' ''' """ """
return super(CapaExplorerSortFilterProxyModel, self).filterAcceptsRow(row, parent) return super(CapaExplorerSortFilterProxyModel, self).filterAcceptsRow(row, parent)

View File

@@ -4,40 +4,43 @@ import idaapi
import idc import idc
from capa.ida.explorer.model import CapaExplorerDataModel from capa.ida.explorer.model import CapaExplorerDataModel
from capa.ida.explorer.item import CapaExplorerFunctionItem from capa.ida.explorer.item import (
CapaExplorerFunctionItem,
CapaExplorerRuleItem,
)
class CapaExplorerQtreeView(QtWidgets.QTreeView): class CapaExplorerQtreeView(QtWidgets.QTreeView):
''' capa explorer QTreeView implementation """ capa explorer QTreeView implementation
view controls UI action responses and displays data from view controls UI action responses and displays data from
CapaExplorerDataModel CapaExplorerDataModel
view does not modify CapaExplorerDataModel directly - data view does not modify CapaExplorerDataModel directly - data
modifications should be implemented in CapaExplorerDataModel modifications should be implemented in CapaExplorerDataModel
''' """
def __init__(self, model, parent=None): def __init__(self, model, parent=None):
''' initialize CapaExplorerQTreeView """ initialize CapaExplorerQTreeView
TODO TODO
@param model: TODO @param model: TODO
@param parent: TODO @param parent: TODO
''' """
super(CapaExplorerQtreeView, self).__init__(parent) super(CapaExplorerQtreeView, self).__init__(parent)
self.setModel(model) self.setModel(model)
# TODO: get from parent?? # TODO: get from parent??
self._model = model self.model = model
self._parent = parent self.parent = parent
# configure custom UI controls # configure custom UI controls
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.setExpandsOnDoubleClick(False) self.setExpandsOnDoubleClick(False)
self.setSortingEnabled(True) self.setSortingEnabled(True)
self._model.setDynamicSortFilter(False) self.model.setDynamicSortFilter(False)
# configure view columns to auto-resize # configure view columns to auto-resize
for idx in range(CapaExplorerDataModel.COLUMN_COUNT): for idx in range(CapaExplorerDataModel.COLUMN_COUNT):
@@ -48,223 +51,222 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
self.collapsed.connect(self.resize_columns_to_content) self.collapsed.connect(self.resize_columns_to_content)
# connect slots # connect slots
self.customContextMenuRequested.connect(self._slot_custom_context_menu_requested) self.customContextMenuRequested.connect(self.slot_custom_context_menu_requested)
self.doubleClicked.connect(self._slot_double_click) self.doubleClicked.connect(self.slot_double_click)
# self.clicked.connect(self._slot_click) # self.clicked.connect(self.slot_click)
self.setStyleSheet('QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}') self.setStyleSheet('QTreeView::item {padding-right: 15 px;padding-bottom: 2 px;}')
def reset(self): def reset(self):
''' reset user interface changes """ reset user interface changes
called when view should reset any user interface changes called when view should reset any user interface changes
made since the last reset e.g. IDA window highlighting made since the last reset e.g. IDA window highlighting
''' """
self.collapseAll() self.collapseAll()
self.resize_columns_to_content() self.resize_columns_to_content()
def resize_columns_to_content(self): def resize_columns_to_content(self):
''' reset view columns to contents """ reset view columns to contents
TODO: prevent columns from shrinking TODO: prevent columns from shrinking
''' """
self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents) self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents)
def _map_index_to_source_item(self, mindex): def map_index_to_source_item(self, model_index):
''' map proxy model index to source model item """ map proxy model index to source model item
@param mindex: QModelIndex* @param model_index: QModelIndex*
@retval QObject* @retval QObject*
''' """
return self._model.mapToSource(mindex).internalPointer() return self.model.mapToSource(model_index).internalPointer()
def _send_data_to_clipboard(self, data): def send_data_to_clipboard(self, data):
''' copy data to the clipboard """ copy data to the clipboard
@param data: data to be copied @param data: data to be copied
''' """
clip = QtWidgets.QApplication.clipboard() clip = QtWidgets.QApplication.clipboard()
clip.clear(mode=clip.Clipboard) clip.clear(mode=clip.Clipboard)
clip.setText(data, mode=clip.Clipboard) clip.setText(data, mode=clip.Clipboard)
def _new_action(self, display, data, slot): def new_action(self, display, data, slot):
''' create action for context menu """ create action for context menu
@param display: text displayed to user in context menu @param display: text displayed to user in context menu
@param data: data passed to slot @param data: data passed to slot
@param slot: slot to connect @param slot: slot to connect
@retval QAction* @retval QAction*
''' """
action = QtWidgets.QAction(display, self._parent) action = QtWidgets.QAction(display, self.parent)
action.setData(data) action.setData(data)
action.triggered.connect(lambda checked: slot(action)) action.triggered.connect(lambda checked: slot(action))
return action return action
def _load_default_context_menu_actions(self, data): def load_default_context_menu_actions(self, data):
''' yield actions specific to function custom context menu """ yield actions specific to function custom context menu
@param data: tuple @param data: tuple
@yield QAction* @yield QAction*
''' """
default_actions = [ default_actions = [
('Copy column', data, self._slot_copy_column), ('Copy column', data, self.slot_copy_column),
('Copy row', data, self._slot_copy_row), ('Copy row', data, self.slot_copy_row),
# ('Filter', data, self._slot_filter),
] ]
# add default actions # add default actions
for action in default_actions: for action in default_actions:
yield self._new_action(*action) yield self.new_action(*action)
def _load_function_context_menu_actions(self, data): def load_function_context_menu_actions(self, data):
''' yield actions specific to function custom context menu """ yield actions specific to function custom context menu
@param data: tuple @param data: tuple
@yield QAction* @yield QAction*
''' """
function_actions = [ function_actions = [
('Rename function', data, self._slot_rename_function), ('Rename function', data, self.slot_rename_function),
] ]
# add function actions # add function actions
for action in function_actions: for action in function_actions:
yield self._new_action(*action) yield self.new_action(*action)
# add default actions # add default actions
for action in self._load_default_context_menu_actions(data): for action in self.load_default_context_menu_actions(data):
yield action yield action
def _load_default_context_menu(self, pos, item, mindex): def load_default_context_menu(self, pos, item, model_index):
''' create default custom context menu """ create default custom context menu
creates custom context menu containing default actions creates custom context menu containing default actions
@param pos: TODO @param pos: TODO
@param item: TODO @param item: TODO
@param mindex: TODO @param model_index: TODO
@retval QMenu* @retval QMenu*
''' """
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
for action in self._load_default_context_menu_actions((pos, item, mindex)): for action in self.load_default_context_menu_actions((pos, item, model_index)):
menu.addAction(action) menu.addAction(action)
return menu return menu
def _load_function_item_context_menu(self, pos, item, mindex): def load_function_item_context_menu(self, pos, item, model_index):
''' create function custom context menu """ create function custom context menu
creates custom context menu containing actions specific to functions creates custom context menu containing actions specific to functions
and the default actions and the default actions
@param pos: TODO @param pos: TODO
@param item: TODO @param item: TODO
@param mindex: TODO @param model_index: TODO
@retval QMenu* @retval QMenu*
''' """
menu = QtWidgets.QMenu() menu = QtWidgets.QMenu()
for action in self._load_function_context_menu_actions((pos, item, mindex)): for action in self.load_function_context_menu_actions((pos, item, model_index)):
menu.addAction(action) menu.addAction(action)
return menu return menu
def _show_custom_context_menu(self, menu, pos): def show_custom_context_menu(self, menu, pos):
''' display custom context menu in view """ display custom context menu in view
@param menu: TODO @param menu: TODO
@param pos: TODO @param pos: TODO
''' """
if not menu: if not menu:
return return
menu.exec_(self.viewport().mapToGlobal(pos)) menu.exec_(self.viewport().mapToGlobal(pos))
def _slot_copy_column(self, action): def slot_copy_column(self, action):
''' slot connected to custom context menu """ slot connected to custom context menu
allows user to select a column and copy the data allows user to select a column and copy the data
to clipboard to clipboard
@param action: QAction* @param action: QAction*
''' """
_, item, mindex = action.data() _, item, model_index = action.data()
self._send_data_to_clipboard(item.data(mindex.column())) self.send_data_to_clipboard(item.data(model_index.column()))
def _slot_copy_row(self, action): def slot_copy_row(self, action):
''' slot connected to custom context menu """ slot connected to custom context menu
allows user to select a row and copy the space-delimeted allows user to select a row and copy the space-delimeted
data to clipboard data to clipboard
@param action: QAction* @param action: QAction*
''' """
_, item, _ = action.data() _, item, _ = action.data()
self._send_data_to_clipboard(str(item)) self.send_data_to_clipboard(str(item))
def _slot_rename_function(self, action): def slot_rename_function(self, action):
''' slot connected to custom context menu """ slot connected to custom context menu
allows user to select a edit a function name and push allows user to select a edit a function name and push
changes to IDA changes to IDA
@param action: QAction* @param action: QAction*
''' """
_, item, mindex = action.data() _, item, model_index = action.data()
# make item temporary edit, reset after user is finished # make item temporary edit, reset after user is finished
item.setIsEditable(True) item.setIsEditable(True)
self.edit(mindex) self.edit(model_index)
item.setIsEditable(False) item.setIsEditable(False)
def _slot_custom_context_menu_requested(self, pos): def slot_custom_context_menu_requested(self, pos):
''' slot connected to custom context menu request """ slot connected to custom context menu request
displays custom context menu to user containing action displays custom context menu to user containing action
relevant to the data item selected relevant to the data item selected
@param pos: TODO @param pos: TODO
''' """
mindex = self.indexAt(pos) model_index = self.indexAt(pos)
if not mindex.isValid(): if not model_index.isValid():
return return
item = self._map_index_to_source_item(mindex) item = self.map_index_to_source_item(model_index)
column = mindex.column() column = model_index.column()
menu = None menu = None
if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column and isinstance(item, CapaExplorerFunctionItem): if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column and isinstance(item, CapaExplorerFunctionItem):
# user hovered function item # user hovered function item
menu = self._load_function_item_context_menu(pos, item, mindex) menu = self.load_function_item_context_menu(pos, item, model_index)
else: else:
# user hovered default item # user hovered default item
menu = self._load_default_context_menu(pos, item, mindex) menu = self.load_default_context_menu(pos, item, model_index)
# show custom context menu at view position # show custom context menu at view position
self._show_custom_context_menu(menu, pos) self.show_custom_context_menu(menu, pos)
def _slot_click(self): def slot_click(self):
''' slot connected to single click event ''' """ slot connected to single click event """
pass pass
def _slot_double_click(self, mindex): def slot_double_click(self, model_index):
''' slot connected to double click event """ slot connected to double click event
@param mindex: QModelIndex* @param model_index: QModelIndex*
''' """
if not mindex.isValid(): if not model_index.isValid():
return return
item = self._map_index_to_source_item(mindex) item = self.map_index_to_source_item(model_index)
column = mindex.column() column = model_index.column()
if CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS == column: if CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS == column:
# user double-clicked virtual address column - navigate IDA to address # user double-clicked virtual address column - navigate IDA to address
@@ -275,7 +277,7 @@ class CapaExplorerQtreeView(QtWidgets.QTreeView):
if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column: if CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION == column:
# user double-clicked information column - un/expand # user double-clicked information column - un/expand
if self.isExpanded(mindex): if self.isExpanded(model_index):
self.collapse(mindex) self.collapse(model_index)
else: else:
self.expand(mindex) self.expand(model_index)

View File

@@ -2,24 +2,11 @@ import os
import logging import logging
import collections import collections
from PyQt5.QtWidgets import ( from PyQt5 import (
QHeaderView, QtWidgets,
QAbstractItemView, QtGui,
QMenuBar, QtCore
QAction,
QTabWidget,
QWidget,
QTextEdit,
QMenu,
QApplication,
QVBoxLayout,
QToolTip,
QCheckBox,
QTableWidget,
QTableWidgetItem
) )
from PyQt5.QtGui import QCursor, QIcon
from PyQt5.QtCore import Qt
import idaapi import idaapi
@@ -27,6 +14,7 @@ import capa.main
import capa.rules import capa.rules
import capa.features.extractors.ida import capa.features.extractors.ida
import capa.ida.helpers import capa.ida.helpers
import capa.render.utils as rutils
from capa.ida.explorer.view import CapaExplorerQtreeView from capa.ida.explorer.view import CapaExplorerQtreeView
from capa.ida.explorer.model import CapaExplorerDataModel from capa.ida.explorer.model import CapaExplorerDataModel
@@ -40,254 +28,286 @@ logger = logging.getLogger('capa')
class CapaExplorerIdaHooks(idaapi.UI_Hooks): class CapaExplorerIdaHooks(idaapi.UI_Hooks):
def __init__(self, screen_ea_changed_hook, action_hooks): def __init__(self, screen_ea_changed_hook, action_hooks):
''' facilitate IDA UI hooks """ facilitate IDA UI hooks
@param screen_ea_changed: TODO @param screen_ea_changed: TODO
@param action_hooks: TODO @param action_hooks: TODO
''' """
super(CapaExplorerIdaHooks, self).__init__() super(CapaExplorerIdaHooks, self).__init__()
self._screen_ea_changed_hook = screen_ea_changed_hook self.screen_ea_changed_hook = screen_ea_changed_hook
self._process_action_hooks = action_hooks self.process_action_hooks = action_hooks
self._process_action_handle = None self.process_action_handle = None
self._process_action_meta = {} self.process_action_meta = {}
def preprocess_action(self, name): def preprocess_action(self, name):
''' called prior to action completed """ called prior to action completed
@param name: name of action defined by idagui.cfg @param name: name of action defined by idagui.cfg
@retval must be 0 @retval must be 0
''' """
self._process_action_handle = self._process_action_hooks.get(name, None) self.process_action_handle = self.process_action_hooks.get(name, None)
if self._process_action_handle: if self.process_action_handle:
self._process_action_handle(self._process_action_meta) self.process_action_handle(self.process_action_meta)
# must return 0 for IDA # must return 0 for IDA
return 0 return 0
def postprocess_action(self): def postprocess_action(self):
''' called after action completed ''' """ called after action completed """
if not self._process_action_handle: if not self.process_action_handle:
return return
self._process_action_handle(self._process_action_meta, post=True) self.process_action_handle(self.process_action_meta, post=True)
self._reset() self.reset()
def screen_ea_changed(self, curr_ea, prev_ea): def screen_ea_changed(self, curr_ea, prev_ea):
''' called after screen ea is changed """ called after screen location is changed
@param curr_ea: current ea @param curr_ea: current location
@param prev_ea: prev ea @param prev_ea: prev location
''' """
self._screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea) self.screen_ea_changed_hook(idaapi.get_current_widget(), curr_ea, prev_ea)
def _reset(self): def reset(self):
''' reset internal state ''' """ reset internal state """
self._process_action_handle = None self.process_action_handle = None
self._process_action_meta.clear() self.process_action_meta.clear()
class CapaExplorerForm(idaapi.PluginForm): class CapaExplorerForm(idaapi.PluginForm):
def __init__(self): def __init__(self):
''' ''' """ """
super(CapaExplorerForm, self).__init__() super(CapaExplorerForm, self).__init__()
self.form_title = PLUGIN_NAME self.form_title = PLUGIN_NAME
self.parent = None self.parent = None
self._file_loc = __file__ self.file_loc = __file__
self._ida_hooks = None self.ida_hooks = None
# models # models
self._model_data = None self.model_data = None
self._model_proxy = None self.model_proxy = None
# user interface elements # user interface elements
self._view_limit_results_by_function = None self.view_limit_results_by_function = None
self._view_tree = None self.view_tree = None
self._view_summary = None self.view_summary = None
self._view_tabs = None self.view_attack = None
self._view_menu_bar = None self.view_tabs = None
self.view_menu_bar = None
def OnCreate(self, form): def OnCreate(self, form):
''' ''' """ """
self.parent = self.FormToPyQtWidget(form) self.parent = self.FormToPyQtWidget(form)
self._load_interface() self.load_interface()
self._load_capa_results() self.load_capa_results()
self._load_ida_hooks() self.load_ida_hooks()
self._view_tree.reset() self.view_tree.reset()
logger.info('form created.') logger.info('form created.')
def Show(self): def Show(self):
''' ''' """ """
return idaapi.PluginForm.Show(self, self.form_title, options=( return idaapi.PluginForm.Show(self, self.form_title, options=(
idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WCLS_CLOSE_LATER idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WCLS_CLOSE_LATER
)) ))
def OnClose(self, form): def OnClose(self, form):
''' form is closed ''' """ form is closed """
self._unload_ida_hooks() self.unload_ida_hooks()
self._ida_reset() self.ida_reset()
logger.info('form closed.') logger.info('form closed.')
def _load_interface(self): def load_interface(self):
''' load user interface ''' """ load user interface """
# load models # load models
self._model_data = CapaExplorerDataModel() self.model_data = CapaExplorerDataModel()
self._model_proxy = CapaExplorerSortFilterProxyModel() self.model_proxy = CapaExplorerSortFilterProxyModel()
self._model_proxy.setSourceModel(self._model_data) self.model_proxy.setSourceModel(self.model_data)
# load tree # load tree
self._view_tree = CapaExplorerQtreeView(self._model_proxy, self.parent) self.view_tree = CapaExplorerQtreeView(self.model_proxy, self.parent)
# load summary table # load summary table
self._load_view_summary() self.load_view_summary()
self.load_view_attack()
# load parent tab and children tab views # load parent tab and children tab views
self._load_view_tabs() self.load_view_tabs()
self._load_view_checkbox_limit_by() self.load_view_checkbox_limit_by()
self._load_view_summary_tab() self.load_view_summary_tab()
self._load_view_tree_tab() self.load_view_attack_tab()
self.load_view_tree_tab()
# load menu bar and sub menus # load menu bar and sub menus
self._load_view_menu_bar() self.load_view_menu_bar()
self._load_file_menu() self.load_file_menu()
# load parent view # load parent view
self._load_view_parent() self.load_view_parent()
def _load_view_tabs(self): def load_view_tabs(self):
''' ''' """ """
tabs = QTabWidget() tabs = QtWidgets.QTabWidget()
self.view_tabs = tabs
self._view_tabs = tabs def load_view_menu_bar(self):
""" """
bar = QtWidgets.QMenuBar()
self.view_menu_bar = bar
def _load_view_menu_bar(self): def load_view_summary(self):
''' ''' """ """
bar = QMenuBar() table_headers = [
# bar.hovered.connect(self._slot_menu_bar_hovered) 'Capability',
'Namespace',
]
self._view_menu_bar = bar table = QtWidgets.QTableWidget()
def _load_view_summary(self): table.setColumnCount(len(table_headers))
''' '''
table = QTableWidget()
table.setColumnCount(4)
table.verticalHeader().setVisible(False) table.verticalHeader().setVisible(False)
table.setSortingEnabled(False) table.setSortingEnabled(False)
table.setEditTriggers(QAbstractItemView.NoEditTriggers) table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
table.setFocusPolicy(Qt.NoFocus) table.setFocusPolicy(QtCore.Qt.NoFocus)
table.setSelectionMode(QAbstractItemView.NoSelection) table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
table.setHorizontalHeaderLabels([ table.setHorizontalHeaderLabels(table_headers)
'Objectives', table.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft)
'Behaviors',
'Techniques',
'Rule Hits'
])
table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
table.setStyleSheet('QTableWidget::item { border: none; padding: 15px; }')
table.setShowGrid(False) table.setShowGrid(False)
table.setStyleSheet('QTableWidget::item { padding: 25px; }')
self._view_summary = table self.view_summary = table
def _load_view_checkbox_limit_by(self): def load_view_attack(self):
''' ''' """ """
check = QCheckBox('Limit results to current function') table_headers = [
'ATT&CK Tactic',
'ATT&CK Technique ',
]
table = QtWidgets.QTableWidget()
table.setColumnCount(len(table_headers))
table.verticalHeader().setVisible(False)
table.setSortingEnabled(False)
table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
table.setFocusPolicy(QtCore.Qt.NoFocus)
table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
table.setHorizontalHeaderLabels(table_headers)
table.horizontalHeader().setDefaultAlignment(QtCore.Qt.AlignLeft)
table.setShowGrid(False)
table.setStyleSheet('QTableWidget::item { padding: 25px; }')
self.view_attack = table
def load_view_checkbox_limit_by(self):
""" """
check = QtWidgets.QCheckBox('Limit results to current function')
check.setChecked(False) check.setChecked(False)
check.stateChanged.connect(self._slot_checkbox_limit_by_changed) check.stateChanged.connect(self.slot_checkbox_limit_by_changed)
self._view_checkbox_limit_by = check self.view_checkbox_limit_by = check
def _load_view_parent(self): def load_view_parent(self):
''' load view parent ''' """ load view parent """
layout = QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(self._view_tabs) layout.addWidget(self.view_tabs)
layout.setMenuBar(self._view_menu_bar) layout.setMenuBar(self.view_menu_bar)
self.parent.setLayout(layout) self.parent.setLayout(layout)
def _load_view_tree_tab(self): def load_view_tree_tab(self):
''' load view tree tab ''' """ load view tree tab """
layout = QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(self._view_checkbox_limit_by) layout.addWidget(self.view_checkbox_limit_by)
layout.addWidget(self._view_tree) layout.addWidget(self.view_tree)
tab = QWidget() tab = QtWidgets.QWidget()
tab.setLayout(layout) tab.setLayout(layout)
self._view_tabs.addTab(tab, 'Tree View') self.view_tabs.addTab(tab, 'Tree View')
def _load_view_summary_tab(self): def load_view_summary_tab(self):
''' ''' """ """
layout = QVBoxLayout() layout = QtWidgets.QVBoxLayout()
layout.addWidget(self._view_summary) layout.addWidget(self.view_summary)
tab = QWidget() tab = QtWidgets.QWidget()
tab.setLayout(layout) tab.setLayout(layout)
self._view_tabs.addTab(tab, 'Summary') self.view_tabs.addTab(tab, 'Summary')
def _load_file_menu(self): def load_view_attack_tab(self):
''' load file menu actions ''' """ """
layout = QtWidgets.QVBoxLayout()
layout.addWidget(self.view_attack)
tab = QtWidgets.QWidget()
tab.setLayout(layout)
self.view_tabs.addTab(tab, 'MITRE')
def load_file_menu(self):
""" load file menu actions """
actions = ( actions = (
('Reset view', 'Reset plugin view', self.reset), ('Reset view', 'Reset plugin view', self.reset),
('Run analysis', 'Run capa analysis on current database', self.reload), ('Run analysis', 'Run capa analysis on current database', self.reload),
) )
menu = self._view_menu_bar.addMenu('File') menu = self.view_menu_bar.addMenu('File')
for name, _, handle in actions: for name, _, handle in actions:
action = QAction(name, self.parent) action = QtWidgets.QAction(name, self.parent)
action.triggered.connect(handle) action.triggered.connect(handle)
# action.setToolTip(tip) # action.setToolTip(tip)
menu.addAction(action) menu.addAction(action)
def _load_ida_hooks(self): def load_ida_hooks(self):
''' ''' """ """
action_hooks = { action_hooks = {
'MakeName': self._ida_hook_rename, 'MakeName': self.ida_hook_rename,
'EditFunction': self._ida_hook_rename, 'EditFunction': self.ida_hook_rename,
} }
self._ida_hooks = CapaExplorerIdaHooks(self._ida_hook_screen_ea_changed, action_hooks) self.ida_hooks = CapaExplorerIdaHooks(self.ida_hook_screen_ea_changed, action_hooks)
self._ida_hooks.hook() self.ida_hooks.hook()
def _unload_ida_hooks(self): def unload_ida_hooks(self):
''' unhook IDA user interface ''' """ unhook IDA user interface """
if self._ida_hooks: if self.ida_hooks:
self._ida_hooks.unhook() self.ida_hooks.unhook()
def _ida_hook_rename(self, meta, post=False): def ida_hook_rename(self, meta, post=False):
''' hook for IDA rename action """ hook for IDA rename action
called twice, once before action and once after called twice, once before action and once after
action completes action completes
@param meta: TODO @param meta: TODO
@param post: TODO @param post: TODO
''' """
ea = idaapi.get_screen_ea() location = idaapi.get_screen_ea()
if not ea or not capa.ida.helpers.is_func_start(ea): if not location or not capa.ida.helpers.is_func_start(location):
return return
curr_name = idaapi.get_name(ea) curr_name = idaapi.get_name(location)
if post: if post:
# post action update data model w/ current name # post action update data model w/ current name
self._model_data.update_function_name(meta.get('prev_name', ''), curr_name) self.model_data.update_function_name(meta.get('prev_name', ''), curr_name)
else: else:
# pre action so save current name for replacement later # pre action so save current name for replacement later
meta['prev_name'] = curr_name meta['prev_name'] = curr_name
def _ida_hook_screen_ea_changed(self, widget, new_ea, old_ea): def ida_hook_screen_ea_changed(self, widget, new_ea, old_ea):
''' ''' """ """
if not self._view_checkbox_limit_by.isChecked(): if not self.view_checkbox_limit_by.isChecked():
# ignore if checkbox not selected # ignore if checkbox not selected
return return
@@ -311,10 +331,10 @@ class CapaExplorerForm(idaapi.PluginForm):
match = '' match = ''
# filter on virtual address to avoid updating filter string if function name is changed # filter on virtual address to avoid updating filter string if function name is changed
self._model_proxy.add_single_string_filter(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS, match) self.model_proxy.add_single_string_filter(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS, match)
def _load_capa_results(self): def load_capa_results(self):
''' ''' """ """
logger.info('-' * 80) logger.info('-' * 80)
logger.info(' Using default embedded rules.') logger.info(' Using default embedded rules.')
logger.info(' ') logger.info(' ')
@@ -322,7 +342,7 @@ class CapaExplorerForm(idaapi.PluginForm):
logger.info(' https://github.com/fireeye/capa-rules') logger.info(' https://github.com/fireeye/capa-rules')
logger.info('-' * 80) logger.info('-' * 80)
rules_path = os.path.join(os.path.dirname(self._file_loc), '../..', 'rules') rules_path = os.path.join(os.path.dirname(self.file_loc), '../..', 'rules')
rules = capa.main.get_rules(rules_path) rules = capa.main.get_rules(rules_path)
rules = capa.rules.RuleSet(rules) rules = capa.rules.RuleSet(rules)
capabilities = capa.main.find_capabilities(rules, capa.features.extractors.ida.IdaFeatureExtractor(), True) capabilities = capa.main.find_capabilities(rules, capa.features.extractors.ida.IdaFeatureExtractor(), True)
@@ -343,122 +363,154 @@ class CapaExplorerForm(idaapi.PluginForm):
capa.ida.helpers.inform_user_ida_ui('capa encountered warnings during analysis') capa.ida.helpers.inform_user_ida_ui('capa encountered warnings during analysis')
if capa.main.is_file_limitation(rules, capabilities, is_standalone=False): if capa.main.has_file_limitation(rules, capabilities, is_standalone=False):
capa.ida.helpers.inform_user_ida_ui('capa encountered warnings during analysis') capa.ida.helpers.inform_user_ida_ui('capa encountered warnings during analysis')
logger.info('analysis completed.') logger.info('analysis completed.')
self._model_data.render_capa_results(rules, capabilities) doc = capa.render.convert_capabilities_to_result_document(rules, capabilities)
self._render_capa_summary(rules, capabilities)
self._view_tree.sortByColumn(CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION, Qt.AscendingOrder) import json
with open("C:\\Users\\spring\\Desktop\\hmm.json", "w") as twitter_data_file:
json.dump(doc, twitter_data_file, indent=4, sort_keys=True, cls=capa.render.CapaJsonObjectEncoder)
self.model_data.render_capa_doc(doc)
self.render_capa_doc_summary(doc)
self.render_capa_doc_mitre_summary(doc)
self.view_tree.sortByColumn(CapaExplorerDataModel.COLUMN_INDEX_RULE_INFORMATION, QtCore.Qt.AscendingOrder)
logger.info('render views completed.') logger.info('render views completed.')
def _render_capa_summary(self, ruleset, results): def render_capa_doc_summary(self, doc):
''' render results summary table """ """
for (row, rule) in enumerate(rutils.capability_rules(doc)):
count = len(rule['matches'])
keep sync with capa.main if count == 1:
capability = rule['meta']['name']
else:
capability = '%s (%d matches)' % (rule['meta']['name'], count)
@param ruleset: TODO self.view_summary.setRowCount(row + 1)
@param results: TODO
'''
rules = set(filter(lambda x: not ruleset.rules[x].meta.get('lib', False), results.keys()))
objectives = set()
behaviors = set()
techniques = set()
for rule in rules: self.view_summary.setItem(row, 0, self.render_new_table_header_item(capability))
parts = ruleset.rules[rule].meta.get(capa.main.RULE_CATEGORY, '').split('/') self.view_summary.setItem(row, 1, QtWidgets.QTableWidgetItem(rule['meta']['namespace']))
if len(parts) == 0 or list(parts) == ['']:
continue
if len(parts) > 0:
objective = parts[0].replace('-', ' ')
objectives.add(objective)
if len(parts) > 1:
behavior = parts[1].replace('-', ' ')
behaviors.add(behavior)
if len(parts) > 2:
technique = parts[2].replace('-', ' ')
techniques.add(technique)
if len(parts) > 3:
raise capa.rules.InvalidRule(capa.main.RULE_CATEGORY + ' tag must have at most three components')
# set row count to max set size
self._view_summary.setRowCount(max(map(len, (rules, objectives, behaviors, techniques))))
# format rule hits
rules = map(lambda x: '%s (%d)' % (x, len(results[x])), rules)
# sort results
columns = list(map(lambda x: sorted(x, key=lambda s: s.lower()), (objectives, behaviors, techniques, rules)))
# load results into table by column
for idx, column in enumerate(columns):
self._load_view_summary_column(idx, column)
# resize columns to content # resize columns to content
self._view_summary.resizeColumnsToContents() self.view_summary.resizeColumnsToContents()
def _load_view_summary_column(self, column, texts): def render_capa_doc_mitre_summary(self, doc):
''' ''' """ """
for row, text in enumerate(texts): tactics = collections.defaultdict(set)
self._view_summary.setItem(row, column, QTableWidgetItem(text)) for rule in rutils.capability_rules(doc):
if not rule['meta'].get('att&ck'):
continue
def _ida_reset(self): for attack in rule['meta']['att&ck']:
''' reset IDA user interface ''' tactic, _, rest = attack.partition('::')
self._model_data.reset() if '::' in rest:
self._view_tree.reset() technique, _, rest = rest.partition('::')
self._view_checkbox_limit_by.setChecked(False) subtechnique, _, id = rest.rpartition(' ')
tactics[tactic].add((technique, subtechnique, id))
else:
technique, _, id = rest.rpartition(' ')
tactics[tactic].add((technique, id))
column_one = []
column_two = []
for tactic, techniques in sorted(tactics.items()):
column_one.append(tactic.upper())
column_one.extend(['' for i in range(len(techniques) - 1)])
for spec in sorted(techniques):
if len(spec) == 2:
technique, id = spec
column_two.append('%s %s' % (technique, id))
elif len(spec) == 3:
technique, subtechnique, id = spec
column_two.append('%s::%s %s' % (technique, subtechnique, id))
else:
raise RuntimeError('unexpected ATT&CK spec format')
self.view_attack.setRowCount(max(len(column_one), len(column_two)))
for row, value in enumerate(column_one):
self.view_attack.setItem(row, 0, self.render_new_table_header_item(value))
for row, value in enumerate(column_two):
self.view_attack.setItem(row, 1, QtWidgets.QTableWidgetItem(value))
# resize columns to content
self.view_attack.resizeColumnsToContents()
def render_new_table_header_item(self, text):
""" """
item = QtWidgets.QTableWidgetItem(text)
item.setForeground(QtGui.QColor(88, 139, 174))
font = QtGui.QFont()
font.setBold(True)
item.setFont(font)
return item
def ida_reset(self):
""" reset IDA user interface """
self.model_data.reset()
self.view_tree.reset()
self.view_checkbox_limit_by.setChecked(False)
def reload(self): def reload(self):
''' reload views and re-run capa analysis ''' """ reload views and re-run capa analysis """
self._ida_reset() self.ida_reset()
self._model_proxy.invalidate() self.model_proxy.invalidate()
self._model_data.clear() self.model_data.clear()
self._view_summary.setRowCount(0) self.view_summary.setRowCount(0)
self._load_capa_results() self.load_capa_results()
logger.info('reload complete.') logger.info('reload complete.')
idaapi.info('%s reload completed.' % PLUGIN_NAME) idaapi.info('%s reload completed.' % PLUGIN_NAME)
def reset(self): def reset(self):
''' reset user interface elements """ reset user interface elements
e.g. checkboxes and IDA highlighting e.g. checkboxes and IDA highlighting
''' """
self._ida_reset() self.ida_reset()
logger.info('reset completed.') logger.info('reset completed.')
idaapi.info('%s reset completed.' % PLUGIN_NAME) idaapi.info('%s reset completed.' % PLUGIN_NAME)
def _slot_menu_bar_hovered(self, action): def slot_menu_bar_hovered(self, action):
''' display menu action tooltip """ display menu action tooltip
@param action: QAction* @param action: QtWidgets.QAction*
@reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu @reference: https://stackoverflow.com/questions/21725119/why-wont-qtooltips-appear-on-qactions-within-a-qmenu
''' """
QToolTip.showText(QCursor.pos(), action.toolTip(), self._view_menu_bar, self._view_menu_bar.actionGeometry(action)) QtWidgets.QToolTip.showText(QtGui.QCursor.pos(), action.toolTip(), self.view_menu_bar, self.view_menu_bar.actionGeometry(action))
def _slot_checkbox_limit_by_changed(self): def slot_checkbox_limit_by_changed(self):
''' slot activated if checkbox clicked """ slot activated if checkbox clicked
if checked, configure function filter if screen ea is located if checked, configure function filter if screen location is located
in function, otherwise clear filter in function, otherwise clear filter
''' """
match = '' match = ''
if self._view_checkbox_limit_by.isChecked(): if self.view_checkbox_limit_by.isChecked():
ea = capa.ida.helpers.get_func_start_ea(idaapi.get_screen_ea()) location = capa.ida.helpers.get_func_start_ea(idaapi.get_screen_ea())
if ea: if location:
match = capa.ida.explorer.item.ea_to_hex_str(ea) match = capa.ida.explorer.item.location_to_hex(location)
self._model_proxy.add_single_string_filter(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS, match) self.model_proxy.add_single_string_filter(CapaExplorerDataModel.COLUMN_INDEX_VIRTUAL_ADDRESS, match)
self._view_tree.resize_columns_to_content() self.view_tree.resize_columns_to_content()
def main(): def main():
''' TODO: move to idaapi.plugin_t class ''' """ TODO: move to idaapi.plugin_t class """
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
if not capa.ida.helpers.is_supported_file_type(): if not capa.ida.helpers.is_supported_file_type():