Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cutter integration #89

Open
wants to merge 17 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Lighthouse is a cross-platform (Windows, macOS, Linux) Python 2/3 plugin. It tak
1. From your disassembler's python console, run the following command to find its plugin directory:
- **IDA Pro**: `os.path.join(idaapi.get_user_idadir(), "plugins")`
- **Binary Ninja**: `binaryninja.user_plugin_path()`
- **Cutter**: Go to `Edit`->`Preferences`->`Plugins` on the top should be the path to the plugins directory. Copy the contents in the python folder.

2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
3. Restart your disassembler.
Expand All @@ -54,6 +55,8 @@ While Lighthouse is in use, it will 'paint' the active coverage data across all

In Binary Ninja, only the linear disassembly, graph, and IL views are supported. Support for painting decompiler output in Binary Ninja will be added to Lighthouse in the *near future* as the feature stabilizes.

Cutter only supports graph view painting currently.

# Coverage Overview

The Coverage Overview is a dockable widget that will open up once coverage has been loaded into Lighthouse.
Expand Down
7 changes: 6 additions & 1 deletion plugins/lighthouse/integration/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ def load(self):
# the plugin color palette
self.palette = LighthousePalette()
self.palette.theme_changed(self.refresh_theme)

def create_coverage_overview(name, parent, dctx):
lctx = self.get_context(dctx, startup=False)
widget = disassembler.create_dockable_widget(parent, name)
Expand Down Expand Up @@ -213,6 +213,11 @@ def open_coverage_overview(self, dctx=None):

# trigger an update check (this should only ever really 'check' once)
self.check_for_update()
# 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, dctx=None):
"""
Expand Down
70 changes: 70 additions & 0 deletions plugins/lighthouse/integration/cutter_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import logging

import cutter
#from lighthouse.integration.core import Lighthouse
from lighthouse.context import LighthouseContext
from lighthouse.integration.core import LighthouseCore
from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI
from lighthouse.util.qt import *

logger = logging.getLogger("Lighthouse.Cutter.Integration")


#------------------------------------------------------------------------------
# Lighthouse Cutter Integration
#------------------------------------------------------------------------------

class LighthouseCutter(LighthouseCore):
"""
Lighthouse UI Integration for Cutter.
"""

def __init__(self, plugin, main):
super(LighthouseCutter, self).__init__()
self.plugin = plugin
self.main = main
self.lighthouse_contexts = {}
# Small hack to give main window to DockWidget
#disassembler.main = self.main

def get_context(self, dctx, startup=True):
if dctx not in self.lighthouse_contexts:
# create a new 'context' representing this DB
lctx = LighthouseContext(self, dctx)
if startup:
lctx.start()
# save the created ctx for future calls
self.lighthouse_contexts[dctx] = lctx
# return the lighthouse context object for this DB
return self.lighthouse_contexts[dctx]

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 = 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 = 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")

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

48 changes: 48 additions & 0 deletions plugins/lighthouse/integration/cutter_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import logging

import CutterBindings
from lighthouse.integration.cutter_integration import LighthouseCutter
from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI

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):
super(LighthouseCutterPlugin, self).__init__()
self.ui = None

def setupPlugin(self):
pass

def setupInterface(self, main):
self.main = main
self.ui = LighthouseCutter(self, main)
disassembler.main = main
self.ui.load()

def terminate(self):
if self.ui:
self.ui.unload()


def create_cutter_plugin():
try:
return LighthouseCutterPlugin()
except Exception as e:
print('ERROR ---- ', e)
import sys, traceback
traceback.print_exc()
raise e

72 changes: 72 additions & 0 deletions plugins/lighthouse/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -912,6 +912,52 @@ def _binja_refresh_nodes(self, disassembler_ctx):
#for edge in node.outgoing_edges:
# function_metadata.edges[edge_src].append(edge.target.start)

def _cutter_refresh_nodes(self, dctx):
"""
Refresh function node metadata using Cutter/radare2 API
"""
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'],
dctx)
#
# 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

#
# 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):
"""
Walk the function CFG to determine approximate cyclomatic complexity.
Expand Down Expand Up @@ -1092,6 +1138,25 @@ def _binja_cache_node(self, disassembler_ctx):
# save the number of instructions in this block
self.instruction_count = len(self.instructions)

def _cutter_cache_node(self, disassembler_ctx):
"""
Collect node metadata from the underlying database.
"""
current_address = self.address
node_end = self.address + self.size

