From 8e27e00cda2b92aea1142ed5883e3434878fbd2c Mon Sep 17 00:00:00 2001 From: Florian Magin Date: Fri, 29 Sep 2017 14:33:39 +0200 Subject: [PATCH 01/39] Change displayed function names to demangled names (IDA<=6.95) --- plugin/lighthouse/metadata.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index f67149a4..d22072ec 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -7,6 +7,7 @@ import idaapi import idautils +import idc from lighthouse.util import * @@ -583,7 +584,12 @@ def _refresh_name(self): if using_ida7api: self.name = idaapi.get_func_name(self.address) else: - self.name = idaapi.get_func_name2(self.address) + name = idaapi.get_func_name2(self.address) + demangled_name = idc.Demangle(name, idc.GetLongPrm(idc.INF_SHORT_DN)) + if demangled_name: + self.name = demangled_name + else: + self.name = name # # the replace is sort of a 'special case' for the 'Prefix' IDA From 6a80d3421f951478b458952070255462d9e738d7 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 1 Oct 2017 14:07:22 -0400 Subject: [PATCH 02/39] Name demangling IDA 6.8 --> IDA 7.0, issue #13 --- plugin/lighthouse/metadata.py | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 574e9d3f..34b1ef67 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -7,7 +7,6 @@ import idaapi import idautils -import idc from lighthouse.util import * @@ -581,26 +580,7 @@ def _refresh_name(self): """ Refresh the function name against the open database. """ - if using_ida7api: - self.name = idaapi.get_func_name(self.address) - else: - name = idaapi.get_func_name2(self.address) - demangled_name = idc.Demangle(name, idc.GetLongPrm(idc.INF_SHORT_DN)) - if demangled_name: - self.name = demangled_name - else: - self.name = name - - # - # 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("%", "_") + self.name = idaapi.get_short_name(self.address) def _refresh_nodes(self): """ From 1aea26d0182da163b5d47f72fdd664056342afdb Mon Sep 17 00:00:00 2001 From: _yrp Date: Tue, 24 Oct 2017 09:35:50 -0700 Subject: [PATCH 03/39] Adds frida script for gathering code coverage (#17) --- coverage/frida/README.md | 64 +++++++ coverage/frida/frida-drcov.py | 322 ++++++++++++++++++++++++++++++++++ 2 files changed, 386 insertions(+) create mode 100644 coverage/frida/README.md create mode 100755 coverage/frida/frida-drcov.py diff --git a/coverage/frida/README.md b/coverage/frida/README.md new file mode 100644 index 00000000..f5f0c82d --- /dev/null +++ b/coverage/frida/README.md @@ -0,0 +1,64 @@ +# frida-drcov.py + +A quick and dirty frida-based bb-tracer, with an emphasis on ease of use. + +If your target is complex, you'll likely want to use a better, dedicated +tracing engine like drcov or a pin based tracer. This tracer has some +significant shortcomings, which are exagerated on larger or more complex +binaries: +* It is roughly one order of magnitude slower than native execution +* It drops coverage, especially near `exit()` +* It cannot easily detect new threads being created, thus cannot instrument +them +* Self modifying code will confuse it, though to be fair I'm not sure how +drcov, pin, or otheres deal with self modifying code either + +These shortcomines are probably 10% frida's fault and 90% the author's. Despite +these flaws however, it is hard to beat the ease of use frida provides. + +## Install + +`$ pip install frida` + +## Usage + +`$ ./frida-drcov.py ` + +You can whitelist specific modules inside your target. Say you have binary +`foo` which imports `libbiz`, `libbaz`, and `libbar`. You only want to trace +`libbiz` and `libbaz`: + +`$ ./frida-drcov.py -w libbiz -w libbaz foo` + +By default, this script will trace all modules. This script will create and +write to `frida-drcov.log` in the current working directory. You can change +this with `-o`: + +`$ ./frida-drcov.py -o more-coverage.log foo` + +For slightly more advanced usage, on multi-threaded applications, tracing all +threads can impose significant overhead, especially if you only care about +particular threads. For these cases you can filter based on thread id. Say you +have another tool which identifies interesting threads 543 and 678 inside your +target. + +`$ ./frida-drcov.py -t 543 -t 678 foo` + +Will only trace those threads. By default, all threads are traced. + +## Example + +``` +$ sudo ./frida-drcov.py bb-bench +[+] Got module info +Starting to stalk threads... +Stalking thread 775 +Done stalking threads. +[*] Now collecting info, control-D to terminate.... +[*] Detatching, this might take a second... # ^d +[+] Detatched. Got 320 basic blocks. +[*] Formatting coverage and saving... +[!] Done +$ ls -lh frida-cov.log # this is the file you will load into lighthouse +-rw-r--r-- 1 root staff 7.2K 21 Oct 11:58 frida-cov.log +``` diff --git a/coverage/frida/frida-drcov.py b/coverage/frida/frida-drcov.py new file mode 100755 index 00000000..8b8eaec1 --- /dev/null +++ b/coverage/frida/frida-drcov.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +import json +import sys + +import frida + +""" +Frida BB tracer that outputs in DRcov format. + +Frida script is responsible for: +- Getting and sending the process module map initially +- Getting the code execution events +- Parsing the raw event into a GumCompileEvent +- Converting from GumCompileEvent to DRcov block +- Sending a list of DRcov blocks to python + +Python side is responsible for: +- Attaching and detaching from the target process +- Removing duplicate DRcov blocks +- Formatting module map and blocks +- Writing the output file +""" + +# Our frida script, takes two string arguments to embed +# 1. whitelist of modules, in the form "['module_a', 'module_b']" or "['all']" +# 2. threads to trace, in the form "[345, 765]" or "['all']" +js = """ +"use strict"; + +var whitelist = %s; +var threadlist = %s; + +// Get the module map +function make_maps() { + var maps = Process.enumerateModulesSync(); + var i = 0; + // We need to add the module id + maps.map(function(o) { o.id = i++; }); + // .. and the module end point + maps.map(function(o) { o.end = o.base.add(o.size); }); + + return maps; +} + +var maps = make_maps() + +send({'map': maps}); + +// We want to use frida's ModuleMap to create DRcov events, however frida's +// Module object doesn't have the 'id' we added above. To get around this, +// we'll create a mapping from path -> id, and have the ModuleMap look up the +// path. While the ModuleMap does contain the base address, if we cache it +// here, we can simply look up the path rather than the entire Module object. +var module_ids = {}; + +maps.map(function (e) { + module_ids[e.path] = {id: e.id, start: e.base}; +}); + +var filtered_maps = new ModuleMap(function (m) { + if (whitelist.indexOf('all') >= 0) { return true; } + + return whitelist.indexOf(m.name) >= 0; +}); + +// This function takes a list of GumCompileEvents and converts it into a DRcov +// entry. Note that we'll get duplicated events when two traced threads +// execute the same code, but this will be handled by the python side. +function drcov_bbs(bbs, fmaps, path_ids) { + // We're going to use send(..., data) so we need an array buffer to send + // our results back with. Let's go ahead and alloc the max possible + // reply size + + /* + // Data structure for the coverage info itself + typedef struct _bb_entry_t { + uint start; // offset of bb start from the image base + ushort size; + ushort mod_id; + } bb_entry_t; + */ + + var entry_sz = 8; + + var bb = new ArrayBuffer(entry_sz * bbs.length); + + var num_entries = 0; + + for (var i = 0; i < bbs.length; ++i) { + var e = bbs[i]; + + var start = e[0]; + var end = e[1]; + + var path = fmaps.findPath(start); + + if (path == null) { continue; } + + var mod_info = path_ids[path]; + + var offset = start.sub(mod_info.start).toInt32(); + var size = end.sub(start).toInt32(); + var mod_id = mod_info.id; + + // We're going to create two memory views into the array we alloc'd at + // the start. + + // we want one u32 after all the other entries we've created + var x = new Uint32Array(bb, num_entries * entry_sz, 1); + x[0] = offset; + + // we want two u16's offset after the 4 byte u32 above + var y = new Uint16Array(bb, num_entries * entry_sz + 4, 2); + y[0] = size; + y[1] = mod_id; + + ++num_entries; + } + + // We can save some space here, rather than sending the entire array back, + // we can create a new view into the already allocated memory, and just + // send back that linear chunk. + return new Uint8Array(bb, 0, num_entries * entry_sz); +} +// Punt on self modifying code -- should improve speed and lighthouse will +// barf on it anyways +Stalker.trustThreshold = 0; + +console.log('Starting to stalk threads...'); + +// Note, we will miss any bbs hit by threads that are created after we've +// attached +Process.enumerateThreads({ + onMatch: function (thread) { + if (threadlist.indexOf(thread.id) < 0 && + threadlist.indexOf('all') < 0) { + // This is not the thread you're look for + return; + } + + console.log('Stalking thread ' + thread.id + '.'); + + Stalker.follow(thread.id, { + events: { + compile: true + }, + onReceive: function (event) { + var bb_events = Stalker.parse(event, + {stringify: false, annotate: false}); + var bbs = drcov_bbs(bb_events, filtered_maps, module_ids); + + // We're going to send a dummy message, the actual bb is in the + // data field. We're sending a dict to keep it consistent with + // the map. We're also creating the drcov event in javascript, + // so on the py recv side we can just blindly add it to a set. + send({bbs: 1}, bbs); + } + }); + }, + onComplete: function () { console.log('Done stalking threads.'); } +}); +""" + +# These are global so we can easily access them from the frida callbacks +# It's important that bbs is a set, as we're going to depend on it's uniquing +# behavior for deduplication +modules = [] +bbs = set([]) + +# This converts the object frida sends which has string addresses into +# a python dict +def populate_modules(image_list): + global modules + + for image in image_list: + idx = image['id'] + path = image['path'] + base = int(image['base'], 0) + end = int(image['end'], 0) + size = image['size'] + + m = { + 'id': idx, + 'path': path, + 'base': base, + 'end': end, + 'size': size} + + modules.append(m) + + print('[+] Got module info.') + +# called when we get coverage data from frida +def populate_bbs(data): + global bbs + + # we know every drcov block is 8 bytes, so lets just blindly slice and + # insert. This will dedup for us. + block_sz = 8 + for i in range(0, len(data), block_sz): + bbs.add(data[i:i+block_sz]) + +# take the module dict and format it as a drcov logfile header +def create_header(mods): + header = '' + header += 'DRCOV VERSION: 2\n' + header += 'DRCOV FLAVOR: frida\n' + header += 'Module Table: version 2, count %d\n' % len(mods) + header += 'Columns: id, base, end, entry, checksum, timestamp, path\n' + + entries = [] + + for m in mods: + # drcov: id, base, end, entry, checksum, timestamp, path + # frida doesnt give us entry, checksum, or timestamp + # luckily, I don't think we need them. + entry = '%3d, %#016x, %#016x, %#016x, %#08x, %#08x, %s' % ( + m['id'], m['base'], m['end'], 0, 0, 0, m['path']) + + entries.append(entry) + + header_modules = '\n'.join(entries) + + return header + header_modules + '\n' + +# take the recv'd basic blocks, finish the header, and append the coverage +def create_coverage(data): + bb_header = 'BB Table: %d bbs\n' % len(data) + return bb_header + ''.join(data) + +def on_message(msg, data): + #print(msg) + pay = msg['payload'] + if 'map' in pay: + maps = pay['map'] + populate_modules(maps) + else: + populate_bbs(data) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('target', + help='target process name or pid', + default='-1') + parser.add_argument('-o', '--outfile', + help='coverage file', + default='frida-cov.log') + parser.add_argument('-w', '--whitelist-modules', + help='module to trace, may be specified multiple times [all]', + action='append', default=[]) + parser.add_argument('-t', '--thread-id', + help='threads to trace, may be specified multiple times [all]', + action='append', type=int, default=[]) + parser.add_argument('-D', '--device', + help='select a device by id [local]', + default='local') + + args = parser.parse_args() + + device = frida.get_device(args.device) + + target = -1 + for p in device.enumerate_processes(): + if args.target in [str(p.pid), p.name]: + if target == -1: + target = p.pid + else: + print('[-] Warning: multiple processes on device match \'%s\'' + + ', using pid: %d' % (args.target, target)) + + if target == -1: + print('[-] Error: could not find process matching \'%s\'' + + ' on device \'%s\'' % (args.target, device.id)) + sys.exit(1) + + whitelist_modules = ['all'] + if len(args.whitelist_modules): + whitelist_modules = args.whitelist_modules + + threadlist = ['all'] + if len(args.thread_id): + threadlist = args.thread_id + + json_whitelist_modules = json.dumps(whitelist_modules) + json_threadlist = json.dumps(threadlist) + + print('[*] Attaching to pid \'%d\' on device \'%s\'...' % + (target, device.id)) + + session = device.attach(target) + print('[+] Attached. Loading script...') + + script = session.create_script(js % (json_whitelist_modules, json_threadlist)) + script.on('message', on_message) + script.load() + + print('[*] Now collecting info, control-D to terminate....') + + sys.stdin.read() + + print('[*] Detaching, this might take a second...') + session.detach() + + print('[+] Detached. Got %d basic blocks.' % len(bbs)) + print('[*] Formatting coverage and saving...') + + header = create_header(modules) + body = create_coverage(bbs) + + with open(args.outfile, 'wb') as h: + h.write(header) + h.write(body) + + print('[!] Done') + + sys.exit(0) + +if __name__ == '__main__': + main() From ca4524df897ec32b5216d135a5136059e9ee0435 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 31 Oct 2017 16:24:57 -0400 Subject: [PATCH 04/39] dock Coverage Overview in debug mode, fixes #16 --- plugin/lighthouse/ui/coverage_overview.py | 41 +++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 6c8482d8..2935432b 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -54,6 +54,7 @@ #------------------------------------------------------------------------------ # Pseudo Widget Filter #------------------------------------------------------------------------------ +debugger_docked = False class EventProxy(QtCore.QObject): def __init__(self, target): @@ -61,8 +62,48 @@ def __init__(self, target): self._target = target def eventFilter(self, source, event): + + # + # hook the destroy event of the coverage overview widget so that we can + # cleanup after ourselves in the interest of stability + # + if int(event.type()) == 16: # NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7? self._target.terminate() + + # + # this is an unknown event, but it seems to fire when the widget is + # being saved/restored by a QMainWidget. We use this to try and ensure + # the Coverage Overview stays docked when flipping between Reversing + # and Debugging states in IDA. + # + # See issue #16 on github for more information. + # + + if int(event.type()) == 2002: + + # + # if the general registers IDA View exists, we make the assumption + # that the user has probably started debugging. + # + + # NOTE / COMPAT: + if using_ida7api: + debug_mode = bool(idaapi.find_widget("General registers")) + else: + debug_mode = bool(idaapi.find_tform("General registers")) + + # + # if this is the first time the user has started debugging, dock + # the coverage overview in the debug QMainWidget workspace. its + # dock status / position should persist future debugger launches. + # + + global debugger_docked + if debug_mode and not debugger_docked: + idaapi.set_dock_pos(self._target._title, "Structures", idaapi.DP_TAB) + debugger_docked = True + return False #------------------------------------------------------------------------------ From c233f3a606a1b9adc0f5aa8a18c1f9b486a41c7f Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 1 Nov 2017 09:36:43 -0400 Subject: [PATCH 05/39] UI_Hooks seem straight broken for some users, removing. --- plugin/lighthouse/painting.py | 58 ++++++++++++----------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/plugin/lighthouse/painting.py b/plugin/lighthouse/painting.py index 71b6e7ce..06f92681 100644 --- a/plugin/lighthouse/painting.py +++ b/plugin/lighthouse/painting.py @@ -34,6 +34,18 @@ def __init__(self, director, palette): self._painted_nodes = set() self._painted_instructions = set() + #---------------------------------------------------------------------- + # HexRays Hooking + #---------------------------------------------------------------------- + + # + # we attempt to hook hexrays the *first* time a repaint request is + # made. the assumption being that IDA is fully loaded and if hexrays is + # present, it will definitely be available (for hooking) by this time + # + + self._attempted_hook = False + #---------------------------------------------------------------------- # Async #---------------------------------------------------------------------- @@ -63,11 +75,6 @@ def __init__(self, director, palette): # Callbacks #---------------------------------------------------------------------- - 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) self._director.coverage_modified(self.repaint) @@ -85,23 +92,9 @@ def terminate(self): # Initialization #-------------------------------------------------------------------------- - def _init_hexrays_hooks(self, widget, _=None): + def _init_hexrays_hooks(self): """ Install Hex-Rrays hooks (when available). - - NOTE: - - 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 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 @@ -111,14 +104,6 @@ def _init_hexrays_hooks(self, widget, _=None): logger.debug("HexRays hooked: %r" % result) - # - # we only use self._hooks (IDP_Hooks) to install our hexrays hooks. - # since this 'init' function should only ever be called once, remove - # our IDP_Hooks now to clean up after ourselves. - # - - self._hooks.unhook() - #------------------------------------------------------------------------------ # Painting #------------------------------------------------------------------------------ @@ -127,6 +112,13 @@ def repaint(self): """ Paint coverage defined by the current database mappings. """ + + # attempt to hook hexrays *once* + if not self._attempted_hook: + self._init_hexrays_hooks() + self._attempted_hook = True + + # signal the painting thread that it's time to repaint coverage self._repaint_requested = True self._repaint_request.set() @@ -695,13 +687,3 @@ def _async_action(self, paint_action, work_iterable): # operation completed successfully return True - -#------------------------------------------------------------------------------ -# Painter Hooks -#------------------------------------------------------------------------------ - -class PainterHooks(idaapi.UI_Hooks): - """ - This is a concrete stub of IDA's UI_Hooks. - """ - pass From 4c4863ff216f46207813dfa1fd85ff562dafbb99 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 1 Nov 2017 09:38:05 -0400 Subject: [PATCH 06/39] removes old dev script --- dev_scripts/flip_python.bat | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 dev_scripts/flip_python.bat diff --git a/dev_scripts/flip_python.bat b/dev_scripts/flip_python.bat deleted file mode 100644 index 8e514a91..00000000 --- a/dev_scripts/flip_python.bat +++ /dev/null @@ -1,8 +0,0 @@ - -if exist C:\Python27_32 ( - MOVE C:\Python27 C:\Python27_64 - MOVE C:\Python27_32 C:\Python27 -) else ( - MOVE C:\Python27 C:\Python27_32 - MOVE C:\Python27_64 C:\Python27 -) \ No newline at end of file From d6853f1358e3ea40dc795a083a56cbb80b19cc69 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 1 Nov 2017 16:49:59 -0400 Subject: [PATCH 07/39] fix missing icons bug (install location dependent) --- dev_scripts/reload_IDA_7.bat | 10 +++++----- dev_scripts/reload_IDA_7_ida.bat | 10 +++++----- plugin/lighthouse/util/misc.py | 5 +++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/dev_scripts/reload_IDA_7.bat b/dev_scripts/reload_IDA_7.bat index 11219830..b8b6845f 100644 --- a/dev_scripts/reload_IDA_7.bat +++ b/dev_scripts/reload_IDA_7.bat @@ -5,13 +5,13 @@ REM - Purge old lighthouse log files del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*" REM - Delete the old plugin bits -del /F /Q "C:\tools\disassemblers\IDA 7.0\plugins\*lighthouse_plugin.py" -rmdir "C:\tools\disassemblers\IDA 7.0\plugins\lighthouse" /s /q +del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py" +rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q REM - Copy over the new plugin bits -xcopy /s/y "..\plugin\*" "C:\tools\disassemblers\IDA 7.0\plugins\" -del /F /Q "C:\tools\disassemblers\IDA 7.0\plugins\.#lighthouse_plugin.py" +xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\" +del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py" -REM - Relaunch two IDA sessions +REM - Launch a new IDA session start "" "C:\tools\disassemblers\IDA 7.0\ida64.exe" "..\..\testcase\boombox7.i64" diff --git a/dev_scripts/reload_IDA_7_ida.bat b/dev_scripts/reload_IDA_7_ida.bat index 0904ead7..160b5dd3 100644 --- a/dev_scripts/reload_IDA_7_ida.bat +++ b/dev_scripts/reload_IDA_7_ida.bat @@ -5,13 +5,13 @@ REM - Purge old lighthouse log files del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*" REM - Delete the old plugin bits -del /F /Q "C:\tools\disassemblers\IDA 7.0\plugins\*lighthouse_plugin.py" -rmdir "C:\tools\disassemblers\IDA 7.0\plugins\lighthouse" /s /q +del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py" +rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q REM - Copy over the new plugin bits -xcopy /s/y "..\plugin\*" "C:\tools\disassemblers\IDA 7.0\plugins\" -del /F /Q "C:\tools\disassemblers\IDA 7.0\plugins\.#lighthouse_plugin.py" +xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\" +del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py" -REM - Relaunch two IDA sessions +REM - Launch a new IDA session start "" "C:\tools\disassemblers\IDA 7.0\ida.exe" "..\..\testcase\idaq7.idb" diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py index 343b6783..9af7e67d 100644 --- a/plugin/lighthouse/util/misc.py +++ b/plugin/lighthouse/util/misc.py @@ -8,13 +8,14 @@ # Plugin Util #------------------------------------------------------------------------------ +PLUGIN_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + def plugin_resource(resource_name): """ Return the full path for a given plugin resource file. """ return os.path.join( - idaapi.idadir(idaapi.PLG_SUBDIR), - "lighthouse", + PLUGIN_PATH, "ui", "resources", resource_name From 7038a74f74d76172126e093bc1d0b87a9670a10c Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 29 Nov 2017 16:05:22 -0500 Subject: [PATCH 08/39] disable metadata refresh on coverage load after initial cache --- plugin/lighthouse/metadata.py | 5 +++++ plugin/lighthouse_plugin.py | 14 ++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 34b1ef67..03674e86 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -71,6 +71,9 @@ def __init__(self): # database defined functions self.functions = {} + # database metadata cache status + self.cached = False + # lookup list members self._stale_lookup = False self._name2func = {} @@ -345,6 +348,7 @@ def abort_refresh(self, join=False): if not (worker and worker.is_alive()): self._stop_threads = False + self._refresh_worker = None return # signal the worker thread to stop @@ -367,6 +371,7 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback): # send the refresh result (good/bad) incase anyone is still listening if completed: + self.cached = True result_queue.put(self) else: result_queue.put(None) diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index a85f2346..3828ff4e 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -386,13 +386,15 @@ def interactive_load_batch(self): Interactive loading & aggregation of coverage files. """ self.palette.refresh_colors() + metadata_is_cached = self.director.metadata.cached # # kick off an asynchronous metadata refresh. this collects underlying # database metadata while the user will be busy selecting coverage files. # - future = self.director.metadata.refresh(progress_callback=metadata_progress) + if not metadata_is_cached: + future = self.director.metadata.refresh(progress_callback=metadata_progress) # # we will now prompt the user with an interactive file dialog so they @@ -426,7 +428,8 @@ def interactive_load_batch(self): # idaapi.show_wait_box("Building database metadata...") - await_future(future) + if not metadata_is_cached: + await_future(future) # aggregate all the selected files into one new coverage set new_coverage = self._aggregate_batch(loaded_files) @@ -490,6 +493,7 @@ def interactive_load_file(self): Interactive loading of individual coverage files. """ self.palette.refresh_colors() + metadata_is_cached = self.director.metadata.cached created_coverage = [] # @@ -497,7 +501,8 @@ def interactive_load_file(self): # database metadata while the user will be busy selecting coverage files. # - future = self.director.metadata.refresh(progress_callback=metadata_progress) + if not metadata_is_cached: + future = self.director.metadata.refresh(progress_callback=metadata_progress) # # we will now prompt the user with an interactive file dialog so they @@ -518,7 +523,8 @@ def interactive_load_file(self): # idaapi.show_wait_box("Building database metadata...") - await_future(future) + if not metadata_is_cached: + await_future(future) # # stop the director's aggregate from updating. this is in the interest From e071528cd98d8575a36f73e6828de0ac293633fe Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 29 Nov 2017 18:36:58 -0500 Subject: [PATCH 09/39] defer composer shell searches on large databases --- plugin/lighthouse/composer/shell.py | 47 +++++++++++++++++++++++++---- plugin/lighthouse/metadata.py | 6 ++++ plugin/lighthouse/util/misc.py | 10 ++++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index 020f9d5b..a054d87b 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -29,6 +29,10 @@ def __init__(self, director, model, table=None): self._model = model self._table = table + # command / input + self._search_text = "" + self._command_timer = QtCore.QTimer() + # the last known user AST self._last_ast = None @@ -299,9 +303,7 @@ def _execute_search(self, text): """ Execute the search semantics. """ - - # the given text is a real search query, apply it as a filter now - self._model.filter_string(text[1:]) + self._search_text = text[1:] # # if the user input is only "/" (starting to type something), hint @@ -312,15 +314,48 @@ def _execute_search(self, text): self._line_label.setText("Search") return + # + # stop an existing command timer if there is one running. we are about + # to schedule a new one or execute inline. so the old/deferred command + # is no longer needed. + # + + self._command_timer.stop() + + # + # if the functions list is HUGE, we want to defer the filtering until + # we think the user has stopped typing as each pass may take awhile + # to compute (while blocking the main thread...) + # + + if self._director.metadata.is_big(): + self._command_timer = singleshot(1000, self._execute_search_internal) + self._command_timer.start() + + # + # the database is not *massive*, let's execute the search immediately + # + + else: + self._execute_search_internal() + + # done + return + + def _execute_search_internal(self): + """ + Execute the actual search filtering & coverage metrics. + """ + + # the given text is a real search query, apply it as a filter now + self._model.filter_string(self._search_text) + # compute coverage % of the visible (filtered) results percent = self._model.get_modeled_coverage_percent() # show the coverage % of the search results in the shell label self._line_label.setText("%1.2f%%" % percent) - # done - return - def _highlight_search(self): """ Syntax highlight a search query. diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 03674e86..810d7866 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -262,6 +262,12 @@ def flatten_blocks(self, basic_blocks): # return the list of addresses return output + def is_big(self): + """ + Return an size classification of the database / metadata. + """ + return len(self.functions) > 100000 + #-------------------------------------------------------------------------- # Refresh #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py index 9af7e67d..1781133d 100644 --- a/plugin/lighthouse/util/misc.py +++ b/plugin/lighthouse/util/misc.py @@ -33,6 +33,16 @@ def MonospaceFont(): font.setStyleHint(QtGui.QFont.TypeWriter) return font +def singleshot(ms, function=None): + """ + A Qt Singleshot timer that can be stopped. + """ + timer = QtCore.QTimer() + timer.setInterval(ms) + timer.setSingleShot(True) + timer.timeout.connect(function) + return timer + #------------------------------------------------------------------------------ # Python Util #------------------------------------------------------------------------------ From d39f07c5a7367d302ca5da62b2f854c0bff08272 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Wed, 29 Nov 2017 18:37:36 -0500 Subject: [PATCH 10/39] disable a verbose/heavy hexrays log item --- plugin/lighthouse/util/ida.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index dc9a1a5c..4f9d49cb 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -46,7 +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)) + #logger.debug("Line Text: %s" % binascii.hexlify(line_text)) return line2citem From 58f1260f0c56878d504b0d6d06f2831c5312d62f Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 1 Dec 2017 11:11:51 -0500 Subject: [PATCH 11/39] bugfix: some IDA 7 users (AUTOIMPORT_COMPAT_IDA695=NO) could not save compositions --- plugin/lighthouse/composer/shell.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index a054d87b..d1e727ac 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -582,14 +582,14 @@ def _accept_composition(self): # composition name # - coverage_name = idaapi.askstr( - 0, - str("COMP_%s" % self.text), - "Save composition as..." + ok, coverage_name = prompt_string( + "Composition Name:", + "Please enter a name for this composition", + "COMP_%s" % self.text ) # the user did not enter a coverage name or hit cancel - abort the save - if not coverage_name: + if not (ok and coverage_name): return # From 112034c565505d18878d6e7896f7338668097261 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Fri, 1 Dec 2017 13:48:49 -0500 Subject: [PATCH 12/39] bugfix: fixed a race condition that would crash lighthtouse when saving compositions --- plugin/lighthouse/director.py | 41 +++++++++++++++---- plugin/lighthouse/util/ida.py | 77 +++++++++++++++++++++++++++-------- 2 files changed, 95 insertions(+), 23 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index ead5e3c4..88916c6c 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -154,6 +154,7 @@ def __init__(self, palette): #---------------------------------------------------------------------- self._ast_queue = Queue.Queue() + self._composition_lock = threading.Lock() self._composition_cache = CompositionCache() self._composition_worker = threading.Thread( @@ -693,8 +694,6 @@ def add_composition(self, composite_name, ast): # evaluate the last AST into a coverage set composite_coverage = self._evaluate_composition(ast) - composite_coverage.update_metadata(self.metadata) - composite_coverage.refresh() # TODO: hash refresh? # save the evaluated coverage under the given name self._update_coverage(composite_name, composite_coverage) @@ -741,15 +740,14 @@ def _async_evaluate_ast(self): # produce a single composite coverage object as described by the AST composite_coverage = self._evaluate_composition(ast) - # map the composited coverage data to the database metadata - composite_coverage.update_metadata(self.metadata) - composite_coverage.refresh() - # we always save the most recent composite to the hotshell entry self._special_coverage[HOT_SHELL] = composite_coverage + # # if the hotshell entry is the active coverage selection, notify # listeners of its update + # + if self.coverage_name == HOT_SHELL: self._notify_coverage_modified() @@ -767,8 +765,37 @@ def _evaluate_composition(self, ast): if isinstance(ast, TokenNull): return self._NULL_COVERAGE + # + # the director's composition evaluation code (this function) is most + # generally called via the background caching evaluation thread known + # as self._composition_worker. But this function can also be called + # inline via the 'add_composition' function from a different thread + # (namely, the main thread) + # + # because of this, we must control access to the resources the AST + # evaluation code operates by restricting the code to one thread + # at a time. + # + # should we call _evaluate_composition from the context of the main + # IDA thread, it is important that we do so in a pseudo non-blocking + # such that we don't hang IDA. await_lock(...) will allow the Qt/IDA + # main thread to yield to other threads while waiting for the lock + # + + await_lock(self._composition_lock) + # recursively evaluate the AST - return self._evaluate_composition_recursive(ast) + composite_coverage = self._evaluate_composition_recursive(ast) + + # map the composited coverage data to the database metadata + composite_coverage.update_metadata(self.metadata) + composite_coverage.refresh() # TODO: hash refresh? + + # done operating on shared data (coverage), release the lock + self._composition_lock.release() + + # return the evaluated composition + return composite_coverage def _evaluate_composition_recursive(self, node): """ diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index 4f9d49cb..f8396820 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -475,23 +475,19 @@ def thunk(): # IDA Async Magic #------------------------------------------------------------------------------ -@mainthread -def await_future(future, block=True, timeout=1.0): +def await_future(future): """ This is effectively a technique I use to get around completely blocking IDA's mainthread while waiting for a threaded result that may need to make - use of the sync operators. + use of the execute_sync operators. Waiting for a 'future' thread result to come through via this function lets other execute_sync actions to slip through (at least Read, Fast). """ - - elapsed = 0 # total time elapsed processing this future object interval = 0.02 # the interval which we wait for a response - end_time = time.time() + timeout - # run until the the future completes or the timeout elapses - while block or (time.time() < end_time): + # run until the the future arrives + while True: # block for a brief period to see if the future completes try: @@ -503,26 +499,75 @@ def await_future(future, block=True, timeout=1.0): # except Queue.Empty as e: - logger.debug("Flushing future...") + pass + + logger.debug("Awaiting future...") + + # + # if we are executing (well, blocking) as the main thread, we need + # to flush the event loop so IDA does not hang + # + + if idaapi.is_main_thread(): + flush_ida_sync_requests() + +def await_lock(lock): + """ + Attempt to acquire a lock without blocking the IDA mainthread. + + See await_future() for more details. + """ + + elapsed = 0 # total time elapsed waiting for the lock + interval = 0.02 # the interval (in seconds) between acquire attempts + timeout = 60.0 # the total time allotted to acquiring the lock + end_time = time.time() + timeout + + # wait until the the lock is available + while time.time() < end_time: + + # + # attempt to acquire the given lock without blocking (via 'False'). + # if we succesfully aquire the lock, then we can return (success) + # + + if lock.acquire(False): + logger.debug("Acquired lock!") + return + + # + # the lock is not available yet. we need to sleep so we don't choke + # the cpu, and try to acquire the lock again next time through... + # + + logger.debug("Awaiting lock...") + time.sleep(interval) + + # + # if we are executing (well, blocking) as the main thread, we need + # to flush the event loop so IDA does not hang + # + + if idaapi.is_main_thread(): flush_ida_sync_requests() + # + # we spent 60 seconds trying to acquire the lock, but never got it... + # to avoid hanging IDA indefinitely (or worse), we abort via signal + # + + raise RuntimeError("Failed to acquire lock after %f seconds!" % timeout) + @mainthread def flush_ida_sync_requests(): """ Flush all execute_sync requests. - - NOTE: This MUST be called from the IDA Mainthread to be effective. """ - if not idaapi.is_main_thread(): - return False # this will trigger/flush the IDA UI loop qta = QtCore.QCoreApplication.instance() qta.processEvents() - # done - return True - @mainthread def prompt_string(label, title, default=""): """ From c070308f4918e970000eaf6133514fcc60cd778d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 2 Dec 2017 21:46:06 -0500 Subject: [PATCH 13/39] abstract away metadata caching in the director --- plugin/lighthouse/director.py | 26 ++++++++++++++++++++++++++ plugin/lighthouse/metadata.py | 4 ++-- plugin/lighthouse_plugin.py | 14 ++++---------- 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 88916c6c..f65ab3c4 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -976,9 +976,35 @@ def refresh(self): # (re)map each set of loaded coverage data to the database self._refresh_database_coverage(delta) + def refresh_metadata(self, progress_callback=None, force=False): + """ + Refresh the database metadata cache utilized by the director. + + Returns a future (Queue) that will carry the completion message. + """ + + # + # if the metadata has already been collected once by lighthouse + # during this coverage session (eg, it is cached), ignore a request + # to refresh it unless explicitly told to refresh via force=True + # + + if self.metadata.cached and not force: + fake_queue = Queue.Queue() + fake_queue.put(False) + return fake_queue + + # start the asynchronous metadata refresh + result_queue = self.metadata.refresh(progress_callback=progress_callback) + + # return the channel that will carry asynchronous result + return result_queue + def _refresh_database_metadata(self): """ Refresh the database metadata cache utilized by the director. + + NOTE: this is currently unused. """ logger.debug("Refreshing database metadata") diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 810d7866..fcf32f71 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -378,9 +378,9 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback): # send the refresh result (good/bad) incase anyone is still listening if completed: self.cached = True - result_queue.put(self) + result_queue.put(True) else: - result_queue.put(None) + result_queue.put(False) # clean up our thread's reference as it is basically done/dead self._refresh_worker = None diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 3828ff4e..ee08c431 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -386,15 +386,13 @@ def interactive_load_batch(self): Interactive loading & aggregation of coverage files. """ self.palette.refresh_colors() - metadata_is_cached = self.director.metadata.cached # # kick off an asynchronous metadata refresh. this collects underlying # database metadata while the user will be busy selecting coverage files. # - if not metadata_is_cached: - future = self.director.metadata.refresh(progress_callback=metadata_progress) + future = self.director.refresh_metadata(progress_callback=metadata_progress) # # we will now prompt the user with an interactive file dialog so they @@ -428,8 +426,7 @@ def interactive_load_batch(self): # idaapi.show_wait_box("Building database metadata...") - if not metadata_is_cached: - await_future(future) + await_future(future) # aggregate all the selected files into one new coverage set new_coverage = self._aggregate_batch(loaded_files) @@ -493,7 +490,6 @@ def interactive_load_file(self): Interactive loading of individual coverage files. """ self.palette.refresh_colors() - metadata_is_cached = self.director.metadata.cached created_coverage = [] # @@ -501,8 +497,7 @@ def interactive_load_file(self): # database metadata while the user will be busy selecting coverage files. # - if not metadata_is_cached: - future = self.director.metadata.refresh(progress_callback=metadata_progress) + future = self.director.refresh_metadata(progress_callback=metadata_progress) # # we will now prompt the user with an interactive file dialog so they @@ -523,8 +518,7 @@ def interactive_load_file(self): # idaapi.show_wait_box("Building database metadata...") - if not metadata_is_cached: - await_future(future) + await_future(future) # # stop the director's aggregate from updating. this is in the interest From 0bc487668db244930ecea4e5f50ad3fc7ad38bbb Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 2 Dec 2017 22:25:50 -0500 Subject: [PATCH 14/39] move callback plumbing out of the director --- plugin/lighthouse/director.py | 99 ++++++---------------------------- plugin/lighthouse/util/misc.py | 78 +++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 82 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index f65ab3c4..56e880b9 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -1,7 +1,6 @@ import time import string import logging -import weakref import threading import collections @@ -172,10 +171,11 @@ def __init__(self, palette): # events or changes to the underlying data they consume. # # Callbacks provide a way for us to notify any interested parties - # of these key events. + # of these key events. Below are lists of registered notification + # callbacks. see 'Callbacks' section below for more info. # - # lists of registered notification callbacks, see 'Callbacks' below + # coverage callbacks self._coverage_switched_callbacks = [] self._coverage_modified_callbacks = [] self._coverage_created_callbacks = [] @@ -247,126 +247,61 @@ def coverage_switched(self, callback): """ Subscribe a callback for coverage switch events. """ - self._register_callback(self._coverage_switched_callbacks, callback) + register_callback(self._coverage_switched_callbacks, callback) def _notify_coverage_switched(self): """ Notify listeners of a coverage switch event. """ - self._notify_callback(self._coverage_switched_callbacks) + notify_callback(self._coverage_switched_callbacks) def coverage_modified(self, callback): """ Subscribe a callback for coverage modification events. """ - self._register_callback(self._coverage_modified_callbacks, callback) + register_callback(self._coverage_modified_callbacks, callback) def _notify_coverage_modified(self): """ Notify listeners of a coverage modification event. """ - self._notify_callback(self._coverage_modified_callbacks) + notify_callback(self._coverage_modified_callbacks) def coverage_created(self, callback): """ Subscribe a callback for coverage creation events. """ - self._register_callback(self._coverage_created_callbacks, callback) + register_callback(self._coverage_created_callbacks, callback) def _notify_coverage_created(self): """ Notify listeners of a coverage creation event. """ - self._notify_callback(self._coverage_created_callbacks) # TODO: send list of names created? + notify_callback(self._coverage_created_callbacks) # TODO: send list of names created? def coverage_deleted(self, callback): """ Subscribe a callback for coverage deletion events. """ - self._register_callback(self._coverage_deleted_callbacks, callback) + register_callback(self._coverage_deleted_callbacks, callback) def _notify_coverage_deleted(self): """ Notify listeners of a coverage deletion event. """ - self._notify_callback(self._coverage_deleted_callbacks) # TODO: send list of names deleted? + notify_callback(self._coverage_deleted_callbacks) # TODO: send list of names deleted? - def _register_callback(self, callback_list, callback): + def metadata_modified(self, callback): """ - Register a given callable (callback) to the given callback_list. - - Adapted from http://stackoverflow.com/a/21941670 + Subscribe a callback for metadata modification events. """ + register_callback(self._metadata_modified_callbacks, callback) - # create a weakref callback to an object method - try: - callback_ref = weakref.ref(callback.__func__), weakref.ref(callback.__self__) - - # create a wweakref callback to a stand alone function - except AttributeError: - callback_ref = weakref.ref(callback), None - - # 'register' the callback - callback_list.append(callback_ref) - - def _notify_callback(self, callback_list): + def _notify_metadata_modified(self): """ - Notify the given list of registered callbacks. - - The given list (callback_list) is a list of weakref'd callables - registered through the _register_callback function. To notify the - callbacks we simply loop through the list and call them. - - This routine self-heals by removing dead callbacks for deleted objects. - - Adapted from http://stackoverflow.com/a/21941670 + Notify listeners of a metadata modification event. """ - cleanup = [] - - # - # loop through all the registered callbacks in the given callback_list, - # notifying active callbacks, and removing dead ones. - # - - for callback_ref in callback_list: - callback, obj_ref = callback_ref[0](), callback_ref[1] - - # - # if the callback is an instance method, deference the instance - # (an object) first to check that it is still alive - # - - if obj_ref: - obj = obj_ref() - - # if the object instance is gone, mark this callback for cleanup - if obj is None: - cleanup.append(callback_ref) - continue - - # call the object instance callback - 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: - - # if the static method is deleted, mark this callback for cleanup - if callback is None: - cleanup.append(callback_ref) - continue - - # call the static callback - callback(self) - - # remove the deleted callbacks - for callback_ref in cleanup: - callback_list.remove(callback_ref) + notify_callback(self._metadata_modified_callbacks) #---------------------------------------------------------------------- # Batch Loading diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py index 1781133d..50ab3364 100644 --- a/plugin/lighthouse/util/misc.py +++ b/plugin/lighthouse/util/misc.py @@ -1,4 +1,5 @@ import os +import weakref import collections import idaapi @@ -64,6 +65,83 @@ def hex_list(items): """ return '[{}]'.format(', '.join('0x%X' % x for x in items)) +def register_callback(callback_list, callback): + """ + Register a given callable (callback) to the given callback_list. + + Adapted from http://stackoverflow.com/a/21941670 + """ + + # create a weakref callback to an object method + try: + callback_ref = weakref.ref(callback.__func__), weakref.ref(callback.__self__) + + # create a wweakref callback to a stand alone function + except AttributeError: + callback_ref = weakref.ref(callback), None + + # 'register' the callback + callback_list.append(callback_ref) + +def notify_callback(callback_list): + """ + Notify the given list of registered callbacks. + + The given list (callback_list) is a list of weakref'd callables + registered through the _register_callback function. To notify the + callbacks we simply loop through the list and call them. + + This routine self-heals by removing dead callbacks for deleted objects. + + Adapted from http://stackoverflow.com/a/21941670 + """ + cleanup = [] + + # + # loop through all the registered callbacks in the given callback_list, + # notifying active callbacks, and removing dead ones. + # + + for callback_ref in callback_list: + callback, obj_ref = callback_ref[0](), callback_ref[1] + + # + # if the callback is an instance method, deference the instance + # (an object) first to check that it is still alive + # + + if obj_ref: + obj = obj_ref() + + # if the object instance is gone, mark this callback for cleanup + if obj is None: + cleanup.append(callback_ref) + continue + + # call the object instance callback + 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: + + # if the static method is deleted, mark this callback for cleanup + if callback is None: + cleanup.append(callback_ref) + continue + + # call the static callback + callback() + + # remove the deleted callbacks + for callback_ref in cleanup: + callback_list.remove(callback_ref) + #------------------------------------------------------------------------------ # Coverage Util #------------------------------------------------------------------------------ From 5769533f94c9498c0a8baf9954f8aa0cbec1a2d4 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 2 Dec 2017 22:28:35 -0500 Subject: [PATCH 15/39] renamed _database_metadata, unnecessary and confusing --- plugin/lighthouse/director.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 56e880b9..f485a639 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -45,7 +45,7 @@ def __init__(self, palette): self._palette = palette # database metadata cache - self._database_metadata = DatabaseMetadata() + self.metadata = DatabaseMetadata() # flag to suspend/resume the automatic coverage aggregation self._aggregation_suspended = False @@ -197,13 +197,6 @@ def terminate(self): # Properties #-------------------------------------------------------------------------- - @property - def metadata(self): - """ - The active database metadata cache. - """ - return self._database_metadata - @property def coverage(self): """ @@ -951,7 +944,7 @@ def _refresh_database_metadata(self): delta = MetadataDelta(new_metadata, self.metadata) # save the new metadata in place of the old metadata - self._database_metadata = new_metadata + self.metadata = new_metadata # finally, return the list of nodes that have changed (the delta) return delta From 61804887ee2b8b9792ad50db256caa0c456f3d80 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 2 Dec 2017 22:50:15 -0500 Subject: [PATCH 16/39] update metadata cache on function rename, fixes #23 --- plugin/lighthouse/director.py | 20 ++++- plugin/lighthouse/metadata.py | 98 ++++++++++++++++++++--- plugin/lighthouse/ui/coverage_overview.py | 8 ++ 3 files changed, 110 insertions(+), 16 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index f485a639..26854f22 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -181,6 +181,9 @@ def __init__(self, palette): self._coverage_created_callbacks = [] self._coverage_deleted_callbacks = [] + # metadata callbacks + self._metadata_modified_callbacks = [] + def terminate(self): """ Cleanup & terminate the director. @@ -912,9 +915,20 @@ def refresh_metadata(self, progress_callback=None, force=False): """ # - # if the metadata has already been collected once by lighthouse - # during this coverage session (eg, it is cached), ignore a request - # to refresh it unless explicitly told to refresh via force=True + # if this is the first time the director is going to use / populate + # the database metadata, register the director for notifications of + # metadata modification (this should only happen once) + # + # TODO: this is a little dirty, but it will suffice. + # + + if not self.metadata.cached: + self.metadata.function_renamed(self._notify_metadata_modified) + + # + # if the lighthouse has collected metadata previously for this IDB + # session (eg, it is cached), ignore a request to refresh it unless + # explicitly told to refresh via force=True # if self.metadata.cached and not force: diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index fcf32f71..57a6c500 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -81,6 +81,13 @@ def __init__(self): self._node_addresses = [] self._function_addresses = [] + # hook to listen for rename events from IDA + self._rename_hooks = RenameHooks() + self._rename_hooks.renamed = self._name_changed + + # metadata callbacks (see director for more info) + self._function_renamed_callbacks = [] + # asynchrnous metadata collection thread self._refresh_worker = None self._stop_threads = False @@ -369,12 +376,21 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback): Internal asynchronous metadata collection worker. """ + # pause our rename listening hooks, for speed + self._rename_hooks.unhook() + # collect metadata - completed = self._async_collect_metadata(function_addresses, progress_callback) + completed = self._async_collect_metadata( + function_addresses, + progress_callback + ) # refresh the lookup lists self._refresh_lookup() + # resume our rename listening hooks + self._rename_hooks.hook() + # send the refresh result (good/bad) incase anyone is still listening if completed: self.cached = True @@ -535,6 +551,63 @@ def _update_functions(self, fresh_metadata): # return the delta for other interested consumers to use return delta + #-------------------------------------------------------------------------- + # Signal Handlers + #-------------------------------------------------------------------------- + + @mainthread + def _name_changed(self, address, new_name, local_name): + """ + Handler for rename event in IDA. + """ + + # we should never care about local renames (eg, loc_40804b), ignore + if local_name: + return + + # get the function that this address falls within + function = self.get_function(address) + + # if the address does not fall within a function (might happen?), ignore + if not function: + return + + # + # ensure the renamed address matches the function start before + # renaming the function in our metadata cache. + # + # I am not sure when this would not be the case (globals? maybe) + # but I'd rather not find out. + # + + if address == function.address: + logger.debug("Name changing @ 0x%X" % address) + logger.debug(" Old name: %s" % function.name) + function.name = idaapi.get_short_name(address) + logger.debug(" New name: %s" % function.name) + + # notify any listeners that a function rename occurred + self._notify_function_renamed() + + # necessary for IDP/IDB_Hooks + return 0 + + #-------------------------------------------------------------------------- + # Callbacks + #-------------------------------------------------------------------------- + + def function_renamed(self, callback): + """ + Subscribe a callback for function rename events. + """ + register_callback(self._function_renamed_callbacks, callback) + + def _notify_function_renamed(self): + """ + Notify listeners of a function rename event. + """ + notify_callback(self._function_renamed_callbacks) + #------------------------------------------------------------------------------ # Function Level Metadata #------------------------------------------------------------------------------ @@ -672,18 +745,6 @@ def _finalize(self): 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 - #-------------------------------------------------------------------------- - - def name_changed(self, new_name): - """ - Handler for rename event in IDA. - - TODO: hook this up - """ - self.name = new_name - #-------------------------------------------------------------------------- # Operator Overloads #-------------------------------------------------------------------------- @@ -994,3 +1055,14 @@ def metadata_progress(completed, total): Handler for metadata collection callback, updates progress dialog. """ idaapi.replace_wait_box("Collected metadata for %u/%u Functions" % (completed, total)) + +#-------------------------------------------------------------------------- +# Event Hooks +#-------------------------------------------------------------------------- + +if using_ida7api: + class RenameHooks(idaapi.IDB_Hooks): + pass +else: + class RenameHooks(idaapi.IDP_Hooks): + pass diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 2935432b..6124cdcf 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -441,6 +441,7 @@ def __init__(self, director, parent=None): # register for cues from the director self._director.coverage_switched(self._internal_refresh) self._director.coverage_modified(self._internal_refresh) + self._director.metadata_modified(self._data_changed) #-------------------------------------------------------------------------- # AbstractItemModel Overloads @@ -707,6 +708,13 @@ def _internal_refresh(self): # sort the data set according to the last selected sorted column self.sort(self._last_sort, self._last_sort_order) + @idafast + def _data_changed(self): + """ + Notify attached views that simple model data has been updated/modified. + """ + self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex()) + def _refresh_data(self): """ Initialize the mapping to go from displayed row to function. From 99e758ae2126bd1573213e12a45805855d085daa Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sat, 2 Dec 2017 23:18:19 -0500 Subject: [PATCH 17/39] only log with LIGHTHOUSE_LOGGING env variable set, fixes #12 --- plugin/lighthouse/util/log.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py index ec70b442..f83eb38a 100644 --- a/plugin/lighthouse/util/log.py +++ b/plugin/lighthouse/util/log.py @@ -101,6 +101,18 @@ def cleanup_log_directory(log_directory): def start_logging(): global logger + # create the Lighthouse logger + logger = logging.getLogger("Lighthouse") + + # + # only enable logging if the LIGHTHOUSE_LOGGING environment variable is + # present. we simply return a stub logger to sinkhole messages. + # + + if os.getenv("LIGHTHOUSE_LOGGING") == None: + logger.disabled = True + return logger + # create a directory for lighthouse logs if it does not exist log_dir = get_log_dir() if not os.path.exists(log_dir): @@ -117,9 +129,6 @@ def start_logging(): level=logging.DEBUG ) - # create the Lighthouse logger - logger = logging.getLogger("Lighthouse") - # proxy STDOUT/STDERR to the log files too stdout_logger = logging.getLogger('Lighthouse.STDOUT') stderr_logger = logging.getLogger('Lighthouse.STDERR') From 54e5e1362842d6fa80ffbe4856b652152ddb4dbc Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 00:04:26 -0500 Subject: [PATCH 18/39] bugfix: coverage % of shell search are now size relative --- plugin/lighthouse/ui/coverage_overview.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 6124cdcf..7d466467 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -655,8 +655,19 @@ def get_modeled_coverage_percent(self): """ Get the coverage % represented by the current (visible) model. """ - sum_coverage = sum(cov.instruction_percent for cov in self._visible_coverage.itervalues()) - return (sum_coverage / (self._row_count or 1))*100 + + # sum the # of instructions in all the visible functions + instruction_count = sum( + meta.instruction_count for meta in self._visible_metadata.itervalues() + ) + + # sum the # of instructions executed in all the visible functions + instructions_executed = sum( + cov.instructions_executed for cov in self._visible_coverage.itervalues() + ) + + # compute coverage percentage of the visible functions + return (float(instructions_executed) / (instruction_count or 1))*100 #-------------------------------------------------------------------------- # Filters From fa43bb8aabb89db32f499f46745c5a55f223d246 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 00:05:31 -0500 Subject: [PATCH 19/39] set default table color background for consistency --- plugin/lighthouse/composer/shell.py | 2 +- plugin/lighthouse/palette.py | 6 +++--- plugin/lighthouse/ui/coverage_overview.py | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index d1e727ac..49c46b35 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -92,7 +92,7 @@ def _ui_init_shell(self): # configure the shell background & default text color palette = self._line.palette() - palette.setColor(QtGui.QPalette.Base, self._palette.composer_bg) + palette.setColor(QtGui.QPalette.Base, self._palette.overview_bg) palette.setColor(QtGui.QPalette.Text, self._palette.composer_fg) palette.setColor(QtGui.QPalette.WindowText, self._palette.composer_fg) self._line.setPalette(palette) diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/palette.py index 1af72da9..eeff1358 100644 --- a/plugin/lighthouse/palette.py +++ b/plugin/lighthouse/palette.py @@ -48,7 +48,7 @@ def __init__(self): # Composing Shell # - self._composer_bg = [QtGui.QColor(30, 30, 30), QtGui.QColor(30, 30, 30)] + self._overview_bg = [QtGui.QColor(30, 30, 30), QtGui.QColor(30, 30, 30)] self._composer_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)] self._valid_text = [0x80F0FF, 0x0000FF] self._invalid_text = [0xF02070, 0xFF0000] @@ -207,8 +207,8 @@ def ida_coverage(self): #-------------------------------------------------------------------------- @property - def composer_bg(self): - return self._composer_bg[self.qt_theme] + def overview_bg(self): + return self._overview_bg[self.qt_theme] @property def composer_fg(self): diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 7d466467..7053db5c 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -183,11 +183,12 @@ def _ui_init_table(self, director): """ Initialize the coverage table. """ + palette = director._palette self._table = QtWidgets.QTableView() self._table.setFocusPolicy(QtCore.Qt.NoFocus) self._table.setStyleSheet( - "QTableView { gridline-color: black; } " + - "QTableView::item:selected { color: white; background-color: %s; } " % director._palette.selection.name() + "QTableView { gridline-color: black; background-color: %s } " % palette.overview_bg.name() + + "QTableView::item:selected { color: white; background-color: %s; } " % palette.selection.name() ) # set these properties so the user can arbitrarily shrink the table From 74285024717fccf1529a401a2ca13e83bf1fa247 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 01:44:31 -0500 Subject: [PATCH 20/39] symbolic deletion of the aggregate set (clear all) --- plugin/lighthouse/director.py | 49 ++++++++++++++++++++--- plugin/lighthouse/ui/coverage_combobox.py | 25 ++++++++++-- 2 files changed, 65 insertions(+), 9 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 26854f22..d4b3a59c 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -440,16 +440,42 @@ def delete_coverage(self, coverage_name): """ Delete a database coverage object by name. """ - assert coverage_name in self.coverage_names # # if the delete request targets the currently active coverage, we want - # to switch into a safer coverage to try and avoid any ill effects. + # to switch into a safer coverage set to try and avoid any ill effects. # - if self.coverage_name == coverage_name: + if coverage_name in [self.coverage_name, AGGREGATE]: self.select_coverage(NEW_COMPOSITION) + # + # the user is trying to delete one of their own loaded/created coverages + # + + if coverage_name in self.coverage_names: + self._delete_user_coverage(coverage_name) + + # + # the user is trying to delete the aggregate coverage set, which simply + # means clears *all* loaded coverages + # + + elif coverage_name == AGGREGATE: + self._delete_aggregate_coverage(coverage_name) + + # unsupported / unknown coverage + else: + raise ValueError("Cannot delete %s, does not exist" % coverage_name) + + # notify any listeners that we have deleted coverage + self._notify_coverage_deleted() + + def _delete_user_coverage(self, coverage_name): + """ + Delete a user created database coverage object by name. + """ + # release the shorthand alias held by this coverage self._release_shorthand_alias(coverage_name) @@ -461,8 +487,21 @@ def delete_coverage(self, coverage_name): if not self._aggregation_suspended: self._refresh_aggregate() - # notify any listeners that we have deleted coverage - self._notify_coverage_deleted() + def _delete_aggregate_coverage(self, coverage_name): + """ + Delete the aggregate set, effectiveely clearing all loaded covearge. + """ + + # loop through all the loaded coverage sets and release them + for coverage_name in self.coverage_names: + self._release_shorthand_alias(coverage_name) + self._database_coverage.pop(coverage_name) + + # TODO: check if there's any references to the coverage aggregate... + + # assign a new, blank aggregate set + self._special_coverage[AGGREGATE] = DatabaseCoverage(None, self._palette) + self._refresh_aggregate() # probably not needed def get_coverage(self, name): """ diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py index 37a42ba5..e8d560c3 100644 --- a/plugin/lighthouse/ui/coverage_combobox.py +++ b/plugin/lighthouse/ui/coverage_combobox.py @@ -317,7 +317,7 @@ def refresh(self): # 'Aggregate', or the 'seperator' indexes # - if model.data(model.index(row, 0), QtCore.Qt.AccessibleDescriptionRole) != ENTRY_USER: + if not model.data(model.index(row, 1), QtCore.Qt.DecorationRole): self.setSpan(row, 0, 1, model.columnCount()) # this is a user entry, ensure there is no span present (clear it) @@ -475,10 +475,27 @@ def data(self, index, role=QtCore.Qt.DisplayRole): if index.column() == COLUMN_COVERAGE_STRING and index.row() != self._seperator_index: return self._director.get_coverage_string(self._entries[index.row()]) - # 'X' icon data request + # icon display request elif role == QtCore.Qt.DecorationRole: - if index.column() == COLUMN_DELETE and index.row() > self._seperator_index: - return self._delete_icon + + # the icon request is for the 'X' column + if index.column() == COLUMN_DELETE: + + # + # if the coverage entry is below the seperator, it is a user + # loaded coverage and should always be deletable + # + + if index.row() > self._seperator_index: + return self._delete_icon + + # + # as a special case, we allow the aggregate to have a clear + # icon, which will clear all user loaded coverages + # + + elif self._entries[index.row()] == "Aggregate": + return self._delete_icon # entry type request elif role == QtCore.Qt.AccessibleDescriptionRole: From 46705fbb860848cffb6f8dfec55bfddebf8a8e9e Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 16:06:25 -0500 Subject: [PATCH 21/39] started right click contetxt menu for the coverage overview --- plugin/lighthouse/ui/coverage_overview.py | 133 +++++++++++++++++++++- plugin/lighthouse/util/ida.py | 39 ++++++- plugin/lighthouse/util/misc.py | 8 ++ 3 files changed, 176 insertions(+), 4 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 7053db5c..aaa1875c 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -174,6 +174,7 @@ def _ui_init(self, director): # initialize our ui elements self._ui_init_table(director) self._ui_init_toolbar(director) + self._ui_init_ctx_menu_actions() self._ui_init_signals() # layout the populated ui just before showing it @@ -315,6 +316,24 @@ def _ui_init_toolbar_elements(self, director): # give the shell expansion preference over the combobox self._splitter.setStretchFactor(0, 1) + def _ui_init_ctx_menu_actions(self): + """ + Initialize the right click context menu actions. + """ + + # function actions + self._action_rename = QtWidgets.QAction("Rename", None) + self._action_copy_name = QtWidgets.QAction("Copy name", None) + self._action_copy_address = QtWidgets.QAction("Copy address", None) + + # function prefixing actions + self._action_prefix = QtWidgets.QAction("Prefix selected functions", None) + self._action_clear_prefix = QtWidgets.QAction("Clear prefixes", None) + + # misc actions + self._action_refresh_metadata = QtWidgets.QAction( + "Refresh metadata (slow)", None) + def _ui_init_signals(self): """ Connect UI signals. @@ -324,8 +343,8 @@ def _ui_init_signals(self): self._table.doubleClicked.connect(self._ui_entry_double_click) # right click popup menu - #self._table.setContextMenuPolicy(Qt.CustomContextMenu) - #self._table.customContextMenuRequested.connect(...) + self._table.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self._table.customContextMenuRequested.connect(self._ui_ctx_menu_handler) # toggle 0% coverage checkbox self._hide_zero_checkbox.stateChanged.connect(self._ui_hide_zero_toggle) @@ -349,19 +368,127 @@ def _ui_layout(self): def _ui_entry_double_click(self, index): """ - Handle double click event on the coverage table view. + Handle double click event on the coverage table. A double click on the coverage table view will jump the user to the corresponding function in the IDA disassembly view. """ idaapi.jumpto(self._model.row2func[index.row()]) + def _ui_ctx_menu_handler(self, position): + """ + Handle right click context menu event on the coverage table. + """ + + # create a right click menu based on the state and context + ctx_menu = self._populate_ctx_menu() + if not ctx_menu: + return + + # show the popup menu to the user, and wait for their selection + action = ctx_menu.exec_(self._table.viewport().mapToGlobal(position)) + + # process the user action + self._process_ctx_menu_action(action) + def _ui_hide_zero_toggle(self, checked): """ Handle state change of 'Hide 0% Coverage' checkbox. """ self._model.filter_zero_coverage(checked) + #-------------------------------------------------------------------------- + # Context Menu + #-------------------------------------------------------------------------- + + def _populate_ctx_menu(self): + """ + Populate a context menu for the table view based on selection. + + Returns a populated QMenu, or None. + """ + + # get the list rows currently selected in the coverage table + selected_rows = self._table.selectionModel().selectedRows() + if len(selected_rows) == 0: + return None + + # the context menu we will dynamically populate + ctx_menu = QtWidgets.QMenu() + + # + # if there is only one table entry (a function) selected, then + # show the menu actions available for a single function such as + # copy function name, address, or renaming the function. + # + + if len(selected_rows) == 1: + ctx_menu.addAction(self._action_rename) + ctx_menu.addAction(self._action_copy_name) + ctx_menu.addAction(self._action_copy_address) + ctx_menu.addSeparator() + + # function prefixing actions + ctx_menu.addAction(self._action_prefix) + ctx_menu.addAction(self._action_clear_prefix) + ctx_menu.addSeparator() + + # misc actions + ctx_menu.addAction(self._action_refresh_metadata) + + # return the completed context menu + return ctx_menu + + def _process_ctx_menu_action(self, action): + """ + Process the given (user selected) context menu action. + """ + + # a right click menu action was not clicked. nothing else to do + if not action: + return + + # get the list rows currently selected in the coverage table + selected_rows = self._table.selectionModel().selectedRows() + if len(selected_rows) == 0: + return + + # + # check the universal actions first + # + + # TODO: ... + + # + # the following actions are only applicable if there is only one + # row/function selected in the coverage overview table. don't + # bother to check multi-function selections against these + # + + if len(selected_rows) != 1: + return + + # unpack the single QModelIndex + index = selected_rows[0] + + # get the function address from the table row + address_index = self._model.index(index.row(), FUNC_ADDR) + function_address = self._model.data(address_index, QtCore.Qt.DisplayRole) + + # handle the 'Rename' action + if action == self._action_rename: + rename_function(int(function_address, 16)) + + # handle the 'Copy name' action + elif action == self._action_copy_name: + name_index = self._model.index(index.row(), FUNC_NAME) + function_name = self._model.data(name_index, QtCore.Qt.DisplayRole) + copy_to_clipboard(function_name) + + # handle the 'Copy address' action + elif action == self._action_copy_address: + copy_to_clipboard(function_address) + #-------------------------------------------------------------------------- # Refresh #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index f8396820..58b3ec0a 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -568,6 +568,10 @@ def flush_ida_sync_requests(): qta = QtCore.QCoreApplication.instance() qta.processEvents() +#------------------------------------------------------------------------------ +# IDA Util +#------------------------------------------------------------------------------ + @mainthread def prompt_string(label, title, default=""): """ @@ -586,5 +590,38 @@ def prompt_string(label, title, default=""): dlg.fontMetrics().averageCharWidth()*10 ) ok = dlg.exec_() - text = dlg.textValue() + text = str(dlg.textValue()) return (ok, text) + +def rename_function(function_address): + """ + Rename a function in the IDB. + """ + + # get the original function name from the database + if using_ida7api: + original_name = idaapi.get_name(function_address) + else: + original_name = idaapi.get_true_name(idaapi.BADADDR, function_address) + + # sanity check + if original_name == None: + raise ValueError("Invalid function address") + + # prompt the user for a new function name + ok, new_name = prompt_string( + "Please enter function name", + "Rename Function", + original_name + ) + + # + # if the user clicked cancel, or the name they entered + # is identical to the original, there's nothing to do + # + + if not (ok or new_name != original_name): + return + + # rename the function + idaapi.set_name(function_address, new_name, idaapi.SN_NOCHECK) diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py index 50ab3364..c38ad13d 100644 --- a/plugin/lighthouse/util/misc.py +++ b/plugin/lighthouse/util/misc.py @@ -44,6 +44,14 @@ def singleshot(ms, function=None): timer.timeout.connect(function) return timer +def copy_to_clipboard(data): + """ + Copy the given data (a string) to the user clipboard. + """ + cb = QtWidgets.QApplication.clipboard() + cb.clear(mode=cb.Clipboard) + cb.setText(data, mode=cb.Clipboard) + #------------------------------------------------------------------------------ # Python Util #------------------------------------------------------------------------------ From 8e626a66a59ae53f5999f905efee50b59ed7e254 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 16:59:29 -0500 Subject: [PATCH 22/39] right click function prefixing in coverage overview --- plugin/lighthouse/ui/coverage_overview.py | 32 +++++-- plugin/lighthouse/util/ida.py | 109 +++++++++++++++++++--- 2 files changed, 122 insertions(+), 19 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index aaa1875c..7e74fdca 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -453,11 +453,31 @@ def _process_ctx_menu_action(self, action): if len(selected_rows) == 0: return + # + # extract the function addresses for the list of selected rows + # as they will probably come in handy later. + # + + function_addresses = [] + for index in selected_rows: + address = self._model.row2func[index.row()] + function_addresses.append(address) + # # check the universal actions first # - # TODO: ... + # handle the 'Prefix functions' action + if action == self._action_prefix: + gui_prefix_functions(function_addresses) + + # handle the 'Clear prefix' action + elif action == self._action_clear_prefix: + clear_prefixes(function_addresses) + + # handle the 'Refresh metadata' action + elif action == self._action_refresh_metadata: + print "TODO: refresh metadata" # # the following actions are only applicable if there is only one @@ -470,14 +490,11 @@ def _process_ctx_menu_action(self, action): # unpack the single QModelIndex index = selected_rows[0] - - # get the function address from the table row - address_index = self._model.index(index.row(), FUNC_ADDR) - function_address = self._model.data(address_index, QtCore.Qt.DisplayRole) + function_address = function_addresses[0] # handle the 'Rename' action if action == self._action_rename: - rename_function(int(function_address, 16)) + gui_rename_function(function_address) # handle the 'Copy name' action elif action == self._action_copy_name: @@ -487,7 +504,8 @@ def _process_ctx_menu_action(self, action): # handle the 'Copy address' action elif action == self._action_copy_address: - copy_to_clipboard(function_address) + address_string = "0x%X" % function_address + copy_to_clipboard(address_string) #-------------------------------------------------------------------------- # Refresh diff --git a/plugin/lighthouse/util/ida.py b/plugin/lighthouse/util/ida.py index 58b3ec0a..55d9686d 100644 --- a/plugin/lighthouse/util/ida.py +++ b/plugin/lighthouse/util/ida.py @@ -572,6 +572,79 @@ def flush_ida_sync_requests(): # IDA Util #------------------------------------------------------------------------------ +# taken from https://github.com/gaasedelen/prefix +PREFIX_DEFAULT = "MyPrefix" +PREFIX_SEPARATOR = '%' + +def prefix_function(function_address, prefix): + """ + Prefix a function name with the given string. + """ + original_name = get_function_name(function_address) + new_name = str(prefix) + PREFIX_SEPARATOR + str(original_name) + + # rename the function with the newly prefixed name + idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN) + +def prefix_functions(function_addresses, prefix): + """ + Prefix a list of functions with the given string. + """ + for function_address in function_addresses: + prefix_function(function_address, prefix) + +def clear_prefix(function_address): + """ + Clear the prefix from a given function. + """ + original_name = get_function_name(function_address) + + # + # locate the last (rfind) prefix separator in the function name as + # we will want to keep everything that comes after it + # + + i = original_name.rfind(PREFIX_SEPARATOR) + + # if there is no prefix (separator), there is nothing to trim + if i == -1: + return + + # trim the prefix off the original function name and discard it + new_name = original_name[i+1:] + + # rename the function with the prefix stripped + idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN) + +def clear_prefixes(function_addresses): + """ + Clear the prefix from a list of given functions. + """ + for function_address in function_addresses: + clear_prefix(function_address) + +def get_function_name(function_address): + """ + Get a function's true name. + """ + + # get the original function name from the database + if using_ida7api: + original_name = idaapi.get_name(function_address) + else: + original_name = idaapi.get_true_name(idaapi.BADADDR, function_address) + + # sanity check + if original_name == None: + raise ValueError("Invalid function address") + + # return the function name + return original_name + +#------------------------------------------------------------------------------ +# Interactive +#------------------------------------------------------------------------------ + @mainthread def prompt_string(label, title, default=""): """ @@ -593,20 +666,12 @@ def prompt_string(label, title, default=""): text = str(dlg.textValue()) return (ok, text) -def rename_function(function_address): +@mainthread +def gui_rename_function(function_address): """ - Rename a function in the IDB. + Interactive rename of a function in the IDB. """ - - # get the original function name from the database - if using_ida7api: - original_name = idaapi.get_name(function_address) - else: - original_name = idaapi.get_true_name(idaapi.BADADDR, function_address) - - # sanity check - if original_name == None: - raise ValueError("Invalid function address") + original_name = get_function_name(function_address) # prompt the user for a new function name ok, new_name = prompt_string( @@ -625,3 +690,23 @@ def rename_function(function_address): # rename the function idaapi.set_name(function_address, new_name, idaapi.SN_NOCHECK) + +@mainthread +def gui_prefix_functions(function_addresses): + """ + Interactive prefixing of functions in the IDB. + """ + + # prompt the user for a new function name + ok, prefix = prompt_string( + "Please enter a function prefix", + "Prefix Function(s)", + PREFIX_DEFAULT + ) + + # bail if the user clicked cancel or failed to enter a prefix + if not (ok and prefix): + return + + # prefix the given functions with the user specified prefix + prefix_functions(function_addresses, prefix) From 1c7e4ebc6a440d4e888bb3af130bafb7c4436c86 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 19:04:52 -0500 Subject: [PATCH 23/39] keep local director reference in the coverage overview --- plugin/lighthouse/ui/coverage_overview.py | 30 +++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 7e74fdca..460e13f4 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -54,6 +54,7 @@ #------------------------------------------------------------------------------ # Pseudo Widget Filter #------------------------------------------------------------------------------ + debugger_docked = False class EventProxy(QtCore.QObject): @@ -121,7 +122,10 @@ def __init__(self, director): plugin_resource(os.path.join("icons", "overview.png")) ) - # internal + # local reference to the director + self._director = director + + # underlying data model for the coverage overview self._model = CoverageModel(director, self._widget) # pseudo widget science @@ -130,7 +134,7 @@ def __init__(self, director): self._widget.installEventFilter(self._events) # initialize the plugin UI - self._ui_init(director) + self._ui_init() # refresh the data UI such that it reflects the most recent data self.refresh() @@ -162,7 +166,7 @@ def isVisible(self): # Initialization - UI #-------------------------------------------------------------------------- - def _ui_init(self, director): + def _ui_init(self): """ Initialize UI elements. """ @@ -172,19 +176,19 @@ def _ui_init(self, director): self._font_metrics = QtGui.QFontMetricsF(self._font) # initialize our ui elements - self._ui_init_table(director) - self._ui_init_toolbar(director) + self._ui_init_table() + self._ui_init_toolbar() self._ui_init_ctx_menu_actions() self._ui_init_signals() # layout the populated ui just before showing it self._ui_layout() - def _ui_init_table(self, director): + def _ui_init_table(self): """ Initialize the coverage table. """ - palette = director._palette + palette = self._director._palette self._table = QtWidgets.QTableView() self._table.setFocusPolicy(QtCore.Qt.NoFocus) self._table.setStyleSheet( @@ -236,13 +240,13 @@ def _ui_init_table(self, director): self._table.setSortingEnabled(True) hh.setSortIndicator(FUNC_ADDR, QtCore.Qt.AscendingOrder) - def _ui_init_toolbar(self, director): + def _ui_init_toolbar(self): """ Initialize the coverage toolbar. """ # initialize toolbar elements - self._ui_init_toolbar_elements(director) + self._ui_init_toolbar_elements() # populate the toolbar self._toolbar = QtWidgets.QToolBar() @@ -268,20 +272,20 @@ def _ui_init_toolbar(self, director): self._toolbar.addWidget(self._hide_zero_label) self._toolbar.addWidget(self._hide_zero_checkbox) - def _ui_init_toolbar_elements(self, director): + def _ui_init_toolbar_elements(self): """ Initialize the coverage toolbar UI elements. """ # the composing shell self._shell = ComposingShell( - director, + self._director, weakref.proxy(self._model), self._table ) # the coverage combobox - self._combobox = CoverageComboBox(director) + self._combobox = CoverageComboBox(self._director) # the checkbox to hide 0% coverage entries self._hide_zero_label = QtWidgets.QLabel("Hide 0% Coverage: ") @@ -533,7 +537,7 @@ def __init__(self, director, parent=None): super(CoverageModel, self).__init__(parent) self._blank_coverage = FunctionCoverage(idaapi.BADADDR) - # the data source + # local reference to the director self._director = director # mapping to correlate a given row in the table to its function coverage From c155755b170d2f4ac54df4be308105c05d2deb5d Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Sun, 3 Dec 2017 20:48:01 -0500 Subject: [PATCH 24/39] improving function rename event handler --- plugin/lighthouse/metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 57a6c500..222b5691 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -562,15 +562,15 @@ def _name_changed(self, address, new_name, local_name): """ # we should never care about local renames (eg, loc_40804b), ignore - if local_name: - return + if local_name or new_name.startswith("loc_"): + return 0 # get the function that this address falls within function = self.get_function(address) # if the address does not fall within a function (might happen?), ignore if not function: - return + return 0 # # ensure the renamed address matches the function start before From 618b6ce2787cc99a93a6b62bfe345036efecda27 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 01:58:26 -0500 Subject: [PATCH 25/39] color tweaks --- plugin/lighthouse/palette.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/palette.py index eeff1358..a132c6fa 100644 --- a/plugin/lighthouse/palette.py +++ b/plugin/lighthouse/palette.py @@ -42,14 +42,15 @@ def __init__(self): # IDA Views / HexRays # - self._ida_coverage = [0x990000, 0xC8E696] # NOTE: IDA uses BBGGRR + self._ida_coverage = [0x990000, 0xFFE2A8] # NOTE: IDA uses BBGGRR # # Composing Shell # - self._overview_bg = [QtGui.QColor(30, 30, 30), QtGui.QColor(30, 30, 30)] - self._composer_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)] + self._overview_bg = [QtGui.QColor(20, 20, 20), QtGui.QColor(20, 20, 20)] + self._composer_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)] + self._valid_text = [0x80F0FF, 0x0000FF] self._invalid_text = [0xF02070, 0xFF0000] self._invalid_highlight = [0x990000, 0xFF0000] From 6a1a326e364094a7ad53f11316e8ef2693d297f4 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 15:17:54 -0500 Subject: [PATCH 26/39] fuzzier module name detection --- plugin/lighthouse/parsers/drcov.py | 53 +++++++++++++++++++++++++----- plugin/lighthouse_plugin.py | 2 +- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/plugin/lighthouse/parsers/drcov.py b/plugin/lighthouse/parsers/drcov.py index 5911839c..726fc3a7 100644 --- a/plugin/lighthouse/parsers/drcov.py +++ b/plugin/lighthouse/parsers/drcov.py @@ -40,21 +40,58 @@ def __init__(self, filepath=None): # Public #-------------------------------------------------------------------------- - def filter_by_module(self, module_name): + def get_module(self, module_name, fuzzy=True): + """ + Get a module by its name. + + Note that this is a 'fuzzy' lookup by default. + """ + + # fuzzy module name lookup + if fuzzy: + + # attempt lookup using case-insensitive filename + for module in self.modules: + if module_name.lower() in module.filename.lower(): + return module + + # + # no hits yet... let's cleave the extension from the given module + # name (if present) and try again + # + + if "." in module_name: + module_name = module_name.split(".")[0] + + # attempt lookup using case-insensitive filename without extension + for module in self.modules: + if module_name.lower() in module.filename.lower(): + return module + + # strict lookup + else: + for module in self.modules: + if module_name == module.filename: + return module + + # no matching module exists + return None + + def get_blocks_by_module(self, module_name): """ Extract coverage blocks pertaining to the named module. """ # locate the coverage that matches the given module_name - for module in self.modules: - if module.filename.lower() == module_name.lower(): - mod_id = module.id - break + module = self.get_module(module_name) - # failed to find a module that matches the given name, bail - else: + # if we fail to find a module that matches the given name, bail + if not module: raise ValueError("Failed to find module '%s' in coverage data" % module_name) + # extract module id for speed + mod_id = module.id + # loop through the coverage data and filter out data for only this module coverage_blocks = [(bb.start, bb.size) for bb in self.basic_blocks if bb.mod_id == mod_id] @@ -357,5 +394,3 @@ class DrcovBasicBlock(Structure): x = DrcovData(argv[1]) for bb in x.basic_blocks: print "0x%08x" % bb.start - - diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index ee08c431..47818ef6 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -706,7 +706,7 @@ def _normalize_coverage(self, coverage_data, metadata): # extract the coverage relevant to this IDB (well, the root binary) root_filename = idaapi.get_root_filename() - coverage_blocks = coverage_data.filter_by_module(root_filename) + coverage_blocks = coverage_data.get_blocks_by_module(root_filename) # rebase the basic blocks base = idaapi.get_imagebase() From 10dd7782337a08b94f749f57757ec8ba9cfad2c9 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 17:09:44 -0500 Subject: [PATCH 27/39] exclude exception handling chunks from metadata --- plugin/lighthouse/metadata.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 222b5691..8a85ccd6 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -705,6 +705,23 @@ def _refresh_nodes(self): if node_start == node_end: continue + # + # if the current node_start address does not fall within the + # original / entry 'function chunk', we want to ignore it. + # + # this check is used as an attempt to ignore the try/catch/SEH + # exception handling blocks that IDA 7 parses and displays in + # the graph view (and therefore, the flowcahrt). + # + # practically speaking, 99% of the time people aren't going to be + # interested in the coverage information on their exception + # handlers. I am skeptical that dynamic instrumentation tools + # would be able to collect coverage in these handlers anway... + # + + if idaapi.get_func_chunknum(function, node_start): + continue + # create a new metadata object for this node node_metadata = NodeMetadata(node_start, node_end, node_id) From 61d4cdb777f730a8978957bb1cfb81958e5afd62 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 17:36:28 -0500 Subject: [PATCH 28/39] make jump case insensitive for 'sub_XXXXXXXX' targets --- plugin/lighthouse/composer/shell.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py index 49c46b35..f72fa578 100644 --- a/plugin/lighthouse/composer/shell.py +++ b/plugin/lighthouse/composer/shell.py @@ -434,10 +434,15 @@ def _compute_jump(self, text): # 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. # - # attempt to convert the user input from a function name eg - # 'sub_1400016F0' to a function address validated by the director + # attempt to convert the user input from a function name, eg 'main', + # or 'sub_1400016F0' to a function address validated by the director. # + # special case to make 'sub_*' prefixed user inputs case insensitive + if text.lower().startswith("sub_"): + text = "sub_" + text[4:].upper() + + # look up the text function name within the director's metadata function_metadata = self._director.metadata.get_function_by_name(text) if function_metadata: return function_metadata.address From 8a2aec35f9276b1309dc89f08604f810c2827a03 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 19:35:21 -0500 Subject: [PATCH 29/39] bugfix: corrects database wide coverage % --- plugin/lighthouse/coverage.py | 36 ++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index e5b5249d..0b639055 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -124,6 +124,7 @@ def __init__(self, data, palette): self.nodes = {} self.functions = {} + self.instruction_percent = 0.0 # # we instantiate a single weakref of ourself (the DatbaseMapping @@ -151,23 +152,6 @@ def coverage(self): """ return self._hitmap.viewkeys() - @property - def instruction_percent(self): - """ - The database coverage % by instructions executed in all defined functions. - """ - num_funcs = len(self._metadata.functions) - - # avoid a zero division error - if not num_funcs: - return 0 - - # sum all the function coverage %'s - func_sum = sum(f.instruction_percent for f in self.functions.itervalues()) - - # return the average function coverage % aka 'the database coverage %' - return func_sum / num_funcs - #-------------------------------------------------------------------------- # Metadata Population #-------------------------------------------------------------------------- @@ -214,6 +198,7 @@ def _finalize(self, dirty_nodes, dirty_functions): """ self._finalize_nodes(dirty_nodes) self._finalize_functions(dirty_functions) + self._finalize_instruction_percent() def _finalize_nodes(self, dirty_nodes): """ @@ -229,6 +214,23 @@ def _finalize_functions(self, dirty_functions): for function_coverage in dirty_functions.itervalues(): function_coverage.finalize() + def _finalize_instruction_percent(self): + """ + Finalize the database coverage % by instructions executed in all defined functions. + """ + + # sum all the instructions in the database metadata + total = sum(f.instruction_count for f in self._metadata.functions.itervalues()) + if not total: + self.instruction_percent = 0.0 + return + + # sum all the instructions executed by the coverage + executed = sum(f.instructions_executed for f in self.functions.itervalues()) + + # return the average function coverage % aka 'the database coverage %' + self.instruction_percent = float(executed) / total + #-------------------------------------------------------------------------- # Data Operations #-------------------------------------------------------------------------- From ad7062e556e35e95b41f0f077891dc44061d6e5a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 4 Dec 2017 20:32:42 -0500 Subject: [PATCH 30/39] implemented the full refresh via right click --- plugin/lighthouse/coverage.py | 5 +-- plugin/lighthouse/director.py | 39 +++++++---------------- plugin/lighthouse/metadata.py | 10 ++++++ plugin/lighthouse/ui/coverage_overview.py | 39 +++++++++++++++++++---- 4 files changed, 56 insertions(+), 37 deletions(-) diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py index 0b639055..dd2fdc70 100644 --- a/plugin/lighthouse/coverage.py +++ b/plugin/lighthouse/coverage.py @@ -163,10 +163,7 @@ def update_metadata(self, metadata, delta=None): # install the new metadata self._metadata = weakref.proxy(metadata) - - # unmap all the coverage affected by the metadata delta - if delta: - self._unmap_delta(delta) + self.unmap_all() def refresh(self): """ diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index d4b3a59c..8cb7e8bd 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -4,8 +4,10 @@ import threading import collections +import idaapi # TODO: remove in v0.8 + from lighthouse.util import * -from lighthouse.metadata import DatabaseMetadata, MetadataDelta +from lighthouse.metadata import DatabaseMetadata, metadata_progress from lighthouse.coverage import DatabaseCoverage from lighthouse.composer.parser import * @@ -941,10 +943,11 @@ def refresh(self): logger.debug("Refreshing the CoverageDirector") # (re)build our metadata cache of the underlying database - delta = self._refresh_database_metadata() + future = self.refresh_metadata(metadata_progress, True) + await_future(future) # (re)map each set of loaded coverage data to the database - self._refresh_database_coverage(delta) + self._refresh_database_coverage() def refresh_metadata(self, progress_callback=None, force=False): """ @@ -981,37 +984,19 @@ def refresh_metadata(self, progress_callback=None, force=False): # return the channel that will carry asynchronous result return result_queue - def _refresh_database_metadata(self): - """ - Refresh the database metadata cache utilized by the director. - - NOTE: this is currently unused. - """ - logger.debug("Refreshing database metadata") - - # compute the metadata for the current state of the database - new_metadata = DatabaseMetadata() - new_metadata.build_metadata() - - # compute the delta between the old metadata, and latest - delta = MetadataDelta(new_metadata, self.metadata) - - # save the new metadata in place of the old metadata - self.metadata = new_metadata - - # finally, return the list of nodes that have changed (the delta) - return delta - - def _refresh_database_coverage(self, delta): + def _refresh_database_coverage(self): """ Refresh all the database coverage mappings managed by the director. """ logger.debug("Refreshing database coverage mappings") - for name in self.all_names: + for i, name in enumerate(self.all_names, 1): logger.debug(" - %s" % name) + idaapi.replace_wait_box( + "Refreshing coverage mapping %u/%u" % (i, len(self.all_names)) + ) coverage = self.get_coverage(name) - coverage.update_metadata(self.metadata, delta) + coverage.update_metadata(self.metadata) coverage.refresh() def _refresh_aggregate(self): diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 8a85ccd6..27afe84c 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -303,6 +303,15 @@ def refresh(self, function_addresses=None, progress_callback=None): removed_functions = self.functions.viewkeys() - set(function_addresses) for function_address in removed_functions: + + # the function to delete + function_metadata = self.functions[function_address] + + # delete all node metadata owned by this function from the db list + for node in function_metadata.nodes.itervalues(): + del self.nodes[node.address] + + # now delete the function metadata from the db list del self.functions[function_address] # schedule a deferred lookup list refresh if we deleted any functions @@ -428,6 +437,7 @@ def _refresh_lookup(self): return False # update the lookup lists + self._last_node = [] 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()) diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py index 460e13f4..44a708c7 100644 --- a/plugin/lighthouse/ui/coverage_overview.py +++ b/plugin/lighthouse/ui/coverage_overview.py @@ -8,7 +8,7 @@ from lighthouse.util import * from .coverage_combobox import CoverageComboBox from lighthouse.composer import ComposingShell -from lighthouse.metadata import FunctionMetadata +from lighthouse.metadata import FunctionMetadata, metadata_progress from lighthouse.coverage import FunctionCoverage logger = logging.getLogger("Lighthouse.UI.Overview") @@ -335,8 +335,7 @@ def _ui_init_ctx_menu_actions(self): self._action_clear_prefix = QtWidgets.QAction("Clear prefixes", None) # misc actions - self._action_refresh_metadata = QtWidgets.QAction( - "Refresh metadata (slow)", None) + self._action_refresh_metadata = QtWidgets.QAction("Full refresh (slow)", None) def _ui_init_signals(self): """ @@ -481,7 +480,16 @@ def _process_ctx_menu_action(self, action): # handle the 'Refresh metadata' action elif action == self._action_refresh_metadata: - print "TODO: refresh metadata" + + idaapi.show_wait_box("Building database metadata...") + self._director.refresh() + + # ensure the table's model gets refreshed + idaapi.replace_wait_box("Refreshing Coverage Overview...") + self.refresh() + + # all done + idaapi.hide_wait_box() # # the following actions are only applicable if there is only one @@ -647,8 +655,27 @@ def data(self, index, role=QtCore.Qt.DisplayRole): column = index.column() # lookup the function info for this row - function_address = self.row2func[index.row()] - function_metadata = self._director.metadata.functions[function_address] + try: + function_address = self.row2func[index.row()] + function_metadata = self._director.metadata.functions[function_address] + + # + # if we hit a KeyError, it is probably because the database metadata + # is being refreshed and the model (this object) has yet to be + # updated. + # + # this should only ever happen as a result of the user using the + # right click 'Refresh metadata' action. And even then, only when + # a function they undefined in the IDB is visible in the coverage + # overview table view. + # + # In theory, the table should get refreshed *after* the metadata + # refresh completes. So for now, we simply return return the filler + # string '?' + # + + except KeyError: + return "?" # # remember, if a function does *not* have coverage data, it will From 2775ae29222e4088d3a677ef09b69ee31cc52506 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 5 Dec 2017 16:48:29 -0500 Subject: [PATCH 31/39] cleaning up some types in pintool, closes #25 --- coverage/pin/CodeCoverage.cpp | 10 +++++----- coverage/pin/README.md | 23 +++++++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp index 740ab710..b157474f 100644 --- a/coverage/pin/CodeCoverage.cpp +++ b/coverage/pin/CodeCoverage.cpp @@ -134,7 +134,7 @@ static VOID OnImageLoad(IMG img, VOID* v) 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()); + printf("Loaded image: %p:%p -> %s\n", (void *)low, (void *)high, img_name.c_str()); // Save the loaded image with its original full name/path. PIN_GetLock(&context.m_loaded_images_lock, 1); @@ -204,8 +204,8 @@ static VOID OnFini(INT32 code, VOID* v) // 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()); + context.m_trace->write_string("%2u, %p, %p, 0x0000000000000000, 0x00000000, 0x00000000, %s\n", + i, (void *)image.low_, (void *)image.high_, image.name_.c_str()); } // Add non terminated threads to the list of terminated threads. @@ -239,8 +239,8 @@ static VOID OnFini(INT32 code, VOID* v) if (it == context.m_loaded_images.end()) continue; - tmp.id = std::distance(context.m_loaded_images.begin(), it); - tmp.start = address - it->low_; + tmp.id = (uint16_t)std::distance(context.m_loaded_images.begin(), it); + tmp.start = (uint32_t)(address - it->low_); tmp.size = data->m_block_size[address]; context.m_trace->write_binary(&tmp, sizeof(tmp)); diff --git a/coverage/pin/README.md b/coverage/pin/README.md index ee116b2c..626aeca3 100644 --- a/coverage/pin/README.md +++ b/coverage/pin/README.md @@ -15,8 +15,11 @@ Follow the build instructions below for your respective platform. On MacOS or Liunux, one can compile the pintool using the following commands. ``` -cd ~/lighthouse/coverage/pin # Location of this repo / pintool source -export PIN_ROOT=~/pin # Location where you extracted Pin +# Location of this repo / pintool source +cd ~/lighthouse/coverage/pin + +# Location where you extracted Pin +export PIN_ROOT=~/pin export PATH=$PATH:$PIN_ROOT make ``` @@ -36,8 +39,12 @@ Launch a command prompt and build the pintool with the following commands. ``` "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 + +REM Location of this repo / pintool source +cd C:\Users\user\lighthouse\coverage\pin + +REM Location where you extracted Pin +set PIN_ROOT=C:\pin set PATH=%PATH%;%PIN_ROOT% build-x86.bat ``` @@ -46,8 +53,12 @@ build-x86.bat ``` "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 + +REM Location of this repo / pintool source +cd C:\Users\user\lighthouse\coverage\pin + +REM Location where you extracted Pin +set PIN_ROOT=C:\pin set PATH=%PATH%;%PIN_ROOT% build-x64.bat ``` From 9e3a6fa4c47afefbea83c8c0c87a41cc10b18b6a Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 5 Dec 2017 15:35:50 -0800 Subject: [PATCH 32/39] fixes formatting error on print statements --- coverage/frida/frida-drcov.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage/frida/frida-drcov.py b/coverage/frida/frida-drcov.py index 8b8eaec1..8f0e37f6 100755 --- a/coverage/frida/frida-drcov.py +++ b/coverage/frida/frida-drcov.py @@ -268,12 +268,12 @@ def main(): if target == -1: target = p.pid else: - print('[-] Warning: multiple processes on device match \'%s\'' + - ', using pid: %d' % (args.target, target)) + print('[-] Warning: multiple processes on device match ' + '\'%s\', using pid: %d' % (args.target, target)) if target == -1: - print('[-] Error: could not find process matching \'%s\'' + - ' on device \'%s\'' % (args.target, device.id)) + print('[-] Error: could not find process matching ' + '\'%s\' on device \'%s\'' % (args.target, device.id)) sys.exit(1) whitelist_modules = ['all'] From e21e238903582cde027b128f4a9a7632ec643d96 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Tue, 5 Dec 2017 15:38:30 -0800 Subject: [PATCH 33/39] updated frida README.md --- coverage/frida/README.md | 90 ++++++++++++++++++++++------------------ 1 file changed, 49 insertions(+), 41 deletions(-) diff --git a/coverage/frida/README.md b/coverage/frida/README.md index f5f0c82d..f50351a1 100644 --- a/coverage/frida/README.md +++ b/coverage/frida/README.md @@ -1,64 +1,72 @@ # frida-drcov.py -A quick and dirty frida-based bb-tracer, with an emphasis on ease of use. - -If your target is complex, you'll likely want to use a better, dedicated -tracing engine like drcov or a pin based tracer. This tracer has some -significant shortcomings, which are exagerated on larger or more complex -binaries: -* It is roughly one order of magnitude slower than native execution -* It drops coverage, especially near `exit()` -* It cannot easily detect new threads being created, thus cannot instrument -them -* Self modifying code will confuse it, though to be fair I'm not sure how -drcov, pin, or otheres deal with self modifying code either - -These shortcomines are probably 10% frida's fault and 90% the author's. Despite -these flaws however, it is hard to beat the ease of use frida provides. +In this folder you will find the code coverage collection script `frida-drcov.py` that run ontop of the [Frida](https://www.frida.re/) DBI toolkit. This script will produce code coverage (using Frida) in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). -## Install - -`$ pip install frida` - -## Usage +Frida is best supported on mobile platforms such as iOS or Android, claiming some support for Windows, MacOS, Linux, and QNX. Practically speaking, `frida-drcov.py` should only be used for collecting coverage data on mobile applications. -`$ ./frida-drcov.py ` +This script is labeled only as a prototype. -You can whitelist specific modules inside your target. Say you have binary -`foo` which imports `libbiz`, `libbaz`, and `libbar`. You only want to trace -`libbiz` and `libbaz`: - -`$ ./frida-drcov.py -w libbiz -w libbaz foo` +## Install -By default, this script will trace all modules. This script will create and -write to `frida-drcov.log` in the current working directory. You can change -this with `-o`: +To use `frida-drcov.py`, you must have [Frida](https://www.frida.re/) installed. This can be done via python's `pip`: -`$ ./frida-drcov.py -o more-coverage.log foo` +``` +sudo pip install frida +``` -For slightly more advanced usage, on multi-threaded applications, tracing all -threads can impose significant overhead, especially if you only care about -particular threads. For these cases you can filter based on thread id. Say you -have another tool which identifies interesting threads 543 and 678 inside your -target. +## Usage -`$ ./frida-drcov.py -t 543 -t 678 foo` +Once frida is installed, the `frida-drcov.py` script in this repo can be used to collect coverage against a running process as demonstrated below. By default, the code coverage data will be written to the file `frida-drcov.log` at the end of execution. -Will only trace those threads. By default, all threads are traced. +``` +python frida-drcov.py +``` -## Example +Here is an example of us instrumenting the running process `bb-bench`. ``` -$ sudo ./frida-drcov.py bb-bench +$ sudo python ./frida-drcov.py bb-bench [+] Got module info Starting to stalk threads... Stalking thread 775 Done stalking threads. [*] Now collecting info, control-D to terminate.... -[*] Detatching, this might take a second... # ^d -[+] Detatched. Got 320 basic blocks. +[*] Detaching, this might take a second... # ^d +[+] Detached. Got 320 basic blocks. [*] Formatting coverage and saving... [!] Done $ ls -lh frida-cov.log # this is the file you will load into lighthouse -rw-r--r-- 1 root staff 7.2K 21 Oct 11:58 frida-cov.log ``` + +Using the `-o` flag, one can specify a custom name/location for the coverage log file: + +``` +python frida-drcov.py -o more-coverage.log foo +``` + +## Module Whitelisting + +One can whitelist specific modules inside the target process. Say you have binary `foo` which imports the libraries `libfoo`, `libbar`, and `libbaz`. Using the `-w` flag (whitelist) on the command line, we can explicitly target modules of interest: + +``` +$ python frida-drcov.py -w libfoo -w libbaz foo +``` + +This will reduce the amount of information collected and improve performance. If no `-w` arguments are supplied, `frida-drcov.py` will trace all loaded images. + +## Thread Targeting + +On multi-threaded applications, tracing all threads can impose significant overhead. For these cases you can filter coverage collection based on thread id if you only care about specific threads. + +In the following example, we target thread id `543`, and `678` running in the process named `foo`. + +``` +python frida-drcov.py -t 543 -t 678 foo +``` + +Without the `-t` flag, all threads that exist in the process at the time of attach will be traced. + +# Authors + +* yrp ([@yrp604](https://twitter.com/yrp604)) From 7027d3726037e13c2bfc970d42288cea8c780f05 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 7 Dec 2017 04:27:06 -0500 Subject: [PATCH 34/39] improve pintool performance using PIN_FAST_ANALYSIS_CALL --- coverage/pin/CodeCoverage.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp index b157474f..71e94ee3 100644 --- a/coverage/pin/CodeCoverage.cpp +++ b/coverage/pin/CodeCoverage.cpp @@ -161,7 +161,7 @@ static VOID OnImageUnload(IMG img, VOID* v) } // Basic block hit event handler. -static VOID OnBasicBlockHit(THREADID tid, ADDRINT addr, UINT32 size, VOID* v) +static VOID PIN_FAST_ANALYSIS_CALL OnBasicBlockHit(THREADID tid, ADDRINT addr, UINT32 size, VOID* v) { auto& context = *reinterpret_cast(v); ThreadData* data = context.GetThreadLocalData(tid); @@ -184,6 +184,7 @@ static VOID OnTrace(TRACE trace, VOID* v) for (; BBL_Valid(bbl); bbl = BBL_Next(bbl)) { addr = BBL_Address(bbl); BBL_InsertCall(bbl, IPOINT_ANYWHERE, (AFUNPTR)OnBasicBlockHit, + IARG_FAST_ANALYSIS_CALL, IARG_THREAD_ID, IARG_ADDRINT, addr, IARG_UINT32, BBL_Size(bbl), From 724516f2f8bb20b5da7bc8c4419264e6155de110 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 7 Dec 2017 04:40:49 -0500 Subject: [PATCH 35/39] lower the 'big' IDB threshold to 50k functions to be safe --- plugin/lighthouse/metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 27afe84c..5cfa9686 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -273,7 +273,7 @@ def is_big(self): """ Return an size classification of the database / metadata. """ - return len(self.functions) > 100000 + return len(self.functions) > 50000 #-------------------------------------------------------------------------- # Refresh From 4eb0b445eda3353597160476574908d4c7660d3c Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 7 Dec 2017 05:27:15 -0500 Subject: [PATCH 36/39] version bump to v0.7.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 47818ef6..d609737d 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -20,7 +20,7 @@ # IDA Plugin #------------------------------------------------------------------------------ -PLUGIN_VERSION = "0.6.1" +PLUGIN_VERSION = "0.7.0" AUTHORS = "Markus Gaasedelen" DATE = "2017" From d715736940ced19e8d0048240a7dcee71f5725c5 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Thu, 7 Dec 2017 05:27:53 -0500 Subject: [PATCH 37/39] updated README.md's --- README.md | 29 +++++++++++++++++++++++++---- coverage/frida/README.md | 2 +- coverage/pin/README.md | 2 +- screenshots/context_menu.gif | Bin 0 -> 98834 bytes 4 files changed, 27 insertions(+), 6 deletions(-) create mode 100644 screenshots/context_menu.gif diff --git a/README.md b/README.md index 0bffee0a..c4cfb2f4 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration. ## Releases +* v0.7 -- Frida, C++ demangling, context menu, function prefixing, tweaks, bugfixes. * 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. @@ -29,7 +30,7 @@ Install Lighthouse into the IDA plugins folder. - On MacOS, the folder is at `/Applications/IDA\ Pro\ 6.8/idaq.app/Contents/MacOS/plugins` - On Linux, the folder may be at `/opt/IDA/plugins/` -The plugin is platform agnostic, but has only been tested on Windows for IDA 6.8 --> 7.0 +The plugin is compatible with IDA Pro 6.8 --> 7.0 on Windows, MacOS, and Linux. ## Usage @@ -67,6 +68,16 @@ The Coverage Overview is a dockable widget that provides a function level view o This table can be sorted by column, and entries can be double clicked to jump to their corresponding disassembly. +## Context Menu + +Right clicking the table in the Coverage Overview will produce a context menu with a few basic amenities. + +

