From 11f108bca79674865f6ab242370ec29546d6f87f Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 25 Jul 2017 12:10:35 -0700 Subject: [PATCH 01/43] more verbose debug prints for issue #7 --- plugin/lighthouse/painting.py | 19 ++++++++++++++++++- plugin/lighthouse/util/ida.py | 2 ++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index 3a94c9f6..26b38945 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -81,8 +81,13 @@ def _init_hexrays_hooks(self): NOTE: This is called when the ui_ready_to_run event fires. """ + result = False + if idaapi.init_hexrays_plugin(): - idaapi.install_hexrays_callback(self._hxe_callback) + logger.debug("HexRays present, installing hooks...") + result = idaapi.install_hexrays_callback(self._hxe_callback) + + logger.debug("HexRays hooked: %r" % result) # # we only use self._hooks (UI_Hooks) to install our hexrays hooks. @@ -306,6 +311,9 @@ def paint_hexrays(self, cfunc, database_coverage): """ Paint decompilation text for the given HexRays Window. """ + logger.debug("Painting Hexrays for 0x%X" % cfunc.entry_ea) + + # more code-friendly, readable aliases database_metadata = database_coverage._metadata decompilation_text = cfunc.get_pseudocode() @@ -324,6 +332,7 @@ def paint_hexrays(self, cfunc, database_coverage): # line2citem = map_line2citem(decompilation_text) + logger.debug(line2citem) # # now that we have some understanding of how citems contribute to each @@ -332,6 +341,7 @@ def paint_hexrays(self, cfunc, database_coverage): # line2node = map_line2node(cfunc, database_metadata, line2citem) + logger.debug(line2node) # great, now we have all the information we need to paint @@ -367,6 +377,7 @@ def paint_hexrays(self, cfunc, database_coverage): # if there was nothing painted yet, there's no point in continuing... if not lines_painted: + logger.debug("No HexRays output was painted...") return # @@ -379,6 +390,8 @@ def paint_hexrays(self, cfunc, database_coverage): decompilation_text[line_number].bgcolor = self.palette.ida_coverage lines_painted += 1 + logger.debug("Done painting HexRays request...") + # finally, refresh the view idaapi.refresh_idaview_anyway() @@ -389,9 +402,13 @@ def _hxe_callback(self, event, *args): # decompilation text generation is complete and it is about to be shown if event == idaapi.hxe_text_ready: + + # more code-friendly, readable aliases vdui = args[0] cfunc = vdui.cfunc + logger.debug("Caught HexRays 'Text Ready' event for 0x%X" % cfunc.entry_ea) + # if there's no coverage data for this function, there's nothing to do if not cfunc.entry_ea in self._director.coverage.functions: return 0 diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index a6895223..f99e9df1 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -1,6 +1,7 @@ import time import Queue import logging +import binascii import functools import idaapi @@ -45,6 +46,7 @@ def map_line2citem(decompilation_text): for line_number in xrange(decompilation_text.size()): line_text = decompilation_text[line_number].line line2citem[line_number] = lex_citem_indexes(line_text) + logger.debug("Line Text: %s" % binascii.hexlify(line_text)) return line2citem From 070923f945d99bad142c4f07f86200a8010699ab Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 15:40:12 -0700 Subject: [PATCH 02/43] bugfix: stops double init of some plugin members --- plugin/lighthouse_plugin.py | 66 ++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index f80be79e..0c7fd7c4 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -39,26 +39,6 @@ class Lighthouse(plugin_t): wanted_name = "Lighthouse" wanted_hotkey = "" - def __init__(self): - - # plugin color palette - self.palette = LighthousePalette() - - # the coverage engine - self.director = CoverageDirector(self.palette) - - # the coverage painter - self.painter = CoveragePainter(self.director, self.palette) - - # the coverage overview widget - self._ui_coverage_overview = None - - # members for the 'Load Code Coverage' menu entry - self._icon_id_load = idaapi.BADADDR - - # members for the 'Coverage Overview' menu entry - self._icon_id_overview = idaapi.BADADDR - #-------------------------------------------------------------------------- # IDA Plugin Overloads #-------------------------------------------------------------------------- @@ -114,8 +94,41 @@ def _install_plugin(self): """ Initialize & integrate the plugin into IDA. """ + self._init() self._install_ui() + def _init(self): + """ + Initialize plugin members. + """ + + # plugin color palette + self.palette = LighthousePalette() + + # the coverage engine + self.director = CoverageDirector(self.palette) + + # the coverage painter + self.painter = CoveragePainter(self.director, self.palette) + + # the coverage overview widget + self._ui_coverage_overview = None + + # members for the 'Load Code Coverage' menu entry + self._icon_id_load = idaapi.BADADDR + + # members for the 'Coverage Overview' menu entry + self._icon_id_overview = idaapi.BADADDR + + def _install_ui(self): + """ + Initialize & integrate all UI elements. + """ + + # install the 'Load Coverage' file dialog + self._install_load_file_dialog() + self._install_open_coverage_overview() + def print_banner(self): """ Print the Lighthouse plugin banner. @@ -132,19 +145,6 @@ def print_banner(self): lmsg("-"*75) lmsg("") - #-------------------------------------------------------------------------- - # Initialization - UI - #-------------------------------------------------------------------------- - - def _install_ui(self): - """ - Initialize & integrate all UI elements. - """ - - # install the 'Load Coverage' file dialog - self._install_load_file_dialog() - self._install_open_coverage_overview() - #-------------------------------------------------------------------------- # Termination #-------------------------------------------------------------------------- From dbb1dbbcab44049176580de6c6b4e388ab6e3e01 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 16:40:41 -0700 Subject: [PATCH 03/43] wait for threads to exit while unloading plugin --- plugin/lighthouse/director.py | 13 ++++++++++++- plugin/lighthouse/metadata.py | 6 +++++- plugin/lighthouse/painting.py | 20 +++++++++++++++++--- plugin/lighthouse_plugin.py | 12 ++++++++---- 4 files changed, 42 insertions(+), 9 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 8d6be19d..de9f891e 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -157,7 +157,6 @@ def __init__(self, palette): target=self._async_evaluate_ast, name="EvaluateAST" ) - self._composition_worker.daemon = True self._composition_worker.start() #---------------------------------------------------------------------- @@ -178,6 +177,18 @@ def __init__(self, palette): self._coverage_created_callbacks = [] self._coverage_deleted_callbacks = [] + def terminate(self): + """ + Cleanup & terminate the director. + """ + + # stop the composition worker + self._ast_queue.put(None) + self._composition_worker.join() + + # stop any ongoing metadata refresh + self.metadata.abort_refresh(join=True) + #-------------------------------------------------------------------------- # Properties #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 4f6120d1..646b81ee 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -276,7 +276,7 @@ def refresh(self, function_addresses=None, progress_callback=None): return result_queue - def abort_refresh(self): + def abort_refresh(self, join=False): """ Abort a running refresh. @@ -309,6 +309,10 @@ def abort_refresh(self): # signal the worker thread to stop self._stop_threads = True + # if requested, don't return until the worker thread has stopped... + if join: + worker.join() + def _async_refresh(self, result_queue, function_addresses, progress_callback): """ Internal asynchronous metadata collection worker. diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index 26b38945..5fcf9c94 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -55,7 +55,6 @@ def __init__(self, director, palette): target=self._async_database_painter, name="DatabasePainter" ) - self._painting_worker.daemon = True self._painting_worker.start() #---------------------------------------------------------------------- @@ -71,6 +70,13 @@ def __init__(self, director, palette): self._director.coverage_switched(self.repaint) self._director.coverage_modified(self.repaint) + def terminate(self): + """ + Cleanup & terminate the painter. + """ + self._repaint_queue.put(False) + self._painting_worker.join() + #-------------------------------------------------------------------------- # Initialization #-------------------------------------------------------------------------- @@ -528,8 +534,16 @@ def _async_database_painter(self): # Asynchronous Database Painting Loop # - # block until a paint has been requested - while self._repaint_queue.get(): + while True: + + # block until a paint has been requested (a bool) + repaint_request = self._repaint_queue.get() + + # signal to stop + if not repaint_request: + break + + # more code-friendly, readable aliases database_coverage = self._director.coverage database_metadata = self._director.metadata diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 0c7fd7c4..8cce6845 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -154,10 +154,7 @@ def _uninstall_plugin(self): Cleanup & uninstall the plugin from IDA. """ self._uninstall_ui() - - #-------------------------------------------------------------------------- - # Termination - UI - #-------------------------------------------------------------------------- + self._cleanup() def _uninstall_ui(self): """ @@ -166,6 +163,13 @@ def _uninstall_ui(self): self._uninstall_open_coverage_overview() self._uninstall_load_file_dialog() + def _cleanup(self): + """ + Signal threads to exit and wait. + """ + self.director.terminate() + self.painter.terminate() + #-------------------------------------------------------------------------- # IDA Actions #-------------------------------------------------------------------------- From 2e88ffb4ca1abe60cf000a8df9e8539a027ef334 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 16:44:49 -0700 Subject: [PATCH 04/43] bugfix: hexrays hooks were not getting installed for some builds of IDA #7 --- plugin/lighthouse/painting.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index 5fcf9c94..e96381a9 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -63,7 +63,7 @@ def __init__(self, director, palette): # hook hexrays on startup self._hooks = PainterHooks() - self._hooks.ready_to_run = self._init_hexrays_hooks + self._hooks.auto_queue_empty = self._init_hexrays_hooks self._hooks.hook() # register for cues from the director @@ -81,11 +81,19 @@ def terminate(self): # Initialization #-------------------------------------------------------------------------- - def _init_hexrays_hooks(self): + def _init_hexrays_hooks(self, _=None): """ Install Hex-Rrays hooks (when available). - NOTE: This is called when the ui_ready_to_run event fires. + NOTE: + + This is called when the auto_empty_queue event fires. The + use of auto_queue_empty is somewhat arbitrary. It is simply an + event that fires at least once after things seem mostly setup. + + We were using UI_Hooks.ready_to_run previously, but it appears + that fires *before* this plugin is loaded on some builds of IDA. + """ result = False @@ -96,12 +104,13 @@ def _init_hexrays_hooks(self): logger.debug("HexRays hooked: %r" % result) # - # we only use self._hooks (UI_Hooks) to install our hexrays hooks. + # we only use self._hooks (IDP_Hooks) to install our hexrays hooks. # since this 'init' function should only ever be called once, remove - # our UI_Hooks now to clean up after ourselves. + # our IDP_Hooks now to clean up after ourselves. # self._hooks.unhook() + return 0 #------------------------------------------------------------------------------ # Painting @@ -615,7 +624,7 @@ def _async_action(self, paint_action, work_iterable): # Painter Hooks #------------------------------------------------------------------------------ -class PainterHooks(idaapi.UI_Hooks): +class PainterHooks(idaapi.IDP_Hooks): """ This is a concrete stub of IDA's UI_Hooks. """ From 72b7a48b8af1f6f7fe49aec11ee9759bc8d4b7df Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 17:15:16 -0700 Subject: [PATCH 05/43] bugfix: fixes crash on close for some builds of IDA --- plugin/lighthouse/ui/coverage_combobox.py | 6 +++--- plugin/lighthouse/ui/coverage_overview.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py index af395401..37a42ba5 100644 --- a/plugin/lighthouse/ui/coverage_combobox.py +++ b/plugin/lighthouse/ui/coverage_combobox.py @@ -55,8 +55,8 @@ def _ui_init(self): self.setFont(self._font) # create the underlying model & table to power the combobox dropwdown - self.setModel(CoverageComboBoxModel(self._director)) - self.setView(CoverageComboBoxView(self.model())) + self.setModel(CoverageComboBoxModel(self._director, self)) + self.setView(CoverageComboBoxView(self.model(), self)) # # the combobox will pick a size based on its contents when it is first @@ -356,7 +356,7 @@ class CoverageComboBoxModel(QtCore.QAbstractTableModel): """ def __init__(self, director, parent=None): - super(CoverageComboBoxModel, self).__init__() + super(CoverageComboBoxModel, self).__init__(parent) self.setObjectName(self.__class__.__name__) self._director = director diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 8a8a4e26..7035dd12 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -61,7 +61,7 @@ def __init__(self, director): # internal self._director = director - self._model = CoverageModel(director) + self._model = CoverageModel(director, self._widget) # initialize the plugin UI self._ui_init() From 9073fe5d905a5ff5a205d7a86799f217a26c5a1d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 17:27:34 -0700 Subject: [PATCH 06/43] replace the '%' character with '_' in lifted function names --- plugin/lighthouse/metadata.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 646b81ee..921942a3 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -536,6 +536,17 @@ def _refresh_name(self): else: self.name = idaapi.get_func_name2(self.address) + # + # the replace is sort of a 'special case' for the 'Prefix' IDA + # plugin: https://github.com/gaasedelen/prefix + # + # % signs are used as a marker byte for the prefix. we simply + # replace the % signs with a '_' before displaying them. this + # technically mirrors the behavior of IDA's functions view + # + + self.name = self.name.replace("%", "_") + def _refresh_nodes(self): """ Refresh the function nodes against the open database. From 5ecf965342161d4c2adfc173bad696233d2abdf6 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 17:46:36 -0700 Subject: [PATCH 07/43] remember & reuse the last directory coverage was loaded from --- plugin/lighthouse_plugin.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 8cce6845..13a02eb2 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -120,6 +120,9 @@ def _init(self): # members for the 'Coverage Overview' menu entry self._icon_id_overview = idaapi.BADADDR + # the directory to start the coverage file dialog in + self._last_directory = os.path.dirname(idaapi.cvar.database_idb) + os.sep + def _install_ui(self): """ Initialize & integrate all UI elements. @@ -425,12 +428,25 @@ def _select_coverage_files(self): """ # create & configure a Qt File Dialog for immediate use - file_dialog = QtWidgets.QFileDialog(None, 'Open Code Coverage File(s)') + file_dialog = QtWidgets.QFileDialog( + None, + 'Open Code Coverage File(s)', + self._last_directory, + 'All Files (*.*)' + ) file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles) # prompt the user with the file dialog, and await filename(s) filenames, _ = file_dialog.getOpenFileNames() + # + # remember the last directory we were in (parsed from a selected file) + # for the next time the user comes to load coverage files + # + + if filenames: + self._last_directory = os.path.dirname(filenames[0]) + os.sep + # log the captured (selected) filenames from the dialog logger.debug("Captured filenames from file dialog:") logger.debug('\n - ' + '\n - '.join(filenames)) From 9f05fdf68919e1d85db7980882547a1be2d79532 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 7 Aug 2017 18:53:21 -0700 Subject: [PATCH 08/43] case insensitive search when no capitilzation present --- plugin/lighthouse/ui/coverage_overview.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 7035dd12..58362947 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -1,7 +1,9 @@ -import idaapi +import string import logging from operator import itemgetter, attrgetter +import idaapi + from lighthouse.util import * from .coverage_combobox import CoverageComboBox from lighthouse.composer import ComposingShell @@ -631,6 +633,18 @@ def _refresh_data(self): metadata = self._director.metadata coverage = self._director.coverage + # + # if the search string is all lowercase, then we are going to perform + # a case insensitive search/filter. + # + # that means we we want to 'normalize' all the function names by + # making them lowercase before searching for our needle (search str) + # + + normalize = lambda x: x + if not (set(self._search_string) & set(string.ascii_uppercase)): + normalize = lambda x: string.lower(x) + # # it's time to rebuild the list of coverage items to make visible in # the coverage overview list. during this process, we filter out entries @@ -649,7 +663,7 @@ def _refresh_data(self): continue # OPTIONS: ignore items that do not match the search string - if not self._search_string in metadata.functions[function_address].name: + if not self._search_string in normalize(metadata.functions[function_address].name): continue #------------------------------------------------------------------ From 46b6dd86f50f317db0d07db1daa667fcb5b25676 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 15:29:01 -0700 Subject: [PATCH 09/43] store metadata for intra-function edges --- plugin/lighthouse/metadata.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 921942a3..8e4005e6 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -4,6 +4,7 @@ import ctypes import logging import threading +import collections import idaapi import idautils @@ -495,10 +496,12 @@ def __init__(self, address): # node metadata self.nodes = {} + self.edges = [] # fixed/baked/computed metrics self.size = 0 self.node_count = 0 + self.edge_count = 0 self.instruction_count = 0 # collect metdata from the underlying database @@ -597,12 +600,32 @@ def _refresh_nodes(self): node_metadata.function = function_metadata function_metadata.nodes[start_ea] = node_metadata + # + # enumerate the edges produced by this node with a destination + # that falls within this function. + # + + edge_src = next(reversed(node_metadata.instructions)) + + # NOTE/COMPAT: we do a single api check *outside* the loop for perf + if using_ida7api: + for edge_dst in idautils.CodeRefsFrom(edge_src, True): + edge_function = idaapi.get_func(edge_dst) + if edge_function and edge_function.start_ea == function.startEA: # NOTE: start_ea vs startEA + function_metadata.edges.append((edge_src, edge_dst)) + else: + for edge_dst in idautils.CodeRefsFrom(edge_src, True): + edge_function = idaapi.get_func(edge_dst) + if edge_function and edge_function.startEA == function.startEA: # NOTE: startEA vs start_ea + function_metadata.edges.append((edge_src, edge_dst)) + def _finalize(self): """ Finalize function metadata for use. """ self.size = sum(node.size for node in self.nodes.itervalues()) self.node_count = len(self.nodes) + self.edge_count = len(self.edges) self.instruction_count = sum(node.instruction_count for node in self.nodes.itervalues()) #-------------------------------------------------------------------------- @@ -657,7 +680,7 @@ def __init__(self, start_ea, end_ea, node_id=idaapi.BADADDR): self.function = None # maps instruction_address --> instruction_metadata - self.instructions = {} + self.instructions = collections.OrderedDict() #---------------------------------------------------------------------- From bf8866f3ea3473ef339ddd3184ea31cac898f84b Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 15:30:49 -0700 Subject: [PATCH 10/43] added cyclomatic complexity metric --- plugin/lighthouse/metadata.py | 2 ++ plugin/lighthouse/ui/coverage_overview.py | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 8e4005e6..95015b86 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -503,6 +503,7 @@ def __init__(self, address): self.node_count = 0 self.edge_count = 0 self.instruction_count = 0 + self.cyclomatic_complexity = 0 # collect metdata from the underlying database self._build_metadata() @@ -627,6 +628,7 @@ def _finalize(self): self.node_count = len(self.nodes) self.edge_count = len(self.edges) self.instruction_count = sum(node.instruction_count for node in self.nodes.itervalues()) + self.cyclomatic_complexity = self.edge_count - self.node_count + 2 #-------------------------------------------------------------------------- # Signal Handlers diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 58362947..4c1ce504 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -23,6 +23,7 @@ BLOCKS_HIT = 3 INST_HIT = 4 FUNC_SIZE = 5 +COMPLEXITY = 6 FINAL_COLUMN = 7 # column -> field name mapping @@ -33,7 +34,8 @@ FUNC_ADDR: "address", BLOCKS_HIT: "nodes_executed", INST_HIT: "instructions_executed", - FUNC_SIZE: "size" + FUNC_SIZE: "size", + COMPLEXITY: "cyclomatic_complexity" } # column headers of the table @@ -44,6 +46,8 @@ " 0x140001b20 ", " 100 / 100 ", " 1000 / 1000 ", + " 10000000 ", + " 1000000 " ] #------------------------------------------------------------------------------ @@ -324,6 +328,7 @@ def __init__(self, director, parent=None): BLOCKS_HIT: "Blocks Hit", INST_HIT: "Instructions Hit", FUNC_SIZE: "Function Size", + COMPLEXITY: "Complexity", FINAL_COLUMN: "" # NOTE: stretch section, left blank for now } @@ -445,12 +450,16 @@ def data(self, index, role=QtCore.Qt.DisplayRole): # Instructions Hit elif column == INST_HIT: return "%4u / %-4u" % (function_coverage.instructions_executed, - function_metadata.instruction_count) + function_metadata.instruction_count) # Function Size elif column == FUNC_SIZE: return "%u" % function_metadata.size + # Cyclomatic Complexity + elif column == COMPLEXITY: + return "%u" % function_metadata.cyclomatic_complexity + # cell background color request elif role == QtCore.Qt.BackgroundRole: function_address = self.row2func[index.row()] @@ -504,7 +513,7 @@ def sort(self, column, sort_order): # # sort the table entries by a function metadata attribute - if column in [FUNC_NAME, FUNC_ADDR, FUNC_SIZE]: + if column in [FUNC_NAME, FUNC_ADDR, FUNC_SIZE, COMPLEXITY]: sorted_functions = sorted( self._visible_metadata.itervalues(), key=attrgetter(sort_field), From 71a896ce0e12725543546dd1620345e424c38491 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 15:31:50 -0700 Subject: [PATCH 11/43] small naming refactor --- plugin/lighthouse/metadata.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 95015b86..b8dcff36 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -575,11 +575,11 @@ def _refresh_nodes(self): # NOTE/COMPAT: if using_ida7api: - start_ea = node.start_ea - end_ea = node.end_ea + node_start = node.start_ea + node_end = node.end_ea else: - start_ea = node.startEA - end_ea = node.endEA + node_start = node.startEA + node_end = node.endEA # # the node size as this flowchart sees it is 'zero'. This means @@ -587,11 +587,11 @@ def _refresh_nodes(self): # ignore it. # - if start_ea == end_ea: + if node_start == node_end: continue # create a new metadata object for this node - node_metadata = NodeMetadata(start_ea, end_ea, node_id) + node_metadata = NodeMetadata(node_start, node_end, node_id) # # establish a relationship between this node (basic block) and @@ -599,7 +599,7 @@ def _refresh_nodes(self): # node_metadata.function = function_metadata - function_metadata.nodes[start_ea] = node_metadata + function_metadata.nodes[node_start] = node_metadata # # enumerate the edges produced by this node with a destination From b392e416ed1129631c3b70e02a269c117ab08d1b Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 15:33:24 -0700 Subject: [PATCH 12/43] refactor of get_node() and get_function() --- plugin/lighthouse/composer/shell.py | 19 +++++-------------- plugin/lighthouse/coverage.py | 5 ++--- plugin/lighthouse/metadata.py | 29 ++++++++++++++--------------- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index 4392573d..cfdd9851 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -386,27 +386,21 @@ def _compute_jump(self, text): # to its corresponding function address validated by the director # - try: - - address = int(text, 16) - function_metadata = self._director.metadata.get_function(address) + address = int(text, 16) + function_metadata = self._director.metadata.get_function(address) + if function_metadata: return function_metadata.address # # the user string did not translate to a parsable hex number (address) # or the function it falls within could not be found in the director. # - - except ValueError: - pass - - # # attempt to convert the user input from a function name eg # 'sub_1400016F0' to a function address validated by the director # - try: - function_metadata = self._director.metadata.get_function_by_name(text) + function_metadata = self._director.metadata.get_function_by_name(text) + if function_metadata: return function_metadata.address # @@ -414,9 +408,6 @@ def _compute_jump(self, text): # be found in the director. # - except ValueError: - pass - # failure, the user input (text) isn't a jump ... return 0 diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index c470e2ce..06949ddb 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -347,15 +347,14 @@ def _map_nodes(self): address = addresses_to_map.popleft() # get the node (basic block) that contains this address - try: - node_metadata = self._metadata.get_node(address) + node_metadata = self._metadata.get_node(address) # # failed to locate the node (basic block) for this address. # this address must not fall inside of a defined function... # - except ValueError: + if not node_metadata: continue # diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index b8dcff36..1125f06f 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -104,7 +104,7 @@ def get_node(self, address): target address. If the target address falls within the probed node, the node's - metadata is returned. Otherwise, a ValueError is raised. + metadata is returned. Failure returns None. """ # @@ -149,20 +149,13 @@ def get_node(self, address): # if the identified node contains our target address, it is a match # - try: - node = self.nodes[self._node_addresses[node_index]] - if address in node: - self._last_node = node - return node - except (IndexError, KeyError): - pass - - # - # if the selected node was not a match, there are no second chances. - # the address simply does not exist within a defined node. - # + node = self.nodes.get(self._node_addresses[node_index], None) + if node and address in node: + self._last_node = node + return node - raise ValueError("Given address does not fall within a known node") + # node not found... + return None def get_function(self, address): """ @@ -171,9 +164,15 @@ def get_function(self, address): See get_node() for more information. If the target address falls within a function, the function's - metadata is returned. Otherwise, a ValueError is raised. + metadata is returned. Failure returns None. """ + + # locate the node the given address falls within node_metadata = self.get_node(address) + if not node_metadata: + return None + + # return the function metadata corresponding to this node. return node_metadata.function def get_function_by_name(self, function_name): From 945b2a612190eb25146fa3363f674b80cf3bd86c Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 19:35:27 -0700 Subject: [PATCH 13/43] IDA 7 compatibility fixes --- plugin/lighthouse/painting.py | 23 +++++++++++++++++------ plugin/lighthouse_plugin.py | 7 ++++--- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index e96381a9..b7874110 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -6,7 +6,7 @@ import idaapi import idautils -from lighthouse.util import chunks +from lighthouse.util import chunks, using_ida7api from lighthouse.util.ida import * logger = logging.getLogger("Lighthouse.Painting") @@ -61,10 +61,15 @@ def __init__(self, director, palette): # Callbacks #---------------------------------------------------------------------- - # hook hexrays on startup - self._hooks = PainterHooks() - self._hooks.auto_queue_empty = self._init_hexrays_hooks - self._hooks.hook() + # NOTE/COMPAT: hook hexrays on startup + if using_ida7api: + self._hooks = PainterHooks7() + self._hooks.ready_to_run = self._init_hexrays_hooks + self._hooks.hook() + else: + self._hooks = PainterHooks6() + self._hooks.auto_queue_empty = self._init_hexrays_hooks + self._hooks.hook() # register for cues from the director self._director.coverage_switched(self.repaint) @@ -624,7 +629,13 @@ def _async_action(self, paint_action, work_iterable): # Painter Hooks #------------------------------------------------------------------------------ -class PainterHooks(idaapi.IDP_Hooks): +class PainterHooks6(idaapi.IDP_Hooks): + """ + This is a concrete stub of IDA's IDP_Hooks. + """ + pass + +class PainterHooks7(idaapi.UI_Hooks): """ This is a concrete stub of IDA's UI_Hooks. """ diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 13a02eb2..03d4466c 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -1,6 +1,7 @@ import os -from idaapi import plugin_t +import idaapi +import idautils from lighthouse.ui import * from lighthouse.util import * @@ -28,7 +29,7 @@ def PLUGIN_ENTRY(): """ return Lighthouse() -class Lighthouse(plugin_t): +class Lighthouse(idaapi.plugin_t): """ The Lighthouse IDA Plugin. """ @@ -121,7 +122,7 @@ def _init(self): self._icon_id_overview = idaapi.BADADDR # the directory to start the coverage file dialog in - self._last_directory = os.path.dirname(idaapi.cvar.database_idb) + os.sep + self._last_directory = idautils.GetIdbDir() def _install_ui(self): """ From a91d04df4a027bd75c8aa162efe2b5e574218d31 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 19:40:15 -0700 Subject: [PATCH 14/43] bugfix a regression from refactoring --- plugin/lighthouse/util/ida.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index f99e9df1..74cacba8 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -103,11 +103,10 @@ def map_line2node(cfunc, metadata, line2citem): continue # find the graph node (eg, basic block) that generated this citem - try: - node = metadata.get_node(address) + node = metadata.get_node(address) # address not mapped to a node... weird. continue to the next citem - except ValueError: + if not node: #logger.warning("Failed to map node to basic block") continue From 3b402839f6232cccf103d4493605712ceb2ace7a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 20:42:59 -0700 Subject: [PATCH 15/43] reuse & refresh existing coverage overview when available --- plugin/lighthouse/ui/coverage_overview.py | 6 ++++++ plugin/lighthouse_plugin.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 4c1ce504..1284c838 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -82,6 +82,12 @@ def show(self): self.refresh() super(CoverageOverview, self).show() + def visible(self): + """ + Widget visibility status. + """ + return self._widget.isVisible() + #-------------------------------------------------------------------------- # Initialization - UI #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 03d4466c..31a6df62 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -309,6 +309,13 @@ def open_coverage_overview(self): """ Open the 'Coverage Overview' dialog. """ + + # the coverage overview is already open & visible, simply refresh it + if self._ui_coverage_overview and self._ui_coverage_overview.visible(): + self._ui_coverage_overview.refresh() + return + + # create a new coverage overview if there is not one visible self._ui_coverage_overview = CoverageOverview(self.director) self._ui_coverage_overview.show() From d5446d8a91a51dc1ae3ec595c141d4c213648693 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 8 Aug 2017 23:23:39 -0700 Subject: [PATCH 16/43] better handling of malformed input / wrong files --- plugin/lighthouse_plugin.py | 52 +++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 31a6df62..b5f1e787 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -349,7 +349,10 @@ def load_coverage(self): # load the selected coverage files from disk # - coverage_data = self._load_coverage_files(filenames) + loaded_coverage = self._load_coverage_files(filenames) + if not loaded_coverage: + self.director.metadata.abort_refresh() + return # # refresh the theme aware color palette for lighthouse @@ -381,8 +384,8 @@ def load_coverage(self): self.director.start_batch() - # a list to output the names of successfully loaded coverage files - loaded = [] + # a list to output the names of successfully mapped coverage files + mapped_coverage = [] # # loop through the coverage data we have loaded from disk, and begin @@ -390,10 +393,10 @@ def load_coverage(self): # insertion into the director (as a list of instruction addresses) # - for i, data in enumerate(coverage_data, 1): + for i, data in enumerate(loaded_coverage, 1): # keep the user informed about our progress while loading coverage - idaapi.replace_wait_box("Normalizing and mapping coverage %u/%u" % (i, len(coverage_data))) + idaapi.replace_wait_box("Normalizing and mapping coverage %u/%u" % (i, len(loaded_coverage))) # TODO: it would be nice to get rid of this try/catch in the long run try: @@ -406,26 +409,26 @@ def load_coverage(self): self.director.add_coverage(coverage_name, addresses) # if we made it this far, the coverage must have loaded okay... - loaded.append(coverage_name) + mapped_coverage.append(coverage_name) except Exception as e: - lmsg("Failed to load coverage:") + lmsg("Failed to map coverage %s" % data.filepath) lmsg("- %s" % e) - logger.error("Error details:") + logger.exception("Error details:") continue # collapse the batch job to recompute the director's aggregate coverage set self.director.end_batch() - # select the 'first' coverage file loaded from this round - if loaded: - self.director.select_coverage(loaded[0]) + # select the 'first' coverage file loaded and mapped from this round + if mapped_coverage: + self.director.select_coverage(mapped_coverage[0]) # all done, hide the IDA wait box idaapi.hide_wait_box() # print a success message to the output window - lmsg("loaded %u coverage file(s)..." % len(loaded)) + lmsg("Successfully loaded %u coverage file(s)..." % len(mapped_coverage)) # show the coverage overview self.open_coverage_overview() @@ -490,7 +493,30 @@ def _load_coverage_files(self, filenames): """ Load multiple code coverage files from disk. """ - return [self._load_coverage_file(filename) for filename in filenames] + loaded_coverage = [] + + # + # loop through each of the given filenames and attempt to load/parse + # their coverage data from disk + # + + for filename in filenames: + + # attempt to load/parse a single coverage data file from disk + try: + coverage_data = self._load_coverage_file(filename) + + # catch all for parse errors / bad input / malformed files + except Exception as e: + lmsg("Failed to load coverage %s" % filename) + logger.exception("Error details:") + continue + + # save the loaded coverage data to the output list + loaded_coverage.append(coverage_data) + + # return all the succesfully loaded coverage files + return loaded_coverage def _load_coverage_file(self, filename): """ From 2fb4b1b1e3e5653d70fb9668df9a41a5a9ca2332 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 16:17:41 -0700 Subject: [PATCH 17/43] created add_addresses for coverage objects --- plugin/lighthouse/coverage.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index 06949ddb..6c3f2719 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -245,6 +245,25 @@ def add_data(self, data): # mark these touched addresses as dirty self._unmapped_data |= data.viewkeys() + def add_addresses(self, addresses, update=True): + """ + Add a list of instruction addresses to this mapping (eg, a trace). + """ + + # increment the hit count for an address + for address in addresses: + self._hitmap[address] += 1 + + # do not update other internal structures if requested + if not update: + return + + # update the coverage hash incase the hitmap changed + self._update_coverage_hash() + + # mark these touched addresses as dirty + self._unmapped_data |= set(addresses) + def subtract_data(self, data): """ Subtract runtime data from this mapping. From 62e712417d8c777e2c6483fa66c07fcd407c51fd Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 16:22:13 -0700 Subject: [PATCH 18/43] rename add_coverage to create_coverage --- plugin/lighthouse/director.py | 14 ++++++++++---- plugin/lighthouse_plugin.py | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index de9f891e..47bc063a 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -408,17 +408,17 @@ def select_coverage(self, coverage_name): # notify any listeners that we have switched our active coverage self._notify_coverage_switched() - def add_coverage(self, coverage_name, coverage_data): + def create_coverage(self, coverage_name, coverage_data): """ - Add new coverage to the director. + Create a new coverage object maintained by the director. This is effectively an alias of self.update_coverage """ - self.update_coverage(coverage_name, coverage_data) + return self.update_coverage(coverage_name, coverage_data) def update_coverage(self, coverage_name, coverage_data): """ - Add or update coverage maintained by the director. + Create or update a coverage object. """ assert not (coverage_name in RESERVED_NAMES) updating_coverage = coverage_name in self.coverage_names @@ -431,8 +431,11 @@ def update_coverage(self, coverage_name, coverage_data): # create & map a new database coverage object using the given data new_coverage = self._build_coverage(coverage_data) + # # coverage mapping complete, looks like we're good. add the new # coverage to the director's coverage table and surface it for use. + # + self._update_coverage(coverage_name, new_coverage) # assign a shorthand alias (if available) to new coverage additions @@ -445,6 +448,9 @@ def update_coverage(self, coverage_name, coverage_data): else: self._notify_coverage_created() + # return the created/updated coverage + return new_coverage + def _update_coverage(self, coverage_name, new_coverage): """ Internal add/update of coverage. diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index b5f1e787..c96af9a5 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -406,7 +406,7 @@ def load_coverage(self): # enlighten the coverage director to this new runtime data coverage_name = os.path.basename(data.filepath) - self.director.add_coverage(coverage_name, addresses) + self.director.create_coverage(coverage_name, addresses) # if we made it this far, the coverage must have loaded okay... mapped_coverage.append(coverage_name) From 588f73f93143f6665d5ec149c753641737d4e57a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 16:24:20 -0700 Subject: [PATCH 19/43] make the coverage object's unmap_all a public function --- plugin/lighthouse/coverage.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index 6c3f2719..51b6258c 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -293,11 +293,7 @@ def subtract_data(self, data): # current implementation of things # - self._unmap_all() - - #-------------------------------------------------------------------------- - # Coverage Operations - #-------------------------------------------------------------------------- + self.unmap_all() def mask_data(self, coverage_mask): """ @@ -495,7 +491,7 @@ def _map_functions(self, dirty_nodes): # done return dirty_functions - def _unmap_all(self): + def unmap_all(self): """ Unmap all mapped data. """ From 9cadf4cb25fbe1512a184b77cae75e14e9a60b25 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 16:38:51 -0700 Subject: [PATCH 20/43] refactor naming of 'load_coverage' to 'load_file' --- plugin/lighthouse_plugin.py | 83 +++++++++++++++++++++++-------------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index c96af9a5..24d4c6d8 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -115,10 +115,8 @@ def _init(self): # the coverage overview widget self._ui_coverage_overview = None - # members for the 'Load Code Coverage' menu entry - self._icon_id_load = idaapi.BADADDR - - # members for the 'Coverage Overview' menu entry + # menu entry icons + self._icon_id_file = idaapi.BADADDR self._icon_id_overview = idaapi.BADADDR # the directory to start the coverage file dialog in @@ -128,9 +126,7 @@ def _install_ui(self): """ Initialize & integrate all UI elements. """ - - # install the 'Load Coverage' file dialog - self._install_load_file_dialog() + self._install_load_file() self._install_open_coverage_overview() def print_banner(self): @@ -165,7 +161,7 @@ def _uninstall_ui(self): Cleanup & uninstall the plugin UI from IDA. """ self._uninstall_open_coverage_overview() - self._uninstall_load_file_dialog() + self._uninstall_load_file() def _cleanup(self): """ @@ -178,44 +174,45 @@ def _cleanup(self): # IDA Actions #-------------------------------------------------------------------------- - ACTION_LOAD_COVERAGE = "lighthouse:load_coverage" + ACTION_LOAD_FILE = "lighthouse:load_file" ACTION_COVERAGE_OVERVIEW = "lighthouse:coverage_overview" - def _install_load_file_dialog(self): + def _install_load_file(self): """ - Install the 'File->Load->Code Coverage File(s)...' menu entry. + Install the 'File->Load->Code coverage file...' menu entry. """ # create a custom IDA icon icon_path = plugin_resource(os.path.join("icons", "load.png")) icon_data = str(open(icon_path, "rb").read()) - self._icon_id_load = idaapi.load_custom_icon(data=icon_data) + self._icon_id_file = idaapi.load_custom_icon(data=icon_data) # describe a custom IDA UI action action_desc = idaapi.action_desc_t( - self.ACTION_LOAD_COVERAGE, # The action name. - "~C~ode Coverage File(s)...", # The action text. - IDACtxEntry(self.load_coverage), # The action handler. - None, # Optional: action shortcut - "Load a code coverage file for this IDB", # Optional: tooltip - self._icon_id_load # Optional: the action icon + self.ACTION_LOAD_FILE, # The action name. + "~C~ode coverage file...", # The action text. + IDACtxEntry(self.interactive_load_file), # The action handler. + None, # Optional: action shortcut + "Load individual code coverage file(s)", # Optional: tooltip + self._icon_id_file # Optional: the action icon ) # register the action with IDA result = idaapi.register_action(action_desc) if not result: - RuntimeError("Failed to register load coverage action with IDA") + RuntimeError("Failed to register load_file action with IDA") # attach the action to the File-> dropdown menu result = idaapi.attach_action_to_menu( "File/Load file/", # Relative path of where to add the action - self.ACTION_LOAD_COVERAGE, # The action ID (see above) + self.ACTION_LOAD_FILE, # The action ID (see above) idaapi.SETMENU_APP # We want to append the action after ^ ) if not result: - RuntimeError("Failed action attach to 'File/Load file/' dropdown") + RuntimeError("Failed action attach load_file") + + logger.info("Installed the 'Code coverage file' menu entry") - logger.info("Installed the 'Load Code Coverage' menu entry") def _install_open_coverage_overview(self): """ @@ -253,29 +250,53 @@ def _install_open_coverage_overview(self): logger.info("Installed the 'Coverage Overview' menu entry") - def _uninstall_load_file_dialog(self): + def _uninstall_load_file(self): + """ + Remove the 'File->Load file->Code coverage file...' menu entry. + """ + + # remove the entry from the File-> menu + result = idaapi.detach_action_from_menu( + "File/Load file/", + self.ACTION_LOAD_FILE + ) + if not result: + return False + + # unregister the action + result = idaapi.unregister_action(self.ACTION_LOAD_FILE) + if not result: + return False + + # delete the entry's icon + idaapi.free_custom_icon(self._icon_id_file) + self._icon_id_file = idaapi.BADADDR + + logger.info("Uninstalled the 'Code coverage file' menu entry") + + def _uninstall_load_batch(self): """ - Remove the 'File->Load file->Code Coverage File(s)...' menu entry. + Remove the 'File->Load file->Code coverage batch...' menu entry. """ # remove the entry from the File-> menu result = idaapi.detach_action_from_menu( "File/Load file/", - self.ACTION_LOAD_COVERAGE + self.ACTION_LOAD_BATCH ) if not result: return False # unregister the action - result = idaapi.unregister_action(self.ACTION_LOAD_COVERAGE) + result = idaapi.unregister_action(self.ACTION_LOAD_BATCH) if not result: return False # delete the entry's icon - idaapi.free_custom_icon(self._icon_id_load) - self._icon_id_load = idaapi.BADADDR + idaapi.free_custom_icon(self._icon_id_batch) + self._icon_id_batch = idaapi.BADADDR - logger.info("Uninstalled the 'Load Code Coverage' menu entry") + logger.info("Uninstalled the 'Code coverage batch' menu entry") def _uninstall_open_coverage_overview(self): """ @@ -319,7 +340,7 @@ def open_coverage_overview(self): self._ui_coverage_overview = CoverageOverview(self.director) self._ui_coverage_overview.show() - def load_coverage(self): + def interactive_load_file(self): """ An interactive file dialog flow for loading code coverage files. """ @@ -441,7 +462,7 @@ def _select_coverage_files(self): # create & configure a Qt File Dialog for immediate use file_dialog = QtWidgets.QFileDialog( None, - 'Open Code Coverage File(s)', + 'Open code coverage file', self._last_directory, 'All Files (*.*)' ) From 33ff18447ca5ec6c183789fe66bf3c36a49858e7 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 16:55:44 -0700 Subject: [PATCH 21/43] added batch menu items --- plugin/lighthouse_plugin.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 24d4c6d8..f54e3d13 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -117,6 +117,7 @@ def _init(self): # menu entry icons self._icon_id_file = idaapi.BADADDR + self._icon_id_batch = idaapi.BADADDR self._icon_id_overview = idaapi.BADADDR # the directory to start the coverage file dialog in @@ -127,6 +128,7 @@ def _install_ui(self): Initialize & integrate all UI elements. """ self._install_load_file() + self._install_load_batch() self._install_open_coverage_overview() def print_banner(self): @@ -161,6 +163,7 @@ def _uninstall_ui(self): Cleanup & uninstall the plugin UI from IDA. """ self._uninstall_open_coverage_overview() + self._uninstall_load_batch() self._uninstall_load_file() def _cleanup(self): @@ -175,6 +178,7 @@ def _cleanup(self): #-------------------------------------------------------------------------- ACTION_LOAD_FILE = "lighthouse:load_file" + ACTION_LOAD_BATCH = "lighthouse:load_batch" ACTION_COVERAGE_OVERVIEW = "lighthouse:coverage_overview" def _install_load_file(self): @@ -213,6 +217,41 @@ def _install_load_file(self): logger.info("Installed the 'Code coverage file' menu entry") + def _install_load_batch(self): + """ + Install the 'File->Load->Code coverage batch...' menu entry. + """ + + # create a custom IDA icon + icon_path = plugin_resource(os.path.join("icons", "load.png")) # TODO + icon_data = str(open(icon_path, "rb").read()) + self._icon_id_batch = idaapi.load_custom_icon(data=icon_data) + + # describe a custom IDA UI action + action_desc = idaapi.action_desc_t( + self.ACTION_LOAD_BATCH, # The action name. + "~C~ode coverage batch...", # The action text. + IDACtxEntry(self.interactive_load_batch), # The action handler. + None, # Optional: action shortcut + "Load and aggregate code coverage files", # Optional: tooltip + self._icon_id_batch # Optional: the action icon + ) + + # register the action with IDA + result = idaapi.register_action(action_desc) + if not result: + RuntimeError("Failed to register load_batch action with IDA") + + # attach the action to the File-> dropdown menu + result = idaapi.attach_action_to_menu( + "File/Load file/", # Relative path of where to add the action + self.ACTION_LOAD_BATCH, # The action ID (see above) + idaapi.SETMENU_APP # We want to append the action after ^ + ) + if not result: + RuntimeError("Failed action attach load_batch") + + logger.info("Installed the 'Code coverage batch' menu entry") def _install_open_coverage_overview(self): """ @@ -340,6 +379,11 @@ def open_coverage_overview(self): self._ui_coverage_overview = CoverageOverview(self.director) self._ui_coverage_overview.show() + def interactive_load_batch(self): + """ + TODO + """ + print "hello world" def interactive_load_file(self): """ An interactive file dialog flow for loading code coverage files. From 4395b0cbdf7f8a4398fa7708a1c1b73072a0133d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 20:25:24 -0700 Subject: [PATCH 22/43] fix for shell regression --- plugin/lighthouse/composer/shell.py | 12 ++++++++---- plugin/lighthouse/metadata.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index cfdd9851..020f9d5b 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -386,10 +386,14 @@ def _compute_jump(self, text): # to its corresponding function address validated by the director # - address = int(text, 16) - function_metadata = self._director.metadata.get_function(address) - if function_metadata: - return function_metadata.address + try: + address = int(text, 16) + except ValueError: + pass + else: + function_metadata = self._director.metadata.get_function(address) + if function_metadata: + return function_metadata.address # # the user string did not translate to a parsable hex number (address) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 1125f06f..fc808e59 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -183,7 +183,7 @@ def get_function_by_name(self, function_name): return self.functions[self._name2func[function_name]] except (IndexError, KeyError): pass - raise ValueError("Given function name does not exist") + return None def flatten_blocks(self, basic_blocks): """ From 589f69dc3f22cba1d2f3d6124a5cb43eeaacdc06 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 20:35:58 -0700 Subject: [PATCH 23/43] add shorthand peeking to the director --- plugin/lighthouse/director.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 47bc063a..6eb03c87 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -617,6 +617,15 @@ def get_shorthand(self, coverage_name): except KeyError: return None + def peek_shorthand(self): + """ + Peek at the next available shorthand symbol. + """ + try: + return self._shorthand[0] + except IndexError: + return None + def _request_shorthand_alias(self, coverage_name): """ Assign the next shorthand A-Z alias to the given coverage. From b356dce551f69d35efb3bdf4da7efef192a77e4d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 20:50:30 -0700 Subject: [PATCH 24/43] misc updates --- plugin/lighthouse/coverage.py | 9 ++++++++- plugin/lighthouse/director.py | 7 +++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index 51b6258c..e5b5249d 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -195,6 +195,9 @@ def refresh(self): # bake our coverage map self._finalize(dirty_nodes, dirty_functions) + # update the coverage hash incase the hitmap changed + self._update_coverage_hash() + # dump the unmappable coverage data #self.dump_unmapped() @@ -230,7 +233,7 @@ def _finalize_functions(self, dirty_functions): # Data Operations #-------------------------------------------------------------------------- - def add_data(self, data): + def add_data(self, data, update=True): """ Add runtime data to this mapping. """ @@ -239,6 +242,10 @@ def add_data(self, data): for address, hit_count in data.iteritems(): self._hitmap[address] += hit_count + # do not update other internal structures if requested + if not update: + return + # update the coverage hash incase the hitmap changed self._update_coverage_hash() diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 6eb03c87..3bf76d38 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -48,6 +48,9 @@ def __init__(self, palette): # database metadata cache self._database_metadata = DatabaseMetadata() + # flag indicating a batch load is in progress + self._batch_in_progress = False + #---------------------------------------------------------------------- # Coverage #---------------------------------------------------------------------- @@ -429,7 +432,7 @@ def update_coverage(self, coverage_name, coverage_data): logger.debug("Adding coverage %s" % coverage_name) # create & map a new database coverage object using the given data - new_coverage = self._build_coverage(coverage_data) + new_coverage = self._new_coverage(coverage_data) # # coverage mapping complete, looks like we're good. add the new @@ -482,7 +485,7 @@ def _update_coverage(self, coverage_name, new_coverage): if not self._batch_in_progress: self._refresh_aggregate() - def _build_coverage(self, coverage_data): + def _new_coverage(self, coverage_data): """ Build a new database coverage object from the given data. """ From 26ee4eb2bccf577a7c0b16bc07f99f2c1ecc45cb Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 9 Aug 2017 20:53:34 -0700 Subject: [PATCH 25/43] rough implementation of batch loading --- plugin/lighthouse_plugin.py | 135 +++++++++++++++++++++++++++++++++++- 1 file changed, 133 insertions(+), 2 deletions(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index f54e3d13..b94b7717 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -9,6 +9,7 @@ from lighthouse.palette import LighthousePalette from lighthouse.painting import CoveragePainter from lighthouse.director import CoverageDirector +from lighthouse.coverage import DatabaseCoverage from lighthouse.metadata import DatabaseMetadata, metadata_progress # start the global logger *once* @@ -381,9 +382,80 @@ def open_coverage_overview(self): def interactive_load_batch(self): """ - TODO + Interactive batch coverage load. """ - print "hello world" + + # select and load coverage files from disk + loaded_files = self._select_while_refresh() + if not loaded_files: + return + + # aggregate all the selected files into one new coverage set + new_coverage = self._aggregate_batch(loaded_files) + + self.palette.refresh_colors() + + # prompt the user to name the new coverage aggregate + default_name = "BATCH_%s" % self.director.peek_shorthand() + coverage_name = idaapi.askstr(0, default_name, "Batch name") + if not coverage_name: + lmsg("No batch name provided. Aborting load...") + return + + # inject the the aggregated coverage set + self.director.create_coverage(coverage_name, new_coverage.data) + + # select the newly created batch coverage + self.director.select_coverage(coverage_name) + + # print a success message to the output window + lmsg("Successfully loaded batch %s..." % coverage_name) + + # show the coverage overview + self.open_coverage_overview() + + def _aggregate_batch(self, loaded_files): + """ + Aggregate the given loaded_files data into a single coverage object. + """ + idaapi.show_wait_box("Aggregating coverage batch...") + + # create a new coverage set to manually aggregate data into + coverage = DatabaseCoverage({}, self.palette) + + # + # loop through the coverage data we have loaded from disk, and begin + # the normalization process to translate / filter / flatten it for + # insertion into the director (as a list of instruction addresses) + # + + for i, data in enumerate(loaded_files, 1): + + # keep the user informed about our progress while loading coverage + idaapi.replace_wait_box( + "Aggregating batch data %u/%u" % (i, len(loaded_files)) + ) + + # normalize coverage data to the database + try: + addresses = self._normalize_coverage(data, self.director.metadata) + + # normalization failed, print & log it + except Exception as e: + lmsg("Failed to map coverage %s" % data.filepath) + lmsg("- %s" % e) + logger.exception("Error details:") + continue + + # aggregate the addresses into the output coverage object + coverage.add_addresses(addresses, False) + + # all done, hide the IDA wait box + idaapi.hide_wait_box() + + # return the created coverage name + return coverage + def interactive_load_file(self): """ An interactive file dialog flow for loading code coverage files. @@ -498,6 +570,65 @@ def interactive_load_file(self): # show the coverage overview self.open_coverage_overview() + def _select_while_refresh(self): + """ + Interactive file selection with asynchronous metadata refresh. + """ + + # + # kick off an asynchronous metadata refresh. this collects underlying + # database metadata while the user will be busy selecting coverage files. + # + # the collected metadata enables the director to process, map, and + # manipulate loaded coverage data in a performant, asynchronous manner. + # + + future = self.director.metadata.refresh(progress_callback=metadata_progress) + + # + # we will now prompt the user with an interactive file dialog so they + # can select the coverage files they would like to load from disk. + # + + loaded_files = self._select_and_load_coverage_files() + + # if no valid coveragee files were selected (and loaded), bail + if not loaded_files: + self.director.metadata.abort_refresh() + return + + # + # to continue any further, we need the database metadata. hopefully + # it has finished with its asynchronous collection, otherwise we will + # block until it completes. the user will be shown a progress dialog. + # + + idaapi.show_wait_box("Building database metadata...") + await_future(future) + idaapi.hide_wait_box() + + # return the loaded coverage files + return loaded_files + + def _select_and_load_coverage_files(self): + """ + Interactive coverage file selection. + """ + + # + # prompt the user with a QtFileDialog so that they can select any + # number of coverage files to load at once. + # + # if no files are selected, we abort the coverage loading process. + # + + filenames = self._select_coverage_files() + if not filenames: + return None + + # load the selected coverage files from disk and return them + return self._load_coverage_files(filenames) + def _select_coverage_files(self): """ Open the 'Load Code Coverage' dialog and capture file selections. From f563be02472d0840b7198616b789a5e94b96231f Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 09:54:59 -0700 Subject: [PATCH 26/43] fixes issue #9 --- plugin/lighthouse/director.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index de9f891e..c8edb32a 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -341,7 +341,13 @@ def _notify_callback(self, callback_list): continue # call the object instance callback - callback(obj) + try: + callback(obj) + + # assume a Qt cleanup/deletion occured + except RuntimeError as e: + cleanup.append(callback_ref) + continue # if the callback is a static method... else: From 6bde62c677619f530f8bf2c276ff49b056da4798 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 09:57:05 -0700 Subject: [PATCH 27/43] fixes potentially unsafe behavior --- plugin/lighthouse/ui/coverage_overview.py | 42 ++++++++++++++++++++--- plugin/lighthouse_plugin.py | 2 +- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 1284c838..ff22112e 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -1,5 +1,6 @@ import string import logging +import weakref from operator import itemgetter, attrgetter import idaapi @@ -50,6 +51,20 @@ " 1000000 " ] +#------------------------------------------------------------------------------ +# Pseudo Widget Filter +#------------------------------------------------------------------------------ + +class EventProxy(QtCore.QObject): + def __init__(self, target): + super(EventProxy, self).__init__() + self._target = target + + def eventFilter(self, source, event): + if event.type() == QtCore.QEvent.Destroy: + self._target.terminate() + return False + #------------------------------------------------------------------------------ # Coverage Overview #------------------------------------------------------------------------------ @@ -69,24 +84,39 @@ def __init__(self, director): self._director = director self._model = CoverageModel(director, self._widget) + # pseudo widget science + self._visible = False + self._events = EventProxy(self) + self._widget.installEventFilter(self._events) + # initialize the plugin UI self._ui_init() # refresh the data UI such that it reflects the most recent data self.refresh() + #-------------------------------------------------------------------------- + # Pseudo Widget Functions + #-------------------------------------------------------------------------- + def show(self): """ Show the CoverageOverview UI / widget. """ self.refresh() super(CoverageOverview, self).show() + self._visible = True - def visible(self): + def terminate(self): """ - Widget visibility status. + The CoverageOverview is being hidden / deleted. """ - return self._widget.isVisible() + self._visible = False + self._model = None + self._widget = None + + def isVisible(self): + return self._visible #-------------------------------------------------------------------------- # Initialization - UI @@ -202,7 +232,11 @@ def _ui_init_toolbar_elements(self): """ # the composing shell - self._shell = ComposingShell(self._director, self._model, self._table) + self._shell = ComposingShell( + self._director, + weakref.proxy(self._model), + self._table + ) # the coverage combobox self._combobox = CoverageComboBox(self._director) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index b5f1e787..018beee1 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -311,7 +311,7 @@ def open_coverage_overview(self): """ # the coverage overview is already open & visible, simply refresh it - if self._ui_coverage_overview and self._ui_coverage_overview.visible(): + if self._ui_coverage_overview and self._ui_coverage_overview.isVisible(): self._ui_coverage_overview.refresh() return From 78d0ccc5537c45bbf4359fd29daf6b97e9ece4d6 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 11:30:04 -0700 Subject: [PATCH 28/43] removing the self._director reference from the CoverageOverview --- plugin/lighthouse/ui/coverage_overview.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index ff22112e..097780b4 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -81,7 +81,6 @@ def __init__(self, director): ) # internal - self._director = director self._model = CoverageModel(director, self._widget) # pseudo widget science @@ -90,7 +89,7 @@ def __init__(self, director): self._widget.installEventFilter(self._events) # initialize the plugin UI - self._ui_init() + self._ui_init(director) # refresh the data UI such that it reflects the most recent data self.refresh() @@ -122,7 +121,7 @@ def isVisible(self): # Initialization - UI #-------------------------------------------------------------------------- - def _ui_init(self): + def _ui_init(self, director): """ Initialize UI elements. """ @@ -132,14 +131,14 @@ def _ui_init(self): self._font_metrics = QtGui.QFontMetricsF(self._font) # initialize our ui elements - self._ui_init_table() - self._ui_init_toolbar() + self._ui_init_table(director) + self._ui_init_toolbar(director) self._ui_init_signals() # layout the populated ui just before showing it self._ui_layout() - def _ui_init_table(self): + def _ui_init_table(self, director): """ Initialize the coverage table. """ @@ -147,7 +146,7 @@ def _ui_init_table(self): self._table.setFocusPolicy(QtCore.Qt.NoFocus) self._table.setStyleSheet( "QTableView { gridline-color: black; } " + - "QTableView::item:selected { color: white; background-color: %s; } " % self._director._palette.selection.name() + "QTableView::item:selected { color: white; background-color: %s; } " % director._palette.selection.name() ) # set these properties so the user can arbitrarily shrink the table @@ -194,13 +193,13 @@ def _ui_init_table(self): self._table.setSortingEnabled(True) hh.setSortIndicator(FUNC_ADDR, QtCore.Qt.AscendingOrder) - def _ui_init_toolbar(self): + def _ui_init_toolbar(self, director): """ Initialize the coverage toolbar. """ # initialize toolbar elements - self._ui_init_toolbar_elements() + self._ui_init_toolbar_elements(director) # populate the toolbar self._toolbar = QtWidgets.QToolBar() @@ -226,20 +225,20 @@ def _ui_init_toolbar(self): self._toolbar.addWidget(self._hide_zero_label) self._toolbar.addWidget(self._hide_zero_checkbox) - def _ui_init_toolbar_elements(self): + def _ui_init_toolbar_elements(self, director): """ Initialize the coverage toolbar UI elements. """ # the composing shell self._shell = ComposingShell( - self._director, + director, weakref.proxy(self._model), self._table ) # the coverage combobox - self._combobox = CoverageComboBox(self._director) + self._combobox = CoverageComboBox(director) # the checkbox to hide 0% coverage entries self._hide_zero_label = QtWidgets.QLabel("Hide 0% Coverage: ") From 36ca02da40004daabb2bde45616a367827612e4d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 19:46:01 -0700 Subject: [PATCH 29/43] added batch.png icon --- plugin/lighthouse/ui/resources/icons/batch.png | Bin 0 -> 8199 bytes plugin/lighthouse_plugin.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 plugin/lighthouse/ui/resources/icons/batch.png diff --git a/plugin/lighthouse/ui/resources/icons/batch.png b/plugin/lighthouse/ui/resources/icons/batch.png new file mode 100644 index 0000000000000000000000000000000000000000..cda1778d6fd9012575e6fc5b2a378b0890b853e3 GIT binary patch literal 8199 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 R?`0JLR1`Gj>ttbJ{|CkFUg-b; literal 0 HcmV?d00001 diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index b94b7717..c500020d 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -224,7 +224,7 @@ def _install_load_batch(self): """ # create a custom IDA icon - icon_path = plugin_resource(os.path.join("icons", "load.png")) # TODO + icon_path = plugin_resource(os.path.join("icons", "batch.png")) icon_data = str(open(icon_path, "rb").read()) self._icon_id_batch = idaapi.load_custom_icon(data=icon_data) From 7901ac7f10caa66725a80dc4c81849a460c7b1b6 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 19:46:48 -0700 Subject: [PATCH 30/43] added prompt_string() as a non-IDA blocking string input dialog --- plugin/lighthouse/util/ida.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index 74cacba8..9958c8bc 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -413,3 +413,24 @@ def flush_ida_sync_requests(): # done return True + +@mainthread +def prompt_string(label, title, default=""): + """ + Prompt the user with a dialog to enter a string. + + This does not block the IDA main thread (unlike idaapi.askstr) + """ + dlg = QtWidgets.QInputDialog(None) + dlg.setWindowFlags(dlg.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) + dlg.setInputMode(QtWidgets.QInputDialog.TextInput) + dlg.setLabelText(label) + dlg.setWindowTitle(title) + dlg.setTextValue(default) + dlg.resize( + dlg.fontMetrics().averageCharWidth()*80, + dlg.fontMetrics().averageCharWidth()*10 + ) + ok = dlg.exec_() + text = dlg.textValue() + return (ok, text) From 259bc72f3bfc09d9703356c08e030d3c5ea252b1 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 19:47:40 -0700 Subject: [PATCH 31/43] refactor & simplify loading process flow --- plugin/lighthouse_plugin.py | 197 ++++++++++++++++-------------------- 1 file changed, 86 insertions(+), 111 deletions(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index c500020d..d5c6ce21 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -382,33 +382,63 @@ def open_coverage_overview(self): def interactive_load_batch(self): """ - Interactive batch coverage load. + Interactive loading & aggregation of coverage files. """ - # select and load coverage files from disk - loaded_files = self._select_while_refresh() - if not loaded_files: - return + # + # kick off an asynchronous metadata refresh. this collects underlying + # database metadata while the user will be busy selecting coverage files. + # - # aggregate all the selected files into one new coverage set - new_coverage = self._aggregate_batch(loaded_files) + future = self.director.metadata.refresh(progress_callback=metadata_progress) + + # + # we will now prompt the user with an interactive file dialog so they + # can select the coverage files they would like to load from disk. + # - self.palette.refresh_colors() + loaded_files = self._select_and_load_coverage_files() + + # if no valid coveragee files were selected (and loaded), bail + if not loaded_files: + self.director.metadata.abort_refresh() + return # prompt the user to name the new coverage aggregate default_name = "BATCH_%s" % self.director.peek_shorthand() - coverage_name = idaapi.askstr(0, default_name, "Batch name") - if not coverage_name: - lmsg("No batch name provided. Aborting load...") + ok, coverage_name = prompt_string( + "Batch Name:", + "Please enter a name for this coverage", + default_name + ) + + # if user didn't enter a name for the batch, or hit cancel, we abort + if not (ok and coverage_name): + lmsg("Aborting batch load...") return + # + # to continue any further, we need the database metadata. hopefully + # it has finished with its asynchronous collection, otherwise we will + # block until it completes. the user will be shown a progress dialog. + # + + idaapi.show_wait_box("Building database metadata...") + await_future(future) + + # aggregate all the selected files into one new coverage set + new_coverage = self._aggregate_batch(loaded_files) + # inject the the aggregated coverage set + idaapi.replace_wait_box("Mapping coverage...") self.director.create_coverage(coverage_name, new_coverage.data) # select the newly created batch coverage + idaapi.replace_wait_box("Selecting coverage...") self.director.select_coverage(coverage_name) - # print a success message to the output window + # all done, hide the IDA wait box + idaapi.hide_wait_box() lmsg("Successfully loaded batch %s..." % coverage_name) # show the coverage overview @@ -418,7 +448,7 @@ def _aggregate_batch(self, loaded_files): """ Aggregate the given loaded_files data into a single coverage object. """ - idaapi.show_wait_box("Aggregating coverage batch...") + idaapi.replace_wait_box("Aggregating coverage batch...") # create a new coverage set to manually aggregate data into coverage = DatabaseCoverage({}, self.palette) @@ -436,7 +466,7 @@ def _aggregate_batch(self, loaded_files): "Aggregating batch data %u/%u" % (i, len(loaded_files)) ) - # normalize coverage data to the database + # normalize coverage data to the open database try: addresses = self._normalize_coverage(data, self.director.metadata) @@ -450,53 +480,34 @@ def _aggregate_batch(self, loaded_files): # aggregate the addresses into the output coverage object coverage.add_addresses(addresses, False) - # all done, hide the IDA wait box - idaapi.hide_wait_box() - # return the created coverage name return coverage def interactive_load_file(self): """ - An interactive file dialog flow for loading code coverage files. + Interactive loading of individual coverage files. """ + created_coverage = [] # # kick off an asynchronous metadata refresh. this collects underlying # database metadata while the user will be busy selecting coverage files. # - # the collected metadata enables the director to process, map, and - # manipulate loaded coverage data in a performant, asynchronous manner. - # future = self.director.metadata.refresh(progress_callback=metadata_progress) # - # prompt the user with a QtFileDialog so that they can select any - # number of coverage files to load at once. - # - # if no files are selected, we abort the coverage loading process. + # we will now prompt the user with an interactive file dialog so they + # can select the coverage files they would like to load from disk. # - filenames = self._select_coverage_files() - if not filenames: - return - - # - # load the selected coverage files from disk - # + loaded_files = self._select_and_load_coverage_files() - loaded_coverage = self._load_coverage_files(filenames) - if not loaded_coverage: + # if no valid coveragee files were selected (and loaded), bail + if not loaded_files: self.director.metadata.abort_refresh() return - # - # refresh the theme aware color palette for lighthouse - # - - self.palette.refresh_colors() - # # to continue any further, we need the database metadata. hopefully # it has finished with its asynchronous collection, otherwise we will @@ -507,108 +518,73 @@ def interactive_load_file(self): await_future(future) # - # at this point the metadata caching is guaranteed to be complete. - # the coverage data has been loaded and is ready for mapping and - # management by the director. - # - - idaapi.replace_wait_box("Normalizing and mapping coverage data...") - - # - # start a batch coverage data load for better performance incase we - # are loading more than one new coverage file / data to the director. + # stop the director's aggregate from updating. this is in the interest + # of better performance when loading more than one new coverage set + # into the director. # - self.director.start_batch() - - # a list to output the names of successfully mapped coverage files - mapped_coverage = [] + self.director.start_batch() # TODO rename # # loop through the coverage data we have loaded from disk, and begin - # the normalization process to translate / filter / flatten it for - # insertion into the director (as a list of instruction addresses) + # the normalization process to translate / filter / flatten its blocks + # into a generic format the director can understand (a list of addresses) # - for i, data in enumerate(loaded_coverage, 1): + for i, data in enumerate(loaded_files, 1): # keep the user informed about our progress while loading coverage - idaapi.replace_wait_box("Normalizing and mapping coverage %u/%u" % (i, len(loaded_coverage))) + idaapi.replace_wait_box( + "Normalizing and mapping coverage %u/%u" % (i, len(loaded_files)) + ) - # TODO: it would be nice to get rid of this try/catch in the long run + # normalize coverage data to the open database try: - - # normalize coverage data to the database addresses = self._normalize_coverage(data, self.director.metadata) - - # enlighten the coverage director to this new runtime data - coverage_name = os.path.basename(data.filepath) - self.director.create_coverage(coverage_name, addresses) - - # if we made it this far, the coverage must have loaded okay... - mapped_coverage.append(coverage_name) - except Exception as e: lmsg("Failed to map coverage %s" % data.filepath) lmsg("- %s" % e) logger.exception("Error details:") continue - # collapse the batch job to recompute the director's aggregate coverage set - self.director.end_batch() - - # select the 'first' coverage file loaded and mapped from this round - if mapped_coverage: - self.director.select_coverage(mapped_coverage[0]) - - # all done, hide the IDA wait box - idaapi.hide_wait_box() + # + # ask the director to create and track a new coverage set from + # the normalized coverage data we provide + # - # print a success message to the output window - lmsg("Successfully loaded %u coverage file(s)..." % len(mapped_coverage)) + coverage_name = os.path.basename(data.filepath) + self.director.create_coverage(coverage_name, addresses) - # show the coverage overview - self.open_coverage_overview() - - def _select_while_refresh(self): - """ - Interactive file selection with asynchronous metadata refresh. - """ - - # - # kick off an asynchronous metadata refresh. this collects underlying - # database metadata while the user will be busy selecting coverage files. - # - # the collected metadata enables the director to process, map, and - # manipulate loaded coverage data in a performant, asynchronous manner. - # - - future = self.director.metadata.refresh(progress_callback=metadata_progress) + # save the coverage name to the list of succesful loads + created_coverage.append(coverage_name) # - # we will now prompt the user with an interactive file dialog so they - # can select the coverage files they would like to load from disk. + # resume the director's aggregation capabilities, triggering an update + # to recompute the aggregate with the newly loaded coverage # - loaded_files = self._select_and_load_coverage_files() + idaapi.replace_wait_box("Recomputing coverage aggregate...") + self.director.end_batch() - # if no valid coveragee files were selected (and loaded), bail - if not loaded_files: - self.director.metadata.abort_refresh() + # if nothing was mapped, then there's nothing else to do + if not created_coverage: + lmsg("No coverage files could be mapped...") + idaapi.hide_wait_box() return # - # to continue any further, we need the database metadata. hopefully - # it has finished with its asynchronous collection, otherwise we will - # block until it completes. the user will be shown a progress dialog. + # select one (the first) of the newly loaded coverage file(s) # - idaapi.show_wait_box("Building database metadata...") - await_future(future) + idaapi.replace_wait_box("Selecting coverage...") + self.director.select_coverage(created_coverage[0]) + + # all done, hide the IDA wait box idaapi.hide_wait_box() + lmsg("Successfully loaded %u coverage file(s)..." % len(mapped_coverage)) - # return the loaded coverage files - return loaded_files + # show the coverage overview + self.open_coverage_overview() def _select_and_load_coverage_files(self): """ @@ -738,4 +714,3 @@ def _normalize_coverage(self, coverage_data, metadata): # flatten the blobs into individual instructions or addresses return metadata.flatten_blocks(condensed_blocks) - From 501c0306cfd11e8054b888b112340d01d24c1474 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 10 Aug 2017 20:05:50 -0700 Subject: [PATCH 32/43] updated batch icon to be less awful --- .../lighthouse/ui/resources/icons/batch.png | Bin 8199 -> 8871 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/plugin/lighthouse/ui/resources/icons/batch.png b/plugin/lighthouse/ui/resources/icons/batch.png index cda1778d6fd9012575e6fc5b2a378b0890b853e3..8a8d42e9fa3cd2946aabe8439c7782d27f40fc42 100644 GIT binary patch delta 6176 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 XgF3_|{AWWz|CENRwn~*UKKQ=?+1FPH From 7e16ec6cc7ce7ac0a8f7772beadc2f65b7ef4c77 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 11 Aug 2017 14:21:26 -0700 Subject: [PATCH 33/43] more reliable color selection --- plugin/lighthouse/painting.py | 25 ++--- plugin/lighthouse/palette.py | 14 ++- plugin/lighthouse/util/ida.py | 169 ++++++++++++++++++++++++++++------ plugin/lighthouse_plugin.py | 5 +- 4 files changed, 161 insertions(+), 52 deletions(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index b7874110..89b288cf 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -61,15 +61,10 @@ def __init__(self, director, palette): # Callbacks #---------------------------------------------------------------------- - # NOTE/COMPAT: hook hexrays on startup - if using_ida7api: - self._hooks = PainterHooks7() - self._hooks.ready_to_run = self._init_hexrays_hooks - self._hooks.hook() - else: - self._hooks = PainterHooks6() - self._hooks.auto_queue_empty = self._init_hexrays_hooks - self._hooks.hook() + self._hooks = PainterHooks() + self._hooks.tform_visible = self._init_hexrays_hooks # IDA 6.x + self._hooks.widget_visible = self._init_hexrays_hooks # IDA 7.x + self._hooks.hook() # register for cues from the director self._director.coverage_switched(self.repaint) @@ -86,7 +81,7 @@ def terminate(self): # Initialization #-------------------------------------------------------------------------- - def _init_hexrays_hooks(self, _=None): + def _init_hexrays_hooks(self, widget, _=None): """ Install Hex-Rrays hooks (when available). @@ -115,7 +110,6 @@ def _init_hexrays_hooks(self, _=None): # self._hooks.unhook() - return 0 #------------------------------------------------------------------------------ # Painting @@ -629,15 +623,8 @@ def _async_action(self, paint_action, work_iterable): # Painter Hooks #------------------------------------------------------------------------------ -class PainterHooks6(idaapi.IDP_Hooks): - """ - This is a concrete stub of IDA's IDP_Hooks. - """ - pass - -class PainterHooks7(idaapi.UI_Hooks): +class PainterHooks(idaapi.UI_Hooks): """ This is a concrete stub of IDA's UI_Hooks. """ pass - diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/palette.py index 4865bbbd..1af72da9 100644 --- a/plugin/lighthouse/palette.py +++ b/plugin/lighthouse/palette.py @@ -16,6 +16,9 @@ def __init__(self): Initialize default palette colors for Lighthouse. """ + # one-time initialization flag, used for selecting initial palette + self._initialized = False + # the active theme name self._qt_theme = "Light" self._ida_theme = "Light" @@ -86,6 +89,10 @@ def refresh_colors(self): to select colors that will hopefully keep things most readable. """ + # TODO: temporary until I have a better mechanism to do one-time init + if self._initialized: + return + # # NOTE/TODO: # @@ -97,6 +104,9 @@ def refresh_colors(self): self._qt_theme = "Dark" # self._qt_theme_hint() self._ida_theme = self._ida_theme_hint() + # mark the palette as initialized + self._initialized = True + def _ida_theme_hint(self): """ Binary hint of the IDA color theme. @@ -109,10 +119,10 @@ def _ida_theme_hint(self): # # determine whether to use a 'dark' or 'light' paint based on the - # background color of the user's disassembly view + # background color of the user's IDA text based windows # - bg_color = get_disas_bg_color() + bg_color = get_ida_bg_color() # return 'Dark' or 'Light' return test_color_brightness(bg_color) diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index 9958c8bc..9206c4a2 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -185,7 +185,57 @@ def lex_citem_indexes(line): # Misc #------------------------------------------------------------------------------ -def get_disas_bg_color(): +def touch_window(target): + """ + Touch a window/widget/form to ensure it gets drawn by IDA. + + XXX/HACK: + + We need to ensure that widget we will analyze actually gets drawn + so that there are colors for us to steal. + + To do this, we switch to it, and switch back. I tried a few different + ways to trigger this from Qt, but could only trigger the full + painting by going through the IDA routines. + + """ + + # get the currently active widget/form title (the form itself seems transient...) + if using_ida7api: + twidget = idaapi.get_current_widget() + title = idaapi.get_widget_title(twidget) + else: + form = idaapi.get_current_tform() + title = idaapi.get_tform_title(form) + + # touch/draw the widget by playing musical chairs + if using_ida7api: + + # touch the target window by switching to it + idaapi.activate_widget(target, True) + flush_ida_sync_requests() + + # locate our previous selection + previous_twidget = idaapi.find_widget(title) + + # return us to our previous selection + idaapi.activate_widget(previous_twidget, True) + flush_ida_sync_requests() + + else: + + # touch the target window by switching to it + idaapi.switchto_tform(target, True) + flush_ida_sync_requests() + + # locate our previous selection + previous_form = idaapi.find_tform(title) + + # lookup our original form and switch back to it + idaapi.switchto_tform(previous_form, True) + flush_ida_sync_requests() + +def get_ida_bg_color(): """ Get the background color of an IDA disassembly view. @@ -203,63 +253,122 @@ def get_disas_bg_color(): PS: please expose the get_graph_color(...) palette accessor, Ilfak ;_; """ if using_ida7api: - return get_disas_bg_color_ida7() + return get_ida_bg_color_ida7() else: - return get_disas_bg_color_ida6() + return get_ida_bg_color_ida6() -def get_disas_bg_color_ida7(): +def get_ida_bg_color_ida7(): """ Get the background color of an IDA disassembly view. (IDA 7+) """ - import sip + names = ["Enums", "Structures"] + names += ["Hex View-%u" % i for i in range(5)] + names += ["IDA View-%c" % chr(ord('A') + i) for i in range(5)] - # find a widget (eg, IDA view) to steal a pixel from - for i in xrange(5): - twidget = idaapi.find_widget("IDA View-%c" % chr(ord('A') + i)) + # find a form (eg, IDA view) to analyze colors from + for window_name in names: + twidget = idaapi.find_widget(window_name) if twidget: break else: - raise RuntimeError("Failed to find donor IDA View") + raise RuntimeError("Failed to find donor view") - # take 2px tall screenshot of the widget - widget = sip.wrapinstance(long(twidget), QtWidgets.QWidget) # NOTE: LOL - pixmap = widget.grab(QtCore.QRect(0, 0, widget.width(), 2)) + # touch the target form so we know it is populated + touch_window(twidget) - # extract 1 pixel like a pleb (hopefully a background pixel :|) - img = QtGui.QImage(pixmap.toImage()) - color = QtGui.QColor(img.pixel(img.width()/2,1)) + # locate the Qt Widget for a form and take 1px image slice of it + import sip + widget = sip.wrapinstance(long(twidget), QtWidgets.QWidget) + pixmap = widget.grab(QtCore.QRect(0, 10, widget.width(), 1)) + + # convert the raw pixmap into an image (easier to interface with) + image = QtGui.QImage(pixmap.toImage()) - # return the color of the pixel we extracted - return color + # return the predicted background color + return QtGui.QColor(predict_bg_color(image)) -def get_disas_bg_color_ida6(): +def get_ida_bg_color_ida6(): """ Get the background color of an IDA disassembly view. (IDA 6.x) """ + names = ["Enums", "Structures"] + names += ["Hex View-%u" % i for i in range(5)] + names += ["IDA View-%c" % chr(ord('A') + i) for i in range(5)] - # find a form (eg, IDA view) to steal a pixel from - for i in xrange(5): - form = idaapi.find_tform("IDA View-%c" % chr(ord('A') + i)) + # find a form (eg, IDA view) to analyze colors from + for window_name in names: + form = idaapi.find_tform(window_name) if form: break else: - raise RuntimeError("Failed to find donor IDA View") + raise RuntimeError("Failed to find donor View") + + # touch the target form so we know it is populated + touch_window(form) - # locate the Qt Widget for an IDA View form and take 2px tall screenshot + # locate the Qt Widget for a form and take 1px image slice of it if using_pyqt5: widget = idaapi.PluginForm.FormToPyQtWidget(form) - pixmap = widget.grab(QtCore.QRect(0, 0, widget.width(), 2)) + pixmap = widget.grab(QtCore.QRect(0, 10, widget.width(), 1)) else: widget = idaapi.PluginForm.FormToPySideWidget(form) - region = QtCore.QRect(0, 0, widget.width(), 2) + region = QtCore.QRect(0, 10, widget.width(), 1) pixmap = QtGui.QPixmap.grabWidget(widget, region) - # extract 1 pixel like a pleb (hopefully a background pixel :|) - img = QtGui.QImage(pixmap.toImage()) - color = QtGui.QColor(img.pixel(img.width()/2,1)) + # convert the raw pixmap into an image (easier to interface with) + image = QtGui.QImage(pixmap.toImage()) + + # return the predicted background color + return QtGui.QColor(predict_bg_color(image)) + +def predict_bg_color(image): + """ + Predict the background color of an IDA View from a given image slice. + + We hypothesize that the 'background color' of a given image slice of an + IDA form will be the color that appears in the longest 'streaks' or + continuous sequences. This will probably be true 99% of the time. + + This function takes an image, and analyzes its first row of pixels. It + will return the color that it believes to be the 'background color' based + on its sequence length. + """ + assert image.width() and image.height() + + # the details for the longest known color streak will be saved in these + longest = 1 + speculative_bg = image.pixel(0, 0) + + # this will be the computed length of the current color streak + sequence = 1 + + # find the longest streak of color in a single pixel slice + for x in xrange(1, image.width()): + + # the color of this pixel matches the last pixel, extend the streak count + if image.pixel(x, 0) == image.pixel(x-1,0): + sequence += 1 + + # + # this catches the case where the longest color streak is in fact + # the last one. this ensures the streak color will get saved. + # + + if x != image.width(): + continue + + # color change, determine if this was the longest continuous color streak + if sequence > longest: + + # save the last pixel as the longest seqeuence / most likely BG color + longest = sequence + speculative_bg = image.pixel(x-1, 0) + + # reset the sequence counter + sequence = 1 - # return the color of the pixel we extracted - return color + # return the color we speculate to be the background color + return speculative_bg #------------------------------------------------------------------------------ # IDA execute_sync decorators diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index d5c6ce21..4c4183fa 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -370,6 +370,7 @@ def open_coverage_overview(self): """ Open the 'Coverage Overview' dialog. """ + self.palette.refresh_colors() # the coverage overview is already open & visible, simply refresh it if self._ui_coverage_overview and self._ui_coverage_overview.visible(): @@ -384,6 +385,7 @@ def interactive_load_batch(self): """ Interactive loading & aggregation of coverage files. """ + self.palette.refresh_colors() # # kick off an asynchronous metadata refresh. this collects underlying @@ -487,6 +489,7 @@ def interactive_load_file(self): """ Interactive loading of individual coverage files. """ + self.palette.refresh_colors() created_coverage = [] # @@ -581,7 +584,7 @@ def interactive_load_file(self): # all done, hide the IDA wait box idaapi.hide_wait_box() - lmsg("Successfully loaded %u coverage file(s)..." % len(mapped_coverage)) + lmsg("Successfully loaded %u coverage file(s)..." % len(created_coverage)) # show the coverage overview self.open_coverage_overview() From d9c223c1a04a6659f1e17cfd147a62f8e4e13316 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 11 Aug 2017 16:26:22 -0700 Subject: [PATCH 34/43] bugfix for possible underflow during priority painting --- plugin/lighthouse/painting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index 89b288cf..fc4d285f 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -467,7 +467,7 @@ def _priority_paint_functions(self, target_address): # determine range of functions to repaint func_num = idaapi.get_func_num(target_address) - func_num_start = func_num - FUNCTION_BUFFER + func_num_start = min(func_num - FUNCTION_BUFFER, 0) func_num_end = func_num + FUNCTION_BUFFER + 1 # we will save the instruction addresses painted by our function paints @@ -511,7 +511,7 @@ def _priority_paint_instructions(self, target_address, ignore=set()): INSTRUCTION_BUFFER = 200 # determine range of instructions to repaint - inst_start = target_address - INSTRUCTION_BUFFER + inst_start = min(target_address - INSTRUCTION_BUFFER, 0) inst_end = target_address + INSTRUCTION_BUFFER instructions = set(idautils.Heads(inst_start, inst_end)) From 41086479339aee54b840a153a22d1c616144e41c Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 11 Aug 2017 17:07:24 -0700 Subject: [PATCH 35/43] fixes bug with IDA 7 / PyQt5 --- plugin/lighthouse/ui/coverage_overview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 097780b4..6c8482d8 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -61,7 +61,7 @@ def __init__(self, target): self._target = target def eventFilter(self, source, event): - if event.type() == QtCore.QEvent.Destroy: + if int(event.type()) == 16: # NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7? self._target.terminate() return False From 3d7ae9e459f2de1b81fe699861a012ffafb27b7e Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 12 Aug 2017 10:34:22 -0700 Subject: [PATCH 36/43] speed and memory performance increases for metadata cache --- plugin/lighthouse/metadata.py | 84 +++++++++++++++++++++++++++-------- 1 file changed, 65 insertions(+), 19 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index fc808e59..fbbde40d 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -4,7 +4,6 @@ import ctypes import logging import threading -import collections import idaapi import idautils @@ -52,7 +51,6 @@ # reasonably low cost refresh. # - #------------------------------------------------------------------------------ # Database Level Metadata #------------------------------------------------------------------------------ @@ -65,7 +63,7 @@ class DatabaseMetadata(object): def __init__(self): # database defined instructions - self.instructions = {} + self.instructions = [] # database defined nodes (basic blocks) self.nodes = {} @@ -82,6 +80,7 @@ def __init__(self): self._name2func = {} self._last_node = [] # TODO/HACK: blank iterable for now self._node_addresses = [] + self._function_addresses = [] # asynchrnous metadata collection thread self._refresh_worker = None @@ -91,6 +90,14 @@ def __init__(self): # Providers #-------------------------------------------------------------------------- + def get_instructions_slice(self, start_address, end_address): + """ + Get the instructions in the given range of addresses. + """ + index_start = bisect.bisect_left(self.instructions, start_address) + index_end = bisect.bisect_left(self.instructions, end_address) + return self.instructions[index_start:index_end] + def get_node(self, address): """ Get the node (basic block) metadata for a given address. @@ -175,6 +182,38 @@ def get_function(self, address): # return the function metadata corresponding to this node. return node_metadata.function + def get_closest_function(self, address): + """ + Get the function metadata for the function closest to the give address. + """ + + # get the closest insertion point of the given address + pos = bisect.bisect_left(self._function_addresses, address) + + # the given address is a min, return the first known function + if pos == 0: + return self.functions[self._function_addresses[0]] + + # given address is a max, return the last known function + if pos == len(self._function_addresses): + return self.functions[self._function_addresses[-1]] + + # select the two candidate addresses + before = self._function_addresses[pos - 1] + after = self._function_addresses[pos] + + # return the function closest to the given address + if after - address < address - before: + return self.functions[after] + else: + return self.functions[before] + + def get_function_num(self, address): + """ + Get the function number for a given address. + """ + return self._function_addresses.index(address) + def get_function_by_name(self, function_name): """ Get the function metadata for a given function name. @@ -185,6 +224,12 @@ def get_function_by_name(self, function_name): pass return None + def get_function_by_num(self, function_num): + """ + Get the function metadata for a given function number. + """ + return self.functions[self._function_addresses[function_num]] + def flatten_blocks(self, basic_blocks): """ Flatten a list of basic blocks (address, size) to instruction addresses. @@ -202,18 +247,14 @@ def flatten_blocks(self, basic_blocks): """ output = [] + # sanity check + if not basic_blocks: + return output + # loop through every given basic block (input) for address, size in basic_blocks: - end_address = address + size - - # loop through the byte range defined by the basic block - while address < end_address: - - # save the current address as an instruction address - output.append(address) - - # move forward to the next instruction (or byte) address - address += self.instructions.get(address, 1) + instructions = self.get_instructions_slice(address, address+size) + output.extend(instructions) # return the list of addresses return output @@ -362,6 +403,7 @@ def _refresh_lookup(self): # update the lookup lists self._name2func = { f.name: f.address for f in self.functions.itervalues() } self._node_addresses = sorted(self.nodes.keys()) + self._function_addresses = sorted(self.functions.keys()) # lookup lists are no longer stale, reset the stale flag as such self._stale_lookup = False @@ -404,6 +446,10 @@ def _async_collect_metadata(self, function_addresses, progress_callback): # sleep some so we don't choke the main IDA thread time.sleep(.0015) + # dedupe and sort the instructions + self.instructions = list(set(self.instructions)) + self.instructions.sort() + # completed normally return True @@ -464,7 +510,7 @@ def _update_functions(self, fresh_metadata): for function_metadata in delta.itervalues(): self.nodes.update(function_metadata.nodes) for node_metadata in function_metadata.nodes.itervalues(): - self.instructions.update(node_metadata.instructions) + self.instructions.extend(node_metadata.instructions) # # if the function or node count has changed, we will know that @@ -516,7 +562,7 @@ def instructions(self): """ The instruction addresses in this function. """ - return set([ea for node in self.nodes.itervalues() for ea in node.instructions.keys()]) + return set([ea for node in self.nodes.itervalues() for ea in node.instructions]) #-------------------------------------------------------------------------- # Metadata Population @@ -605,7 +651,7 @@ def _refresh_nodes(self): # that falls within this function. # - edge_src = next(reversed(node_metadata.instructions)) + edge_src = node_metadata.instructions[-1] # NOTE/COMPAT: we do a single api check *outside* the loop for perf if using_ida7api: @@ -680,8 +726,8 @@ def __init__(self, start_ea, end_ea, node_id=idaapi.BADADDR): # parent function_metadata self.function = None - # maps instruction_address --> instruction_metadata - self.instructions = collections.OrderedDict() + # instruction addresses + self.instructions = [] #---------------------------------------------------------------------- @@ -706,7 +752,7 @@ def _build_metadata(self): while current_address < node_end: instruction_size = idaapi.get_item_end(current_address) - current_address - self.instructions[current_address] = instruction_size + self.instructions.append(current_address) current_address += instruction_size # save the number of instructions in this block From cfef581b873f9380dad73d72d4eecef631b1f5e7 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 29 Aug 2017 12:25:57 -0700 Subject: [PATCH 37/43] fixes drcov log format for DR 7.0.0 on linux. #10 --- plugin/lighthouse/parsers/drcov.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/plugin/lighthouse/parsers/drcov.py b/plugin/lighthouse/parsers/drcov.py index 73a7819b..5911839c 100644 --- a/plugin/lighthouse/parsers/drcov.py +++ b/plugin/lighthouse/parsers/drcov.py @@ -170,7 +170,10 @@ def _parse_module_table_columns(self, f): eg: (Not present) Format used in DynamoRIO v7.0.0-RC1 (and hopefully above) - eg: 'Columns: id, base, end, entry, checksum, timestamp, path' + Windows: + 'Columns: id, base, end, entry, checksum, timestamp, path' + Mac/Linux: + 'Columns: id, base, end, entry, path' """ @@ -185,10 +188,9 @@ def _parse_module_table_columns(self, f): #assert field_name == "Columns" # seperate column names - # eg: id, base, end, entry, checksum, timestamp, path + # Windows: id, base, end, entry, checksum, timestamp, path + # Mac/Linux: id, base, end, entry, path columns = field_data.split(", ") - #if self.module_table_version == 2: - #assert columns == ["id", "base", "end", "entry", "checksum", "timestamp", "path"] def _parse_module_table_modules(self, f): """ @@ -304,9 +306,10 @@ def _parse_module_v2(self, data): self.base = int(data[1], 16) self.end = int(data[2], 16) self.entry = int(data[3], 16) - self.checksum = int(data[4], 16) - self.timestamp = int(data[5], 16) - self.path = str(data[6]) + if len(data) == 7: # Windows Only + self.checksum = int(data[4], 16) + self.timestamp = int(data[5], 16) + self.path = str(data[-1]) self.size = self.end-self.base self.filename = os.path.basename(self.path) From 177f462e8251d0fdd8001468ed66cd104b39f2e8 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 15 Sep 2017 06:25:46 -0700 Subject: [PATCH 38/43] fix rare crash or hang of IDA on close --- plugin/lighthouse/metadata.py | 4 + plugin/lighthouse/painting.py | 273 ++++++++++++++++++++++------------ plugin/lighthouse/util/ida.py | 6 +- plugin/lighthouse_plugin.py | 4 +- 4 files changed, 184 insertions(+), 103 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index fbbde40d..cf19eb68 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -187,6 +187,10 @@ def get_closest_function(self, address): Get the function metadata for the function closest to the give address. """ + # sanity check + if not self._function_addresses: + return None + # get the closest insertion point of the given address pos = bisect.bisect_left(self._function_addresses, address) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index fc4d285f..71b6e7ce 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -1,12 +1,12 @@ import time +import bisect import logging import threading import idc import idaapi -import idautils -from lighthouse.util import chunks, using_ida7api +from lighthouse.util import * from lighthouse.util.ida import * logger = logging.getLogger("Lighthouse.Painting") @@ -40,12 +40,14 @@ def __init__(self, director, palette): # # to communicate with the asynchronous painting thread, we send a - # a message over the queue to kick off a new paint event, and the - # bool to interrupt a running paint request. + # a message via the thread event to signal a new paint request, and + # use the repaint_requested bool to interrupt a running paint request. # - self._repaint_queue = Queue.Queue() + self._action_complete = threading.Event() + self._repaint_request = threading.Event() self._repaint_requested = False + self._end_threads = False # # asynchronous database painting thread @@ -74,7 +76,9 @@ def terminate(self): """ Cleanup & terminate the painter. """ - self._repaint_queue.put(False) + self._end_threads = True + self._repaint_requested = True + self._repaint_request.set() self._painting_worker.join() #-------------------------------------------------------------------------- @@ -87,12 +91,16 @@ def _init_hexrays_hooks(self, widget, _=None): NOTE: - This is called when the auto_empty_queue event fires. The - use of auto_queue_empty is somewhat arbitrary. It is simply an + This is called when the tform/widget_visible event fires. The + use of this event is somewhat arbitrary. It is simply an event that fires at least once after things seem mostly setup. We were using UI_Hooks.ready_to_run previously, but it appears - that fires *before* this plugin is loaded on some builds of IDA. + that this event fires *before* this plugin is loaded depending + on the user's individual copy of IDA. + + This approach seems relatively consistent for inividuals and builds + from IDA 6.8 --> 7.0. """ result = False @@ -115,18 +123,12 @@ def _init_hexrays_hooks(self, widget, _=None): # Painting #------------------------------------------------------------------------------ - @idawrite def repaint(self): """ Paint coverage defined by the current database mappings. """ - - # immediately paint the regions of the database the user is looking at - self._priority_paint() - - # request a complete repaint self._repaint_requested = True - self._repaint_queue.put(True) + self._repaint_request.set() #------------------------------------------------------------------------------ # Painting - Instructions / Items (Lines) @@ -148,19 +150,25 @@ def clear_instructions(self, instructions): idaapi.set_item_color(address, idc.DEFCOLOR) self._painted_instructions.discard(address) - @idawrite + @idawrite_async def _paint_instructions(self, instructions): """ Internal routine to force called action to the main thread. """ + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug self.paint_instructions(instructions) + self._action_complete.set() + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug - @idawrite + @idawrite_async def _clear_instructions(self, instructions): """ Internal routine to force called action to the main thread. """ + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug self.clear_instructions(instructions) + self._action_complete.set() + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug #------------------------------------------------------------------------------ # Painting - Nodes (Basic Blocks) @@ -233,48 +241,38 @@ def clear_nodes(self, nodes_metadata): self._painted_nodes.discard(node_metadata.address) - @idawrite + @idawrite_async def _paint_nodes(self, nodes_coverage): """ Internal routine to force called action to the main thread. """ + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug self.paint_nodes(nodes_coverage) + self._action_complete.set() + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug - @idawrite + @idawrite_async def _clear_nodes(self, nodes_metadata): """ Internal routine to force called action to the main thread. """ + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug self.clear_nodes(nodes_metadata) + self._action_complete.set() + time.sleep(0) # HACK: workaround for the idapython idaapi.MFF_NOWAIT bug #------------------------------------------------------------------------------ # Painting - Functions #------------------------------------------------------------------------------ - def paint_function(self, function): + def paint_function(self, address): """ Paint function instructions & nodes with the current database mappings. """ - # sanity check - if not function: - return - - # more code-friendly, readable aliases - metadata = self._director.metadata - coverage = self._director.coverage - - # NOTE/COMPAT: - if using_ida7api: - start_ea = function.start_ea - end_ea = function.end_ea - else: - start_ea = function.startEA - end_ea = function.endEA - # collect function information - function_metadata = metadata.functions[start_ea] - function_coverage = coverage.functions.get(start_ea, None) + function_metadata = self._director.metadata.functions[address] + function_coverage = self._director.coverage.functions.get(address, None) # function coverage exists, so let's do a cleaner paint if function_coverage: @@ -304,18 +302,35 @@ def paint_function(self, function): # ~ painting ~ # - # clear the instructions that will not get painted over - self.clear_instructions(stale_instructions) - self.paint_instructions(function_coverage.instructions) + # clear instructions + if not self._async_action(self._clear_instructions, stale_instructions): + return False + + # clear nodes + if not self._async_action(self._clear_nodes, stale_nodes): + return False + + # paint instructions + if not self._async_action(self._paint_instructions, function_coverage.instructions): + return False - # clear the nodes that will not get painted over - self.clear_nodes(stale_nodes) - self.paint_nodes(function_coverage.nodes.itervalues()) + # paint nodes + if not self._async_action(self._paint_nodes, function_coverage.nodes.itervalues()): + return False # no coverage, just clear the function's instruction & nodes else: - self.clear_instructions(function_metadata.instructions) - self.clear_nodes(function_metadata.nodes.itervalues()) + + # clear instructions + if not self._async_action(self._clear_instructions, function_metadata.instructions): + return False + + # clear nodes + if not self._async_action(self._clear_nodes, function_metadata.nodes.itervalues()): + return False + + # not interrupted + return True #------------------------------------------------------------------------------ # Painting - HexRays (Decompilation / Source) @@ -325,7 +340,7 @@ def paint_hexrays(self, cfunc, database_coverage): """ Paint decompilation text for the given HexRays Window. """ - logger.debug("Painting Hexrays for 0x%X" % cfunc.entry_ea) + logger.debug("Painting HexRays for 0x%X" % cfunc.entry_ea) # more code-friendly, readable aliases database_metadata = database_coverage._metadata @@ -346,7 +361,6 @@ def paint_hexrays(self, cfunc, database_coverage): # line2citem = map_line2citem(decompilation_text) - logger.debug(line2citem) # # now that we have some understanding of how citems contribute to each @@ -355,7 +369,6 @@ def paint_hexrays(self, cfunc, database_coverage): # line2node = map_line2node(cfunc, database_metadata, line2citem) - logger.debug(line2node) # great, now we have all the information we need to paint @@ -391,7 +404,6 @@ def paint_hexrays(self, cfunc, database_coverage): # if there was nothing painted yet, there's no point in continuing... if not lines_painted: - logger.debug("No HexRays output was painted...") return # @@ -404,8 +416,6 @@ def paint_hexrays(self, cfunc, database_coverage): decompilation_text[line_number].bgcolor = self.palette.ida_coverage lines_painted += 1 - logger.debug("Done painting HexRays request...") - # finally, refresh the view idaapi.refresh_idaview_anyway() @@ -421,8 +431,6 @@ def _hxe_callback(self, event, *args): vdui = args[0] cfunc = vdui.cfunc - logger.debug("Caught HexRays 'Text Ready' event for 0x%X" % cfunc.entry_ea) - # if there's no coverage data for this function, there's nothing to do if not cfunc.entry_ea in self._director.coverage.functions: return 0 @@ -439,64 +447,75 @@ def _hxe_callback(self, event, *args): def _priority_paint(self): """ Immediately repaint regions of the database visible to the user. - - TODO: - - it would be nice to loop through the address history and grab - other database hotspots where the user has been recently. - """ - cursor_address = idaapi.get_screen_ea() + cursor_address = idaapi.get_screen_ea() # TODO: threadsafe? # paint functions around the cursor address painted = self._priority_paint_functions(cursor_address) + # the operation has been interrupted by a repaint request + if self._repaint_requested: + return False + # paint instructions around the cursor address self._priority_paint_instructions(cursor_address, ignore=painted) + # the operation has been interrupted by a repaint request + if self._repaint_requested: + return False + + # succesful completion + return True + def _priority_paint_functions(self, target_address): """ Paint functions in the immediate vicinity of the given address. This will paint both the instructions & graph nodes of defined functions. """ + database_metadata = self._director.metadata database_coverage = self._director.coverage + function_instructions = set() # the number of functions before and after the cursor to paint FUNCTION_BUFFER = 1 - # determine range of functions to repaint - func_num = idaapi.get_func_num(target_address) - func_num_start = min(func_num - FUNCTION_BUFFER, 0) - func_num_end = func_num + FUNCTION_BUFFER + 1 + # get the function metadata for the function closest to our cursor + function_metadata = database_metadata.get_closest_function(target_address) + if not function_metadata: + return function_instructions # this will be empty - # we will save the instruction addresses painted by our function paints - function_instructions = set() + # select the range of functions around us that we would like to paint + func_num = database_metadata.get_function_num(function_metadata.address) + func_num_start = max(func_num - FUNCTION_BUFFER, 0) + func_num_end = func_num + FUNCTION_BUFFER + 1 # repaint the specified range of functions - for num in xrange(func_num_start, func_num_end): - function = idaapi.getn_func(num) - if not function: + for current_num in xrange(func_num_start, func_num_end): + + # get the next function to paint + try: + function_metadata = database_metadata.get_function_by_num(current_num) + except IndexError: continue # repaint the function - self.paint_function(function) - - # NOTE/COMPAT: - if using_ida7api: - start_ea = function.start_ea - else: - start_ea = function.startEA + if not self.paint_function(function_metadata.address): + break # paint interrupted # get the function coverage data for the target address - function_coverage = database_coverage.functions.get(start_ea, None) + function_coverage = database_coverage.functions.get(function_metadata.address, None) if not function_coverage: continue - # extract the painted instructions in this function + # accumulate the painted instructions by this pass function_instructions |= function_coverage.instructions - # return the instruction addresses painted + # the operation has been interrupted by a repaint request + if self._repaint_requested: + break + + # return the addresses of all the instruction we painted over return function_instructions def _priority_paint_instructions(self, target_address, ignore=set()): @@ -505,15 +524,16 @@ def _priority_paint_instructions(self, target_address, ignore=set()): Optionally, one can provide a set of addresses to ignore while painting. """ + database_metadata = self._director.metadata database_coverage = self._director.coverage # the number of instruction bytes before and after the cursor to paint INSTRUCTION_BUFFER = 200 # determine range of instructions to repaint - inst_start = min(target_address - INSTRUCTION_BUFFER, 0) - inst_end = target_address + INSTRUCTION_BUFFER - instructions = set(idautils.Heads(inst_start, inst_end)) + start_address = max(target_address - INSTRUCTION_BUFFER, 0) + end_address = target_address + INSTRUCTION_BUFFER + instructions = set(database_metadata.get_instructions_slice(start_address, end_address)) # remove any instructions painted by the function paints instructions -= ignore @@ -521,9 +541,17 @@ def _priority_paint_instructions(self, target_address, ignore=set()): # mask only the instructions with coverage data in this region instructions_coverage = instructions & database_coverage.coverage + # # clear all instructions in this region, repaint the coverage data - self.clear_instructions(instructions) - self.paint_instructions(instructions_coverage) + # + + # clear instructions + if not self._async_action(self._clear_instructions, instructions): + return set() + + # paint instructions + if not self._async_action(self._paint_instructions, instructions_coverage): + return set() # return the instruction addresses painted return instructions_coverage @@ -544,23 +572,35 @@ def _async_database_painter(self): while True: - # block until a paint has been requested (a bool) - repaint_request = self._repaint_queue.get() + # wait for the next external repaint request + self._repaint_request.wait() - # signal to stop - if not repaint_request: + # if we've been signaled to spindown the painting thread, exit now + if self._end_threads: break + # clear the repaint flag + self._repaint_request.clear() + self._repaint_requested = False + # more code-friendly, readable aliases database_coverage = self._director.coverage database_metadata = self._director.metadata - # clear the repaint flag - self._repaint_requested = False - start = time.time() #------------------------------------------------------------------ + # + # immediately paint the regions of the database the user is looking at + # + + if not self._priority_paint(): + continue # a repaint was requested + + # + # perform a more comprehensive paint + # + # compute the painted instructions that will not get painted over stale_instructions = self._painted_instructions - database_coverage.coverage @@ -586,7 +626,7 @@ def _async_database_painter(self): #------------------------------------------------------------------ end = time.time() - logger.debug("Paint took %s seconds" % (end - start)) + logger.debug("Full Paint took %s seconds" % (end - start)) # thread exit logger.debug("Exiting DatabasePainter thread...") @@ -603,17 +643,54 @@ def _async_action(self, paint_action, work_iterable): for work_chunk in chunks(list(work_iterable), CHUNK_SIZE): # - # paint/clear a chunk of 'work' (nodes, or instructions) with - # the given work action (eg, paint_nodes, clear_instructions) + # reset the paint event signal so that it is ready for the next + # paint request. it will let us know when the asynchrnous paint + # action has completed in the IDA main thread # - paint_action(work_chunk) + self._action_complete.clear() + + # + # paint or unpaint a chunk of 'work' (nodes, or instructions) with + # the given paint function (eg, paint_nodes, clear_instructions) + # + + job_id = paint_action(work_chunk) + assert job_id != -1 + + # + # wait for the asynchrnous paint event to complete or a signal that + # we should end this thread (via end_threads) + # + + while not (self._action_complete.wait(timeout=0.1) or self._end_threads): + continue + + # + # our end_threads signal/bool can only originate from the main IDA + # thread (plugin termination). we make the assumption that no more + # MFF_WRITE requests (eg, 'paint_action') will get processed. + # + # we do a best effort to cancel the in-flight job (just in case) + # and return so we can exit the thread. + # + + if self._end_threads: + idaapi.cancel_exec_request(job_id) + return False + + # + # the operation has been interrupted by a repaint request, bail + # immediately so that we can process the next repaint + # - # the operation has been interrupted by a repaint request if self._repaint_requested: return False + # # sleep some so we don't choke the main IDA thread + # + time.sleep(.001) # operation completed successfully diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index 9206c4a2..dc9a1a5c 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -390,14 +390,14 @@ def wrapper(*args, **kwargs): return idaapi.execute_sync(ff, idaapi.MFF_FAST) return wrapper -def idanowait(f): +def idawrite_async(f): """ Decorator for marking a function as completely async. """ @functools.wraps(f) def wrapper(*args, **kwargs): ff = functools.partial(f, *args, **kwargs) - return idaapi.execute_sync(ff, idaapi.MFF_NOWAIT) + return idaapi.execute_sync(ff, idaapi.MFF_NOWAIT | idaapi.MFF_WRITE) return wrapper def idawrite(f): @@ -503,7 +503,7 @@ def await_future(future, block=True, timeout=1.0): # except Queue.Empty as e: - logger.debug("Flushing execute_sync requests") + logger.debug("Flushing future...") flush_ida_sync_requests() @mainthread diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 5ea51b90..6961b484 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -169,10 +169,10 @@ def _uninstall_ui(self): def _cleanup(self): """ - Signal threads to exit and wait. + IDB closing event, last chance to spin down threaded workers. """ - self.director.terminate() self.painter.terminate() + self.director.terminate() #-------------------------------------------------------------------------- # IDA Actions From 7dcb58dc441dfd4db947288fb40e39877e7500fd Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 15 Sep 2017 06:52:37 -0700 Subject: [PATCH 39/43] cleaning up a few TODO's --- plugin/lighthouse/director.py | 28 ++++++++++++++++------------ plugin/lighthouse/metadata.py | 21 +-------------------- plugin/lighthouse_plugin.py | 4 ++-- 3 files changed, 19 insertions(+), 34 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index f2ce7e1b..ead5e3c4 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -48,8 +48,8 @@ def __init__(self, palette): # database metadata cache self._database_metadata = DatabaseMetadata() - # flag indicating a batch load is in progress - self._batch_in_progress = False + # flag to suspend/resume the automatic coverage aggregation + self._aggregation_suspended = False #---------------------------------------------------------------------- # Coverage @@ -371,19 +371,23 @@ def _notify_callback(self, callback_list): # Batch Loading #---------------------------------------------------------------------- - def start_batch(self): + def suspend_aggregation(self): """ - Gate the start of a batch coverage load. + Suspend the aggregate computation for any newly added coverage. + + It is performant to suspend/resume aggregation if loading a number + of individual coverage files. This will prevent the aggregate + coverage set from being re-computed multiple times. """ - self._batch_in_progress = True + self._aggregation_suspended = True - def end_batch(self): + def resume_aggregation(self): """ - Gate the end of a batch coverage load. + Resume the aggregate computation. """ - assert self._batch_in_progress + assert self._aggregation_suspended self._refresh_aggregate() - self._batch_in_progress = False + self._aggregation_suspended = False #---------------------------------------------------------------------- # Coverage @@ -476,7 +480,7 @@ def _update_coverage(self, coverage_name, new_coverage): if coverage_name in self.coverage_names: old_coverage = self._database_coverage[coverage_name] self.aggregate.subtract_data(old_coverage.data) - if not self._batch_in_progress: + if not self._aggregation_suspended: self._refresh_aggregate() # @@ -488,7 +492,7 @@ def _update_coverage(self, coverage_name, new_coverage): # (re)-add the newly loaded/updated coverage to the aggregate set self.aggregate.add_data(new_coverage.data) - if not self._batch_in_progress: + if not self._aggregation_suspended: self._refresh_aggregate() def _new_coverage(self, coverage_data): @@ -522,7 +526,7 @@ def delete_coverage(self, coverage_name): # TODO: check if there's any references to the coverage object here... self.aggregate.subtract_data(coverage.data) - if not self._batch_in_progress: + if not self._aggregation_suspended: self._refresh_aggregate() # notify any listeners that we have deleted coverage diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index cf19eb68..f67149a4 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -71,10 +71,6 @@ def __init__(self): # database defined functions self.functions = {} - # TODO: database defined segments - #self.segments = {} - #self._segment_addresses = {} - # lookup list members self._stale_lookup = False self._name2func = {} @@ -802,21 +798,6 @@ def __eq__(self, other): result &= self.id == other.id return result -#------------------------------------------------------------------------------ -# Instruction Level Metadata -#------------------------------------------------------------------------------ -# TODO: unused for now - -class InstructionMetadata(ctypes.Structure): - """ - Fast access instruction level metadata cache. - """ - _fields_ = \ - [ - ("address", ctypes.c_ulonglong), - ("size", ctypes.c_size_t) - ] - #------------------------------------------------------------------------------ # Metadata Helpers #------------------------------------------------------------------------------ @@ -961,7 +942,7 @@ def _compute_function_delta(self, new_functions, old_functions): self._dirty_functions = set() #-------------------------------------------------------------------------- - # Informational / Debug - TODO: REMOVE + # Informational / DEBUG #-------------------------------------------------------------------------- def dump_delta(self): diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 6961b484..2482b23c 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -526,7 +526,7 @@ def interactive_load_file(self): # into the director. # - self.director.start_batch() # TODO rename + self.director.suspend_aggregation() # # loop through the coverage data we have loaded from disk, and begin @@ -567,7 +567,7 @@ def interactive_load_file(self): # idaapi.replace_wait_box("Recomputing coverage aggregate...") - self.director.end_batch() + self.director.resume_aggregation() # if nothing was mapped, then there's nothing else to do if not created_coverage: From efe4880728169c63ee52df311ce651b7dc51b91a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 20 Sep 2017 11:49:47 -0700 Subject: [PATCH 40/43] pseudo merge of fbl_pintool --- coverage/pin/CodeCoverage.cpp | 297 ++++++++++++++++++++++++++++++++++ coverage/pin/ImageManager.cpp | 75 +++++++++ coverage/pin/ImageManager.h | 54 +++++++ coverage/pin/LICENSE | 21 +++ coverage/pin/Makefile | 10 ++ coverage/pin/README.md | 103 ++++++++++++ coverage/pin/TraceFile.h | 51 ++++++ coverage/pin/build.bat | 49 ++++++ 8 files changed, 660 insertions(+) create mode 100644 coverage/pin/CodeCoverage.cpp create mode 100644 coverage/pin/ImageManager.cpp create mode 100644 coverage/pin/ImageManager.h create mode 100644 coverage/pin/LICENSE create mode 100644 coverage/pin/Makefile create mode 100644 coverage/pin/README.md create mode 100644 coverage/pin/TraceFile.h create mode 100644 coverage/pin/build.bat diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp new file mode 100644 index 00000000..740ab710 --- /dev/null +++ b/coverage/pin/CodeCoverage.cpp @@ -0,0 +1,297 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "pin.H" +#include "TraceFile.h" +#include "ImageManager.h" + +// Pin comes with some old standard libraries. +namespace pintool { +template +using unordered_set = std::tr1::unordered_set; + +template +using unordered_map = std::tr1::unordered_map; +} + +// Tool's arguments. +static KNOB KnobModuleWhitelist(KNOB_MODE_APPEND, "pintool", "w", "", + "Add a module to the white list. If none is specified, everymodule is white-listed. Example: libTIFF.dylib"); + +static KNOB KnobLogFile(KNOB_MODE_WRITEONCE, "pintool", "l", "trace.log", + "Name of the output file. If none is specified, trace.log is used."); + +// Return the file/directory name of a path. +static string base_name(const string& path) +{ +#if defined(TARGET_WINDOWS) +#define PATH_SEPARATOR "\\" +#else +#define PATH_SEPARATOR "/" +#endif + string::size_type idx = path.rfind(PATH_SEPARATOR); + string name = (idx == string::npos) ? path : path.substr(idx + 1); + return name; +} + +// Per thread data structure. This is mainly done to avoid locking. +struct ThreadData { + // Unique list of hit basic blocks. + pintool::unordered_set m_block_hit; + + // Map basic a block address to its size. + pintool::unordered_map m_block_size; +}; + +class ToolContext { +public: + ToolContext() + { + PIN_InitLock(&m_loaded_images_lock); + PIN_InitLock(&m_thread_lock); + m_tls_key = PIN_CreateThreadDataKey(nullptr); + } + + ThreadData* GetThreadLocalData(THREADID tid) + { + return static_cast(PIN_GetThreadData(m_tls_key, tid)); + } + + void setThreadLocalData(THREADID tid, ThreadData* data) + { + PIN_SetThreadData(m_tls_key, data, tid); + } + + // The image manager allows us to keep track of loaded images. + ImageManager* m_images; + + // Trace file used to log execution traces. + TraceFile* m_trace; + + // Keep track of _all_ the loaded images. + std::vector m_loaded_images; + PIN_LOCK m_loaded_images_lock; + + // Thread tracking utilities. + std::set m_seen_threads; + std::vector m_terminated_threads; + PIN_LOCK m_thread_lock; + + // Flag that indicates that tracing is enabled. Always true if there are no whitelisted images. + bool m_tracing_enabled = true; + + // TLS key used to store per-thread data. + TLS_KEY m_tls_key; +}; + +// Thread creation event handler. +static VOID OnThreadStart(THREADID tid, CONTEXT* ctxt, INT32 flags, VOID* v) +{ + // Create a new `ThreadData` object and set it on the TLS. + auto& context = *reinterpret_cast(v); + context.setThreadLocalData(tid, new ThreadData); + + // Save the recently created thread. + PIN_GetLock(&context.m_thread_lock, 1); + { + context.m_seen_threads.insert(tid); + } + PIN_ReleaseLock(&context.m_thread_lock); +} + +// Thread destruction event handler. +static VOID OnThreadFini(THREADID tid, const CONTEXT* ctxt, INT32 c, VOID* v) +{ + // Get thread's `ThreadData` structure. + auto& context = *reinterpret_cast(v); + ThreadData* data = context.GetThreadLocalData(tid); + + // Remove the thread from the seen threads set and add it to the terminated list. + PIN_GetLock(&context.m_thread_lock, 1); + { + context.m_seen_threads.erase(tid); + context.m_terminated_threads.push_back(data); + } + PIN_ReleaseLock(&context.m_thread_lock); +} + +// Image load event handler. +static VOID OnImageLoad(IMG img, VOID* v) +{ + auto& context = *reinterpret_cast(v); + string img_name = base_name(IMG_Name(img)); + + ADDRINT low = IMG_LowAddress(img); + ADDRINT high = IMG_HighAddress(img); + + printf("Loaded image: 0x%.16lx:0x%.16lx -> %s\n", low, high, img_name.c_str()); + + // Save the loaded image with its original full name/path. + PIN_GetLock(&context.m_loaded_images_lock, 1); + { + context.m_loaded_images.push_back(LoadedImage(IMG_Name(img), low, high)); + } + PIN_ReleaseLock(&context.m_loaded_images_lock); + + // If the image is whitelisted save its information. + if (context.m_images->isWhiteListed(img_name)) { + context.m_images->addImage(img_name, low, high); + + // Enable tracing if not already enabled. + if (!context.m_tracing_enabled) + context.m_tracing_enabled = true; + } +} + +// Image unload event handler. +static VOID OnImageUnload(IMG img, VOID* v) +{ + auto& context = *reinterpret_cast(v); + context.m_images->removeImage(IMG_LowAddress(img)); +} + +// Basic block hit event handler. +static VOID OnBasicBlockHit(THREADID tid, ADDRINT addr, UINT32 size, VOID* v) +{ + auto& context = *reinterpret_cast(v); + ThreadData* data = context.GetThreadLocalData(tid); + data->m_block_hit.insert(addr); + data->m_block_size[addr] = size; +} + +// Trace hit event handler. +static VOID OnTrace(TRACE trace, VOID* v) +{ + auto& context = *reinterpret_cast(v); + BBL bbl = TRACE_BblHead(trace); + ADDRINT addr = BBL_Address(bbl); + + // Check if the address is inside a white-listed image. + if (!context.m_tracing_enabled || !context.m_images->isInterestingAddress(addr)) + return; + + // For each basic block in the trace. + for (; BBL_Valid(bbl); bbl = BBL_Next(bbl)) { + addr = BBL_Address(bbl); + BBL_InsertCall(bbl, IPOINT_ANYWHERE, (AFUNPTR)OnBasicBlockHit, + IARG_THREAD_ID, + IARG_ADDRINT, addr, + IARG_UINT32, BBL_Size(bbl), + IARG_PTR, v, + IARG_END); + } +} + +// Program finish event handler. +static VOID OnFini(INT32 code, VOID* v) +{ + auto& context = *reinterpret_cast(v); + context.m_trace->write_string("DRCOV VERSION: 2\n"); + context.m_trace->write_string("DRCOV FLAVOR: drcov\n"); + context.m_trace->write_string("Module Table: version 2, count %u\n", context.m_loaded_images.size()); + context.m_trace->write_string("Columns: id, base, end, entry, checksum, timestamp, path\n"); + + // We don't supply entry, checksum and, timestamp. + for (unsigned i = 0; i < context.m_loaded_images.size(); i++) { + const auto& image = context.m_loaded_images[i]; + context.m_trace->write_string("%2u, 0x%.16llx, 0x%.16llx, 0x0000000000000000, 0x00000000, 0x00000000, %s\n", + i, image.low_, image.high_, image.name_.c_str()); + } + + // Add non terminated threads to the list of terminated threads. + for (THREADID i : context.m_seen_threads) { + ThreadData* data = context.GetThreadLocalData(i); + context.m_terminated_threads.push_back(data); + } + + // Count the global number of basic blocks. + size_t number_of_bbs = 0; + for (const auto& data : context.m_terminated_threads) { + number_of_bbs += data->m_block_hit.size(); + } + + context.m_trace->write_string("BB Table: %u bbs\n", number_of_bbs); + + struct __attribute__((packed)) drcov_bb { + uint32_t start; + uint16_t size; + uint16_t id; + }; + + drcov_bb tmp; + + for (const auto& data : context.m_terminated_threads) { + for (const auto& address : data->m_block_hit) { + auto it = std::find_if(context.m_loaded_images.begin(), context.m_loaded_images.end(), [&address](const LoadedImage& image) { + return address >= image.low_ && address < image.high_; + }); + + if (it == context.m_loaded_images.end()) + continue; + + tmp.id = std::distance(context.m_loaded_images.begin(), it); + tmp.start = address - it->low_; + tmp.size = data->m_block_size[address]; + + context.m_trace->write_binary(&tmp, sizeof(tmp)); + } + } +} + +int main(int argc, char* argv[]) +{ + cout << "CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com)" << endl; + + // Initialize symbol processing + PIN_InitSymbols(); + + // Initialize PIN. + if (PIN_Init(argc, argv)) { + cerr << "Error initializing PIN, PIN_Init failed!" << endl; + return -1; + } + + // Initialize the tool context. + ToolContext *context = new ToolContext(); + + // Create a an image manager that keeps track of the loaded/unloaded images. + context->m_images = new ImageManager(); + for (unsigned i = 0; i < KnobModuleWhitelist.NumberOfValues(); ++i) { + cout << "White-listing image: " << KnobModuleWhitelist.Value(i) << endl; + context->m_images->addWhiteListedImage(KnobModuleWhitelist.Value(i)); + + // We will only enable tracing when any of the whitelisted images gets loaded. + context->m_tracing_enabled = false; + } + + // Create a trace file. + cout << "Logging code coverage information to: " << KnobLogFile.ValueString() << endl; + context->m_trace = new TraceFile(KnobLogFile.ValueString()); + + // Handlers for thread creation and destruction. + PIN_AddThreadStartFunction(OnThreadStart, context); + PIN_AddThreadFiniFunction(OnThreadFini, context); + + // Handlers for image loading and unloading. + IMG_AddInstrumentFunction(OnImageLoad, context); + IMG_AddUnloadFunction(OnImageUnload, context); + + // Handlers for instrumentation events. + TRACE_AddInstrumentFunction(OnTrace, context); + + // Handler for program exits. + PIN_AddFiniFunction(OnFini, context); + + PIN_StartProgram(); + return 0; +} diff --git a/coverage/pin/ImageManager.cpp b/coverage/pin/ImageManager.cpp new file mode 100644 index 00000000..b0225a1c --- /dev/null +++ b/coverage/pin/ImageManager.cpp @@ -0,0 +1,75 @@ +#include "ImageManager.h" +#include "pin.H" + +ImageManager::ImageManager() +{ + PIN_RWMutexInit(&images_lock); +} + +ImageManager::~ImageManager() +{ + PIN_RWMutexFini(&images_lock); +} + +VOID ImageManager::addImage(string image_name, ADDRINT lo_addr, + ADDRINT hi_addr) +{ + PIN_RWMutexWriteLock(&images_lock); + { + images.insert(LoadedImage(image_name, lo_addr, hi_addr)); + } + PIN_RWMutexUnlock(&images_lock); +} + +VOID ImageManager::removeImage(ADDRINT low) +{ + PIN_RWMutexWriteLock(&images_lock); + { + set::iterator i = images.find(LoadedImage("", low)); + if (i != images.end()) { + LoadedImage li = *i; + images.erase(i); + } + } + PIN_RWMutexUnlock(&images_lock); +} + +VOID ImageManager::addWhiteListedImage(const std::string& image_name) +{ + whitelist.insert(image_name); +} + +BOOL ImageManager::isWhiteListed(const std::string& image_name) +{ + return whitelist.find(image_name) != whitelist.end(); +} + +// Checks if the given address falls inside one of the white-listed images we are +// tracing. +BOOL ImageManager::isInterestingAddress(ADDRINT addr) +{ + PIN_RWMutexReadLock(&images_lock); + { + // If there is no white-listed image, everything is white-listed. + if (images.empty() || (addr >= m_cached_low && addr < m_cached_high)) { + PIN_RWMutexUnlock(&images_lock); + return true; + } + + auto i = images.upper_bound(LoadedImage("", addr)); + --i; + + // If the instruction address does not fall inside a valid white listed image, bail out. + if (!(i != images.end() && i->low_ <= addr && addr < i->high_)) { + PIN_RWMutexUnlock(&images_lock); + return false; + } + + // Save the matched image. + m_cached_low = i->low_; + m_cached_high = i->high_; + } + PIN_RWMutexUnlock(&images_lock); + + return true; +} diff --git a/coverage/pin/ImageManager.h b/coverage/pin/ImageManager.h new file mode 100644 index 00000000..83e13e99 --- /dev/null +++ b/coverage/pin/ImageManager.h @@ -0,0 +1,54 @@ +#ifndef IMAGEMANAGER_H_ +#define IMAGEMANAGER_H_ + +#include +#include + +#include "pin.H" + +struct LoadedImage { + std::string name_; + ADDRINT low_; + ADDRINT high_; + + LoadedImage(const std::string& n = "", ADDRINT low = 0, ADDRINT high = 0) + : name_(n) + , low_(low) + , high_(high) + { + } + + // Overloaded method to implement searches over the loaded images list + // and also allow this class to be used on a set like STL container. + bool operator<(const LoadedImage& rhs) const + { + return low_ < rhs.low_; + } +}; + +class ImageManager { +private: + // Set of module names that are allowed to be traced. + std::set images; + PIN_RWMUTEX images_lock; + + // Here we store the names of the images inside our white list. + std::set whitelist; + + // Store the last recently matched image so we can use it as a cache. + ADDRINT m_cached_low; + ADDRINT m_cached_high; + +public: + ImageManager(); + virtual ~ImageManager(); + + VOID addWhiteListedImage(const std::string& image_name); + BOOL isWhiteListed(const std::string& image_name); + BOOL isInterestingAddress(ADDRINT addr); + + VOID addImage(std::string image_name, ADDRINT lo_add, ADDRINT hi_addr); + VOID removeImage(ADDRINT low); +}; + +#endif /* IMAGEMANAGER_H_ */ diff --git a/coverage/pin/LICENSE b/coverage/pin/LICENSE new file mode 100644 index 00000000..e3f3cb13 --- /dev/null +++ b/coverage/pin/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Agustin Gianni + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/coverage/pin/Makefile b/coverage/pin/Makefile new file mode 100644 index 00000000..729dac48 --- /dev/null +++ b/coverage/pin/Makefile @@ -0,0 +1,10 @@ +CONFIG_ROOT := $(PIN_ROOT)/source/tools/Config +include $(CONFIG_ROOT)/makefile.config + +TOOL_CXXFLAGS += -std=c++11 -Wno-format +TOOL_ROOTS := CodeCoverage + +$(OBJDIR)CodeCoverage$(PINTOOL_SUFFIX): $(OBJDIR)CodeCoverage$(OBJ_SUFFIX) $(OBJDIR)ImageManager$(OBJ_SUFFIX) + $(LINKER) $(TOOL_LDFLAGS) $(LINK_EXE)$@ $^ $(TOOL_LPATHS) $(TOOL_LIBS) + +include $(TOOLS_ROOT)/Config/makefile.default.rules \ No newline at end of file diff --git a/coverage/pin/README.md b/coverage/pin/README.md new file mode 100644 index 00000000..e7f1494c --- /dev/null +++ b/coverage/pin/README.md @@ -0,0 +1,103 @@ +# About + +`CodeCoverage` pintool that creates a log file with information regarding the instructions executed +by a process. The tool works on Windows, Linux and OS X. + +This tool aims to facilitate locating interesting parts of a program without incurring in too much +instrumentation overhead. For this reason the pintool can be instructed to only instrument those +modules inside a white-list. + +## Usage example + +Here we run the pintool from the command line and we specify that only the `test` +image should be instrumented. This improves performance and reduce the amount of information collected. +You can specify as many white-listed images as you want by adding several `-w` arguments to the pintool. +If no `-w` arguments are supplied, this means the tool will trace all loaded images. + +``` +$ pin -t obj-intel64/CodeCoverage.dylib -w test -- ./test +CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com) +White-listing image: test +Logging code coverage information to: trace.log +Loaded image: 0x000000010a1df000:0x000000010a1dffff -> test +Loaded image: 0x00007fff65a5c000:0x00007fff65acffff -> dyld +Loaded image: 0x00007fff94b07000:0x00007fff94b5afff -> libc++.1.dylib +Loaded image: 0x00007fff942fa000:0x00007fff942fbfff -> libSystem.B.dylib +Loaded image: 0x00007fff8bf30000:0x00007fff8bf59fff -> libc++abi.dylib +Loaded image: 0x00007fff875ac000:0x00007fff875b0fff -> libcache.dylib + +$ ll trace.log +-rw------- 1 anon staff 3.1K Apr 28 01:01 trace.log + +If you want to instrument all the loaded modules you can leave out the "-w" parameter and it will +trace all the basic blocks. Beware that the resulting log file will be several orders of magnitude +bigger. + +$ pin -t obj-intel64/CodeCoverage.dylib -- ./test +CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com) +White-listed images not specified, instrumenting every module by default. +Logging code coverage information to: trace.log +Loaded image: 0x0000000101bf1000:0x0000000101bf1fff -> test +Loaded image: 0x00007fff6d167000:0x00007fff6d1dafff -> dyld +Loaded image: 0x00007fff94b07000:0x00007fff94b5afff -> libc++.1.dylib +Loaded image: 0x00007fff942fa000:0x00007fff942fbfff -> libSystem.B.dylib +Loaded image: 0x00007fff8bf30000:0x00007fff8bf59fff -> libc++abi.dylib +Loaded image: 0x00007fff875ac000:0x00007fff875b0fff -> libcache.dylib + +$ ll trace.log +-rw------- 1 anon staff 113K Apr 28 00:57 trace.log +``` + +As it can be appreciated in the log file, we have both information about the trace hits and information about the +loaded images (in the example some entries were removed for clarity). This is because when importing the +information into IDA Pro we need to accommodate the addresses to the base address in the IDB. Due to ASLR +the addresses won't match. + +## Compilation + +In order to compile this pintool, you need to download pin from https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool and place it in a cozy place. Once you've done that, make sure to export an environment variable named `PIN_ROOT` and make it point to your pin installation dir. Also make sure the you add `$PIN_ROOT` to your `PATH` environment variable. + +``` +$ export PIN_ROOT=/Users/anon/pin # Location where you unpacked pintool +$ export PATH=$PATH:$PIN_ROOT +$ make + +mkdir -p obj-intel64 +/Applications/Xcode.app/Contents/Developer/usr/bin/make objects +make[1]: Nothing to be done for `objects'. +/Applications/Xcode.app/Contents/Developer/usr/bin/make libs +make[1]: Nothing to be done for `libs'. +/Applications/Xcode.app/Contents/Developer/usr/bin/make dlls +make[1]: Nothing to be done for `dlls'. +/Applications/Xcode.app/Contents/Developer/usr/bin/make apps +make[1]: Nothing to be done for `apps'. +/Applications/Xcode.app/Contents/Developer/usr/bin/make tools + +c++ -DBIGARRAY_MULTIPLIER=1 -Wall -Werror -Wno-unknown-pragmas -D__PIN__=1 -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_MAC -D__DARWIN_ONLY_UNIX_CONFORMANCE=1 -D__DARWIN_UNIX03=0 -I/Users/anon/pin/source/include/pin -I/Users/anon/pin/source/include/pin/gen -isystem /Users/anon/pin/extras/stlport/include -isystem /Users/anon/pin/extras/libstdc++/include -isystem /Users/anon/pin/extras/crt/include -isystem /Users/anon/pin/extras/crt/include/arch-x86_64 -isystem /Users/anon/pin/extras/crt/include/kernel/uapi -isystem /Users/anon/pin/extras/crt/include/kernel/uapi/asm-x86 -I/Users/anon/pin/extras/components/include -I/Users/anon/pin/extras/xed-intel64/include/xed -I/Users/anon/pin/source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing -std=c++11 -Wno-format -c -o obj-intel64/CodeCoverage.o CodeCoverage.cpp + +c++ -DBIGARRAY_MULTIPLIER=1 -Wall -Werror -Wno-unknown-pragmas -D__PIN__=1 -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_MAC -D__DARWIN_ONLY_UNIX_CONFORMANCE=1 -D__DARWIN_UNIX03=0 -I/Users/anon/pin/source/include/pin -I/Users/anon/pin/source/include/pin/gen -isystem /Users/anon/pin/extras/stlport/include -isystem /Users/anon/pin/extras/libstdc++/include -isystem /Users/anon/pin/extras/crt/include -isystem /Users/anon/pin/extras/crt/include/arch-x86_64 -isystem /Users/anon/pin/extras/crt/include/kernel/uapi -isystem /Users/anon/pin/extras/crt/include/kernel/uapi/asm-x86 -I/Users/anon/pin/extras/components/include -I/Users/anon/pin/extras/xed-intel64/include/xed -I/Users/anon/pin/source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing -std=c++11 -Wno-format -c -o obj-intel64/ImageManager.o ImageManager.cpp + +c++ -shared /Users/anon/pin/intel64/runtime/pincrt/crtbeginS.o -w -Wl,-exported_symbols_list,/Users/anon/pin/source/include/pin/pintool.exp -o obj-intel64/CodeCoverage.dylib obj-intel64/CodeCoverage.o obj-intel64/ImageManager.o -L/Users/anon/pin/intel64/runtime/pincrt -L/Users/anon/pin/intel64/lib -L/Users/anon/pin/intel64/lib-ext -L/Users/anon/pin/extras/xed-intel64/lib -lpin -lxed -lpin3dwarf -nostdlib -lstlport-dynamic -lm-dynamic -lc-dynamic -lunwind-dynamic +``` + +The resulting binaries will be placed inside a directory whose name depends on the arch/platform/build type. + +- Linux and OSX + + - obj-intel32/CodeCoverage.[so|dylib] + - obj-intel64/CodeCoverage.[so|dylib] + +- Windows + + - Debug/CodeCoverage.dll + - Release/CodeCoverage.dll + - x64/Debug/CodeCoverage.dll + - x64/Release/CodeCoverage.dll + +## Trace file format +The format of the trace file emulates that of `drcov` tool from `dynamorio`. More information can be found in +http://dynamorio.org/docs/page_drcov.html + +## Authors + +* Agustin Gianni ([@agustingianni](https://twitter.com/agustingianni)) diff --git a/coverage/pin/TraceFile.h b/coverage/pin/TraceFile.h new file mode 100644 index 00000000..cb25ebc6 --- /dev/null +++ b/coverage/pin/TraceFile.h @@ -0,0 +1,51 @@ +#ifndef TRACEFILE_H_ +#define TRACEFILE_H_ + +#include +#include +#include +#include + +class TraceFile { +public: + TraceFile(const std::string& filename) + { + m_file = fopen(filename.c_str(), "w+"); + if (!m_file) { + std::cerr << "Could not open the log file." << std::endl; + std::abort(); + } + } + + ~TraceFile() + { + if (fclose(m_file) != 0) { + std::cerr << "Could not close the log file." << std::endl; + std::abort(); + } + } + + void write_binary(const void* ptr, size_t size) + { + if (fwrite(ptr, size, 1, m_file) != 1) { + std::cerr << "Could not log to the log file." << std::endl; + std::abort(); + } + } + + void write_string(const char* format, ...) + { + va_list args; + va_start(args, format); + if (vfprintf(m_file, format, args) < 0) { + std::cerr << "Could not log to the log file." << std::endl; + std::abort(); + } + va_end(args); + } + +private: + FILE* m_file; +}; + +#endif /* TRACEFILE_H_ */ diff --git a/coverage/pin/build.bat b/coverage/pin/build.bat new file mode 100644 index 00000000..f279bbd1 --- /dev/null +++ b/coverage/pin/build.bat @@ -0,0 +1,49 @@ +@echo off +cls + +cl ^ + /c /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MT /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /fp:precise /nologo /wd4316 ^ + /DTARGET_IA32 /DHOST_IA32 /DTARGET_WINDOWS /DBIGARRAY_MULTIPLIER=1 /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D__i386__ ^ + /I"%PIN_ROOT%\extras\xed-ia32\include\xed" ^ + /I%PIN_ROOT%\source\include\pin ^ + /I%PIN_ROOT%\source\include\pin\gen ^ + /I%PIN_ROOT%\source\tools\InstLib ^ + /I"%PIN_ROOT%\extras\xed-ia32\include" ^ + /I%PIN_ROOT%\extras\components\include ^ + /I%PIN_ROOT%\extras\stlport\include ^ + /I%PIN_ROOT%\extras ^ + /I%PIN_ROOT%\extras\libstdc++\include ^ + /I%PIN_ROOT%\extras\crt\include ^ + /I%PIN_ROOT%\extras\crt ^ + /I"%PIN_ROOT%\extras\crt\include\arch-x86" ^ + /I%PIN_ROOT%\extras\crt\include\kernel\uapi ^ + /I"%PIN_ROOT%\extras\crt\include\kernel\uapi\asm-x86" ^ + /FIinclude/msvc_compat.h CodeCoverage.cpp ImageManager.cpp ImageManager.h TraceFile.h + +link ^ + /ERRORREPORT:QUEUE ^ + /OUT:CodeCoverage.dll ^ + /INCREMENTAL:NO ^ + /NOLOGO ^ + /LIBPATH:%PIN_ROOT%\ia32\lib ^ + /LIBPATH:"%PIN_ROOT%\ia32\lib-ext" ^ + /LIBPATH:"%PIN_ROOT%\extras\xed-ia32\lib" ^ + /LIBPATH:%PIN_ROOT%\ia32\runtime\pincrt pin.lib xed.lib pinvm.lib kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-32.lib" crtbeginS.obj ^ + /NODEFAULTLIB ^ + /MANIFEST:NO ^ + /OPT:NOREF ^ + /TLBID:1 ^ + /ENTRY:"Ptrace_DllMainCRTStartup@12" ^ + /BASE:"0x55000000" ^ + /DYNAMICBASE ^ + /NXCOMPAT ^ + /IMPLIB:CodeCoverage.lib ^ + /MACHINE:X86 ^ + /SAFESEH:NO ^ + /export:main ^ + /ignore:4049 ^ + /ignore:4210 ^ + /ignore:4217 ^ + /DLL CodeCoverage.obj ImageManager.obj + +del *.obj *.pdb *.exp *.lib From ec6c66149c4d4b5720c6ec84b1404e99dcb41254 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 21 Sep 2017 11:48:49 -0700 Subject: [PATCH 41/43] 32bit & 64bit build scripts for windows pintool --- coverage/pin/build-x64.bat | 50 +++++++++++++++++++++++ coverage/pin/{build.bat => build-x86.bat} | 0 2 files changed, 50 insertions(+) create mode 100644 coverage/pin/build-x64.bat rename coverage/pin/{build.bat => build-x86.bat} (100%) diff --git a/coverage/pin/build-x64.bat b/coverage/pin/build-x64.bat new file mode 100644 index 00000000..c80bef16 --- /dev/null +++ b/coverage/pin/build-x64.bat @@ -0,0 +1,50 @@ +@echo off +cls + +cl ^ + /c ^ + /I%PIN_ROOT%\source\include\pin ^ + /I%PIN_ROOT%\source\include\pin\gen ^ + /I%PIN_ROOT%\source\tools\InstLib ^ + /I"%PIN_ROOT%\extras\xed-intel64\include\xed" ^ + /I%PIN_ROOT%\extras\components\include ^ + /I%PIN_ROOT%\extras\stlport\include ^ + /I%PIN_ROOT%\extras ^ + /I%PIN_ROOT%\extras\libstdc++\include ^ + /I%PIN_ROOT%\extras\crt\include ^ + /I%PIN_ROOT%\extras\crt ^ + /I"%PIN_ROOT%\extras\crt\include\arch-x86_64" ^ + /I%PIN_ROOT%\extras\crt\include\kernel\uapi ^ + /I"%PIN_ROOT%\extras\crt\include\kernel\uapi\asm-x86" ^ + /nologo /W3 /WX- /O2 ^ + /D TARGET_IA32E /D HOST_IA32E /D TARGET_WINDOWS /D WIN32 /D __PIN__=1 /D PIN_CRT=1 /D __LP64__ ^ + /Gm- /MT /GS- /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR- /Gd /TP /wd4530 /GR- /GS- /EHs- /EHa- /FP:strict /Oi- ^ + /FIinclude/msvc_compat.h CodeCoverage.cpp ImageManager.cpp ImageManager.h TraceFile.h + +link ^ + /ERRORREPORT:QUEUE ^ + /OUT:CodeCoverage64.dll ^ + /INCREMENTAL:NO ^ + /NOLOGO ^ + /LIBPATH:%PIN_ROOT%\intel64\lib ^ + /LIBPATH:"%PIN_ROOT%\intel64\lib-ext" ^ + /LIBPATH:"%PIN_ROOT%\extras\xed-intel64\lib" ^ + /LIBPATH:%PIN_ROOT%\intel64\runtime\pincrt pin.lib xed.lib pinvm.lib kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-64.lib" crtbeginS.obj ^ + /NODEFAULTLIB ^ + /MANIFEST:NO ^ + /OPT:NOREF ^ + /TLBID:1 ^ + /ENTRY:"Ptrace_DllMainCRTStartup" ^ + /BASE:"0xC5000000" ^ + /DYNAMICBASE ^ + /NXCOMPAT ^ + /IMPLIB:CodeCoverage.lib ^ + /MACHINE:X64 ^ + /SAFESEH:NO ^ + /export:main ^ + /ignore:4049 ^ + /ignore:4210 ^ + /ignore:4217 ^ + /DLL CodeCoverage.obj ImageManager.obj + +del *.obj *.pdb *.exp *.lib \ No newline at end of file diff --git a/coverage/pin/build.bat b/coverage/pin/build-x86.bat similarity index 100% rename from coverage/pin/build.bat rename to coverage/pin/build-x86.bat From a82378e3936e902cf9c9eb08cb7c4ba747ae69c4 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 21 Sep 2017 12:51:09 -0700 Subject: [PATCH 42/43] version bump to v0.6.0 --- plugin/lighthouse_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 2482b23c..f02b4951 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -20,7 +20,7 @@ # IDA Plugin #------------------------------------------------------------------------------ -PLUGIN_VERSION = "0.5.0" +PLUGIN_VERSION = "0.6.0" AUTHORS = "Markus Gaasedelen" DATE = "2017" From fe4ea54994bbe831283a97550b83b81812a0766d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 21 Sep 2017 12:51:41 -0700 Subject: [PATCH 43/43] updated README.md --- README.md | 57 ++++++++++------ coverage/pin/README.md | 147 ++++++++++++++++++++++------------------- screenshots/open.gif | Bin 0 -> 219053 bytes 3 files changed, 115 insertions(+), 89 deletions(-) create mode 100644 screenshots/open.gif diff --git a/README.md b/README.md index 4493f7a4..0bffee0a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ## Overview -Lighthouse is a Code Coverage Plugin for [IDA Pro](https://www.hex-rays.com/products/ida/). The plugin leverages IDA as a platform to map, explore, and visualize externally collected code coverage data when symbols or source may not be available for a given binary. +Lighthouse is a code coverage plugin for [IDA Pro](https://www.hex-rays.com/products/ida/). The plugin leverages IDA as a platform to map, explore, and visualize externally collected code coverage data when symbols or source may not be available for a given binary. This plugin is labeled only as a prototype and IDA / Qt code example for the community. @@ -13,6 +13,7 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration. ## Releases +* v0.6 -- Intel pintool, cyclomatic complexity, batch load, bugfixes. * v0.5 -- Search, IDA 7 support, many improvements, stability. * v0.4 -- Most compute is now asynchronous, bugfixes. * v0.3 -- Coverage composition, interactive composing shell. @@ -32,14 +33,21 @@ The plugin is platform agnostic, but has only been tested on Windows for IDA 6.8 ## Usage -Lighthouse loads automatically when an IDB is opened, installing the following menu entries into the IDA interface: +Lighthouse loads automatically when an IDB is opened, installing a handful of menu entries into the IDA interface. + +

