From 0e73aadf4e787df9e376108eb82e297626045b11 Mon Sep 17 00:00:00 2001 From: Rusty Wagner Date: Wed, 13 Mar 2019 11:26:05 -0400 Subject: Initial example triage UI plugin --- python/examples/triage/__init__.py | 1 + python/examples/triage/entropy.py | 91 ++++++++++++++++++++++ python/examples/triage/headers.py | 155 +++++++++++++++++++++++++++++++++++++ python/examples/triage/imports.py | 117 ++++++++++++++++++++++++++++ python/examples/triage/view.py | 114 +++++++++++++++++++++++++++ 5 files changed, 478 insertions(+) create mode 100644 python/examples/triage/__init__.py create mode 100644 python/examples/triage/entropy.py create mode 100644 python/examples/triage/headers.py create mode 100644 python/examples/triage/imports.py create mode 100644 python/examples/triage/view.py (limited to 'python/examples') diff --git a/python/examples/triage/__init__.py b/python/examples/triage/__init__.py new file mode 100644 index 00000000..90cdbd77 --- /dev/null +++ b/python/examples/triage/__init__.py @@ -0,0 +1 @@ +from . import view diff --git a/python/examples/triage/entropy.py b/python/examples/triage/entropy.py new file mode 100644 index 00000000..03249968 --- /dev/null +++ b/python/examples/triage/entropy.py @@ -0,0 +1,91 @@ +import math +import threading +from PySide2.QtWidgets import QWidget +from PySide2.QtGui import QImage, QColor, QPainter +from PySide2.QtCore import Qt, QSize, QTimer +from binaryninjaui import ViewFrame + + +class EntropyThread(threading.Thread): + def __init__(self, data, image, block_size): + super(EntropyThread, self).__init__() + self.data = data + self.image = image + self.block_size = block_size + self.updated = False + + def run(self): + width = self.image.width() + for i in range(0, width): + block = self.data.read(self.data.start + i * self.block_size, self.block_size) + if len(block) == 0: + v = 0 + else: + dist = [0] * 0x100 + for j in range(0, len(block)): + value = ord(block[j:j+1]) + dist[value] += 1 + s = 0 + for j in range(0, 256): + if dist[j] != 0: + s += (float(dist[j]) / len(block)) * math.log(float(dist[j]) / len(block)) + s = s / math.log(1 / 256.0) + v = int(s * 255) + if v >= 240: + self.image.setPixelColor(i, 0, QColor(v, v, v / 4, 255)) + else: + self.image.setPixelColor(i, 0, QColor(v / 4, v / 4, v, 255)) + self.updated = True + + +class EntropyWidget(QWidget): + def __init__(self, parent, data): + super(EntropyWidget, self).__init__(parent) + self.data = data + self.raw_data = data.file.raw + + self.block_size = 1024 + self.width = int(len(self.raw_data) / self.block_size) + self.image = QImage(self.width, 1, QImage.Format_ARGB32) + self.image.fill(QColor(0, 0, 0, 0)) + + self.thread = EntropyThread(self.raw_data, self.image, self.block_size) + self.started = False + + self.timer = QTimer() + self.timer.timeout.connect(self.timerEvent) + self.timer.setInterval(250) + self.timer.setSingleShot(False) + self.timer.start() + + self.setMinimumHeight(32) + + def paintEvent(self, event): + p = QPainter(self) + p.drawImage(self.rect(), self.image) + p.drawRect(self.rect()) + + def sizeHint(self): + return QSize(640, 32) + + def timerEvent(self): + if not self.started: + self.thread.start() + self.started = True + if self.thread.updated: + self.thread.updated = False + self.update() + + def mousePressEvent(self, event): + if event.button() != Qt.LeftButton: + return + frac = float(event.x()) / self.rect().width() + offset = int(frac * self.width * self.block_size) + addr = self.data.get_address_for_data_offset(offset) + view_frame = ViewFrame.viewFrameForWidget(self) + if view_frame is None: + return + if addr is None: + view_frame.navigate("Hex:Raw", offset) + else: + view_frame.navigate("Linear:" + view_frame.getCurrentDataType(), addr) diff --git a/python/examples/triage/headers.py b/python/examples/triage/headers.py new file mode 100644 index 00000000..d800086c --- /dev/null +++ b/python/examples/triage/headers.py @@ -0,0 +1,155 @@ +import time +from binaryninja.binaryview import StructuredDataView +from PySide2.QtWidgets import QWidget, QLabel, QGridLayout + + +class PEHeaders(object): + def __init__(self, data): + dos = StructuredDataView(data, "DOS_Header", data.start) + pe_offset = data.start + int(dos.e_lfanew) + coff = StructuredDataView(data, "COFF_Header", pe_offset) + pe_magic = data.read(pe_offset + len(coff), 2) + self.fields = [] + if pe_magic == b"\x0b\x01": + peopt = StructuredDataView(data, "PE32_Optional_Header", pe_offset + len(coff)) + self.fields.append(("Type", "PE 32-bit")) + is64bit = False + elif pe_magic == b"\x0b\x02": + peopt = StructuredDataView(data, "PE64_Optional_Header", pe_offset + len(coff)) + self.fields.append(("Type", "PE 64-bit")) + is64bit = True + + machine_value = int(coff.machine) + machine_enum = data.get_type_by_name("coff_machine") + machine_name = str(machine_value) + for member in machine_enum.enumeration.members: + if member.value == machine_value: + machine_name = member.name + if machine_name.startswith("IMAGE_FILE_MACHINE_"): + machine_name = machine_name[len("IMAGE_FILE_MACHINE_"):] + self.fields.append(("Machine", machine_name)) + + subsys_value = int(peopt.subsystem) + subsys_enum = data.get_type_by_name("pe_subsystem") + subsys_name = str(subsys_value) + for member in subsys_enum.enumeration.members: + if member.value == subsys_value: + subsys_name = member.name + if subsys_name.startswith("IMAGE_SUBSYSTEM_"): + subsys_name = subsys_name[len("IMAGE_SUBSYSTEM_"):] + self.fields.append(("Subsystem", subsys_name)) + + self.fields.append(("Timestamp", time.strftime("%c", time.localtime(int(coff.timeDateStamp))))) + + base = int(peopt.imageBase) + self.fields.append(("Image Base", "0x%x" % base)) + + entry_point = base + int(peopt.addressOfEntryPoint) + self.fields.append(("Entry Point", "0x%x" % entry_point)) + + section_align = int(peopt.sectionAlignment) + self.fields.append(("Section Alignment", "0x%x" % section_align)) + + file_align = int(peopt.fileAlignment) + self.fields.append(("File Alignment", "0x%x" % file_align)) + + checksum = int(peopt.checkSum) + self.fields.append(("Checksum", "0x%.8x" % checksum)) + + code_base = base + int(peopt.baseOfCode) + self.fields.append(("Base of Code", "0x%x" % code_base)) + + if not is64bit: + data_base = base + int(peopt.baseOfData) + self.fields.append(("Base of Data", "0x%x" % data_base)) + + code_size = int(peopt.sizeOfCode) + self.fields.append(("Size of Code", "0x%x" % code_size)) + + init_data_size = int(peopt.sizeOfInitializedData) + self.fields.append(("Size of Init Data", "0x%x" % init_data_size)) + + uninit_data_size = int(peopt.sizeOfUninitializedData) + self.fields.append(("Size of Uninit Data", "0x%x" % uninit_data_size)) + + header_size = int(peopt.sizeOfHeaders) + self.fields.append(("Size of Headers", "0x%x" % header_size)) + + image_size = int(peopt.sizeOfImage) + self.fields.append(("Size of Image", "0x%x" % image_size)) + + stack_commit = int(peopt.sizeOfStackCommit) + stack_reserve = int(peopt.sizeOfStackReserve) + self.fields.append(("Stack Size", "0x%x / 0x%x" % (stack_commit, stack_reserve))) + + heap_commit = int(peopt.sizeOfHeapCommit) + heap_reserve = int(peopt.sizeOfHeapReserve) + self.fields.append(("Heap Size", "0x%x / 0x%x" % (heap_commit, heap_reserve))) + + linker_major = int(peopt.majorLinkerVersion) + linker_minor = int(peopt.minorLinkerVersion) + self.fields.append(("Linker Version", "%d.%.2d" % (linker_major, linker_minor))) + + image_major = int(peopt.majorImageVersion) + image_minor = int(peopt.minorImageVersion) + self.fields.append(("Image Version", "%d.%.2d" % (image_major, image_minor))) + + os_major = int(peopt.majorOperatingSystemVersion) + os_minor = int(peopt.minorOperatingSystemVersion) + self.fields.append(("OS Version", "%d.%.2d" % (os_major, os_minor))) + + sub_major = int(peopt.majorSubsystemVersion) + sub_minor = int(peopt.minorSubsystemVersion) + self.fields.append(("Subsystem Version", "%d.%.2d" % (sub_major, sub_minor))) + + coff_char_value = int(coff.characteristics) + coff_char_enum = data.get_type_by_name("coff_characteristics") + coff_char_values = [] + for member in coff_char_enum.enumeration.members: + if (coff_char_value & member.value) != 0: + if member.name.startswith("IMAGE_FILE_"): + coff_char_values.append(member.name[len("IMAGE_FILE_"):]) + else: + coff_char_values.append(member.name) + if len(coff_char_values) > 0: + self.fields.append(("COFF Characteristics", coff_char_values)) + + dll_char_value = int(peopt.dllCharacteristics) + dll_char_enum = data.get_type_by_name("pe_dll_characteristics") + dll_char_values = [] + for member in dll_char_enum.enumeration.members: + if (dll_char_value & member.value) != 0: + if member.name.startswith("IMAGE_DLLCHARACTERISTICS_"): + dll_char_values.append(member.name[len("IMAGE_DLLCHARACTERISTICS_"):]) + else: + dll_char_values.append(member.name) + if len(dll_char_values) > 0: + self.fields.append(("DLL Characteristics", dll_char_values)) + + +class HeaderWidget(QWidget): + def __init__(self, parent, header): + super(HeaderWidget, self).__init__(parent) + layout = QGridLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setVerticalSpacing(1) + row = 0 + col = 0 + for field in header.fields: + name = field[0] + value = field[1] + layout.addWidget(QLabel(name + ": "), row, col) + if isinstance(value, list): + for i in range(0, len(value)): + layout.addWidget(QLabel(value[i]), row, col + 1) + row += 1 + else: + layout.addWidget(QLabel(value), row, col + 1) + row += 1 + if (row >= 9) and (col < 6): + row = 0 + col += 3 + layout.setColumnMinimumWidth(2, 20) + layout.setColumnMinimumWidth(5, 20) + layout.setColumnStretch(8, 1) + self.setLayout(layout) diff --git a/python/examples/triage/imports.py b/python/examples/triage/imports.py new file mode 100644 index 00000000..2b2ed253 --- /dev/null +++ b/python/examples/triage/imports.py @@ -0,0 +1,117 @@ +from PySide2.QtWidgets import QTreeView +from PySide2.QtCore import Qt, QAbstractItemModel, QModelIndex +from binaryninja.enums import SymbolType +import binaryninjaui + + +class GenericImportsModel(QAbstractItemModel): + def __init__(self, data): + super(GenericImportsModel, self).__init__() + self.entries = [] + self.has_modules = False + self.name_col = 1 + self.module_col = None + self.total_cols = 2 + for sym in data.get_symbols_of_type(SymbolType.ImportAddressSymbol): + self.entries.append(sym) + if str(sym.namespace) != "BNINTERNALNAMESPACE": + self.has_modules = True + if self.has_modules: + self.name_col = 2 + self.module_col = 1 + self.total_cols = 3 + + def columnCount(self, parent): + return self.total_cols + + def rowCount(self, parent): + return len(self.entries) + + def data(self, index, role): + if role != Qt.DisplayRole: + return None + if index.row() >= len(self.entries): + return None + if index.column() == 0: + return "0x%x" % self.entries[index.row()].address + if index.column() == self.name_col: + name = self.entries[index.row()].name + if name.endswith("@GOT"): + name = name[:-len("@GOT")] + elif name.endswith("@PLT"): + name = name[:-len("@PLT")] + elif name.endswith("@IAT"): + name = name[:-len("@IAT")] + return name + if index.column() == self.module_col: + return self.getNamespace(self.entries[index.row()]) + return None + + def headerData(self, section, orientation, role): + if orientation == Qt.Vertical: + return None + if role != Qt.DisplayRole: + return None + if section == 0: + return "Entry" + if section == self.name_col: + return "Name" + if section == self.module_col: + return "Module" + return None + + def index(self, row, col, parent): + if parent.isValid(): + return QModelIndex() + if row >= len(self.entries): + return QModelIndex() + if col >= self.total_cols: + return QModelIndex() + return self.createIndex(row, col) + + def parent(self, index): + return QModelIndex() + + def getSymbol(self, index): + if index.row() >= len(self.entries): + return None + return self.entries[index.row()] + + def getNamespace(self, sym): + name = str(sym.namespace) + if name == "BNINTERNALNAMESPACE": + return "" + return name + + def sort(self, col, order): + self.beginResetModel() + if col == 0: + self.entries.sort(key = lambda sym: sym.address, reverse = order != Qt.AscendingOrder) + elif col == self.name_col: + self.entries.sort(key = lambda sym: sym.name, reverse = order != Qt.AscendingOrder) + elif col == self.module_col: + self.entries.sort(key = lambda sym: self.getNamespace(sym), reverse = order != Qt.AscendingOrder) + self.endResetModel() + + +class ImportsWidget(QTreeView): + def __init__(self, parent, view, data): + super(ImportsWidget, self).__init__(parent) + self.data = data + self.view = view + + self.model = GenericImportsModel(self.data) + self.setModel(self.model) + self.setRootIsDecorated(False) + self.setUniformRowHeights(True) + self.setSortingEnabled(True) + self.sortByColumn(0, Qt.AscendingOrder) + + self.setFont(binaryninjaui.getMonospaceFont(self)) + + self.clicked.connect(self.importSelected) + + def importSelected(self, index): + sym = self.model.getSymbol(index) + if sym is not None: + self.view.setCurrentOffset(sym.address) diff --git a/python/examples/triage/view.py b/python/examples/triage/view.py new file mode 100644 index 00000000..e06776ca --- /dev/null +++ b/python/examples/triage/view.py @@ -0,0 +1,114 @@ +import traceback +import binaryninjaui +from binaryninja.settings import Settings +from binaryninja import log +from binaryninjaui import View, ViewType, UIContext +from PySide2.QtWidgets import QScrollArea, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QGroupBox +from . import headers +from . import entropy +from . import imports + + +class TriageView(QScrollArea, View): + def __init__(self, parent, data): + QScrollArea.__init__(self, parent) + View.__init__(self) + self.setupView(self) + self.data = data + self.currentOffset = 0 + + container = QWidget(self) + layout = QVBoxLayout() + + entropyGroup = QGroupBox("Entropy") + entropyLayout = QVBoxLayout() + entropyLayout.addWidget(entropy.EntropyWidget(entropyGroup, self.data)) + entropyGroup.setLayout(entropyLayout) + layout.addWidget(entropyGroup) + + hdr = None + try: + if self.data.view_type == "PE": + hdr = headers.PEHeaders(self.data) + except: + log.log_error(traceback.format_exc()) + + if hdr is not None: + headerGroup = QGroupBox("Headers") + headerLayout = QVBoxLayout() + headerWidget = headers.HeaderWidget(headerGroup, hdr) + headerLayout.addWidget(headerWidget) + headerGroup.setLayout(headerLayout) + layout.addWidget(headerGroup) + + importGroup = QGroupBox("Imports") + importLayout = QVBoxLayout() + importLayout.addWidget(imports.ImportsWidget(importGroup, self, self.data)) + importGroup.setLayout(importLayout) + layout.addWidget(importGroup) + + button_layout = QHBoxLayout() + button_layout.addStretch(1) + self.full_analysis_button = QPushButton("Start Full Analysis") + self.full_analysis_button.clicked.connect(self.startFullAnalysis) + button_layout.addWidget(self.full_analysis_button) + layout.addLayout(button_layout) + + layout.addStretch(1) + container.setLayout(layout) + self.setWidgetResizable(True) + self.setWidget(container) + + if Settings().get_string("analysis.mode", data) == "full": + self.full_analysis_button.hide() + + def getData(self): + return self.data + + def getCurrentOffset(self): + return self.currentOffset + + def setCurrentOffset(self, offset): + self.currentOffset = offset + UIContext.updateStatus(True) + + def getFont(self): + return binaryninjaui.getMonospaceFont(self) + + def navigate(self, addr): + return False + + def startFullAnalysis(self): + Settings().set_string("analysis.mode", "full", self.data) + for f in self.data.functions: + if f.analysis_skipped: + f.reanalyze() + self.data.update_analysis() + self.full_analysis_button.hide() + + +class TriageViewType(ViewType): + def __init__(self): + super(TriageViewType, self).__init__("Triage", "Triage Summary") + + def getPriority(self, data, filename): + is_full = Settings().get_string("analysis.mode", data) == "full" + always_prefer = Settings().get_bool("ui.always_prefer_triage", data) + if data.executable and (always_prefer or not is_full): + return 100 + return 25 + + def create(self, data, view_frame): + return TriageView(view_frame, data) + + +Settings().register_setting("ui.always_prefer_triage", """ + { + "title" : "Always Prefer Triage Summary View", + "type" : "boolean", + "default" : false, + "description" : "Always prefer opening binaries in Triage Summary view, even when performing full analysis." + } + """) + +ViewType.registerViewType(TriageViewType()) -- cgit v1.3.1