diff --git a/LICENSE b/LICENSE index 610373a5..10cf9e2d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2017-2020 Markus Gaasedelen +Copyright (c) 2017-2021 Markus Gaasedelen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 6d9bd879..deac1bf2 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ -# Lighthouse - A Code Coverage Explorer for Reverse Engineers +# Lighthouse - A Coverage Explorer for Reverse Engineers +

Lighthouse Plugin

## Overview -Lighthouse is a powerful code coverage plugin for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/). As an extension of the leading disassemblers, this plugin enables one to interactively explore code coverage data in new and innovative ways when symbols or source may not be available for a given binary. +Lighthouse is a powerful code coverage explorer for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/), providing software researchers with uniquely interactive controls to study execution maps for native applications without requiring symbols or source. -This plugin is labeled only as a prototype & code resource for the community. +This project placed 2nd in IDA's [2017 Plug-In Contest](https://hex-rays.com/contests_details/contest2017/) and was later [nominated](https://pwnies.com/lighthouse/) in the 2021 Pwnie Awards for its contributions to the security research industry. Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration. @@ -27,11 +28,31 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration. Lighthouse is a cross-platform (Windows, macOS, Linux) Python 2/3 plugin. It takes zero third party dependencies, making the code both portable and easy to install. -1. From your disassembler's python console, run the following command to find its plugin directory: - - **IDA Pro**: `os.path.join(idaapi.get_user_idadir(), "plugins")` - - **Binary Ninja**: `binaryninja.user_plugin_path()` +Use the instructions below for your respective disassembler. + +## IDA Installation + +1. From IDA's Python console, run the following command to find its plugin directory: + - `import idaapi, os; print(os.path.join(idaapi.get_user_idadir(), "plugins"))` +2. Copy the contents of this repository's `/plugins/` folder to the listed directory. +3. Restart your disassembler. + +## Binary Ninja Installation + +Lighthouse can be installed through the plugin manager on newer versions of Binary Ninja (>2.4.2918). The plugin will have to be installed manually on older versions. + +### Auto Install + +1. Open Binary Ninja's plugin manager by navigating the following submenus: + - `Edit` -> `Preferences` -> `Manage Plugins` +2. Search for Lighthouse in the plugin manager, and click the `Enable` button in the bottom right. +3. Restart your disassembler. + +### Manual Install -2. Copy the contents of this repository's `/plugin/` folder to the listed directory. +1. Open Binary Ninja's plugin folder by navigating the following submenus: + - `Tools` -> `Open Plugins Folder...` +2. Copy the contents of this repository's `/plugins/` folder to the listed directory. 3. Restart your disassembler. # Usage @@ -76,7 +97,7 @@ If there are any other actions that you think might be useful to add to this con ## Coverage ComboBox -Loaded coverage data and user constructed compositions can be selected or deleted through the coverage combobox. +Loaded coverage and user constructed compositions can be selected or deleted through the coverage combobox.

Lighthouse Coverage ComboBox @@ -84,8 +105,7 @@ Loaded coverage data and user constructed compositions can be selected or delete ## HTML Coverage Report -Lighthouse can generate a rudimentary HTML coverage report of the active coverage. -A sample report can be seen [here](https://rawgit.com/gaasedelen/lighthouse/master/testcase/report.html). +Lighthouse can generate rudimentary HTML coverage reports. A sample report can be seen [here](https://rawgit.com/gaasedelen/lighthouse/master/testcase/report.html).

Lighthouse HTML Report diff --git a/binjastub/README.md b/binjastub/README.md new file mode 100644 index 00000000..f1d704ea --- /dev/null +++ b/binjastub/README.md @@ -0,0 +1,11 @@ +# Lighthouse - A Coverage Explorer for Reverse Engineers + +

+Lighthouse Plugin +