+Lighthouse Menu Entries +

+ +These are the entry points for a user to load and view coverage data. ``` -- File --> Load file --> Code Coverage File(s)... +- File --> Load file --> Code coverage file... +- File --> Load file --> Code coverage batch... - View --> Open subviews --> Coverage Overview ``` -These are the entry points for a user to load and view coverage data. +Batch load can quickly aggregate hundreds (thousands?) of collected coverage files into a single composite at load time. ## Coverage Painting @@ -69,17 +77,17 @@ Building relationships between multiple sets of coverage data often distills dee Pressing `enter` on the shell will evaluate and save a user constructed composition. -### Composition Syntax +## Composition Syntax Coverage composition, or _Composing_ as demonstrated above is achieved through a simple expression grammar and 'shorthand' coverage symbols (A to Z) on the composing shell. -#### Grammar Tokens +### Grammar Tokens * Logical Operators: `|, &, ^, -` * Coverage Symbol: `A, B, C, ..., Z` * Coverage Range: `A,C`, `Q,Z`, ... * Parenthesis: `(...)` -#### Example Compositions +### Example Compositions * `A & B` * `(A & B) | C` * `(C & (A - B)) | (F,H & Q)` @@ -88,7 +96,7 @@ The evaluation of the composition may occur right to left, parenthesis are sugge ## Hot Shell -Additionally, there is a prototype 'Hot Shell' mode that asynchronously evaluates and caches user compositions in real-time. +Additionally, there is a 'Hot Shell' mode that asynchronously evaluates and caches user compositions in real-time.