+Lighthouse Context Menu +

+ +These actions can be used to quickly manipulate or interact with entries in the table. + ## Coverage Composition Building relationships between multiple sets of coverage data often distills deeper meaning than their individual parts. The shell at the bottom of the [Coverage Overview](#coverage-overview) provides an interactive means of constructing these relationships. @@ -134,7 +145,7 @@ Loaded coverage data and user constructed compositions can be selected or delete Before using Lighthouse, one will need to collect code coverage data for their target binary / application. -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. +The examples below demonstrate how one can use [DynamoRIO](http://www.dynamorio.org), [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) or [Frida](https://www.frida.re) to collect Lighthouse compatible coverage against a target. The `.log` files produced by these instrumentation tools can be loaded directly into Lighthouse. ## DynamoRIO @@ -156,7 +167,17 @@ Example usage: pin.exe -t CodeCoverage64.dll -- boombox.exe ``` -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. +For convenience, binaries for the Windows pintool can be found on the [releases](https://github.com/gaasedelen/lighthouse/releases/tag/v0.7.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. + +## Frida (Experimental) + +Lighthouse offers limited support for Frida based code coverage via a custom [instrumentation script](coverage/frida) contributed by [yrp](https://twitter.com/yrp604). + +Example usage: + +``` +sudo python frida-drcov.py bb-bench +``` # Future Work @@ -166,7 +187,7 @@ Time and motivation permitting, future work may include: * ~~Multifile/coverage support~~ * Profiling based heatmaps/painting * Coverage & Profiling Treemaps -* Additional coverage sources, trace formats, etc +* ~~Additional coverage sources, trace formats, etc~~ * Improved Pseudocode painting I welcome external contributions, issues, and feature requests. diff --git a/coverage/frida/README.md b/coverage/frida/README.md index f50351a1..1374cd4d 100644 --- a/coverage/frida/README.md +++ b/coverage/frida/README.md @@ -25,7 +25,7 @@ python frida-drcov.py Here is an example of us instrumenting the running process `bb-bench`. ``` -$ sudo python ./frida-drcov.py bb-bench +$ sudo python frida-drcov.py bb-bench [+] Got module info Starting to stalk threads... Stalking thread 775 diff --git a/coverage/pin/README.md b/coverage/pin/README.md index 626aeca3..489818d2 100644 --- a/coverage/pin/README.md +++ b/coverage/pin/README.md @@ -68,7 +68,7 @@ The resulting binaries will be labaled based on their architecture (eg, 64 is th * 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. +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.7.0) page. # Usage diff --git a/screenshots/context_menu.gif b/screenshots/context_menu.gif new file mode 100644 index 0000000000000000000000000000000000000000..c626f97af6a5e209437ab1ff88fcac792c2a553c GIT binary patch literal 98834 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 Date: Fri, 29 Dec 2017 23:16:16 -0500 Subject: [PATCH 38/39] hotfix for MacOS hangs (and crashes) on close #31 --- plugin/lighthouse/director.py | 4 ++-- plugin/lighthouse/metadata.py | 7 +++++++ plugin/lighthouse_plugin.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py index 8cb7e8bd..7420fc2e 100644 --- a/plugin/lighthouse/director.py +++ b/plugin/lighthouse/director.py @@ -195,8 +195,8 @@ def terminate(self): self._ast_queue.put(None) self._composition_worker.join() - # stop any ongoing metadata refresh - self.metadata.abort_refresh(join=True) + # spin down the live metadata object + self.metadata.terminate() #-------------------------------------------------------------------------- # Properties diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py index 5cfa9686..8e103f90 100644 --- a/plugin/lighthouse/metadata.py +++ b/plugin/lighthouse/metadata.py @@ -92,6 +92,13 @@ def __init__(self): self._refresh_worker = None self._stop_threads = False + def terminate(self): + """ + Cleanup & terminate the metadata object. + """ + self.abort_refresh(join=True) + self._rename_hooks.unhook() + #-------------------------------------------------------------------------- # Providers #-------------------------------------------------------------------------- diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index d609737d..9f7972c5 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -20,7 +20,7 @@ # IDA Plugin #------------------------------------------------------------------------------ -PLUGIN_VERSION = "0.7.0" +PLUGIN_VERSION = "0.7.1" AUTHORS = "Markus Gaasedelen" DATE = "2017" From 36127e74ef30624b6dac762e2ce51d3c4469b526 Mon Sep 17 00:00:00 2001 From: gaasedelen Date: Mon, 23 Apr 2018 21:41:02 -0400 Subject: [PATCH 39/39] hotfix for new drcov log formats, issue #36 --- plugin/lighthouse/parsers/drcov.py | 67 ++++++++++++++++++++++++++++-- plugin/lighthouse_plugin.py | 5 ++- 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/plugin/lighthouse/parsers/drcov.py b/plugin/lighthouse/parsers/drcov.py index 726fc3a7..d724f30c 100644 --- a/plugin/lighthouse/parsers/drcov.py +++ b/plugin/lighthouse/parsers/drcov.py @@ -156,7 +156,7 @@ def _parse_module_table_header(self, f): eg: 'Module Table: 11' Format used in DynamoRIO v7.0.0-RC1 (and hopefully above) - eg: 'Module Table: version 2, count 11' + eg: 'Module Table: version X, count 11' """ @@ -191,6 +191,8 @@ def _parse_module_table_header(self, f): data_name, version = version_data.split(" ") #assert data_name == "version" self.module_table_version = int(version) + if not self.module_table_version in [2, 3, 4]: + raise ValueError("Unsupported (new?) drcov log format...") # parse module count in table from 'count Y' data_name, count = count_data.split(" ") @@ -203,15 +205,27 @@ def _parse_module_table_columns(self, f): ------------------------------------------------------------------- - Format used in DynamoRIO v6.1.1 through 6.2.0 + DynamoRIO v6.1.1, table version 1: eg: (Not present) - Format used in DynamoRIO v7.0.0-RC1 (and hopefully above) + DynamoRIO v7.0.0-RC1, table version 2: Windows: 'Columns: id, base, end, entry, checksum, timestamp, path' Mac/Linux: 'Columns: id, base, end, entry, path' + DynamoRIO v7.0.17594B, table version 3: + Windows: + 'Columns: id, containing_id, start, end, entry, checksum, timestamp, path' + Mac/Linux: + 'Columns: id, containing_id, start, end, entry, path' + + DynamoRIO v7.0.17640, table version 4: + Windows: + 'Columns: id, containing_id, start, end, entry, offset, checksum, timestamp, path' + Mac/Linux: + 'Columns: id, containing_id, start, end, entry, offset, path' + """ # NOTE/COMPAT: there is no 'Columns' line for the v1 table... @@ -308,10 +322,20 @@ def __init__(self, module_data, version): self.timestamp = 0 self.path = "" self.filename = "" + self.containing_id = 0 # parse the module self._parse_module(module_data, version) + @property + def start(self): + """ + Compatability alias for the module base. + + DrCov table version 2 --> 3 changed this paramter name base --> start. + """ + return self.base + def _parse_module(self, module_line, version): """ Parse a module table entry. @@ -323,6 +347,10 @@ def _parse_module(self, module_line, version): self._parse_module_v1(data) elif version == 2: self._parse_module_v2(data) + elif version == 3: + self._parse_module_v3(data) + elif version == 4: + self._parse_module_v4(data) else: raise ValueError("Unknown module format (v%u)" % version) @@ -350,6 +378,39 @@ def _parse_module_v2(self, data): self.size = self.end-self.base self.filename = os.path.basename(self.path) + def _parse_module_v3(self, data): + """ + Parse a module table v3 entry. + """ + self.id = int(data[0]) + self.containing_id = int(data[1]) + self.base = int(data[2], 16) + self.end = int(data[3], 16) + self.entry = int(data[4], 16) + if len(data) == 7: # Windows Only + self.checksum = int(data[5], 16) + self.timestamp = int(data[6], 16) + self.path = str(data[-1]) + self.size = self.end-self.base + self.filename = os.path.basename(self.path) + + def _parse_module_v4(self, data): + """ + Parse a module table v4 entry. + """ + self.id = int(data[0]) + self.containing_id = int(data[1]) + self.base = int(data[2], 16) + self.end = int(data[3], 16) + self.entry = int(data[4], 16) + self.offset = int(data[5], 16) + if len(data) == 7: # Windows Only + self.checksum = int(data[6], 16) + self.timestamp = int(data[7], 16) + self.path = str(data[-1]) + self.size = self.end-self.base + self.filename = os.path.basename(self.path) + #------------------------------------------------------------------------------ # drcov basic block parser #------------------------------------------------------------------------------ diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py index 9f7972c5..18f921b3 100644 --- a/plugin/lighthouse_plugin.py +++ b/plugin/lighthouse_plugin.py @@ -20,7 +20,7 @@ # IDA Plugin #------------------------------------------------------------------------------ -PLUGIN_VERSION = "0.7.1" +PLUGIN_VERSION = "0.7.2" AUTHORS = "Markus Gaasedelen" DATE = "2017" @@ -684,7 +684,8 @@ def _load_coverage_files(self, filenames): # catch all for parse errors / bad input / malformed files except Exception as e: lmsg("Failed to load coverage %s" % filename) - logger.exception("Error details:") + lmsg(" - Error: %s" % str(e)) + logger.exception(" - Traceback:") continue # save the loaded coverage data to the output list