+ +## Overview + +Lighthouse is a powerful code coverage explorer for [IDA Pro](https://www.hex-rays.com/products/ida/) and [Binary Ninja](https://binary.ninja/), providing software researchers with uniquely interactive controls to study execution maps for native applications without requiring symbols or source. + +For additional usage information, please check out the full [README](https://github.com/gaasedelen/lighthouse) on GitHub. diff --git a/binjastub/__init__.py b/binjastub/__init__.py new file mode 100644 index 00000000..f9e67f79 --- /dev/null +++ b/binjastub/__init__.py @@ -0,0 +1,26 @@ +import os +import sys + +#------------------------------------------------------------------------------ +# Binary Ninja 'Plugin Manager' Stub +#------------------------------------------------------------------------------ +# +# This file is an alternative loading stub created specifically to +# support the ability to 'easy' install Lighthouse into Binary Ninja +# via its 'Plugin Manager' functionality. +# +# Please disregard this code / subdirectory if performing **manual** +# installations of Lighthouse in IDA or Binary Ninja. +# + +lh_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "..", "plugins") +sys.path.append(lh_path) + +from lighthouse.util.log import logging_started, start_logging +from lighthouse.util.disassembler import disassembler + +if not logging_started(): + logger = start_logging() + +logger.info("Selecting Binary Ninja loader...") +from lighthouse.integration.binja_loader import * diff --git a/binjastub/plugin.json b/binjastub/plugin.json new file mode 100644 index 00000000..696b50ba --- /dev/null +++ b/binjastub/plugin.json @@ -0,0 +1,24 @@ +{ + "api": [ + "python3" + ], + "author": "Markus Gaasedelen", + "description": "A Coverage Explorer for Reverse Engineers", + "license": { + "name": "MIT", + "text": "Copyright (c) 2021> Markus Gaasedelen\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE." + }, + "longdescription": "", + "minimumbinaryninjaversion": 2918, + "name": "Lighthouse", + "platforms": [ + "Darwin", + "Linux", + "Windows" + ], + "pluginmetadataversion": 2, + "type": [ + "helper" + ], + "version": "0.9.2" +} \ No newline at end of file diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp index 4407f403..12bf4b07 100644 --- a/coverage/pin/CodeCoverage.cpp +++ b/coverage/pin/CodeCoverage.cpp @@ -1,4 +1,3 @@ -using namespace std; #include #include #include diff --git a/coverage/pin/ImageManager.cpp b/coverage/pin/ImageManager.cpp index 7e6938db..79aba679 100644 --- a/coverage/pin/ImageManager.cpp +++ b/coverage/pin/ImageManager.cpp @@ -1,4 +1,3 @@ -using namespace std; #include "ImageManager.h" #include "pin.H" diff --git a/coverage/pin/Makefile b/coverage/pin/Makefile index 3f50f79b..a60128ea 100644 --- a/coverage/pin/Makefile +++ b/coverage/pin/Makefile @@ -1,7 +1,7 @@ CONFIG_ROOT := $(PIN_ROOT)/source/tools/Config include $(CONFIG_ROOT)/makefile.config -TOOL_CXXFLAGS += -std=c++11 -Wno-format -Wno-aligned-new +TOOL_CXXFLAGS += -std=c++11 -Wno-format TOOL_ROOTS := CodeCoverage $(OBJDIR)CodeCoverage$(PINTOOL_SUFFIX): $(OBJDIR)CodeCoverage$(OBJ_SUFFIX) $(OBJDIR)ImageManager$(OBJ_SUFFIX) diff --git a/coverage/pin/build-x64.bat b/coverage/pin/build-x64.bat index 765602f8..dd6476cd 100644 --- a/coverage/pin/build-x64.bat +++ b/coverage/pin/build-x64.bat @@ -2,11 +2,12 @@ cls cl ^ - /c ^ + /c /Fo /nologo /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MD /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /wd4316 /wd4530 /fp:strict ^ + /DTARGET_IA32E /DHOST_IA32E /DTARGET_WINDOWS /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D_STLP_IMPORT_IOSTREAMS /D__LP64__ ^ + /I"%PIN_ROOT%\extras\xed-intel64\include\xed" ^ /I%PIN_ROOT%\source\include\pin ^ /I%PIN_ROOT%\source\include\pin\gen ^ /I%PIN_ROOT%\source\tools\InstLib ^ - /I"%PIN_ROOT%\extras\xed-intel64\include\xed" ^ /I%PIN_ROOT%\extras\components\include ^ /I%PIN_ROOT%\extras\stlport\include ^ /I%PIN_ROOT%\extras ^ @@ -16,9 +17,6 @@ cl ^ /I"%PIN_ROOT%\extras\crt\include\arch-x86_64" ^ /I%PIN_ROOT%\extras\crt\include\kernel\uapi ^ /I"%PIN_ROOT%\extras\crt\include\kernel\uapi\asm-x86" ^ - /nologo /W3 /WX- /O2 ^ - /D TARGET_IA32E /D HOST_IA32E /D TARGET_WINDOWS /D WIN32 /D __PIN__=1 /D PIN_CRT=1 /D __LP64__ ^ - /Gm- /MT /GS- /Gy /fp:precise /Zc:wchar_t /Zc:forScope /Zc:inline /GR- /Gd /TP /wd4530 /GR- /GS- /EHs- /EHa- /FP:strict /Oi- ^ /FIinclude/msvc_compat.h CodeCoverage.cpp ImageManager.cpp ImageManager.h TraceFile.h link ^ @@ -29,7 +27,7 @@ link ^ /LIBPATH:%PIN_ROOT%\intel64\lib ^ /LIBPATH:"%PIN_ROOT%\intel64\lib-ext" ^ /LIBPATH:"%PIN_ROOT%\extras\xed-intel64\lib" ^ - /LIBPATH:%PIN_ROOT%\intel64\runtime\pincrt pin.lib xed.lib pinvm.lib kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-64.lib" crtbeginS.obj ^ + /LIBPATH:%PIN_ROOT%\intel64\runtime\pincrt pin.lib xed.lib pinvm.lib pincrt.lib ntdll-64.lib kernel32.lib crtbeginS.obj ^ /NODEFAULTLIB ^ /MANIFEST:NO ^ /OPT:NOREF ^ diff --git a/coverage/pin/build-x86.bat b/coverage/pin/build-x86.bat index ac89ddd0..bc80c268 100644 --- a/coverage/pin/build-x86.bat +++ b/coverage/pin/build-x86.bat @@ -2,8 +2,8 @@ cls cl ^ - /c /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MT /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /fp:precise /nologo /wd4316 ^ - /DTARGET_IA32 /DHOST_IA32 /DTARGET_WINDOWS /DBIGARRAY_MULTIPLIER=1 /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D__i386__ ^ + /c /Fo /nologo /EHa- /EHs- /GR- /GS- /Gd /Gm- /Gy /MD /O2 /Oi- /Oy- /TP /W3 /WX- /Zc:forScope /Zc:inline /Zc:wchar_t /wd4316 /wd4530 /fp:precise ^ + /DTARGET_IA32 /DHOST_IA32 /DTARGET_WINDOWS /DWIN32 /D__PIN__=1 /DPIN_CRT=1 /D_STLP_IMPORT_IOSTREAMS /D__i386__ ^ /I"%PIN_ROOT%\extras\xed-ia32\include\xed" ^ /I%PIN_ROOT%\source\include\pin ^ /I%PIN_ROOT%\source\include\pin\gen ^ @@ -28,7 +28,7 @@ link ^ /LIBPATH:%PIN_ROOT%\ia32\lib ^ /LIBPATH:"%PIN_ROOT%\ia32\lib-ext" ^ /LIBPATH:"%PIN_ROOT%\extras\xed-ia32\lib" ^ - /LIBPATH:%PIN_ROOT%\ia32\runtime\pincrt pin.lib xed.lib pinvm.lib kernel32.lib "stlport-static.lib" "m-static.lib" "c-static.lib" "os-apis.lib" "ntdll-32.lib" crtbeginS.obj ^ + /LIBPATH:%PIN_ROOT%\ia32\runtime\pincrt pin.lib xed.lib pinvm.lib pincrt.lib ntdll-32.lib kernel32.lib crtbeginS.obj ^ /NODEFAULTLIB ^ /MANIFEST:NO ^ /OPT:NOREF ^ diff --git a/plugins/lighthouse/context.py b/plugins/lighthouse/context.py index 3a229233..6312ad75 100644 --- a/plugins/lighthouse/context.py +++ b/plugins/lighthouse/context.py @@ -66,6 +66,8 @@ def terminate(self): """ Spin down any session subsystems before the session is deleted. """ + if not self._started: + return self.painter.terminate() self.director.terminate() self.metadata.terminate() diff --git a/plugins/lighthouse/director.py b/plugins/lighthouse/director.py index baddde38..15309106 100644 --- a/plugins/lighthouse/director.py +++ b/plugins/lighthouse/director.py @@ -666,7 +666,7 @@ def _optimize_coverage_data(self, coverage_addresses): # block_ratio = len(basic_blocks) / float(len(instructions)) - block_trace_confidence = 0.90 + block_trace_confidence = 0.80 logger.debug("Block confidence %f" % block_ratio) # diff --git a/plugins/lighthouse/integration/binja_integration.py b/plugins/lighthouse/integration/binja_integration.py index 61fe0826..3b9ea6c0 100644 --- a/plugins/lighthouse/integration/binja_integration.py +++ b/plugins/lighthouse/integration/binja_integration.py @@ -25,6 +25,8 @@ def __init__(self): def get_context(self, dctx, startup=True): """ Get the LighthouseContext object for a given database context. + + In Binary Ninja, a dctx is a BinaryView (BV). """ dctx_id = ctypes.addressof(dctx.handle.contents) @@ -65,6 +67,30 @@ def get_context(self, dctx, startup=True): # return the lighthouse context object for this database ctx / bv return lctx + def binja_close_context(self, dctx): + """ + Attempt to close / spin-down the LighthouseContext for the given dctx. + + In Binary Ninja, a dctx is a BinaryView (BV). + """ + dctx_id = ctypes.addressof(dctx.handle.contents) + + # fetch the LighthouseContext for the closing BNDB + try: + lctx = self.lighthouse_contexts.pop(dctx_id) + + # + # if lighthouse was not actually used for this BNDB / session, then + # the lookup will fail as there is nothing to spindown + # + + except KeyError: + return + + # spin down the closing context (stop threads, cleanup qt state, etc) + logger.info("Closing a LighthouseContext...") + lctx.terminate() + #-------------------------------------------------------------------------- # UI Integration (Internal) #-------------------------------------------------------------------------- diff --git a/plugins/lighthouse/integration/binja_loader.py b/plugins/lighthouse/integration/binja_loader.py index 3491c17c..c3b22200 100644 --- a/plugins/lighthouse/integration/binja_loader.py +++ b/plugins/lighthouse/integration/binja_loader.py @@ -30,4 +30,3 @@ except Exception as e: lmsg("Failed to initialize Lighthouse") logger.exception("Exception details:") - diff --git a/plugins/lighthouse/integration/core.py b/plugins/lighthouse/integration/core.py index aec06213..fc2cb551 100644 --- a/plugins/lighthouse/integration/core.py +++ b/plugins/lighthouse/integration/core.py @@ -26,9 +26,9 @@ class LighthouseCore(object): # Plugin Metadata #-------------------------------------------------------------------------- - PLUGIN_VERSION = "0.9.1" + PLUGIN_VERSION = "0.9.2" AUTHORS = "Markus Gaasedelen" - DATE = "2020" + DATE = "2021" #-------------------------------------------------------------------------- # Initialization @@ -87,14 +87,10 @@ def print_banner(self): # build the main banner title banner_params = (self.PLUGIN_VERSION, self.AUTHORS, self.DATE) - banner_title = "Lighthouse v%s - (c) %s - %s" % banner_params + banner_title = "v%s - (c) %s - %s" % banner_params # print plugin banner - lmsg("") - lmsg("-"*75) - lmsg("---[ %s" % banner_title) - lmsg("-"*75) - lmsg("") + lmsg("Loaded %s" % banner_title) #-------------------------------------------------------------------------- # Disassembler / Database Context Selector diff --git a/plugins/lighthouse/integration/ida_loader.py b/plugins/lighthouse/integration/ida_loader.py index 32506397..fde73de6 100644 --- a/plugins/lighthouse/integration/ida_loader.py +++ b/plugins/lighthouse/integration/ida_loader.py @@ -88,7 +88,7 @@ def term(self): except Exception as e: logger.exception("Failed to cleanly unload Lighthouse from IDA.") end = time.time() - print("-"*50) + logger.debug("-"*50) logger.debug("IDA term done... (%.3f seconds...)" % (end-start)) diff --git a/plugins/lighthouse/reader/parsers/drcov.py b/plugins/lighthouse/reader/parsers/drcov.py index cb5d4cf5..64cf6238 100644 --- a/plugins/lighthouse/reader/parsers/drcov.py +++ b/plugins/lighthouse/reader/parsers/drcov.py @@ -192,7 +192,7 @@ 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]: + if not self.module_table_version in [2, 3, 4, 5]: raise ValueError("Unsupported (new?) drcov log format...") # parse module count in table from 'count Y' @@ -310,22 +310,30 @@ def _parse_bb_table_entries(self, f): # parse the plaintext basic block entries one by one else: - text_entry = f.readline().decode('utf-8').strip() + self._parse_bb_table_text_entries(f) - if text_entry != "module id, start, size:": - raise ValueError("Invalid BB header: %r" % text_entry) + def _parse_bb_table_text_entries(self, f): + """ + Parse drcov log basic block table text entries from filestream. + """ + table_header = f.readline().decode('utf-8').strip() - pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-fA-F]+)\,\s*(?P[0-9]+)$") - for bb in self.bbs: - text_entry = f.readline().decode('utf-8').strip() + if table_header != "module id, start, size:": + raise ValueError("Invalid BB header: %r" % table_header) + + pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-fA-F]+)\,\s*(?P[0-9]+)$") + for i, bb in enumerate(self.bbs): + text_entry = f.readline().decode('utf-8').strip() + if not text_entry: + continue - match = pattern.match(text_entry) - if not match: - raise ValueError("Invalid BB entry: %r" % text_entry) + match = pattern.match(text_entry) + if not match: + raise ValueError("Invalid BB entry: %r" % text_entry) - bb.start = int(match.group("start"), 16) - bb.size = int(match.group("size"), 10) - bb.mod_id = int(match.group("mod"), 10) + bb.start = int(match.group("start"), 16) + bb.size = int(match.group("size"), 10) + bb.mod_id = int(match.group("mod"), 10) #------------------------------------------------------------------------------ # drcov module parser @@ -376,6 +384,8 @@ def _parse_module(self, module_line, version): self._parse_module_v3(data) elif version == 4: self._parse_module_v4(data) + elif version == 5: + self._parse_module_v5(data) else: raise ValueError("Unknown module format (v%u)" % version) @@ -436,6 +446,25 @@ def _parse_module_v4(self, data): self.size = self.end-self.base self.filename = os.path.basename(self.path.replace('\\', os.sep)) + def _parse_module_v5(self, data): + """ + Parse a module table v5 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) + self.preferred_base= int(data[6], 16) + if len(data) > 8: # Windows Only + self.checksum = int(data[7], 16) + self.timestamp = int(data[8], 16) + self.path = str(data[-1]) + self.size = self.end-self.base + self.filename = os.path.basename(self.path.replace('\\', os.sep)) + + #------------------------------------------------------------------------------ # drcov basic block parser #------------------------------------------------------------------------------ diff --git a/plugins/lighthouse/reader/parsers/tenet.py b/plugins/lighthouse/reader/parsers/tenet.py new file mode 100644 index 00000000..09a7dcd3 --- /dev/null +++ b/plugins/lighthouse/reader/parsers/tenet.py @@ -0,0 +1,82 @@ +import collections +from ..coverage_file import CoverageFile + +# 'known' instruction pointer labels from Tenet traces +INSTRUCTION_POINTERS = ['EIP', 'RIP', 'PC'] + +class TenetData(CoverageFile): + """ + A Tenet trace log parser. + """ + + def __init__(self, filepath): + self._hitmap = {} + super(TenetData, self).__init__(filepath) + + #-------------------------------------------------------------------------- + # Public + #-------------------------------------------------------------------------- + + def get_addresses(self, module_name=None): + return self._hitmap.keys() + + #-------------------------------------------------------------------------- + # Parsing Routines - Top Level + #-------------------------------------------------------------------------- + + def _parse(self): + """ + Parse absolute instruction addresses from the given Tenet trace. + """ + hitmap = collections.defaultdict(int) + + with open(self.filepath) as f: + + while True: + + # read 128mb chunks of 'lines' from the file + lines = f.readlines(1024 * 1024 * 128) + + # no more lines to process, break + if not lines: + break + + # parse the instruction addresses from lines, into the hitmap + self._process_lines(lines, hitmap) + + # save the hitmap if we completed parsing without crashing + self._hitmap = hitmap + + def _process_lines(self, lines, hitmap): + """ + Parse instruction addresses out of the given text lines. + """ + + for line in lines: + + # split the line (an execution delta) into its individual entries + delta = line.split(",") + + # process each item (a name=value pair) in the execution delta + for item in delta: + + # split name/value pair, and normalize the name for matching + name, value = item.split("=") + name = name.upper() + + # ignore entries that are not the instruction pointer + if not name in INSTRUCTION_POINTERS: + continue + + # save the parsed instruction pointer address to the hitmap + address = int(value, 16) + hitmap[address] += 1 + + # break beacuse we don't expect two IP's on the same line + break + + # continue to the next line + # ... + + # done parsing this chunk of lines + return diff --git a/plugins/lighthouse/ui/coverage_overview.py b/plugins/lighthouse/ui/coverage_overview.py index 51eb1861..75a37fe6 100644 --- a/plugins/lighthouse/ui/coverage_overview.py +++ b/plugins/lighthouse/ui/coverage_overview.py @@ -275,6 +275,22 @@ def eventFilter(self, source, event): if int(event.type()) == self.EventDestroy: source.removeEventFilter(self) + + # + # XXX/V35: This is pretty hacky annoying stuff, but the lifetime + # of the CoverageOverview widget is managed internally by binja + # and gets deleted/cleaned up *after* a database is closed. + # + # it's best we just unload the lighthouse context in binja after + # the UI widgets have been destroyed (which aligns with IDA) + # + + if disassembler.NAME == "BINJA": + lctx = self._target.lctx + core = lctx.core + core.binja_close_context(lctx.dctx) + + # cleanup the UI / qt references for the CoverageOverview elements self._target.terminate() # diff --git a/plugins/lighthouse/ui/coverage_table.py b/plugins/lighthouse/ui/coverage_table.py index edcee16b..ddae1761 100644 --- a/plugins/lighthouse/ui/coverage_table.py +++ b/plugins/lighthouse/ui/coverage_table.py @@ -261,7 +261,12 @@ def _ui_table_ctx_menu_handler(self, position): return # show the popup menu to the user, and wait for their selection - action = ctx_menu.exec_(self.viewport().mapToGlobal(position)) + if USING_PYSIDE6: + exec_func = getattr(ctx_menu, "exec") + else: + exec_func = getattr(ctx_menu, "exec_") + + action = exec_func(self.viewport().mapToGlobal(position)) # process the user action self._process_table_ctx_menu_action(action) @@ -281,7 +286,11 @@ def _ui_header_ctx_menu_handler(self, position): return # show the popup menu to the user, and wait for their selection - action = ctx_menu.exec_(hh.viewport().mapToGlobal(position)) + if USING_PYSIDE6: + exec_func = getattr(ctx_menu, "exec") + else: + exec_func = getattr(ctx_menu, "exec_") + action = exec_func(hh.viewport().mapToGlobal(position)) # process the user action self._process_header_ctx_menu_action(action, column) @@ -728,7 +737,9 @@ def __init__(self, lctx, parent=None): # initialize a monospace font to use for table row / cell text self._entry_font = MonospaceFont() - self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics) + if not USING_PYSIDE6: + #TODO Figure out if this matters? + self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics) self._entry_font.setPointSizeF(normalize_to_dpi(10)) # use the default / system font for the column titles diff --git a/plugins/lighthouse/ui/module_selector.py b/plugins/lighthouse/ui/module_selector.py index 14038789..32b652a0 100644 --- a/plugins/lighthouse/ui/module_selector.py +++ b/plugins/lighthouse/ui/module_selector.py @@ -74,11 +74,11 @@ def _ui_init_header(self): description_text = \ "Lighthouse could not automatically identify the target module in the given coverage file:
" \ "
" \ - "-- Target: %s
" \ - "-- Coverage File: %s
" \ + "-- Target: {0}
" \ + "-- Coverage File: {1}
" \ "
" \ "Please double click the name of the module that matches this database, or close this dialog
" \ - "if you do not see your binary listed in the table below..." % (self._target_name, self._coverage_file) + "if you do not see your binary listed in the table below...".format(self._target_name, self._coverage_file) self._label_description = QtWidgets.QLabel(description_text) self._label_description.setTextFormat(QtCore.Qt.RichText) diff --git a/plugins/lighthouse/util/disassembler/binja_api.py b/plugins/lighthouse/util/disassembler/binja_api.py index d58ea85a..f0998092 100644 --- a/plugins/lighthouse/util/disassembler/binja_api.py +++ b/plugins/lighthouse/util/disassembler/binja_api.py @@ -91,7 +91,7 @@ def _init_version(self): else: # commercial, personal disassembler_version = version_string.split(" ", 1)[0] - major, minor, patch = map(int, disassembler_version.split(".")) + major, minor, patch, *_= disassembler_version.split(".") + ['0'] # save the version number components for later use self._version_major = major @@ -380,7 +380,11 @@ def shouldBeVisible(self, view_frame): if not view_frame: return False - import shiboken2 as shiboken + if USING_PYSIDE6: + import shiboken6 as shiboken + else: + import shiboken2 as shiboken + vf_ptr = shiboken.getCppPointer(view_frame)[0] return self._visible_for_view[vf_ptr] @@ -392,8 +396,13 @@ def notifyViewChanged(self, view_frame): self._active_view = None return - import shiboken2 as shiboken + if USING_PYSIDE6: + import shiboken6 as shiboken + else: + import shiboken2 as shiboken + self._active_view = shiboken.getCppPointer(view_frame)[0] + if self.visible: dock_handler = DockHandler.getActiveDockHandler() dock_handler.setVisible(self.m_name, True) diff --git a/plugins/lighthouse/util/disassembler/ida_api.py b/plugins/lighthouse/util/disassembler/ida_api.py index 46adb893..c1644ddd 100644 --- a/plugins/lighthouse/util/disassembler/ida_api.py +++ b/plugins/lighthouse/util/disassembler/ida_api.py @@ -238,16 +238,30 @@ def _get_ida_bg_color_from_file(self): except OSError: pass - # attempt to parse the user's disassembly background color from the html + # attempt to parse the user's disassembly background color from the html (7.0?) bg_color_text = get_string_between(html, '') if bg_color_text: logger.debug(" - Extracted bgcolor '%s' from regex!" % bg_color_text) return QtGui.QColor(bg_color_text) - # sometimes the above one isn't present... so try this one + # + # sometimes the above one isn't present... so try this one (7.1 - 7.4 maybe?) + # + # TODO: IDA 7.5 says c1 is /* line-fg-default */ ... but it's possible c1 + # had the bg color of the line in other builds of 7.x? I'm not sure but + # this should be double checked at some point and can maybe just be removed + # in favor of c41 (line-bg-default) as that's what we really want + # + bg_color_text = get_string_between(html, '.c1 \{ background-color: ', ';') if bg_color_text: - logger.debug(" - Extracted background-color '%s' from regex!" % bg_color_text) + logger.debug(" - Extracted background-color '%s' from line-fg-default!" % bg_color_text) + return QtGui.QColor(bg_color_text) + + # -- IDA 7.5 says c41 is /* line-bg-default */, a.k.a the bg color for disassembly text + bg_color_text = get_string_between(html, '.c41 \{ background-color: ', ';') + if bg_color_text: + logger.debug(" - Extracted background-color '%s' from line-bg-default!" % bg_color_text) return QtGui.QColor(bg_color_text) logger.debug(" - HTML color regex failed...") diff --git a/plugins/lighthouse/util/qt/shim.py b/plugins/lighthouse/util/qt/shim.py index 2ac1a431..9d7b3b4b 100644 --- a/plugins/lighthouse/util/qt/shim.py +++ b/plugins/lighthouse/util/qt/shim.py @@ -16,18 +16,40 @@ # # this file was critical for retaining compatibility with Qt4 frameworks # used by IDA 6.8/6.95, but it less important now. support for Qt 4 and -# older versions of IDA will be deprecated in Lighthouse v0.9.0 +# older versions of IDA (< 7.0) were deprecated in Lighthouse v0.9.0 # USING_PYQT5 = False USING_PYSIDE2 = False +USING_PYSIDE6 = False + +# +# TODO/QT: This file is getting pretty gross. this whole shim system +# should probably get refactored as I really don't want disassembler +# specific dependencies in here... +# + +try: + import ida_idaapi + USING_IDA = True +except ImportError: + USING_IDA = False + +try: + import binaryninjaui + USING_NEW_BINJA = "qt_major_version" in binaryninjaui.__dict__ and binaryninjaui.qt_major_version == 6 + USING_OLD_BINJA = not(USING_NEW_BINJA) +except ImportError: + USING_NEW_BINJA = False + USING_OLD_BINJA = False #------------------------------------------------------------------------------ # PyQt5 Compatibility #------------------------------------------------------------------------------ -# attempt to load PyQt5 -if QT_AVAILABLE == False: +# attempt to load PyQt5 (IDA 7.0+) +if USING_IDA: + try: import PyQt5.QtGui as QtGui import PyQt5.QtCore as QtCore @@ -45,8 +67,9 @@ # PySide2 Compatibility #------------------------------------------------------------------------------ -# if PyQt5 did not import, try to load PySide -if QT_AVAILABLE == False: +# if PyQt5 did not import, try to load PySide2 (Old Binary Ninja / Cutter) +if not QT_AVAILABLE and USING_OLD_BINJA: + try: import PySide2.QtGui as QtGui import PySide2.QtCore as QtCore @@ -64,3 +87,27 @@ except ImportError: pass +#------------------------------------------------------------------------------ +# PySide6 Compatibility +#------------------------------------------------------------------------------ + +# If all else fails, try to load PySide6 (New Binary Ninja) +if not QT_AVAILABLE and USING_NEW_BINJA: + + try: + import PySide6.QtGui as QtGui + import PySide6.QtCore as QtCore + import PySide6.QtWidgets as QtWidgets + + # alias for less PySide6 <--> PyQt5 shimming + QtCore.pyqtSignal = QtCore.Signal + QtCore.pyqtSlot = QtCore.Slot + QtWidgets.QAction = QtGui.QAction + + # importing went okay, PySide must be available for use + QT_AVAILABLE = True + USING_PYSIDE6 = True + + # import failed. No Qt / UI bindings available... + except ImportError: + pass diff --git a/plugins/lighthouse/util/qt/util.py b/plugins/lighthouse/util/qt/util.py index e87249f4..5b9d01c4 100644 --- a/plugins/lighthouse/util/qt/util.py +++ b/plugins/lighthouse/util/qt/util.py @@ -19,7 +19,7 @@ def MonospaceFont(): Convenience alias for creating a monospace Qt font object. """ font = QtGui.QFont("Courier New") - font.setStyleHint(QtGui.QFont.TypeWriter) + font.setStyleHint(QtGui.QFont.Monospace) return font #------------------------------------------------------------------------------