From d627dc11c5c7d734a1e21b7efb6e7f907df74dc6 Mon Sep 17 00:00:00 2001 From: xarkes Date: Sun, 10 Feb 2019 22:33:36 +0100 Subject: [PATCH 01/18] First pass to add Cutter support --- plugin/lighthouse/core.py | 2 + plugin/lighthouse/cutter_integration.py | 56 ++++ plugin/lighthouse/cutter_loader.py | 45 +++ plugin/lighthouse/metadata.py | 79 +++++ plugin/lighthouse/painting/__init__.py | 2 + plugin/lighthouse/painting/cutter_painter.py | 81 ++++++ .../lighthouse/util/disassembler/__init__.py | 13 + .../util/disassembler/cutter_api.py | 273 ++++++++++++++++++ plugin/lighthouse_plugin.py | 4 + 9 files changed, 555 insertions(+) create mode 100644 plugin/lighthouse/cutter_integration.py create mode 100644 plugin/lighthouse/cutter_loader.py create mode 100644 plugin/lighthouse/painting/cutter_painter.py create mode 100644 plugin/lighthouse/util/disassembler/cutter_api.py diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py index 71cb8f3c..8de28a05 100644 --- a/plugin/lighthouse/core.py +++ b/plugin/lighthouse/core.py @@ -202,6 +202,8 @@ def open_coverage_overview(self): # create a new coverage overview if there is not one visible self._ui_coverage_overview = CoverageOverview(self) + if disassembler.NAME == "CUTTER": + self._ui_coverage_overview.setmain(disassembler.main) self._ui_coverage_overview.show() def open_coverage_xref(self, address): diff --git a/plugin/lighthouse/cutter_integration.py b/plugin/lighthouse/cutter_integration.py new file mode 100644 index 00000000..7d6cc8c7 --- /dev/null +++ b/plugin/lighthouse/cutter_integration.py @@ -0,0 +1,56 @@ +import logging + +import cutter +from PySide2.QtWidgets import QAction +from PySide2.QtCore import QObject, SIGNAL +from lighthouse.core import Lighthouse +from lighthouse.util.disassembler import disassembler + +logger = logging.getLogger("Lighthouse.Cutter.Integration") + + +#------------------------------------------------------------------------------ +# Lighthouse Cutter Integration +#------------------------------------------------------------------------------ + +class LighthouseCutter(Lighthouse): + """ + Lighthouse UI Integration for Cutter. + """ + + def __init__(self, plugin, main): + self.plugin = plugin + self.main = main + super(LighthouseCutter, self).__init__() + disassembler.main = main + + def interactive_load_file(self, unk): + super(LighthouseCutter, self).interactive_load_file() + + def interactive_load_batch(self, unk): + super(LighthouseCutter, self).interactive_load_batch() + + def _install_load_file(self): + action = QAction("Lighthouse - Load code coverage file...", self.main) + action.triggered.connect(self.interactive_load_file) + self.main.addMenuFileAction(action) + logger.info("Installed the 'Code coverage file' menu entry") + + def _install_load_batch(self): + action = QAction("Lighthouse - Load code coverage batch...", self.main) + action.triggered.connect(self.interactive_load_batch) + self.main.addMenuFileAction(action) + logger.info("Installed the 'Code coverage batch' menu entry") + + def _install_open_coverage_overview(self): + logger.info("TODO - Coverage Overview menu entry?") + + def _uninstall_load_file(self): + pass + + def _uninstall_load_batch(self): + pass + + def _uninstall_open_coverage_overview(self): + pass + diff --git a/plugin/lighthouse/cutter_loader.py b/plugin/lighthouse/cutter_loader.py new file mode 100644 index 00000000..740882fe --- /dev/null +++ b/plugin/lighthouse/cutter_loader.py @@ -0,0 +1,45 @@ +import logging + +import CutterBindings +from lighthouse.cutter_integration import LighthouseCutter + +logger = logging.getLogger('Lighthouse.Cutter.Loader') + +#------------------------------------------------------------------------------ +# Lighthouse Cutter Loader +#------------------------------------------------------------------------------ +# +# The Cutter plugin loading process is quite easy. All we need is a function +# create_cutter_plugin that returns an instance of CutterBindings.CutterPlugin + +class LighthouseCutterPlugin(CutterBindings.CutterPlugin): + name = 'Ligthouse' + description = 'Lighthouse plugin for Cutter.' + version = '1.0' + author = 'xarkes' + + def __init__(self): + print('Init is complete.') + super(LighthouseCutterPlugin, self).__init__() + + def setupPlugin(self): + pass + + def setupInterface(self, main): + self.main = main + self.ui = LighthouseCutter(self, main) + self.ui.load() + + +def create_cutter_plugin(): + print('Creating plugin...') + try: + plugin = LighthouseCutterPlugin() + print('Returning plugin ... ', plugin) + return plugin + except Exception as e: + print('ERROR ---- ', e) + import sys, traceback + traceback.print_exc() + raise e + diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 2e0a879d..ebbf8d04 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -834,6 +834,54 @@ def _binja_refresh_nodes(self): for edge in node.outgoing_edges: function_metadata.edges[edge_src].append(edge.target.start) + def _cutter_refresh_nodes(self): + """ + Refresh function node metadata against an open Binary Ninja database. + """ + function_metadata = self + function_metadata.nodes = {} + + # get the function from the Cutter database + # TODO Use Cutter cache/API + #function = cutter.get_function_at(self.address) + function = cutter.cmdj('afbj @ ' + str(self.address)) + + # + # now we will walk the flowchart for this function, collecting + # information on each of its nodes (basic blocks) and populating + # the function & node metadata objects. + # + + for bb in function: + + # create a new metadata object for this node + node_metadata = NodeMetadata(bb['addr'], bb['addr'] + bb['size'], None) + # + # establish a relationship between this node (basic block) and + # this function metadata (its parent) + # + + node_metadata.function = function_metadata + function_metadata.nodes[bb['addr']] = node_metadata + + # + # enumerate the edges produced by this node (basic block) with a + # destination that falls within this function. + # + + #edge_src = node_metadata.instructions[-1] + #for edge in node.outgoing_edges: + # function_metadata.edges[edge_src].append(edge.target.start) + + # TODO Use Cutter cache/API + instructions = cutter.cmdj('pdbj @ ' + str(self.address)) + edge_src = instructions[-1]['offset'] + if bb.get('jump'): + function_metadata.edges[edge_src].append(bb.get('jump')) + if bb.get('fail'): + function_metadata.edges[edge_src].append(bb.get('fail')) + + def _compute_complexity(self): """ Walk the function CFG to determine approximate cyclomatic complexity. @@ -847,6 +895,10 @@ def _compute_complexity(self): complexity calculation. Not doing so will radically throw off the cyclomatic complexity score. """ + + if disassembler.NAME == "CUTTER": + return int(cutter.cmd('afCc @ ' + str(self.address))) + confirmed_nodes = set() confirmed_edges = {} @@ -1006,6 +1058,27 @@ def _binja_build_metadata(self): # save the number of instructions in this block self.instruction_count = len(self.instructions) + def _cutter_build_metadata(self): + """ + Collect node metadata from the underlying database. + """ + current_address = self.address + node_end = self.address + self.size + + # + # Note that we 'iterate over' the instructions using their byte length + # because it is far more performant than Binary Ninja's instruction + # generators which also produce instruction text, tokens etc... + # + + while current_address < node_end: + self.instructions.append(current_address) + # TODO Use Cutter API and that's dirty AF haha + current_address += int(cutter.cmd('?v $l @ ' + str(current_address)), 16) + + ## save the number of instructions in this block + self.instruction_count = len(self.instructions) + #-------------------------------------------------------------------------- # Operator Overloads #-------------------------------------------------------------------------- @@ -1091,5 +1164,11 @@ def metadata_progress(completed, total): FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes NodeMetadata._build_metadata = NodeMetadata._binja_build_metadata +elif disassembler.NAME == "CUTTER": + import cutter + import CutterBindings + FunctionMetadata._refresh_nodes = FunctionMetadata._cutter_refresh_nodes + NodeMetadata._build_metadata = NodeMetadata._cutter_build_metadata + else: raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING") diff --git a/plugin/lighthouse/painting/__init__.py b/plugin/lighthouse/painting/__init__.py index 548affae..cf03f4f1 100644 --- a/plugin/lighthouse/painting/__init__.py +++ b/plugin/lighthouse/painting/__init__.py @@ -5,5 +5,7 @@ from .ida_painter import IDAPainter as CoveragePainter elif disassembler.NAME == "BINJA": from .binja_painter import BinjaPainter as CoveragePainter +elif disassembler.NAME == "CUTTER": + from .cutter_painter import CutterPainter as CoveragePainter else: raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING") diff --git a/plugin/lighthouse/painting/cutter_painter.py b/plugin/lighthouse/painting/cutter_painter.py new file mode 100644 index 00000000..afc320b0 --- /dev/null +++ b/plugin/lighthouse/painting/cutter_painter.py @@ -0,0 +1,81 @@ +import logging + +import cutter + +from lighthouse.palette import to_rgb +from lighthouse.painting import DatabasePainter +from lighthouse.util.disassembler import disassembler + +logger = logging.getLogger("Lighthouse.Painting.Cutter") + +#------------------------------------------------------------------------------ +# Cutter Painter +#------------------------------------------------------------------------------ + +class CutterPainter(DatabasePainter): + """ + Asynchronous Cutter database painter. + """ + PAINTER_SLEEP = 0.01 + + def __init__(self, director, palette): + super(CutterPainter, self).__init__(director, palette) + + #-------------------------------------------------------------------------- + # Paint Primitives + #-------------------------------------------------------------------------- + + # + # NOTE: + # due to the manner in which Binary Ninja implements basic block + # (node) highlighting, I am not sure it is worth it to paint individual + # instructions. for now we, will simply make the instruction + # painting functions no-op's + # + + def _paint_instructions(self, instructions): + self._action_complete.set() + + def _clear_instructions(self, instructions): + self._action_complete.set() + + def _paint_nodes(self, nodes_coverage): + # TODO Paint nodes + pass + #bv = disassembler.bv + #b, g, r = to_rgb(self.palette.coverage_paint) + #color = HighlightColor(red=r, green=g, blue=b) + #for node_coverage in nodes_coverage: + # node_metadata = node_coverage.database._metadata.nodes[node_coverage.address] + # for node in bv.get_basic_blocks_starting_at(node_metadata.address): + # node.highlight = color + # self._painted_nodes.add(node_metadata.address) + #self._action_complete.set() + + def _clear_nodes(self, nodes_metadata): + # TODO Clear nodes + pass + #bv = disassembler.bv + #for node_metadata in nodes_metadata: + # for node in bv.get_basic_blocks_starting_at(node_metadata.address): + # node.highlight = HighlightStandardColor.NoHighlightColor + # self._painted_nodes.discard(node_metadata.address) + #self._action_complete.set() + + def _refresh_ui(self): + pass + + def _cancel_action(self, job): + pass + + #-------------------------------------------------------------------------- + # Priority Painting + #-------------------------------------------------------------------------- + + def _priority_paint(self): + current_address = disassembler.get_current_address() + current_function = disassembler.get_function_at(current_address) + if current_function: + self._paint_function(current_function['offset']) + return True + diff --git a/plugin/lighthouse/util/disassembler/__init__.py b/plugin/lighthouse/util/disassembler/__init__.py index 0d6eba94..263716fa 100644 --- a/plugin/lighthouse/util/disassembler/__init__.py +++ b/plugin/lighthouse/util/disassembler/__init__.py @@ -32,6 +32,19 @@ except ImportError: pass +#-------------------------------------------------------------------------- +# Cutter API Shim +#-------------------------------------------------------------------------- + +if disassembler == None: + try: + from .cutter_api import CutterAPI, DockableWindow + disassembler = CutterAPI() + except ImportError as e: + print('ERROR ---- ', e) + raise e + pass + #-------------------------------------------------------------------------- # Unknown Disassembler #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py new file mode 100644 index 00000000..73803b34 --- /dev/null +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -0,0 +1,273 @@ +# -*- coding: utf-8 -*- +import os +import sys +import logging +import functools +import threading + +import cutter +import CutterBindings + +#------------------------------------------------------------------------------ +# External PyQt5 Dependency +#------------------------------------------------------------------------------ +# +# amend the Python import path with a Libs folder for additional pip +# packages required by Lighthouse (at least on Windows, and maybe macOS) +# +# TODO/FUTURE: it is kind of dirty that we have to do this here. maybe it +# can be moved with a later refactor. in the long run, binary ninja will +# ship with PyQt5 bindings in-box. +# + +#DEPENDENCY_PATH = os.path.join( +# binaryninja.user_plugin_path(), +# "Lib", +# "site-packages" +#) +#sys.path.append(DEPENDENCY_PATH) + +#------------------------------------------------------------------------------ + +from .api import DisassemblerAPI, DockableShim +from ..qt import * +from PySide2.QtWidgets import QAction +from ..misc import is_mainthread, not_mainthread + +logger = logging.getLogger("Lighthouse.API.Cutter") + +#------------------------------------------------------------------------------ +# Utils +#------------------------------------------------------------------------------ + +def execute_sync(function): + """ + Synchronize with the disassembler for safe database access. + """ + + @functools.wraps(function) + def wrapper(*args, **kwargs): + return function(*args, **kwargs) + return wrapper + +#------------------------------------------------------------------------------ +# Disassembler API +#------------------------------------------------------------------------------ + +class CutterAPI(DisassemblerAPI): + """ + The Cutter implementation of the disassembler API abstraction. + """ + NAME = "CUTTER" + + def __init__(self): + super(CutterAPI, self).__init__() + self._init_version() + + def _init_version(self): + version_string = cutter.version() + major, minor, patch = map(int, version_string.split('.')) + + # save the version number components for later use + self._version_major = major + self._version_minor = minor + self._version_patch = patch + + # export Cutter Core + self._core = CutterBindings.CutterCore.instance() + + #-------------------------------------------------------------------------- + # Properties + #-------------------------------------------------------------------------- + + @property + def version_major(self): + return self._version_major + + @property + def version_minor(self): + return self._version_minor + + @property + def version_patch(self): + return self._version_patch + + @property + def headless(self): + return False + + #-------------------------------------------------------------------------- + # Synchronization Decorators + #-------------------------------------------------------------------------- + + @staticmethod + def execute_read(function): + return execute_sync(function) + + @staticmethod + def execute_write(function): + return execute_sync(function) + + @staticmethod + def execute_ui(function): + + @functools.wraps(function) + def wrapper(*args, **kwargs): + ff = functools.partial(function, *args, **kwargs) + ff() + + return wrapper + + #-------------------------------------------------------------------------- + # API Shims + #-------------------------------------------------------------------------- + + def create_rename_hooks(self): + class RenameHooks(object): + def __init__(self): + pass + + def hook(self): + print('hooked') + + def unhook(self): + print('unhooked') + + return RenameHooks() + + def get_current_address(self): + # TODO Use Cutter API + return cutter.cmdj('sj')[0]['offset'] + + def get_function_at(self, address): + return cutter.cmdj('afij @ ' + str(address))[0] + + @execute_read.__func__ + def get_database_directory(self): + # TODO Use Cutter API + return cutter.cmdj('ij')['core']['file'] + + def get_disassembler_user_directory(self): + return os.path.split(binaryninja.user_plugin_path())[0] + + @not_mainthread + def get_function_addresses(self): + # TODO Use Cutter cache/API + return [x['offset'] for x in cutter.cmdj('aflj')] + + @not_mainthread + def get_function_name_at(self, address): + # TODO User Cutter API + return self.get_function_at(address)['name'] + + @execute_read.__func__ + def get_function_raw_name_at(self, address): + return self.get_function_name_at(address) + + @not_mainthread + def get_imagebase(self): + # TODO Use Cutter API + return cutter.cmdj('ij')['bin']['baddr'] + + @not_mainthread + def get_root_filename(self): + # TODO Use Cutter API + return os.path.basename(cutter.cmdj('ij')['core']['file']) + + def navigate(self, address): + return self._core.seek(address) + + @execute_write.__func__ + def set_function_name_at(self, function_address, new_name): + return None + #func = self.bv.get_function_at(function_address) + #if not func: + # return + #if new_name == "": + # new_name = None + #func.name = new_name + + # + # TODO/V35: As a workaround for no symbol events, we trigger a data + # notification for this function instead. + # + + #self.bv.write(function_address, self.bv.read(function_address, 1)) + + #-------------------------------------------------------------------------- + # UI API Shims + #-------------------------------------------------------------------------- + + def get_disassembly_background_color(self): + palette = QtGui.QPalette() + return palette.color(QtGui.QPalette.Button) + + def is_msg_inited(self): + return True + + def warning(self, text): + self.main.messageBoxWarning('Lighthouse warning', text) + + #-------------------------------------------------------------------------- + # Function Prefix API + #-------------------------------------------------------------------------- + + PREFIX_SEPARATOR = "▁" # Unicode 0x2581 + +#------------------------------------------------------------------------------ +# UI +#------------------------------------------------------------------------------ + +class DockableWindow(DockableShim): + """ + A dockable Qt widget for Cutter. + """ + + def __init__(self, window_title, icon_path): + super(DockableWindow, self).__init__(window_title, icon_path) + + # configure dockable widget container + self._widget = QtWidgets.QWidget() + self._dockable = QtWidgets.QDockWidget(window_title) + self._dockable.setWidget(self._widget) + self._dockable.setWindowIcon(self._window_icon) + self._dockable.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self._dockable.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + self._widget.setSizePolicy( + QtWidgets.QSizePolicy.Expanding, + QtWidgets.QSizePolicy.Expanding + ) + + def show(self): + # + # NOTE/HACK: + # this is a little dirty, but is used to set the default width + # of the coverage overview / dockable widget when it is first shown + # + + default_width = self._widget.sizeHint().width() + self._dockable.setMinimumWidth(default_width) + + # show the widget + self._dockable.show() + + # undo the HACK + self._dockable.setMinimumWidth(0) + + def setmain(self, main): + # + # NOTE HACK: + # this is a little dirty, but it's needed because it's not as + # easy as get_qt_main_window() to get the main dock in Cutter + # + self._main = main + # self._widget.setParent(main) + + # dock the widget inside Cutter main dock + self._action = QAction('Lighthouse coverage table') + self._action.setCheckable(True) + main.addPluginDockWidget(self._widget, self._action) + diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index a96be38a..8dc74060 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -22,6 +22,10 @@ logger.info("Selecting Binary Ninja loader...") from lighthouse.binja_loader import * +elif disassembler.NAME == "CUTTER": + logger.info("Selecting Cutter loader...") + from lighthouse.cutter_loader import * + else: raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING") From 20e0572688c8a0d984ffbbb1a4f38cbb74dd08e2 Mon Sep 17 00:00:00 2001 From: xarkes Date: Mon, 11 Feb 2019 00:33:50 +0100 Subject: [PATCH 02/18] Use PySide2 for Cutter --- plugin/lighthouse/cutter_integration.py | 12 ++++----- plugin/lighthouse/cutter_loader.py | 3 --- .../util/disassembler/cutter_api.py | 26 ++----------------- plugin/lighthouse/util/qt/shim.py | 26 +++++++++++++++++++ 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/plugin/lighthouse/cutter_integration.py b/plugin/lighthouse/cutter_integration.py index 7d6cc8c7..de9b546d 100644 --- a/plugin/lighthouse/cutter_integration.py +++ b/plugin/lighthouse/cutter_integration.py @@ -1,10 +1,9 @@ import logging import cutter -from PySide2.QtWidgets import QAction -from PySide2.QtCore import QObject, SIGNAL from lighthouse.core import Lighthouse -from lighthouse.util.disassembler import disassembler +from lighthouse.util.disassembler import disassembler as disas +from lighthouse.util.qt import * logger = logging.getLogger("Lighthouse.Cutter.Integration") @@ -22,7 +21,8 @@ def __init__(self, plugin, main): self.plugin = plugin self.main = main super(LighthouseCutter, self).__init__() - disassembler.main = main + # Small hack to give main window to DockWidget + disas.main = main def interactive_load_file(self, unk): super(LighthouseCutter, self).interactive_load_file() @@ -31,13 +31,13 @@ def interactive_load_batch(self, unk): super(LighthouseCutter, self).interactive_load_batch() def _install_load_file(self): - action = QAction("Lighthouse - Load code coverage file...", self.main) + action = QtWidgets.QAction("Lighthouse - Load code coverage file...", self.main) action.triggered.connect(self.interactive_load_file) self.main.addMenuFileAction(action) logger.info("Installed the 'Code coverage file' menu entry") def _install_load_batch(self): - action = QAction("Lighthouse - Load code coverage batch...", self.main) + action = QtWidgets.QAction("Lighthouse - Load code coverage batch...", self.main) action.triggered.connect(self.interactive_load_batch) self.main.addMenuFileAction(action) logger.info("Installed the 'Code coverage batch' menu entry") diff --git a/plugin/lighthouse/cutter_loader.py b/plugin/lighthouse/cutter_loader.py index 740882fe..01be1f21 100644 --- a/plugin/lighthouse/cutter_loader.py +++ b/plugin/lighthouse/cutter_loader.py @@ -19,7 +19,6 @@ class LighthouseCutterPlugin(CutterBindings.CutterPlugin): author = 'xarkes' def __init__(self): - print('Init is complete.') super(LighthouseCutterPlugin, self).__init__() def setupPlugin(self): @@ -32,10 +31,8 @@ def setupInterface(self, main): def create_cutter_plugin(): - print('Creating plugin...') try: plugin = LighthouseCutterPlugin() - print('Returning plugin ... ', plugin) return plugin except Exception as e: print('ERROR ---- ', e) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 73803b34..d5ca3349 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -8,30 +8,8 @@ import cutter import CutterBindings -#------------------------------------------------------------------------------ -# External PyQt5 Dependency -#------------------------------------------------------------------------------ -# -# amend the Python import path with a Libs folder for additional pip -# packages required by Lighthouse (at least on Windows, and maybe macOS) -# -# TODO/FUTURE: it is kind of dirty that we have to do this here. maybe it -# can be moved with a later refactor. in the long run, binary ninja will -# ship with PyQt5 bindings in-box. -# - -#DEPENDENCY_PATH = os.path.join( -# binaryninja.user_plugin_path(), -# "Lib", -# "site-packages" -#) -#sys.path.append(DEPENDENCY_PATH) - -#------------------------------------------------------------------------------ - from .api import DisassemblerAPI, DockableShim from ..qt import * -from PySide2.QtWidgets import QAction from ..misc import is_mainthread, not_mainthread logger = logging.getLogger("Lighthouse.API.Cutter") @@ -267,7 +245,7 @@ def setmain(self, main): # self._widget.setParent(main) # dock the widget inside Cutter main dock - self._action = QAction('Lighthouse coverage table') + self._action = QtWidgets.QAction('Lighthouse coverage table') self._action.setCheckable(True) - main.addPluginDockWidget(self._widget, self._action) + main.addPluginDockWidget(self._dockable, self._action) diff --git a/plugin/lighthouse/util/qt/shim.py b/plugin/lighthouse/util/qt/shim.py index 20517d92..b2982fbd 100644 --- a/plugin/lighthouse/util/qt/shim.py +++ b/plugin/lighthouse/util/qt/shim.py @@ -25,6 +25,32 @@ USING_PYQT5 = False +USING_PYSIDE2 = False + +#------------------------------------------------------------------------------ +# PySide2 Compatibility +#------------------------------------------------------------------------------ + +# if PyQt5 did not import, try to load PySide +if QT_AVAILABLE == False: + try: + import PySide2.QtGui as QtGui + import PySide2.QtCore as QtCore + import PySide2.QtWidgets as QtWidgets + + # alias for less PySide2 <--> PyQt5 shimming + QtCore.pyqtSignal = QtCore.Signal + QtCore.pyqtSlot = QtCore.Slot + + # importing went okay, PySide must be available for use + QT_AVAILABLE = True + USING_PYSIDE2 = True + USING_PYQT5 = True + + # import failed. No Qt / UI bindings available... + except ImportError: + pass + #------------------------------------------------------------------------------ # PyQt5 Compatibility #------------------------------------------------------------------------------ From a8488ca13bf33f91dbb3134fd8f49c7a2de2296c Mon Sep 17 00:00:00 2001 From: xarkes Date: Mon, 11 Feb 2019 19:32:14 +0100 Subject: [PATCH 03/18] Raising Coverage window upon creation for Cutter --- plugin/lighthouse/cutter_integration.py | 2 +- .../util/disassembler/cutter_api.py | 34 ++++++++----------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/plugin/lighthouse/cutter_integration.py b/plugin/lighthouse/cutter_integration.py index de9b546d..657a285d 100644 --- a/plugin/lighthouse/cutter_integration.py +++ b/plugin/lighthouse/cutter_integration.py @@ -18,9 +18,9 @@ class LighthouseCutter(Lighthouse): """ def __init__(self, plugin, main): + super(LighthouseCutter, self).__init__() self.plugin = plugin self.main = main - super(LighthouseCutter, self).__init__() # Small hack to give main window to DockWidget disas.main = main diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index d5ca3349..3bdbd469 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -106,18 +106,20 @@ def __init__(self): pass def hook(self): + # TODO Handle me print('hooked') def unhook(self): + # TODO Handle me print('unhooked') return RenameHooks() def get_current_address(self): - # TODO Use Cutter API - return cutter.cmdj('sj')[0]['offset'] + return self._core.getOffset() def get_function_at(self, address): + # TODO Use Cutter API return cutter.cmdj('afij @ ' + str(address))[0] @execute_read.__func__ @@ -140,7 +142,7 @@ def get_function_name_at(self, address): @execute_read.__func__ def get_function_raw_name_at(self, address): - return self.get_function_name_at(address) + return self.get_function_at(address)['name'] @not_mainthread def get_imagebase(self): @@ -157,20 +159,9 @@ def navigate(self, address): @execute_write.__func__ def set_function_name_at(self, function_address, new_name): - return None - #func = self.bv.get_function_at(function_address) - #if not func: - # return - #if new_name == "": - # new_name = None - #func.name = new_name - - # - # TODO/V35: As a workaround for no symbol events, we trigger a data - # notification for this function instead. - # - - #self.bv.write(function_address, self.bv.read(function_address, 1)) + old_name = self.get_function_raw_name_at(function_address) + self._core.renameFunction(old_name, new_name) + # TODO Fix refresh :) #-------------------------------------------------------------------------- # UI API Shims @@ -226,14 +217,16 @@ def show(self): # of the coverage overview / dockable widget when it is first shown # - default_width = self._widget.sizeHint().width() - self._dockable.setMinimumWidth(default_width) + #default_width = self._widget.sizeHint().width() + #self._dockable.setMinimumWidth(default_width) # show the widget + print('Calling show on dockable...', self._dockable) self._dockable.show() + self._dockable.raise_() # undo the HACK - self._dockable.setMinimumWidth(0) + #self._dockable.setMinimumWidth(0) def setmain(self, main): # @@ -248,4 +241,5 @@ def setmain(self, main): self._action = QtWidgets.QAction('Lighthouse coverage table') self._action.setCheckable(True) main.addPluginDockWidget(self._dockable, self._action) + print('Added dockable to main window', self._dockable) From 1c21bf4b23f40bcae02d634a0bdcc2448c09c3eb Mon Sep 17 00:00:00 2001 From: xarkes Date: Mon, 11 Feb 2019 19:52:49 +0100 Subject: [PATCH 04/18] Improved rename hook for Cutter --- .../util/disassembler/cutter_api.py | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 3bdbd469..d0d3b278 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -102,18 +102,26 @@ def wrapper(*args, **kwargs): def create_rename_hooks(self): class RenameHooks(object): - def __init__(self): - pass + def __init__(self, core): + self._core = core def hook(self): - # TODO Handle me - print('hooked') + print('Hooked rename') + QtCore.QObject.connect(self._core, + QtCore.SIGNAL('functionRenamed(const QString, const QString)'), + self.update) def unhook(self): - # TODO Handle me - print('unhooked') + print('UnHooked rename') + QtCore.QObject.disconnect(self._core, + QtCore.SIGNAL('functionRenamed(const QString, const QString)'), + self.update) - return RenameHooks() + def update(self, old_name, new_name): + # TODO Wtf this is not triggered? + print('Received update event!', old_name, new_name) + + return RenameHooks(self._core) def get_current_address(self): return self._core.getOffset() @@ -221,7 +229,6 @@ def show(self): #self._dockable.setMinimumWidth(default_width) # show the widget - print('Calling show on dockable...', self._dockable) self._dockable.show() self._dockable.raise_() @@ -241,5 +248,4 @@ def setmain(self, main): self._action = QtWidgets.QAction('Lighthouse coverage table') self._action.setCheckable(True) main.addPluginDockWidget(self._dockable, self._action) - print('Added dockable to main window', self._dockable) From 9fa0e5d4638376bca193a39f089088ab456008bc Mon Sep 17 00:00:00 2001 From: xarkes Date: Sat, 16 Feb 2019 17:16:54 +0100 Subject: [PATCH 05/18] Added graph highlighting for Cutter --- plugin/lighthouse/painting/cutter_painter.py | 30 +++++++------------ .../util/disassembler/cutter_api.py | 8 +++-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/plugin/lighthouse/painting/cutter_painter.py b/plugin/lighthouse/painting/cutter_painter.py index afc320b0..cde93161 100644 --- a/plugin/lighthouse/painting/cutter_painter.py +++ b/plugin/lighthouse/painting/cutter_painter.py @@ -40,27 +40,19 @@ def _clear_instructions(self, instructions): self._action_complete.set() def _paint_nodes(self, nodes_coverage): - # TODO Paint nodes - pass - #bv = disassembler.bv - #b, g, r = to_rgb(self.palette.coverage_paint) - #color = HighlightColor(red=r, green=g, blue=b) - #for node_coverage in nodes_coverage: - # node_metadata = node_coverage.database._metadata.nodes[node_coverage.address] - # for node in bv.get_basic_blocks_starting_at(node_metadata.address): - # node.highlight = color - # self._painted_nodes.add(node_metadata.address) - #self._action_complete.set() + b, g, r = to_rgb(self.palette.coverage_paint) + color = disassembler.get_color(r, g, b) + for node_coverage in nodes_coverage: + node_metadata = node_coverage.database._metadata.nodes[node_coverage.address] + disassembler._core.getBBHighlighter().highlight(node_coverage.address, color) + self._painted_nodes.add(node_metadata.address) + self._action_complete.set() def _clear_nodes(self, nodes_metadata): - # TODO Clear nodes - pass - #bv = disassembler.bv - #for node_metadata in nodes_metadata: - # for node in bv.get_basic_blocks_starting_at(node_metadata.address): - # node.highlight = HighlightStandardColor.NoHighlightColor - # self._painted_nodes.discard(node_metadata.address) - #self._action_complete.set() + for node_metadata in nodes_metadata: + disassembler._core.getBBHighlighter().clear(node_metadata.address) + self._painted_nodes.discard(node_metadata.address) + self._action_complete.set() def _refresh_ui(self): pass diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index d0d3b278..a392812a 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -53,6 +53,7 @@ def _init_version(self): # export Cutter Core self._core = CutterBindings.CutterCore.instance() + self._config = CutterBindings.Configuration.instance() #-------------------------------------------------------------------------- # Properties @@ -171,13 +172,16 @@ def set_function_name_at(self, function_address, new_name): self._core.renameFunction(old_name, new_name) # TODO Fix refresh :) + @staticmethod + def get_color(red, green, blue): + return QtGui.QColor(red, green, blue) + #-------------------------------------------------------------------------- # UI API Shims #-------------------------------------------------------------------------- def get_disassembly_background_color(self): - palette = QtGui.QPalette() - return palette.color(QtGui.QPalette.Button) + return self._config.getColor("gui.background") def is_msg_inited(self): return True From d1f46ffb822f8a6fcab8ef601f28292bf83516d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Sun, 24 Mar 2019 12:55:19 +0100 Subject: [PATCH 06/18] Shutdown Lighthouse on terminate() in Cutter --- plugin/lighthouse/cutter_loader.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/lighthouse/cutter_loader.py b/plugin/lighthouse/cutter_loader.py index 01be1f21..b1a8250e 100644 --- a/plugin/lighthouse/cutter_loader.py +++ b/plugin/lighthouse/cutter_loader.py @@ -29,6 +29,9 @@ def setupInterface(self, main): self.ui = LighthouseCutter(self, main) self.ui.load() + def terminate(self): + self.ui.unload() + def create_cutter_plugin(): try: From 7ade47592cfa9c207348626bb5d46c18b73469b6 Mon Sep 17 00:00:00 2001 From: xarkes Date: Sun, 24 Mar 2019 20:17:55 +0100 Subject: [PATCH 07/18] Code cleanup --- plugin/lighthouse/cutter_loader.py | 7 ++-- plugin/lighthouse/metadata.py | 35 ++++++------------- plugin/lighthouse/painting/cutter_painter.py | 4 ++- plugin/lighthouse/painting/painter.py | 8 ++++- .../util/disassembler/cutter_api.py | 27 ++++++-------- 5 files changed, 35 insertions(+), 46 deletions(-) diff --git a/plugin/lighthouse/cutter_loader.py b/plugin/lighthouse/cutter_loader.py index b1a8250e..4976d94d 100644 --- a/plugin/lighthouse/cutter_loader.py +++ b/plugin/lighthouse/cutter_loader.py @@ -20,6 +20,7 @@ class LighthouseCutterPlugin(CutterBindings.CutterPlugin): def __init__(self): super(LighthouseCutterPlugin, self).__init__() + self.ui = None def setupPlugin(self): pass @@ -30,13 +31,13 @@ def setupInterface(self, main): self.ui.load() def terminate(self): - self.ui.unload() + if self.ui: + self.ui.unload() def create_cutter_plugin(): try: - plugin = LighthouseCutterPlugin() - return plugin + return LighthouseCutterPlugin() except Exception as e: print('ERROR ---- ', e) import sys, traceback diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index ebbf8d04..cf08e49a 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -429,20 +429,6 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn # refresh the internal function/node fast lookup lists self._refresh_lookup() - # - # NOTE: - # - # creating the hooks inline like this is less than ideal, but they - # they have been moved here (from the metadata constructor) to - # accomodate shortcomings of the Binary Ninja API. - # - # TODO/FUTURE/V35: - # - # it would be nice to move these back to the constructor once the - # Binary Ninja API allows us to detect BV / sessions as they are - # created, and able to load plugins on such events. - # - #---------------------------------------------------------------------- # create the disassembler hooks to listen for rename events @@ -566,7 +552,10 @@ def _update_functions(self, fresh_metadata): # if new_metadata.empty: - del self.functions[function_address] + try: + del self.functions[function_address] + except KeyError: + logger.error('Error: Excepted a function at {}'.format(hex(function_address))) continue # add or overwrite the new/updated basic blocks @@ -836,7 +825,7 @@ def _binja_refresh_nodes(self): def _cutter_refresh_nodes(self): """ - Refresh function node metadata against an open Binary Ninja database. + Refresh function node metadata using Cutter/radare2 API """ function_metadata = self function_metadata.nodes = {} @@ -896,8 +885,12 @@ def _compute_complexity(self): cyclomatic complexity score. """ + # Cutter already provides this information, so just fetch it if disassembler.NAME == "CUTTER": - return int(cutter.cmd('afCc @ ' + str(self.address))) + try: + return int(cutter.cmd('afCc @ ' + str(self.address))) + except ValueError: + pass confirmed_nodes = set() confirmed_edges = {} @@ -1065,15 +1058,9 @@ def _cutter_build_metadata(self): current_address = self.address node_end = self.address + self.size - # - # Note that we 'iterate over' the instructions using their byte length - # because it is far more performant than Binary Ninja's instruction - # generators which also produce instruction text, tokens etc... - # - while current_address < node_end: self.instructions.append(current_address) - # TODO Use Cutter API and that's dirty AF haha + # TODO Use/implement Cutter API (that's very dirty) current_address += int(cutter.cmd('?v $l @ ' + str(current_address)), 16) ## save the number of instructions in this block diff --git a/plugin/lighthouse/painting/cutter_painter.py b/plugin/lighthouse/painting/cutter_painter.py index cde93161..67c7c419 100644 --- a/plugin/lighthouse/painting/cutter_painter.py +++ b/plugin/lighthouse/painting/cutter_painter.py @@ -27,7 +27,7 @@ def __init__(self, director, palette): # # NOTE: - # due to the manner in which Binary Ninja implements basic block + # due to the manner in which Cutter implements basic block # (node) highlighting, I am not sure it is worth it to paint individual # instructions. for now we, will simply make the instruction # painting functions no-op's @@ -50,6 +50,8 @@ def _paint_nodes(self, nodes_coverage): def _clear_nodes(self, nodes_metadata): for node_metadata in nodes_metadata: + # TODO Connect BBHighlighter::clear to GraphView refresh + # Or trigger graph refresh from here disassembler._core.getBBHighlighter().clear(node_metadata.address) self._painted_nodes.discard(node_metadata.address) self._action_complete.set() diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py index d3a5398a..52c8a1ae 100644 --- a/plugin/lighthouse/painting/painter.py +++ b/plugin/lighthouse/painting/painter.py @@ -2,6 +2,12 @@ import time import logging import threading +try: + import Queue as queue +except: + import queue + +from six import itervalues, viewkeys, viewvalues from lighthouse.util import * @@ -334,7 +340,7 @@ def _clear_database(self): """ db_metadata = self._director.metadata instructions = db_metadata.instructions - nodes = db_metadata.nodes.viewvalues() + nodes = viewvalues(db_metadata.nodes) # clear all instructions if not self._async_action(self._clear_instructions, instructions): diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index a392812a..44bf79d5 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -137,14 +137,17 @@ def get_database_directory(self): return cutter.cmdj('ij')['core']['file'] def get_disassembler_user_directory(self): - return os.path.split(binaryninja.user_plugin_path())[0] + # TODO Not implemented + return None - @not_mainthread + #@not_mainthread + # TODO Reenable not_mainthread def get_function_addresses(self): # TODO Use Cutter cache/API return [x['offset'] for x in cutter.cmdj('aflj')] - @not_mainthread + #@not_mainthread + # TODO Reenable not_mainthread def get_function_name_at(self, address): # TODO User Cutter API return self.get_function_at(address)['name'] @@ -153,12 +156,14 @@ def get_function_name_at(self, address): def get_function_raw_name_at(self, address): return self.get_function_at(address)['name'] - @not_mainthread + #@not_mainthread + # TODO Reenable not_mainthread def get_imagebase(self): # TODO Use Cutter API return cutter.cmdj('ij')['bin']['baddr'] - @not_mainthread + #@not_mainthread + # TODO Reenable not_mainthread def get_root_filename(self): # TODO Use Cutter API return os.path.basename(cutter.cmdj('ij')['core']['file']) @@ -223,22 +228,10 @@ def __init__(self, window_title, icon_path): ) def show(self): - # - # NOTE/HACK: - # this is a little dirty, but is used to set the default width - # of the coverage overview / dockable widget when it is first shown - # - - #default_width = self._widget.sizeHint().width() - #self._dockable.setMinimumWidth(default_width) - # show the widget self._dockable.show() self._dockable.raise_() - # undo the HACK - #self._dockable.setMinimumWidth(0) - def setmain(self, main): # # NOTE HACK: From 76047d8b1ed3b41a36c6dd1f8ffefbed28c3b859 Mon Sep 17 00:00:00 2001 From: xarkes Date: Wed, 10 Apr 2019 09:20:32 +0200 Subject: [PATCH 08/18] Fixed logic bug in _find_fuzzy_name --- plugin/lighthouse/director.py | 5 +++-- plugin/lighthouse/exceptions.py | 1 + plugin/lighthouse/metadata.py | 20 ++++++++++++++++++-- plugin/lighthouse/painting/painter.py | 4 ---- 4 files changed, 22 insertions(+), 8 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index b971d8bd..bae6c967 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -536,7 +536,7 @@ def _optimize_coverage_data(self, coverage_addresses): # bucketize coverage addresses instructions = addresses & set(self.metadata.instructions) - basic_blocks = instructions & self.metadata.nodes.viewkeys() + basic_blocks = instructions & viewkeys(self.metadata.nodes) unknown = addresses - instructions # bucketize the uncategorized addresses @@ -637,8 +637,9 @@ def _find_fuzzy_name(self, coverage_file, target_name): """ # attempt lookup using case-insensitive filename + target_module_name = os.path.split(target_name)[-1] for module_name in coverage_file.modules: - if module_name.lower() in target_name.lower(): + if target_module_name.lower() in module_name.lower(): return module_name # diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py index 6aadd791..38e09bec 100644 --- a/plugin/lighthouse/exceptions.py +++ b/plugin/lighthouse/exceptions.py @@ -11,6 +11,7 @@ class LighthouseError(Exception): """ def __init__(self, *args, **kwargs): super(LighthouseError, self).__init__(*args, **kwargs) + self.message = "" class CoverageParsingError(LighthouseError): """ diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index cf08e49a..f2cb8a0b 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -429,6 +429,21 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn # refresh the internal function/node fast lookup lists self._refresh_lookup() + # + # NOTE: + # + # creating the hooks inline like this is less than ideal, but they + # they have been moved here (from the metadata constructor) to + # accomodate shortcomings of the Binary Ninja API. + # + # TODO/FUTURE/V35: + # + # it would be nice to move these back to the constructor once the + # Binary Ninja API allows us to detect BV / sessions as they are + # created, and able to load plugins on such events. + # + + #---------------------------------------------------------------------- # create the disassembler hooks to listen for rename events @@ -1059,8 +1074,9 @@ def _cutter_build_metadata(self): node_end = self.address + self.size while current_address < node_end: - self.instructions.append(current_address) - # TODO Use/implement Cutter API (that's very dirty) + # TODO Use/implement Cutter API for both commands (that's very dirty) + instruction_size = cutter.cmdj('aoj')[0]['size'] + self.instructions[current_address] = instruction_size current_address += int(cutter.cmd('?v $l @ ' + str(current_address)), 16) ## save the number of instructions in this block diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py index 52c8a1ae..fe51d09c 100644 --- a/plugin/lighthouse/painting/painter.py +++ b/plugin/lighthouse/painting/painter.py @@ -2,10 +2,6 @@ import time import logging import threading -try: - import Queue as queue -except: - import queue from six import itervalues, viewkeys, viewvalues From 96e0bbfbef939ae9ea7f86634bcff2af95b5e847 Mon Sep 17 00:00:00 2001 From: xarkes Date: Wed, 10 Apr 2019 10:17:20 +0200 Subject: [PATCH 09/18] Cleanup --- plugin/lighthouse/painting/painter.py | 2 -- plugin/lighthouse/util/disassembler/cutter_api.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py index fe51d09c..139f31e0 100644 --- a/plugin/lighthouse/painting/painter.py +++ b/plugin/lighthouse/painting/painter.py @@ -3,8 +3,6 @@ import logging import threading -from six import itervalues, viewkeys, viewvalues - from lighthouse.util import * logger = logging.getLogger("Lighthouse.Painting") diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 44bf79d5..2ceb31ae 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -149,7 +149,7 @@ def get_function_addresses(self): #@not_mainthread # TODO Reenable not_mainthread def get_function_name_at(self, address): - # TODO User Cutter API + # TODO Use Cutter API return self.get_function_at(address)['name'] @execute_read.__func__ From bfd7818ed46622cb48faed6054fcb3a84010a50a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 17 May 2019 19:20:33 -0400 Subject: [PATCH 10/18] additional cleanup --- plugin/lighthouse/painting/cutter_painter.py | 3 +- .../util/disassembler/cutter_api.py | 71 ++++++------------- 2 files changed, 23 insertions(+), 51 deletions(-) diff --git a/plugin/lighthouse/painting/cutter_painter.py b/plugin/lighthouse/painting/cutter_painter.py index 67c7c419..883aa4e0 100644 --- a/plugin/lighthouse/painting/cutter_painter.py +++ b/plugin/lighthouse/painting/cutter_painter.py @@ -2,6 +2,7 @@ import cutter +from lighthouse.util.qt import QtGui from lighthouse.palette import to_rgb from lighthouse.painting import DatabasePainter from lighthouse.util.disassembler import disassembler @@ -41,7 +42,7 @@ def _clear_instructions(self, instructions): def _paint_nodes(self, nodes_coverage): b, g, r = to_rgb(self.palette.coverage_paint) - color = disassembler.get_color(r, g, b) + color = QtGui.QColor(r, g, b) for node_coverage in nodes_coverage: node_metadata = node_coverage.database._metadata.nodes[node_coverage.address] disassembler._core.getBBHighlighter().highlight(node_coverage.address, color) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 2ceb31ae..19a71e99 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -20,7 +20,7 @@ def execute_sync(function): """ - Synchronize with the disassembler for safe database access. + TODO/CUTTER: Synchronize with the disassembler for safe database access. """ @functools.wraps(function) @@ -59,18 +59,6 @@ def _init_version(self): # Properties #-------------------------------------------------------------------------- - @property - def version_major(self): - return self._version_major - - @property - def version_minor(self): - return self._version_minor - - @property - def version_patch(self): - return self._version_patch - @property def headless(self): return False @@ -89,12 +77,10 @@ def execute_write(function): @staticmethod def execute_ui(function): - @functools.wraps(function) def wrapper(*args, **kwargs): ff = functools.partial(function, *args, **kwargs) - ff() - + qt_mainthread.execute_fast(ff) return wrapper #-------------------------------------------------------------------------- @@ -107,19 +93,14 @@ def __init__(self, core): self._core = core def hook(self): - print('Hooked rename') - QtCore.QObject.connect(self._core, - QtCore.SIGNAL('functionRenamed(const QString, const QString)'), - self.update) + #self._core.functionRenamed.connect(self.update) + print("TODO/CUTTER: Hook rename") def unhook(self): - print('UnHooked rename') - QtCore.QObject.disconnect(self._core, - QtCore.SIGNAL('functionRenamed(const QString, const QString)'), - self.update) + #self._core.functionRenamed.disconnect(self.update) + print("TODO/CUTTER: Unhook rename") def update(self, old_name, new_name): - # TODO Wtf this is not triggered? print('Received update event!', old_name, new_name) return RenameHooks(self._core) @@ -128,58 +109,47 @@ def get_current_address(self): return self._core.getOffset() def get_function_at(self, address): - # TODO Use Cutter API + # TODO/CUTTER: Use Cutter API return cutter.cmdj('afij @ ' + str(address))[0] - @execute_read.__func__ def get_database_directory(self): - # TODO Use Cutter API + # TODO/CUTTER: Use Cutter API return cutter.cmdj('ij')['core']['file'] def get_disassembler_user_directory(self): - # TODO Not implemented - return None + if sys.platform == "linux" or sys.platform == "linux2": + return os.path.expanduser("~/.local/share/RadareOrg/Cutter") + elif sys.platform == "darwin": + raise RuntimeError("TODO OSX") + elif sys.platform == "win32": + return os.path.join(os.getenv("APPDATA"), "RadareOrg", "Cutter") + raise RuntimeError("Unknown operating system") - #@not_mainthread - # TODO Reenable not_mainthread def get_function_addresses(self): - # TODO Use Cutter cache/API + # TODO/CUTTER: Use Cutter API return [x['offset'] for x in cutter.cmdj('aflj')] - #@not_mainthread - # TODO Reenable not_mainthread def get_function_name_at(self, address): - # TODO Use Cutter API + # TODO/CUTTER: Use Cutter API return self.get_function_at(address)['name'] - @execute_read.__func__ def get_function_raw_name_at(self, address): return self.get_function_at(address)['name'] - #@not_mainthread - # TODO Reenable not_mainthread def get_imagebase(self): - # TODO Use Cutter API + # TODO/CUTTER: Use Cutter API return cutter.cmdj('ij')['bin']['baddr'] - #@not_mainthread - # TODO Reenable not_mainthread def get_root_filename(self): - # TODO Use Cutter API + # TODO/CUTTER: Use Cutter API return os.path.basename(cutter.cmdj('ij')['core']['file']) def navigate(self, address): return self._core.seek(address) - @execute_write.__func__ def set_function_name_at(self, function_address, new_name): old_name = self.get_function_raw_name_at(function_address) self._core.renameFunction(old_name, new_name) - # TODO Fix refresh :) - - @staticmethod - def get_color(red, green, blue): - return QtGui.QColor(red, green, blue) #-------------------------------------------------------------------------- # UI API Shims @@ -228,16 +198,17 @@ def __init__(self, window_title, icon_path): ) def show(self): - # show the widget self._dockable.show() self._dockable.raise_() def setmain(self, main): + # # NOTE HACK: # this is a little dirty, but it's needed because it's not as # easy as get_qt_main_window() to get the main dock in Cutter # + self._main = main # self._widget.setParent(main) From 0e7945b32c76b9d75b31812aa58b129ea7b6f87d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 18 May 2019 17:50:21 -0400 Subject: [PATCH 11/18] print to cutter console --- plugin/lighthouse/util/disassembler/cutter_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 19a71e99..55adc95b 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -151,6 +151,9 @@ def set_function_name_at(self, function_address, new_name): old_name = self.get_function_raw_name_at(function_address) self._core.renameFunction(old_name, new_name) + def message(self, message): + cutter.message(message) + #-------------------------------------------------------------------------- # UI API Shims #-------------------------------------------------------------------------- From c335d027316845d046b400459222bd4fc0c05a0b Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 18 May 2019 18:28:52 -0400 Subject: [PATCH 12/18] refresh cutter graph on update --- plugin/lighthouse/painting/cutter_painter.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/plugin/lighthouse/painting/cutter_painter.py b/plugin/lighthouse/painting/cutter_painter.py index 883aa4e0..488e7ede 100644 --- a/plugin/lighthouse/painting/cutter_painter.py +++ b/plugin/lighthouse/painting/cutter_painter.py @@ -51,14 +51,12 @@ def _paint_nodes(self, nodes_coverage): def _clear_nodes(self, nodes_metadata): for node_metadata in nodes_metadata: - # TODO Connect BBHighlighter::clear to GraphView refresh - # Or trigger graph refresh from here disassembler._core.getBBHighlighter().clear(node_metadata.address) self._painted_nodes.discard(node_metadata.address) self._action_complete.set() def _refresh_ui(self): - pass + cutter.refresh() # TODO/CUTTER: Need a graph specific refresh... def _cancel_action(self, job): pass From 8dac7ad32667cc09118f51b4726d750d35a48fc2 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 18 May 2019 18:32:07 -0400 Subject: [PATCH 13/18] remove cutter specific conditional from core --- plugin/lighthouse/metadata.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index f2cb8a0b..9b9049d7 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -899,14 +899,6 @@ def _compute_complexity(self): complexity calculation. Not doing so will radically throw off the cyclomatic complexity score. """ - - # Cutter already provides this information, so just fetch it - if disassembler.NAME == "CUTTER": - try: - return int(cutter.cmd('afCc @ ' + str(self.address))) - except ValueError: - pass - confirmed_nodes = set() confirmed_edges = {} From 6f607b3f1e71019c9894771e202ea05ed95afba5 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 29 Mar 2020 07:52:15 -0400 Subject: [PATCH 14/18] fixes bug where external edges were being added to function graph --- plugin/lighthouse/metadata.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index fb43a6cd..21741c1b 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -897,23 +897,20 @@ def _cutter_refresh_nodes(self): node_metadata.function = function_metadata function_metadata.nodes[bb['addr']] = node_metadata - # - # enumerate the edges produced by this node (basic block) with a - # destination that falls within this function. - # - - #edge_src = node_metadata.instructions[-1] - #for edge in node.outgoing_edges: - # function_metadata.edges[edge_src].append(edge.target.start) - - # TODO Use Cutter cache/API - instructions = cutter.cmdj('pdbj @ ' + str(self.address)) - edge_src = instructions[-1]['offset'] - if bb.get('jump'): - function_metadata.edges[edge_src].append(bb.get('jump')) - if bb.get('fail'): - function_metadata.edges[edge_src].append(bb.get('fail')) + # + # TODO/CUTTER: is there a better api for this? like xref from edge_src? + # we have to do it down here (unlike binja) because radare does not + # guarantee a its edges will land within the current function CFG... + # + # compute all of the edges between nodes in the current function + for node_metadata in itervalues(function_metadata.nodes): + edge_src = node_metadata.edge_out + for bb in function: + if bb.get('jump', -1) in function_metadata.nodes: + function_metadata.edges[edge_src].append(bb.get('jump')) + if bb.get('fail', -1) in function_metadata.nodes: + function_metadata.edges[edge_src].append(bb.get('fail')) def _compute_complexity(self): """ @@ -1101,6 +1098,9 @@ def _cutter_build_metadata(self): self.instructions[current_address] = instruction_size current_address += int(cutter.cmd('?v $l @ ' + str(current_address)), 16) + # the source of the outward edge + self.edge_out = current_address - instruction_size + ## save the number of instructions in this block self.instruction_count = len(self.instructions) From 2ded7d86838051942f076cc1d3d3b43b832a21f7 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 29 Mar 2020 08:00:18 -0400 Subject: [PATCH 15/18] rename hooks still don't work?? --- plugin/lighthouse/util/disassembler/cutter_api.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 55adc95b..33b5d1df 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -93,15 +93,19 @@ def __init__(self, core): self._core = core def hook(self): - #self._core.functionRenamed.connect(self.update) - print("TODO/CUTTER: Hook rename") + self._core.functionRenamed.connect(self.update) def unhook(self): - #self._core.functionRenamed.disconnect(self.update) - print("TODO/CUTTER: Unhook rename") + self._core.functionRenamed.disconnect(self.update) def update(self, old_name, new_name): - print('Received update event!', old_name, new_name) + logger.debug('Received update event!', old_name, new_name) + # TODO/CUTTER: HOW DO I GET A FUNCITON'S ADDRESS BY NAME?? + #self.renamed(address, new_name) + + # placeholder, this gets replaced in metadata.py + def renamed(self, address, new_name): + pass return RenameHooks(self._core) From 868229ae722b76aa93590681d0185a864e42509f Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 29 Mar 2020 08:30:28 -0400 Subject: [PATCH 16/18] better handling of cutter api requests --- .../util/disassembler/cutter_api.py | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/util/disassembler/cutter_api.py b/plugin/lighthouse/util/disassembler/cutter_api.py index 33b5d1df..d26fd384 100644 --- a/plugin/lighthouse/util/disassembler/cutter_api.py +++ b/plugin/lighthouse/util/disassembler/cutter_api.py @@ -114,28 +114,57 @@ def get_current_address(self): def get_function_at(self, address): # TODO/CUTTER: Use Cutter API - return cutter.cmdj('afij @ ' + str(address))[0] + try: + return cutter.cmdj('afij @ ' + str(address))[0] + except IndexError: + return None def get_database_directory(self): # TODO/CUTTER: Use Cutter API return cutter.cmdj('ij')['core']['file'] def get_disassembler_user_directory(self): + + # TODO/CUTTER: is there an API for this yet?!? or at least the plugin dir... if sys.platform == "linux" or sys.platform == "linux2": return os.path.expanduser("~/.local/share/RadareOrg/Cutter") elif sys.platform == "darwin": raise RuntimeError("TODO OSX") elif sys.platform == "win32": return os.path.join(os.getenv("APPDATA"), "RadareOrg", "Cutter") + raise RuntimeError("Unknown operating system") def get_function_addresses(self): + + # # TODO/CUTTER: Use Cutter API - return [x['offset'] for x in cutter.cmdj('aflj')] + # + # TODO/CUTTER: Apparently, some of the addresses returned by this are + # ***NOT*** valid function addresses. they fail when passed into get_function_at() + # + + maybe_functions = [x['offset'] for x in cutter.cmdj('aflj')] + + # + # TODO/CUTTER/HACK: this is a gross hack to ensure lighthouse wont choke on *non* + # function addresses given in maybe_functions + # + + good = set() + for address in maybe_functions: + if self.get_function_at(address): + good.add(address) + + # return a list of *ALL FUNCTION ADDRESSES* in the database + return list(good) def get_function_name_at(self, address): # TODO/CUTTER: Use Cutter API - return self.get_function_at(address)['name'] + func = self.get_function_at(address) + if not func: + return None + return func['name'] def get_function_raw_name_at(self, address): return self.get_function_at(address)['name'] From 22dc30d68de73a8f10f2ea3cba406031ab675162 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 29 Mar 2020 09:02:48 -0400 Subject: [PATCH 17/18] misc bugfix --- plugin/lighthouse/director.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index d3f3399e..dc804a68 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -574,7 +574,7 @@ def _optimize_coverage_data(self, coverage_addresses): if not instructions: logger.debug("No mappable instruction addresses in coverage data") - return None + return [] # # TODO/COMMENT From 426cb5472107b96abd2dba3adbeb89f805b3d2f4 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 29 Mar 2020 16:39:58 -0400 Subject: [PATCH 18/18] help catch errors while debugging perf / metadata collection --- plugin/lighthouse/metadata.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 21741c1b..8cae6f71 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -1153,7 +1153,16 @@ def collect_function_metadata(function_addresses): """ Collect function metadata for a list of addresses. """ - return { ea: FunctionMetadata(ea) for ea in function_addresses } + output = {} + for ea in function_addresses: + try: + logger.debug(f"Collecting {ea:08X}") + output[ea] = FunctionMetadata(ea) + except Exception as e: + import traceback + logger.debug(traceback.format_exc()) + return output + #return { ea: FunctionMetadata(ea) for ea in function_addresses } @disassembler.execute_ui def metadata_progress(completed, total):