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

Initial Cutter integration #65

Open
wants to merge 19 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
2 changes: 2 additions & 0 deletions plugin/lighthouse/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,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):
Expand Down
56 changes: 56 additions & 0 deletions plugin/lighthouse/cutter_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import logging

import cutter
from lighthouse.core import Lighthouse
from lighthouse.util.disassembler import disassembler as disas
from lighthouse.util.qt import *

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


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

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

def __init__(self, plugin, main):
super(LighthouseCutter, self).__init__()
self.plugin = plugin
self.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()

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

46 changes: 46 additions & 0 deletions plugin/lighthouse/cutter_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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):
super(LighthouseCutterPlugin, self).__init__()
self.ui = None

def setupPlugin(self):
pass

def setupInterface(self, main):
self.main = main
self.ui = LighthouseCutter(self, 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

5 changes: 3 additions & 2 deletions plugin/lighthouse/director.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -653,8 +653,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

#
Expand Down
1 change: 1 addition & 0 deletions plugin/lighthouse/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class LighthouseError(Exception):
"""
def __init__(self, *args, **kwargs):
super(LighthouseError, self).__init__(*args, **kwargs)
self.message = ""

#------------------------------------------------------------------------------
# Coverage File Exceptions
Expand Down
87 changes: 85 additions & 2 deletions plugin/lighthouse/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
# created, and able to load plugins on such events.
#


#----------------------------------------------------------------------

# create the disassembler hooks to listen for rename events
Expand Down Expand Up @@ -589,7 +590,10 @@ def _update_functions(self, fresh_metadata):
#

if new_metadata.empty:
del self.functions[function_address]
try:
del self.functions[function_address]
Copy link
Owner

@gaasedelen gaasedelen Apr 10, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may have been to account for a bug I fixed in a55ede7, I will see if it is safe to remove this change from your code now.

except KeyError:
logger.error('Error: Excepted a function at {}'.format(hex(function_address)))
continue

# add or overwrite the new/updated basic blocks
Expand Down Expand Up @@ -863,6 +867,51 @@ 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 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'], 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

#
# 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 @@ -1036,6 +1085,25 @@ 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

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 @@ -1085,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):
Expand Down Expand Up @@ -1121,5 +1198,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")
2 changes: 2 additions & 0 deletions plugin/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")
74 changes: 74 additions & 0 deletions plugin/lighthouse/painting/cutter_painter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import logging

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

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 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 _paint_nodes(self, nodes_coverage):
b, g, r = to_rgb(self.palette.coverage_paint)
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)
self._painted_nodes.add(node_metadata.address)
self._action_complete.set()

def _clear_nodes(self, nodes_metadata):
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):
cutter.refresh() # TODO/CUTTER: Need a graph specific refresh...

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

13 changes: 13 additions & 0 deletions plugin/lighthouse/util/disassembler/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#--------------------------------------------------------------------------
Expand Down
Loading