while current_address < node_end:
# 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)

# 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)

#--------------------------------------------------------------------------
# Operator Overloads
#--------------------------------------------------------------------------
Expand Down Expand Up @@ -1177,5 +1242,12 @@ def metadata_progress(completed, total):
BNGetInstructionLength = core.BNGetInstructionLength
BNNewBasicBlockReference = core.BNNewBasicBlockReference

elif disassembler.NAME == "CUTTER":
import cutter
import CutterBindings
FunctionMetadata._refresh_nodes = FunctionMetadata._cutter_refresh_nodes
#NodeMetadata._build_metadata = NodeMetadata._cutter_build_metadata
NodeMetadata._cache_node = NodeMetadata._cutter_cache_node

else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
2 changes: 2 additions & 0 deletions plugins/lighthouse/painting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
109 changes: 109 additions & 0 deletions plugins/lighthouse/painting/cutter_painter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import logging

import cutter
import CutterBindings

from lighthouse.util.qt import QtGui
#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, lctx, director, palette):
super(CutterPainter, self).__init__(lctx, director, palette)

#--------------------------------------------------------------------------
# Paint Primitives
#--------------------------------------------------------------------------

#
# NOTE:
# 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
#

def _paint_instructions(self, instructions):
self._action_complete.set()

def _clear_instructions(self, instructions):
self._action_complete.set()

def _partial_paint(self, addresses, color):
try:
highlighter = disassembler[self.lctx]._core.getBIHighlighter()
for address in addresses:
logger.debug('Partially painting {}'.format(address))
highlighter.highlight(address, color)
except Exception as e:
logger.debug('Exception in partial paint: {}'.format(e))


def _paint_nodes(self, nodes_addresses):
(r, g, b, _) = self.palette.coverage_paint.getRgb()
color = QtGui.QColor(r, g, b)
color_partial = QtGui.QColor(r, 0, 0)

for node_address in nodes_addresses:
node_metadata = self.director.metadata.nodes.get(node_address, None)
node_coverage = self.director.coverage.nodes.get(node_address, None)


# Database unsync. Abort
if not (node_coverage and node_metadata):
logger.warning('Unsynced database. Aborting')
self._msg_queue.put(self.MSG_ABORT)
node_addresses = node_addresses[:node_addresses.index(node_address)]
break

# Node completely executed
if node_coverage.instructions_executed == node_metadata.instruction_count:
logger.debug('Painting node {}'.format(node_address))
disassembler[self.lctx]._core.getBBHighlighter().highlight(node_address, color)
self._painted_nodes.add(node_address)

# Partially executed nodes
else:
logger.debug('Partial block {}'.format(node_address))
self._partial_paint(node_coverage.executed_instructions.keys(), color)

self._action_complete.set()

def _clear_nodes(self, addresses):
for address in addresses:
disassembler[self.lctx]._core.getBBHighlighter().clear(address)
self._painted_nodes.discard(address)
self._action_complete.set()

def _refresh_ui(self):
cutter.refresh() # TODO/CUTTER: Need a graph specific refresh...

def _cancel_action(self, job):
pass

#--------------------------------------------------------------------------
# Priority Painting
#--------------------------------------------------------------------------

def _priority_paint(self):
current_address = disassembler[self.lctx].get_current_address()
current_function = disassembler[self.lctx].get_function_at(current_address)
if current_function:
self._paint_function(current_function['offset'])
return True

def _paint_function(self, function):
pass

2 changes: 1 addition & 1 deletion plugins/lighthouse/ui/coverage_overview.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ def eventFilter(self, source, event):
if self._target.visible and self._first_hit:
self._first_hit = False

if disassembler.NAME == "BINJA":
if disassembler.NAME == "BINJA" or disassembler.NAME == 'CUTTER':
self._target.lctx.start()

if not self._target.director.metadata.cached:
Expand Down
4 changes: 3 additions & 1 deletion plugins/lighthouse/ui/palette.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import shutil
import logging
import traceback
import PySide2

# NOTE: Py2/Py3 compat
try:
Expand Down Expand Up @@ -549,7 +550,8 @@ def _qt_theme_hint(self):
if disassembler.NAME == "BINJA":
test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
else:
test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
#test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
test_widget.setAttribute(PySide2.QtCore.Qt.WidgetAttribute(103))


# render the (invisible) widget
Expand Down
Loading