Lighthouse Hot Shell @@ -122,27 +130,35 @@ Loaded coverage data and user constructed compositions can be selected or delete Lighthouse Coverage ComboBox

-## Collecting Coverage +# Collecting Coverage -At this time, Lighthouse only consumes binary coverage data as produced by DynamoRIO's [drcov](http://dynamorio.org/docs/page_drcov.html) code coverage module. +Before using Lighthouse, one will need to collect code coverage data for their target binary / application. -Collecting blackbox coverage data with `drcov` is relatively straightforward. The following example demonstrates how coverage was produced for the `boombox.exe` testcase provided in this repository. +The examples below demonstrate how one can use [DynamoRIO](http://www.dynamorio.org) or [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) to collect Lighthouse compatible coverage agaainst a target. The `.log` files produced by these instrumentation tools can be loaded directly into Lighthouse. + +## DynamoRIO + +Code coverage data can be collected via DynamoRIO's [drcov](http://dynamorio.org/docs/page_drcov.html) code coverage module. + +Example usage: ``` ..\DynamoRIO-Windows-7.0.0-RC1\bin64\drrun.exe -t drcov -- boombox.exe ``` -This command will produce a `.log` file consisting of the coverage data upon termination of the target application. +## Intel Pin (Experimental) -## Other Coverage Sources +Using a [custom pintool](coverage/pin) contributed by [Agustin Gianni](https://twitter.com/agustingianni), the Intel Pin DBI can also be used to collect coverage data. -[drcov](http://dynamorio.org/docs/page_drcov.html) was selected as the initial coverage data source due to its availability, adoption, multi-platform (Win/Mac/Linux), and multi-architecture (x86/AMD64/ARM) support. +Example usage: -Intel's [PIN](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) for example does not come with a default code coverage pintool. It appears that most implement their own solution and there is no clear format for Lighthouse to standardize on. In the future, Lighthouse may ship with its own pintool. +``` +pin.exe -t CodeCoverage64.dll -- boombox.exe +``` -While Lighthouse is considered a prototype, internally it is largely agnostic of its data source. Future work will hopefully allow one to drop a loader into the `parsers` folder without any need for code changes to Lighthouse. Right now, this is not the case. +For convenience, binaries for the Windows pintool can be found on the [releases](https://github.com/gaasedelen/lighthouse/releases/tag/v0.6.0) page. MacOS and Linux users need to compile the pintool themselves following the [instructions](coverage/pin#compilation) included with the pintool for their respective platforms. -## Future Work +# Future Work Time and motivation permitting, future work may include: @@ -150,10 +166,11 @@ Time and motivation permitting, future work may include: * ~~Multifile/coverage support~~ * Profiling based heatmaps/painting * Coverage & Profiling Treemaps -* Automatic parser pickup -* Parsers for additional coverage sources, eg PIN +* Additional coverage sources, trace formats, etc * Improved Pseudocode painting -## Authors +I welcome external contributions, issues, and feature requests. + +# Authors * Markus Gaasedelen ([@gaasedelen](https://twitter.com/gaasedelen)) diff --git a/coverage/pin/README.md b/coverage/pin/README.md index e7f1494c..ee116b2c 100644 --- a/coverage/pin/README.md +++ b/coverage/pin/README.md @@ -1,38 +1,71 @@ -# About +# CodeCoverage Pintool -`CodeCoverage` pintool that creates a log file with information regarding the instructions executed -by a process. The tool works on Windows, Linux and OS X. +The `CodeCoverage` pintool runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework and collects code coverage data in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). The log produced by this pintool emulates that of [drcov](http://dynamorio.org/docs/page_drcov.html) as shipped with [DynamoRIO](http://www.dynamorio.org). -This tool aims to facilitate locating interesting parts of a program without incurring in too much -instrumentation overhead. For this reason the pintool can be instructed to only instrument those -modules inside a white-list. +This pintool is labeled only as a prototype. -## Usage example +# Compilation -Here we run the pintool from the command line and we specify that only the `test` -image should be instrumented. This improves performance and reduce the amount of information collected. -You can specify as many white-listed images as you want by adding several `-w` arguments to the pintool. -If no `-w` arguments are supplied, this means the tool will trace all loaded images. +To compile the pintool, you first will need to [download](https://software.intel.com/en-us/articles/pin-a-binary-instrumentation-tool-downloads) and extract Pin. + +Follow the build instructions below for your respective platform. + +## Building for MacOS or Linux + +On MacOS or Liunux, one can compile the pintool using the following commands. ``` -$ pin -t obj-intel64/CodeCoverage.dylib -w test -- ./test -CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com) -White-listing image: test -Logging code coverage information to: trace.log -Loaded image: 0x000000010a1df000:0x000000010a1dffff -> test -Loaded image: 0x00007fff65a5c000:0x00007fff65acffff -> dyld -Loaded image: 0x00007fff94b07000:0x00007fff94b5afff -> libc++.1.dylib -Loaded image: 0x00007fff942fa000:0x00007fff942fbfff -> libSystem.B.dylib -Loaded image: 0x00007fff8bf30000:0x00007fff8bf59fff -> libc++abi.dylib -Loaded image: 0x00007fff875ac000:0x00007fff875b0fff -> libcache.dylib +cd ~/lighthouse/coverage/pin # Location of this repo / pintool source +export PIN_ROOT=~/pin # Location where you extracted Pin +export PATH=$PATH:$PIN_ROOT +make +``` -$ ll trace.log --rw------- 1 anon staff 3.1K Apr 28 01:01 trace.log +The resulting binaries will be placed inside a directory whose name depends on the arch/platform/build type. + +* obj-intel32/CodeCoverage.[so|dylib] +* obj-intel64/CodeCoverage.[so|dylib] + +## Building for Windows + +To compile the Windows pintool, you must have at least Visual Studio 2015 installed. -If you want to instrument all the loaded modules you can leave out the "-w" parameter and it will -trace all the basic blocks. Beware that the resulting log file will be several orders of magnitude -bigger. +Launch a command prompt and build the pintool with the following commands. +### 32bit Pintool + +``` +"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86 +cd C:\Users\user\lighthouse\coverage\pin # Location of this repo / pintool source +set PIN_ROOT=C:\pin # Location where you extracted Pin +set PATH=%PATH%;%PIN_ROOT% +build-x86.bat +``` + +### 64bit Pintool + +``` +"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64 +cd C:\Users\user\lighthouse\coverage\pin # Location of this repo / pintool source +set PIN_ROOT=C:\pin # Location where you extracted Pin +set PATH=%PATH%;%PIN_ROOT% +build-x64.bat +``` + +The resulting binaries will be labaled based on their architecture (eg, 64 is the 64bit pintool). + +* CodeCoverage.dll +* CodeCoverage64.dll + +Compiling a pintool on Windows can be more arduous. Because of this, we have provided compiled binaries for Windows on the [releases](https://github.com/gaasedelen/lighthouse/releases/tag/v0.6.0) page. + +# Usage + +Once compiled, usage of the pintool is straightforward. Simply provide the compiled `CodeCoverage` pintool to `pin` via the `-t` argument. The resulting code coverage data will be written to the file `trace.log` at the end of execution. + +Here is an example of us instrumenting a 64bit binary called `test` with our `CodeCoverage` pintool. + +``` $ pin -t obj-intel64/CodeCoverage.dylib -- ./test CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com) White-listed images not specified, instrumenting every module by default. @@ -48,56 +81,32 @@ $ ll trace.log -rw------- 1 anon staff 113K Apr 28 00:57 trace.log ``` -As it can be appreciated in the log file, we have both information about the trace hits and information about the -loaded images (in the example some entries were removed for clarity). This is because when importing the -information into IDA Pro we need to accommodate the addresses to the base address in the IDB. Due to ASLR -the addresses won't match. +## Module Whitelisting -## Compilation +Using the `-w` command line flag, the pintool can be instructed to instrument only the modules you specify. -In order to compile this pintool, you need to download pin from https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool and place it in a cozy place. Once you've done that, make sure to export an environment variable named `PIN_ROOT` and make it point to your pin installation dir. Also make sure the you add `$PIN_ROOT` to your `PATH` environment variable. +Here we run the pintool from the command line and specify that only the `test` image should be instrumented. This improves performance and drastically reduces the amount of data collected by ignoring the execution of shared libraries such as `libc`. ``` -$ export PIN_ROOT=/Users/anon/pin # Location where you unpacked pintool -$ export PATH=$PATH:$PIN_ROOT -$ make - -mkdir -p obj-intel64 -/Applications/Xcode.app/Contents/Developer/usr/bin/make objects -make[1]: Nothing to be done for `objects'. -/Applications/Xcode.app/Contents/Developer/usr/bin/make libs -make[1]: Nothing to be done for `libs'. -/Applications/Xcode.app/Contents/Developer/usr/bin/make dlls -make[1]: Nothing to be done for `dlls'. -/Applications/Xcode.app/Contents/Developer/usr/bin/make apps -make[1]: Nothing to be done for `apps'. -/Applications/Xcode.app/Contents/Developer/usr/bin/make tools - -c++ -DBIGARRAY_MULTIPLIER=1 -Wall -Werror -Wno-unknown-pragmas -D__PIN__=1 -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_MAC -D__DARWIN_ONLY_UNIX_CONFORMANCE=1 -D__DARWIN_UNIX03=0 -I/Users/anon/pin/source/include/pin -I/Users/anon/pin/source/include/pin/gen -isystem /Users/anon/pin/extras/stlport/include -isystem /Users/anon/pin/extras/libstdc++/include -isystem /Users/anon/pin/extras/crt/include -isystem /Users/anon/pin/extras/crt/include/arch-x86_64 -isystem /Users/anon/pin/extras/crt/include/kernel/uapi -isystem /Users/anon/pin/extras/crt/include/kernel/uapi/asm-x86 -I/Users/anon/pin/extras/components/include -I/Users/anon/pin/extras/xed-intel64/include/xed -I/Users/anon/pin/source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing -std=c++11 -Wno-format -c -o obj-intel64/CodeCoverage.o CodeCoverage.cpp - -c++ -DBIGARRAY_MULTIPLIER=1 -Wall -Werror -Wno-unknown-pragmas -D__PIN__=1 -DPIN_CRT=1 -fno-stack-protector -fno-exceptions -funwind-tables -fno-rtti -DTARGET_IA32E -DHOST_IA32E -fPIC -DTARGET_MAC -D__DARWIN_ONLY_UNIX_CONFORMANCE=1 -D__DARWIN_UNIX03=0 -I/Users/anon/pin/source/include/pin -I/Users/anon/pin/source/include/pin/gen -isystem /Users/anon/pin/extras/stlport/include -isystem /Users/anon/pin/extras/libstdc++/include -isystem /Users/anon/pin/extras/crt/include -isystem /Users/anon/pin/extras/crt/include/arch-x86_64 -isystem /Users/anon/pin/extras/crt/include/kernel/uapi -isystem /Users/anon/pin/extras/crt/include/kernel/uapi/asm-x86 -I/Users/anon/pin/extras/components/include -I/Users/anon/pin/extras/xed-intel64/include/xed -I/Users/anon/pin/source/tools/InstLib -O3 -fomit-frame-pointer -fno-strict-aliasing -std=c++11 -Wno-format -c -o obj-intel64/ImageManager.o ImageManager.cpp - -c++ -shared /Users/anon/pin/intel64/runtime/pincrt/crtbeginS.o -w -Wl,-exported_symbols_list,/Users/anon/pin/source/include/pin/pintool.exp -o obj-intel64/CodeCoverage.dylib obj-intel64/CodeCoverage.o obj-intel64/ImageManager.o -L/Users/anon/pin/intel64/runtime/pincrt -L/Users/anon/pin/intel64/lib -L/Users/anon/pin/intel64/lib-ext -L/Users/anon/pin/extras/xed-intel64/lib -lpin -lxed -lpin3dwarf -nostdlib -lstlport-dynamic -lm-dynamic -lc-dynamic -lunwind-dynamic -``` - -The resulting binaries will be placed inside a directory whose name depends on the arch/platform/build type. - -- Linux and OSX - - - obj-intel32/CodeCoverage.[so|dylib] - - obj-intel64/CodeCoverage.[so|dylib] +$ pin -t obj-intel64/CodeCoverage.dylib -w test -- ./test +CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com) +White-listing image: test +Logging code coverage information to: trace.log +Loaded image: 0x000000010a1df000:0x000000010a1dffff -> test +Loaded image: 0x00007fff65a5c000:0x00007fff65acffff -> dyld +Loaded image: 0x00007fff94b07000:0x00007fff94b5afff -> libc++.1.dylib +Loaded image: 0x00007fff942fa000:0x00007fff942fbfff -> libSystem.B.dylib +Loaded image: 0x00007fff8bf30000:0x00007fff8bf59fff -> libc++abi.dylib +Loaded image: 0x00007fff875ac000:0x00007fff875b0fff -> libcache.dylib -- Windows +$ ll trace.log +-rw------- 1 anon staff 3.1K Apr 28 01:01 trace.log +``` - - Debug/CodeCoverage.dll - - Release/CodeCoverage.dll - - x64/Debug/CodeCoverage.dll - - x64/Release/CodeCoverage.dll +You can specify as many white-listed images as you want by adding several `-w` arguments to the pintool. -## Trace file format -The format of the trace file emulates that of `drcov` tool from `dynamorio`. More information can be found in -http://dynamorio.org/docs/page_drcov.html +If no `-w` arguments are supplied, the pintool will trace all loaded images. -## Authors +# Authors * Agustin Gianni ([@agustingianni](https://twitter.com/agustingianni)) diff --git a/screenshots/open.gif b/screenshots/open.gif new file mode 100644 index 0000000000000000000000000000000000000000..be95f2761321a45c4895b27c4156c98abf95720e GIT binary patch literal 219053 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�YU3#t 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 TbZ8(`V{&C>Zd7kaX=VUBiK{TA literal 0 HcmV?d00001