Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0350b0c538 | |||
| 88051b0652 | |||
| 1d7e4b321c | |||
| 602bb611a4 | |||
| c5cc03d7dc | |||
| 720b036a9d | |||
| b8ff268c86 | |||
| 88ceac7b19 | |||
| 9a56463103 | |||
| 562595be9b | |||
| eeb16a5f81 | |||
| 84eeb210c6 | |||
| f944b62cc3 | |||
| 1b883377d6 | |||
| 146eb4fd2d | |||
| de2704b680 | |||
| 96df2c5a1f | |||
| 9c579c6e9d | |||
| ee7ac30f14 | |||
| 5f8fa02de2 | |||
| 87defff5cd | |||
| e1438159da | |||
| 856768d129 | |||
| 7d67e41773 | |||
| 8f3c23bbb6 | |||
| 4992d4b731 | |||
| 550c476e81 | |||
| bba8d91253 | |||
| 0e0e08e564 | |||
| 6912018b89 | |||
| f4642e8b4b | |||
| 6e1dcb8d40 | |||
| 973b40e6d0 | |||
| e3d636a3f4 | |||
| 7245a2d2c4 | |||
| 701952d83f | |||
| 9ed0ccf528 | |||
| ae1072a04b | |||
| 4c19fee630 | |||
| d02fbf13d9 | |||
| 19eddd6470 | |||
| 09a95061ed | |||
| 94759790de | |||
| 374ca999f8 | |||
| b3c8683110 | |||
| aa71810bf7 | |||
| b0f798a3d9 | |||
| 85b08a9611 | |||
| f2031f921f | |||
| 8e98a74eee | |||
| 85818f5b9b | |||
| 8434c80e18 | |||
| a25e77764a | |||
| 91e427e7ec | |||
| 1bad74fc50 | |||
| 68325095ee | |||
| a50298a9d9 | |||
| 18ea9bbc69 | |||
| 5818ffe3fc | |||
| 5258d583c7 | |||
| 5480afe646 | |||
| 7a1abaeaa1 | |||
| 1cf63ac925 | |||
| 2bb4b9ebf3 |
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017-2020 Markus Gaasedelen
|
||||
Copyright (c) 2017-2024 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
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
# Lighthouse - A Code Coverage Explorer for Reverse Engineers
|
||||
# Lighthouse - A Coverage Explorer for Reverse Engineers
|
||||
|
||||
<p align="center">
|
||||
<img alt="Lighthouse Plugin" src="screenshots/overview.gif"/>
|
||||
</p>
|
||||
|
||||
## 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,22 @@ 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.
|
||||
|
||||
2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
|
||||
## 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 Binary Ninja, supporting v3.5 and newer.
|
||||
|
||||
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.
|
||||
|
||||
# Usage
|
||||
@@ -76,7 +88,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.
|
||||
|
||||
<p align="center">
|
||||
<img alt="Lighthouse Coverage ComboBox" src="screenshots/combobox.gif"/>
|
||||
@@ -84,8 +96,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).
|
||||
|
||||
<p align="center">
|
||||
<img alt="Lighthouse HTML Report" src="screenshots/html_report.gif"/>
|
||||
@@ -183,6 +194,7 @@ Lighthouse will remember your theme preference for future loads and uses.
|
||||
|
||||
Time and motivation permitting, future work may include:
|
||||
|
||||
* Nag Vector35 to fix HLIL highlighting ([bug](https://github.com/Vector35/binaryninja-api/issues/2584)) in Binary Ninja
|
||||
* ~~Asynchronous composition, painting, metadata collection~~
|
||||
* ~~Multifile/coverage support~~
|
||||
* Profiling based heatmaps/painting
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Lighthouse - A Coverage Explorer for Reverse Engineers
|
||||
|
||||
<p align="center">
|
||||
<img alt="Lighthouse Plugin" src="https://raw.githubusercontent.com/gaasedelen/lighthouse/master/screenshots/overview.gif"/>
|
||||
</p>
|
||||
|
||||
## 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.
|
||||
@@ -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 *
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"api": [
|
||||
"python3"
|
||||
],
|
||||
"author": "Markus Gaasedelen",
|
||||
"description": "A Coverage Explorer for Reverse Engineers",
|
||||
"license": {
|
||||
"name": "MIT",
|
||||
"text": "Copyright (c) 2024> 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": 4526,
|
||||
"name": "Lighthouse",
|
||||
"platforms": [
|
||||
"Darwin",
|
||||
"Linux",
|
||||
"Windows"
|
||||
],
|
||||
"pluginmetadataversion": 2,
|
||||
"type": [
|
||||
"helper"
|
||||
],
|
||||
"version": "0.9.4"
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Example usage:
|
||||
|
||||
## Intel Pin
|
||||
|
||||
Using a [custom pintool](coverage/pin) contributed by [Agustin Gianni](https://twitter.com/agustingianni), the Intel Pin DBI can also be used to collect coverage data.
|
||||
Using a [custom pintool](pin/README.md) contributed by [Agustin Gianni](https://twitter.com/agustingianni), the Intel Pin DBI can also be used to collect coverage data.
|
||||
|
||||
Example usage:
|
||||
|
||||
@@ -24,11 +24,11 @@ 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) 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) page. macOS and Linux users need to compile the pintool themselves following the [instructions](pin/README.md#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).
|
||||
Lighthouse offers limited support for Frida based code coverage via a custom [instrumentation script](frida/README.md) contributed by [yrp](https://twitter.com/yrp604).
|
||||
|
||||
Example usage:
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ maps.map(function (e) {
|
||||
var filtered_maps = new ModuleMap(function (m) {
|
||||
if (whitelist.indexOf('all') >= 0) { return true; }
|
||||
|
||||
return whitelist.indexOf(m.name) >= 0;
|
||||
return whitelist.some(item => m.name.toLowerCase().includes(item.toLowerCase()));
|
||||
});
|
||||
|
||||
// This function takes a list of GumCompileEvents and converts it into a DRcov
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using namespace std;
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
#include <string>
|
||||
@@ -46,12 +45,9 @@ static std::string base_name(const std::string& path)
|
||||
}
|
||||
|
||||
// Per thread data structure. This is mainly done to avoid locking.
|
||||
// - Per-thread map of executed basic blocks, and their size.
|
||||
struct ThreadData {
|
||||
// Unique list of hit basic blocks.
|
||||
pintool::unordered_set<ADDRINT> m_block_hit;
|
||||
|
||||
// Map basic a block address to its size.
|
||||
pintool::unordered_map<ADDRINT, uint16_t> m_block_size;
|
||||
pintool::unordered_map<ADDRINT, uint16_t> m_blocks;
|
||||
};
|
||||
|
||||
class ToolContext {
|
||||
@@ -166,24 +162,37 @@ static VOID PIN_FAST_ANALYSIS_CALL OnBasicBlockHit(THREADID tid, ADDRINT addr, U
|
||||
{
|
||||
auto& context = *reinterpret_cast<ToolContext*>(v);
|
||||
ThreadData* data = context.GetThreadLocalData(tid);
|
||||
data->m_block_hit.insert(addr);
|
||||
data->m_block_size[addr] = size;
|
||||
data->m_blocks[addr] = size;
|
||||
PIN_RemoveInstrumentationInRange(addr, addr);
|
||||
}
|
||||
|
||||
// Trace hit event handler.
|
||||
static VOID OnTrace(TRACE trace, VOID* v)
|
||||
{
|
||||
auto& context = *reinterpret_cast<ToolContext*>(v);
|
||||
BBL bbl = TRACE_BblHead(trace);
|
||||
ADDRINT addr = BBL_Address(bbl);
|
||||
|
||||
// Check if the address is inside a white-listed image.
|
||||
if (!context.m_tracing_enabled || !context.m_images->isInterestingAddress(addr))
|
||||
if (!context.m_tracing_enabled || !context.m_images->isInterestingAddress(TRACE_Address(trace)))
|
||||
return;
|
||||
|
||||
// For each basic block in the trace.
|
||||
for (; BBL_Valid(bbl); bbl = BBL_Next(bbl)) {
|
||||
addr = BBL_Address(bbl);
|
||||
auto tid = PIN_ThreadId();
|
||||
ThreadData* data = context.GetThreadLocalData(tid);
|
||||
|
||||
// This trace is getting JIT'd, which implies the head must get executed.
|
||||
auto bbl = TRACE_BblHead(trace);
|
||||
auto addr = BBL_Address(bbl);
|
||||
data->m_blocks[addr] = (uint16_t)BBL_Size(bbl);
|
||||
|
||||
// For each basic block in the trace...
|
||||
for (bbl = BBL_Next(bbl); BBL_Valid(bbl); bbl = BBL_Next(bbl))
|
||||
{
|
||||
|
||||
// Ignore blocks that have already been marked as executed in the past...
|
||||
ADDRINT addr = BBL_Address(bbl);
|
||||
if (data->m_blocks.find(addr) != data->m_blocks.end())
|
||||
continue;
|
||||
|
||||
// Instrument blocks that have not yet been executed (at least... by this thread).
|
||||
BBL_InsertCall(bbl, IPOINT_ANYWHERE, (AFUNPTR)OnBasicBlockHit,
|
||||
IARG_FAST_ANALYSIS_CALL,
|
||||
IARG_THREAD_ID,
|
||||
@@ -192,6 +201,7 @@ static VOID OnTrace(TRACE trace, VOID* v)
|
||||
IARG_PTR, v,
|
||||
IARG_END);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Program finish event handler.
|
||||
@@ -219,7 +229,7 @@ static VOID OnFini(INT32 code, VOID* v)
|
||||
// Count the global number of basic blocks.
|
||||
size_t number_of_bbs = 0;
|
||||
for (const auto& data : context.m_terminated_threads) {
|
||||
number_of_bbs += data->m_block_hit.size();
|
||||
number_of_bbs += data->m_blocks.size();
|
||||
}
|
||||
|
||||
context.m_trace->write_string("BB Table: %u bbs\n", number_of_bbs);
|
||||
@@ -233,7 +243,8 @@ static VOID OnFini(INT32 code, VOID* v)
|
||||
drcov_bb tmp;
|
||||
|
||||
for (const auto& data : context.m_terminated_threads) {
|
||||
for (const auto& address : data->m_block_hit) {
|
||||
for (const auto& block : data->m_blocks) {
|
||||
auto address = block.first;
|
||||
auto it = std::find_if(context.m_loaded_images.begin(), context.m_loaded_images.end(), [&address](const LoadedImage& image) {
|
||||
return address >= image.low_ && address < image.high_;
|
||||
});
|
||||
@@ -243,7 +254,7 @@ static VOID OnFini(INT32 code, VOID* v)
|
||||
|
||||
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];
|
||||
tmp.size = data->m_blocks[address];
|
||||
|
||||
context.m_trace->write_binary(&tmp, sizeof(tmp));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using namespace std;
|
||||
#include "ImageManager.h"
|
||||
#include "pin.H"
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# CodeCoverage Pintool
|
||||
|
||||
The `CodeCoverage` pintool runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework and collects code coverage data in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). The log produced by this pintool emulates that of [drcov](http://dynamorio.org/docs/page_drcov.html) as shipped with [DynamoRIO](http://www.dynamorio.org).
|
||||
The `CodeCoverage` pintool runs ontop of the [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) DBI framework and collects code coverage data in a log format compatible with [Lighthouse](https://github.com/gaasedelen/lighthouse). The log produced by this pintool emulates that of [drcov](http://dynamorio.org/docs/page_drcov.html) as shipped with [DynamoRIO](http://www.dynamorio.org).
|
||||
|
||||
This pintool is labeled only as a prototype.
|
||||
|
||||
@@ -12,7 +12,7 @@ Follow the build instructions below for your respective platform.
|
||||
|
||||
## Building for MacOS or Linux
|
||||
|
||||
On MacOS or Liunux, one can compile the pintool using the following commands.
|
||||
On MacOS or Linux, one can compile the pintool using the following commands.
|
||||
|
||||
```
|
||||
# Location of this repo / pintool source
|
||||
@@ -39,7 +39,11 @@ Launch a command prompt and build the pintool with the following commands.
|
||||
### 32bit Pintool
|
||||
|
||||
```
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86
|
||||
REM If you are on VS 2022 or so you can run this line:
|
||||
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x86
|
||||
|
||||
REM VS 2015 or so you can run this line instead:
|
||||
REM "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86
|
||||
|
||||
REM Location of this repo / pintool source
|
||||
cd C:\Users\user\lighthouse\coverage\pin
|
||||
@@ -53,7 +57,11 @@ build-x86.bat
|
||||
### 64bit Pintool
|
||||
|
||||
```
|
||||
"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64
|
||||
REM If you are on VS 2022 or so you can run this line:
|
||||
"C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x86_amd64
|
||||
|
||||
REM VS 2015 or so you can run this line instead:
|
||||
REM "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" x86_amd64
|
||||
|
||||
REM Location of this repo / pintool source
|
||||
cd C:\Users\user\lighthouse\coverage\pin
|
||||
@@ -64,7 +72,7 @@ set PATH=%PATH%;%PIN_ROOT%
|
||||
build-x64.bat
|
||||
```
|
||||
|
||||
The resulting binaries will be labaled based on their architecture (eg, 64 is the 64bit pintool).
|
||||
The resulting binaries will be labeled based on their architecture (eg, 64 is the 64bit pintool).
|
||||
|
||||
* CodeCoverage.dll
|
||||
* CodeCoverage64.dll
|
||||
|
||||
@@ -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 pinipc.lib pincrt.lib kernel32.lib crtbeginS.obj ^
|
||||
/NODEFAULTLIB ^
|
||||
/MANIFEST:NO ^
|
||||
/OPT:NOREF ^
|
||||
|
||||
@@ -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 pinipc.lib pincrt.lib kernel32.lib crtbeginS.obj ^
|
||||
/NODEFAULTLIB ^
|
||||
/MANIFEST:NO ^
|
||||
/OPT:NOREF ^
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
|
||||
#
|
||||
# this global is used to indicate whether Qt bindings for python are present
|
||||
# and available for use by Lighthouse.
|
||||
#
|
||||
|
||||
QT_AVAILABLE = False
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PyQt5 <--> PySide2 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
#
|
||||
# we use this file to shim/re-alias a few Qt API's to ensure compatibility
|
||||
# between the popular Qt frameworks. these shims serve to reduce the number
|
||||
# of compatibility checks in the plugin code that consumes them.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
|
||||
USING_PYQT5 = False
|
||||
USING_PYSIDE2 = False
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PyQt5 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# attempt to load PyQt5
|
||||
if QT_AVAILABLE == False:
|
||||
try:
|
||||
import PyQt5.QtGui as QtGui
|
||||
import PyQt5.QtCore as QtCore
|
||||
import PyQt5.QtWidgets as QtWidgets
|
||||
|
||||
# importing went okay, PyQt5 must be available for use
|
||||
QT_AVAILABLE = True
|
||||
USING_PYQT5 = True
|
||||
|
||||
# import failed, PyQt5 is not available
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PySide2 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# if PyQt5 did not import, try to load PySide
|
||||
if QT_AVAILABLE == False:
|
||||
try:
|
||||
import PySide2.QtGui as QtGui
|
||||
import PySide2.QtCore as QtCore
|
||||
import PySide2.QtWidgets as QtWidgets
|
||||
|
||||
# alias for less PySide2 <--> PyQt5 shimming
|
||||
QtCore.pyqtSignal = QtCore.Signal
|
||||
QtCore.pyqtSlot = QtCore.Slot
|
||||
|
||||
# importing went okay, PySide must be available for use
|
||||
QT_AVAILABLE = True
|
||||
USING_PYSIDE2 = True
|
||||
|
||||
# import failed. No Qt / UI bindings available...
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
@@ -1045,7 +1045,7 @@ class ComposingLine(QtWidgets.QPlainTextEdit):
|
||||
# set the height of the textbox based on some arbitrary math :D
|
||||
LINE_PADDING = self.document().documentMargin()*2
|
||||
line_height = self._font_metrics.height() + LINE_PADDING + 2
|
||||
self.setFixedHeight(line_height)
|
||||
self.setFixedHeight(int(line_height))
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# QPlainTextEdit Overloads
|
||||
@@ -66,6 +66,8 @@ class LighthouseContext(object):
|
||||
"""
|
||||
Spin down any session subsystems before the session is deleted.
|
||||
"""
|
||||
if not self._started:
|
||||
return
|
||||
self.painter.terminate()
|
||||
self.director.terminate()
|
||||
self.metadata.terminate()
|
||||
@@ -2,12 +2,11 @@ import os
|
||||
import time
|
||||
import logging
|
||||
import weakref
|
||||
import datetime
|
||||
import itertools
|
||||
import collections
|
||||
|
||||
from lighthouse.util import *
|
||||
from lighthouse.util.qt import compute_color_on_gradiant
|
||||
from lighthouse.util.qt import compute_color_on_gradient
|
||||
from lighthouse.metadata import DatabaseMetadata
|
||||
|
||||
logger = logging.getLogger("Lighthouse.Coverage")
|
||||
@@ -97,7 +96,7 @@ class DatabaseCoverage(object):
|
||||
# the addresses executed in the coverage log
|
||||
#
|
||||
|
||||
self._hitmap = build_hitmap(data)
|
||||
self._hitmap = collections.Counter(data)
|
||||
self._imagebase = BADADDR
|
||||
|
||||
#
|
||||
@@ -140,9 +139,7 @@ class DatabaseCoverage(object):
|
||||
# initially, all loaded coverage data is marked as unmapped
|
||||
#
|
||||
|
||||
self._unmapped_data = set(self._hitmap.keys())
|
||||
self._unmapped_data.add(BADADDR)
|
||||
self._misaligned_data = set()
|
||||
self.unmapped_addresses = set(self._hitmap.keys())
|
||||
|
||||
#
|
||||
# at runtime, the map_coverage() member function of this class is
|
||||
@@ -166,9 +163,14 @@ class DatabaseCoverage(object):
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instruction_percent = 0.0
|
||||
|
||||
# blocks that have not been fully executed (eg, crash / exception)
|
||||
self.partial_nodes = set()
|
||||
self.partial_instructions = set()
|
||||
|
||||
# addresses that have been executed, but are not in a defined node
|
||||
self.orphan_addresses = set()
|
||||
|
||||
#
|
||||
# we instantiate a single weakref of ourself (the DatbaseCoverage
|
||||
# object) such that we can distribute it to the children we create
|
||||
@@ -191,7 +193,7 @@ class DatabaseCoverage(object):
|
||||
@property
|
||||
def coverage(self):
|
||||
"""
|
||||
Return the instruction-level coverage bitmap/mask.
|
||||
Return the coverage (address) bitmap/mask.
|
||||
"""
|
||||
return viewkeys(self._hitmap)
|
||||
|
||||
@@ -266,6 +268,7 @@ class DatabaseCoverage(object):
|
||||
|
||||
if self._imagebase == BADADDR:
|
||||
self._imagebase = self._metadata.imagebase
|
||||
self._normalize_coverage()
|
||||
|
||||
#
|
||||
# if the imagebase for this coverage exists, then it is susceptible to
|
||||
@@ -301,9 +304,6 @@ class DatabaseCoverage(object):
|
||||
# update the coverage hash incase the hitmap changed
|
||||
self._update_coverage_hash()
|
||||
|
||||
# dump the unmappable coverage data
|
||||
#self.dump_unmapped()
|
||||
|
||||
def refresh_theme(self):
|
||||
"""
|
||||
Refresh UI facing elements to reflect the current theme.
|
||||
@@ -311,7 +311,7 @@ class DatabaseCoverage(object):
|
||||
Does not require @disassembler.execute_ui decorator as no Qt is touched.
|
||||
"""
|
||||
for function in self.functions.values():
|
||||
function.coverage_color = compute_color_on_gradiant(
|
||||
function.coverage_color = compute_color_on_gradient(
|
||||
function.instruction_percent,
|
||||
self.palette.table_coverage_bad,
|
||||
self.palette.table_coverage_good
|
||||
@@ -390,7 +390,7 @@ class DatabaseCoverage(object):
|
||||
self._update_coverage_hash()
|
||||
|
||||
# mark these touched addresses as dirty
|
||||
self._unmapped_data |= viewkeys(data)
|
||||
self.unmapped_addresses |= viewkeys(data)
|
||||
|
||||
def add_addresses(self, addresses, update=True):
|
||||
"""
|
||||
@@ -409,7 +409,7 @@ class DatabaseCoverage(object):
|
||||
self._update_coverage_hash()
|
||||
|
||||
# mark these touched addresses as dirty
|
||||
self._unmapped_data |= set(addresses)
|
||||
self.unmapped_addresses |= set(addresses)
|
||||
|
||||
def subtract_data(self, data):
|
||||
"""
|
||||
@@ -468,6 +468,60 @@ class DatabaseCoverage(object):
|
||||
# Coverage Mapping
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def _normalize_coverage(self):
|
||||
"""
|
||||
Normalize basic block coverage into instruction coverage.
|
||||
|
||||
TODO: It would be interesting if we could do away with this entirely,
|
||||
working off the original instruction/bb coverage data (hitmap) instead.
|
||||
"""
|
||||
coverage_addresses = viewkeys(self._hitmap)
|
||||
if not coverage_addresses:
|
||||
return
|
||||
|
||||
# bucketize the exploded coverage addresses
|
||||
instructions = coverage_addresses & self._metadata.instructions
|
||||
basic_blocks = instructions & viewkeys(self._metadata.nodes)
|
||||
|
||||
#
|
||||
# here we attempt to compute the ratio between basic block addresses,
|
||||
# and instruction addresses in the incoming coverage data.
|
||||
#
|
||||
# this will help us determine if the existing instruction data is
|
||||
# sufficient, or whether we need to explode/flatten the basic block
|
||||
# addresses into their respective child instructions
|
||||
#
|
||||
|
||||
block_ratio = len(basic_blocks) / float(len(instructions))
|
||||
block_trace_confidence = 0.80
|
||||
logger.debug("Block confidence %f" % block_ratio)
|
||||
|
||||
#
|
||||
# a low basic block to instruction ratio implies the data is probably
|
||||
# from an instruction trace, or a drcov trace that was exploded from
|
||||
# (bb_address, size) into its respective addresses
|
||||
#
|
||||
|
||||
if block_ratio < block_trace_confidence:
|
||||
return
|
||||
|
||||
#
|
||||
# take each basic block address, and explode it into a list of all the
|
||||
# instruction addresses contained within the basic block as determined
|
||||
# by the database metadata cache
|
||||
#
|
||||
# it is *possible* that this may introduce 'inaccurate' paint should
|
||||
# the user provide a basic block trace that crashes mid-block. but
|
||||
# that is not something we can account for in a block trace...
|
||||
#
|
||||
|
||||
for bb_address in basic_blocks:
|
||||
bb_hits = self._hitmap[bb_address]
|
||||
for inst_address in self._metadata.nodes[bb_address].instructions:
|
||||
self._hitmap[inst_address] = bb_hits
|
||||
|
||||
logger.debug("Converted basic block trace to instruction trace...")
|
||||
|
||||
def _map_coverage(self):
|
||||
"""
|
||||
Map loaded coverage data to the underlying database metadata.
|
||||
@@ -480,10 +534,11 @@ class DatabaseCoverage(object):
|
||||
"""
|
||||
Map loaded coverage data to database defined nodes (basic blocks).
|
||||
"""
|
||||
db_metadata = self._metadata
|
||||
dirty_nodes = {}
|
||||
|
||||
# the coverage data we will attempt to process in this function
|
||||
coverage_addresses = collections.deque(sorted(self._unmapped_data))
|
||||
coverage_addresses = sorted(self.unmapped_addresses)
|
||||
|
||||
#
|
||||
# the loop below is the core of our coverage mapping process.
|
||||
@@ -501,23 +556,27 @@ class DatabaseCoverage(object):
|
||||
# speed. please be careful if you wish to modify it...
|
||||
#
|
||||
|
||||
while coverage_addresses:
|
||||
i, num_addresses = 0, len(coverage_addresses)
|
||||
|
||||
while i < num_addresses:
|
||||
|
||||
# get the next coverage address to map
|
||||
address = coverage_addresses.popleft()
|
||||
address = coverage_addresses[i]
|
||||
|
||||
# get the node (basic block) metadata that this address falls in
|
||||
node_metadata = self._metadata.get_node(address)
|
||||
node_metadata = db_metadata.get_node(address)
|
||||
|
||||
#
|
||||
# should we fail to locate node metadata for the coverage address
|
||||
# that we are trying to map, then the address must not fall inside
|
||||
# of a defined function.
|
||||
#
|
||||
# in this case, the coverage address will remain unmapped...
|
||||
# of a defined function
|
||||
#
|
||||
|
||||
if not node_metadata:
|
||||
self.orphan_addresses.add(address)
|
||||
if address in db_metadata.instructions:
|
||||
self.unmapped_addresses.discard(address)
|
||||
i += 1
|
||||
continue
|
||||
|
||||
#
|
||||
@@ -540,6 +599,10 @@ class DatabaseCoverage(object):
|
||||
node_coverage = NodeCoverage(node_metadata.address, self._weak_self)
|
||||
self.nodes[node_metadata.address] = node_coverage
|
||||
|
||||
# alias for speed, prior to looping
|
||||
node_start = node_metadata.address
|
||||
node_end = node_start + node_metadata.size
|
||||
|
||||
#
|
||||
# the loop below is as an inlined fast-path that assumes the next
|
||||
# several coverage addresses will likely belong to the same node
|
||||
@@ -552,29 +615,32 @@ class DatabaseCoverage(object):
|
||||
while 1:
|
||||
|
||||
#
|
||||
# map the hitmap data for the current address (an instruction)
|
||||
# to this NodeCoverage and mark the instruction as mapped by
|
||||
# discarding its address from the unmapped data list
|
||||
# map the hitmap data for the current address if it falls on
|
||||
# an actual instruction start within the node
|
||||
#
|
||||
# if the address falls within an instruction, it will just be
|
||||
# 'ignored', remaining in the 'unmapped' / invisible data
|
||||
#
|
||||
|
||||
node_coverage.executed_instructions[address] = self._hitmap[address]
|
||||
self._unmapped_data.discard(address)
|
||||
if address in node_metadata.instructions:
|
||||
node_coverage.executed_instructions[address] = self._hitmap[address]
|
||||
self.unmapped_addresses.discard(address)
|
||||
|
||||
# get the next address to attempt mapping on
|
||||
try:
|
||||
address = coverage_addresses.popleft()
|
||||
i += 1
|
||||
address = coverage_addresses[i]
|
||||
|
||||
# an IndexError implies there is nothing left to map...
|
||||
except IndexError:
|
||||
break;
|
||||
break
|
||||
|
||||
#
|
||||
# if the next address is not in this node, it's time break out
|
||||
# of this loop and send it through the full node lookup path
|
||||
#
|
||||
|
||||
if not (address in node_metadata.instructions):
|
||||
coverage_addresses.appendleft(address)
|
||||
if not (node_start <= address < node_end):
|
||||
break
|
||||
|
||||
# the node was updated, so save its coverage as dirty
|
||||
@@ -642,27 +708,10 @@ class DatabaseCoverage(object):
|
||||
self.functions = {}
|
||||
self.partial_nodes = set()
|
||||
self.partial_instructions = set()
|
||||
self._misaligned_data = set()
|
||||
self.orphan_addresses = set()
|
||||
|
||||
# dump the source coverage data back into an 'unmapped' state
|
||||
self._unmapped_data = set(self._hitmap.keys())
|
||||
self._unmapped_data.add(BADADDR)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Debug
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def dump_unmapped(self):
|
||||
"""
|
||||
Dump the unmapped coverage data.
|
||||
"""
|
||||
lmsg("Unmapped coverage data for %s:" % self.name)
|
||||
if len(self._unmapped_data) == 1: # 1 is going to be BADADDR
|
||||
lmsg(" * (there is no unmapped data!)")
|
||||
return
|
||||
|
||||
for address in self._unmapped_data:
|
||||
lmsg(" * 0x%X" % address)
|
||||
self.unmapped_addresses = set(self._hitmap.keys())
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Function Coverage
|
||||
@@ -749,7 +798,7 @@ class FunctionCoverage(object):
|
||||
self.executions = float(node_sum) / function_metadata.node_count
|
||||
|
||||
# bake colors
|
||||
self.coverage_color = compute_color_on_gradiant(
|
||||
self.coverage_color = compute_color_on_gradient(
|
||||
self.instruction_percent,
|
||||
self.database.palette.table_coverage_bad,
|
||||
self.database.palette.table_coverage_good
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
import string
|
||||
import logging
|
||||
import threading
|
||||
import traceback
|
||||
import collections
|
||||
|
||||
from lighthouse.util.misc import *
|
||||
@@ -67,6 +66,7 @@ class CoverageDirector(object):
|
||||
# the coverage file parser
|
||||
self.reader = CoverageReader()
|
||||
self._target_whitelist = []
|
||||
self.suppressed_errors = set()
|
||||
|
||||
# the name of the active coverage
|
||||
self.coverage_name = NEW_COMPOSITION
|
||||
@@ -337,7 +337,7 @@ class CoverageDirector(object):
|
||||
"""
|
||||
Subscribe a callback for director refresh events.
|
||||
"""
|
||||
register_callback(self._refreshed_callbacks, callback)
|
||||
return register_callback(self._refreshed_callbacks, callback)
|
||||
|
||||
def _notify_refreshed(self):
|
||||
"""
|
||||
@@ -345,6 +345,13 @@ class CoverageDirector(object):
|
||||
"""
|
||||
notify_callback(self._refreshed_callbacks)
|
||||
|
||||
def unregister_refreshed(self, callback_ref):
|
||||
"""
|
||||
Unsubscribe a callback reference from director refresh events.
|
||||
"""
|
||||
|
||||
unregister_callback(self._refreshed_callbacks, callback_ref)
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Batch Loading
|
||||
#----------------------------------------------------------------------
|
||||
@@ -380,6 +387,9 @@ class CoverageDirector(object):
|
||||
errors = collections.defaultdict(list)
|
||||
aggregate_addresses = set()
|
||||
|
||||
# unsupress NO_COVERAGE_ERROR per-load, instead of per-session
|
||||
self.suppressed_errors.discard(CoverageMissingError)
|
||||
|
||||
start = time.time()
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
@@ -413,9 +423,8 @@ class CoverageDirector(object):
|
||||
if not aggregate_addresses:
|
||||
return (None, errors)
|
||||
|
||||
# optimize the aggregated data (once) and save it to the director
|
||||
coverage_data = self._optimize_coverage_data(aggregate_addresses)
|
||||
coverage = self.create_coverage(batch_name, coverage_data)
|
||||
# save the batched coverage data to the director
|
||||
coverage = self.create_coverage(batch_name, aggregate_addresses)
|
||||
|
||||
# evaluate coverage
|
||||
if not coverage.nodes:
|
||||
@@ -439,6 +448,9 @@ class CoverageDirector(object):
|
||||
errors = collections.defaultdict(list)
|
||||
all_coverage = []
|
||||
|
||||
# unsupress NO_COVERAGE_ERROR per-load, instead of per-session
|
||||
self.suppressed_errors.discard(CoverageMissingError)
|
||||
|
||||
start = time.time()
|
||||
#----------------------------------------------------------------------
|
||||
|
||||
@@ -465,7 +477,6 @@ class CoverageDirector(object):
|
||||
try:
|
||||
coverage_file = self.reader.open(filepath)
|
||||
coverage_addresses = self._extract_coverage_data(coverage_file)
|
||||
coverage_data = self._optimize_coverage_data(coverage_addresses)
|
||||
|
||||
# save and suppress warnings generated from loading coverage files
|
||||
except CoverageParsingError as e:
|
||||
@@ -477,18 +488,18 @@ class CoverageDirector(object):
|
||||
errors[CoverageMissingError].append(CoverageMissingError(filepath))
|
||||
continue
|
||||
|
||||
# save the attribution data for this coverage data
|
||||
for address in coverage_data:
|
||||
if address in self.metadata.nodes:
|
||||
self.owners[address].add(filepath)
|
||||
|
||||
#
|
||||
# request a name for the new coverage mapping that the director will
|
||||
# generate from the loaded coverage data
|
||||
#
|
||||
|
||||
coverage_name = self._suggest_coverage_name(filepath)
|
||||
coverage = self.create_coverage(coverage_name, coverage_data, filepath)
|
||||
coverage = self.create_coverage(coverage_name, coverage_addresses, filepath)
|
||||
|
||||
# save the attribution data for this coverage data
|
||||
for address in coverage.data:
|
||||
if address in self.metadata.nodes: # TODO/UNMAPPED: support right click unmapped addrs
|
||||
self.owners[address].add(filepath)
|
||||
|
||||
# evaluate coverage
|
||||
if not coverage.nodes:
|
||||
@@ -541,16 +552,39 @@ class CoverageDirector(object):
|
||||
if not module_name and coverage_file.modules:
|
||||
|
||||
#
|
||||
# if the user closes the dialog without selecting a name, there's
|
||||
# nothing we can do for them ...
|
||||
# earlier in this load, the user opted to ignore future attempts
|
||||
# to alias or select coverage data. this is useful when trying to
|
||||
# load a batch of coverage files, where some coverage files
|
||||
# contain data, but none relevant to this database.
|
||||
#
|
||||
|
||||
if CoverageMissingError in self.suppressed_errors:
|
||||
return []
|
||||
|
||||
#
|
||||
# show the module selection dialog to the user, and wait for them
|
||||
# to select something, or close the dialog
|
||||
#
|
||||
|
||||
dialog = ModuleSelector(database_target, coverage_file.modules, coverage_file.filepath)
|
||||
if not dialog.exec_():
|
||||
return [] # no coverage data extracted ...
|
||||
result = dialog.exec_()
|
||||
|
||||
# check if the user opted to ignore future warnings for missing coverage
|
||||
if dialog.ignore_missing:
|
||||
self.suppressed_errors.add(CoverageMissingError)
|
||||
|
||||
#
|
||||
# if the user closed the dialog without selecting a name, there's
|
||||
# nothing we can do for them. return an empty set of coverage data
|
||||
#
|
||||
|
||||
if not result:
|
||||
return []
|
||||
|
||||
# the user selected a module name! use that to extract coverage
|
||||
module_name = dialog.selected_name
|
||||
|
||||
# the user opted to save the selected name as an 'alias'
|
||||
if dialog.remember_alias:
|
||||
self._target_whitelist.append(module_name)
|
||||
|
||||
@@ -591,81 +625,6 @@ class CoverageDirector(object):
|
||||
# well, this one is probably the fault of the CoverageFile author...
|
||||
raise NotImplementedError("Incomplete CoverageFile implementation")
|
||||
|
||||
def _optimize_coverage_data(self, coverage_addresses):
|
||||
"""
|
||||
Optimize exploded coverage data to the current metadata cache.
|
||||
"""
|
||||
logger.debug("Optimizing coverage data...")
|
||||
addresses = set(coverage_addresses)
|
||||
|
||||
# bucketize the exploded coverage addresses
|
||||
instructions = addresses & set(self.metadata.instructions)
|
||||
basic_blocks = instructions & viewkeys(self.metadata.nodes)
|
||||
|
||||
if not instructions:
|
||||
logger.debug("No mappable instruction addresses in coverage data")
|
||||
return []
|
||||
|
||||
"""
|
||||
#
|
||||
# TODO/LOADING: display undefined/misaligned data somehow?
|
||||
#
|
||||
|
||||
unknown = addresses - instructions
|
||||
|
||||
# bucketize the uncategorized exploded addresses
|
||||
undefined, misaligned = [], []
|
||||
for address in unknown:
|
||||
|
||||
# size == -1 (undefined inst)
|
||||
if self.metadata.get_instruction_size(address):
|
||||
undefined.append(address)
|
||||
|
||||
# size == 0 (misaligned inst)
|
||||
else:
|
||||
misaligned.append(address)
|
||||
"""
|
||||
|
||||
#
|
||||
# here we attempt to compute the ratio between basic block addresses,
|
||||
# and instruction addresses in the incoming coverage data.
|
||||
#
|
||||
# this will help us determine if the existing instruction data is
|
||||
# sufficient, or whether we need to explode/flatten the basic block
|
||||
# addresses into their respective child instructions
|
||||
#
|
||||
|
||||
block_ratio = len(basic_blocks) / float(len(instructions))
|
||||
block_trace_confidence = 0.90
|
||||
logger.debug("Block confidence %f" % block_ratio)
|
||||
|
||||
#
|
||||
# a low basic block to instruction ratio implies the data is probably
|
||||
# from an instruction trace, or a basic block trace has been flattened
|
||||
# exploded already (eg, a drcov log)
|
||||
#
|
||||
|
||||
if block_ratio < block_trace_confidence:
|
||||
logger.debug("Optimized as instruction trace...")
|
||||
return list(instructions)
|
||||
|
||||
#
|
||||
# take each basic block address, and explode it into a list of all the
|
||||
# instruction addresses contained within the basic block as determined
|
||||
# by the database metadata cache
|
||||
#
|
||||
# it is *possible* that this may introduce 'inaccurate' paint should
|
||||
# the user provide a basic block trace that crashes mid-block. but
|
||||
# that is not something we can account for in a block trace...
|
||||
#
|
||||
|
||||
block_instructions = set([])
|
||||
for address in basic_blocks:
|
||||
block_instructions |= set(self.metadata.nodes[address].instructions)
|
||||
|
||||
logger.debug("Optimized as basic block trace...")
|
||||
return list(block_instructions | instructions)
|
||||
|
||||
def _suggest_coverage_name(self, filepath):
|
||||
"""
|
||||
Return a suggested coverage name for the given filepath.
|
||||
@@ -713,13 +672,49 @@ class CoverageDirector(object):
|
||||
"""
|
||||
target_name = target_name.lower()
|
||||
|
||||
#
|
||||
# 0. Pre-process module names, strip filepath if present
|
||||
#
|
||||
|
||||
clean_module_names = {}
|
||||
for module_name_raw in coverage_file.modules:
|
||||
|
||||
# trim 'path' from a 'module name' entry... if present (uncommon)
|
||||
module_name = os.path.basename(module_name_raw)
|
||||
|
||||
#
|
||||
# if this triggers, it's probably because the coverage file is
|
||||
# using full filepaths for 'module names', and that there was
|
||||
# two unique filepaths with the same module name, eg:
|
||||
#
|
||||
# - C:\foo.dll
|
||||
# - C:\bar\foo.dll
|
||||
#
|
||||
# this should be super rare, but we'll just revert to using the
|
||||
# full / unprocessed paths and bail...
|
||||
#
|
||||
|
||||
if module_name in clean_module_names:
|
||||
clean_module_names = {name: name for name in coverage_file.modules}
|
||||
break
|
||||
|
||||
clean_module_names[module_name] = module_name_raw
|
||||
|
||||
#
|
||||
# 1. exact, case-insensitive filename matching
|
||||
#
|
||||
|
||||
for module_name in coverage_file.modules:
|
||||
for module_name in clean_module_names:
|
||||
if target_name == module_name.lower():
|
||||
return module_name
|
||||
return clean_module_names[module_name]
|
||||
|
||||
#
|
||||
# 2. exact, case-insensitive filename matching
|
||||
#
|
||||
|
||||
for module_name in clean_module_names:
|
||||
if target_name == module_name.lower():
|
||||
return clean_module_names[module_name]
|
||||
|
||||
#
|
||||
# 2. cleave the extension from the target module name (the source)
|
||||
@@ -727,9 +722,9 @@ class CoverageDirector(object):
|
||||
#
|
||||
|
||||
target_name, extension = os.path.splitext(target_name)
|
||||
for module_name in coverage_file.modules:
|
||||
for module_name in clean_module_names:
|
||||
if target_name == module_name.lower():
|
||||
return module_name
|
||||
return clean_module_names[module_name]
|
||||
|
||||
# too risky to do fuzzy matching on short names...
|
||||
if len(target_name) < 6:
|
||||
@@ -737,13 +732,13 @@ class CoverageDirector(object):
|
||||
|
||||
#
|
||||
# 3. try to match *{target_name}*{extension} in module_name, assuming
|
||||
# target_name is more than 6 characters and there is no othe ambiguity
|
||||
# target_name is more than 6 characters and there is no other ambiguity
|
||||
#
|
||||
|
||||
possible_names = []
|
||||
for module_name in coverage_file.modules:
|
||||
for module_name in clean_module_names:
|
||||
if target_name in module_name.lower() and extension in module_name.lower():
|
||||
possible_names.append(module_name)
|
||||
possible_names.append(clean_module_names[module_name])
|
||||
|
||||
# there were no matches on the wildcarding, so we're done
|
||||
if not possible_names:
|
||||
@@ -765,7 +760,7 @@ class CoverageDirector(object):
|
||||
|
||||
def get_address_coverage(self, address):
|
||||
"""
|
||||
Return a list of coverage object containing the given address.
|
||||
Return a list of database coverage objects containing the given address.
|
||||
"""
|
||||
found = []
|
||||
|
||||
@@ -986,12 +981,6 @@ class CoverageDirector(object):
|
||||
|
||||
return "%s - %s%% - %s" % (symbol, percent_str, coverage_name)
|
||||
|
||||
def dump_unmapped(self):
|
||||
"""
|
||||
Dump the unmapped coverage data for the active set.
|
||||
"""
|
||||
self.coverage.dump_unmapped()
|
||||
|
||||
#----------------------------------------------------------------------
|
||||
# Aliases
|
||||
#----------------------------------------------------------------------
|
||||
@@ -112,7 +112,7 @@ class CoverageMappingSuspicious(CoverageException):
|
||||
# UI Warnings
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
def warn_errors(errors):
|
||||
def warn_errors(errors, ignore=[]):
|
||||
"""
|
||||
Warn the user of any encountered errors with a messagebox.
|
||||
"""
|
||||
@@ -131,6 +131,10 @@ def warn_errors(errors):
|
||||
for error in error_list:
|
||||
lmsg(" - %s" % error.filepath)
|
||||
|
||||
# suppress popups for certain errors, if the user has specified such
|
||||
if error_type in ignore:
|
||||
continue
|
||||
|
||||
#
|
||||
# popup a more verbose error messagebox for the user to read regarding
|
||||
# this class of error they encountered
|
||||
@@ -25,6 +25,8 @@ class LighthouseBinja(LighthouseCore):
|
||||
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)
|
||||
|
||||
@@ -54,7 +56,7 @@ class LighthouseBinja(LighthouseCore):
|
||||
# starts trying to use lighthouse for their session.
|
||||
#
|
||||
# so we initialize the lighthouse context (with start()) on the
|
||||
# second context request which will go throught the else block
|
||||
# second context request which will go through the else block
|
||||
# below... any subsequent call to start() is effectively a nop!
|
||||
#
|
||||
|
||||
@@ -65,21 +67,52 @@ class LighthouseBinja(LighthouseCore):
|
||||
# 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)
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
#
|
||||
# TODO / HACK / XXX / V35: Some of Binja's UI elements (such as the
|
||||
# TODO / HACK / XXX / V35 / 2021: Some of Binja's UI elements (such as the
|
||||
# terminal) do not get assigned a BV, even if there is only one open.
|
||||
#
|
||||
# this is problematic, because if the user 'clicks' onto the termial, and
|
||||
# this is problematic, because if the user 'clicks' onto the terminal, and
|
||||
# then tries to execute our UIActions (like 'Load Coverage File'), the
|
||||
# given 'context.binaryView' will be None
|
||||
#
|
||||
# in the meantime, we have to use this workaround that will try to grab
|
||||
# the 'current' bv from the dock. this is not ideal, but it will suffice.
|
||||
#
|
||||
# -----------------
|
||||
#
|
||||
# XXX: It's now 2024, Binja's UI / API stack has grown a lot. it's more
|
||||
# powerful and a bunch of the oddities / hacks lighthouse employed for
|
||||
# binja may no longer apply. this whole file should probably be revisited
|
||||
# and re-factored at some point point.. sorry if it's hard to follow
|
||||
#
|
||||
|
||||
def _interactive_load_file(self, context):
|
||||
dctx = disassembler.binja_get_bv_from_dock()
|
||||
@@ -95,8 +128,58 @@ class LighthouseBinja(LighthouseCore):
|
||||
return
|
||||
super(LighthouseBinja, self).interactive_load_batch(dctx)
|
||||
|
||||
def _open_coverage_xref(self, dctx, addr):
|
||||
super(LighthouseBinja, self).open_coverage_xref(addr, dctx)
|
||||
def _open_coverage_xref(self, context):
|
||||
super(LighthouseBinja, self).open_coverage_xref(context.address, context.binaryView)
|
||||
|
||||
def _interactive_coverage_xref(self, context):
|
||||
|
||||
if context is None:
|
||||
return
|
||||
|
||||
#
|
||||
# this is a special case where we check if the ctx exists rather than
|
||||
# blindly creating a new one. again, this is because binja may call
|
||||
# this function at random times to decide whether it should display the
|
||||
# XREF menu option.
|
||||
#
|
||||
# but asking whether or not the xref menu option should be shown is not
|
||||
# a good indication of 'is the user actually using lighthouse' so we
|
||||
# do not want this to be one that creates lighthouse contexts
|
||||
#
|
||||
|
||||
dctx = context.binaryView
|
||||
if not dctx:
|
||||
return
|
||||
|
||||
dctx_id = ctypes.addressof(dctx.handle.contents)
|
||||
lctx = self.lighthouse_contexts.get(dctx_id, None)
|
||||
if not lctx:
|
||||
return
|
||||
|
||||
#
|
||||
# is there even any coverage loaded into lighthouse? if not, the user
|
||||
# probably isn't even using it. so don't bother showing the xref action
|
||||
#
|
||||
|
||||
if not lctx.director.coverage_names:
|
||||
return
|
||||
|
||||
if context.view is None:
|
||||
return
|
||||
|
||||
view = context.view
|
||||
context_menu = view.contextMenu()
|
||||
|
||||
#
|
||||
# Create a new, temporary Coverage Xref action to inject into the
|
||||
# right click context menu that is being shown...
|
||||
#
|
||||
|
||||
action = "Coverage Xref"
|
||||
UIAction.registerAction(action)
|
||||
action_handler = view.actionHandler()
|
||||
action_handler.bindAction(action, UIAction(self._open_coverage_xref))
|
||||
context_menu.addAction(action, "Plugins")
|
||||
|
||||
def _is_xref_valid(self, dctx, addr):
|
||||
|
||||
@@ -126,6 +209,12 @@ class LighthouseBinja(LighthouseCore):
|
||||
return
|
||||
super(LighthouseBinja, self).open_coverage_overview(dctx)
|
||||
|
||||
def _stub(self, context):
|
||||
# XXX: This was added as a last minute bodge prior to releasing v0.9.3,
|
||||
# it fixes a crash-on-close that was manifesting on binja macOS, when
|
||||
# using a lambda instead of a concrete function/stub like this.
|
||||
return None
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Binja Actions
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -139,31 +228,28 @@ class LighthouseBinja(LighthouseCore):
|
||||
action = self.ACTION_LOAD_FILE
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_file))
|
||||
Menu.mainMenu("Tools").addAction(action, "Loading", 0)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 0)
|
||||
logger.info("Installed the 'Code coverage file' menu entry")
|
||||
|
||||
def _install_load_batch(self):
|
||||
action = self.ACTION_LOAD_BATCH
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_batch))
|
||||
Menu.mainMenu("Tools").addAction(action, "Loading", 1)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 1)
|
||||
logger.info("Installed the 'Code coverage batch' menu entry")
|
||||
|
||||
# TODO/V35: convert to a UI action once we can disable/disable them on the fly
|
||||
def _install_open_coverage_xref(self):
|
||||
PluginCommand.register_for_address(
|
||||
self.ACTION_COVERAGE_XREF,
|
||||
"Open the coverage xref window",
|
||||
self._open_coverage_xref,
|
||||
self._is_xref_valid
|
||||
)
|
||||
action = self.ACTION_COVERAGE_XREF
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._stub, self._interactive_coverage_xref))
|
||||
Menu.mainMenu("Plugins").addAction(action, "Loading", 2)
|
||||
|
||||
# NOTE/V35: Binja automatically creates View --> Show Coverage Overview
|
||||
def _install_open_coverage_overview(self):
|
||||
action = self.ACTION_COVERAGE_OVERVIEW
|
||||
UIAction.registerAction(action)
|
||||
UIActionHandler.globalActions().bindAction(action, UIAction(self._open_coverage_overview))
|
||||
Menu.mainMenu("Tools").addAction(action, "Windows", 0)
|
||||
Menu.mainMenu("Plugins").addAction(action, "Windows", 0)
|
||||
logger.info("Installed the 'Open Coverage Overview' menu entry")
|
||||
|
||||
# NOTE/V35: Binja doesn't really 'unload' plugins, so whatever...
|
||||
@@ -30,4 +30,3 @@ try:
|
||||
except Exception as e:
|
||||
lmsg("Failed to initialize Lighthouse")
|
||||
logger.exception("Exception details:")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
import abc
|
||||
import logging
|
||||
|
||||
@@ -7,10 +6,10 @@ import lighthouse
|
||||
from lighthouse.util import lmsg
|
||||
from lighthouse.util.qt import *
|
||||
from lighthouse.util.update import check_for_update
|
||||
from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI
|
||||
from lighthouse.util.disassembler import disassembler
|
||||
|
||||
from lighthouse.ui import *
|
||||
from lighthouse.metadata import DatabaseMetadata, metadata_progress
|
||||
from lighthouse.metadata import metadata_progress
|
||||
from lighthouse.exceptions import *
|
||||
|
||||
logger = logging.getLogger("Lighthouse.Core")
|
||||
@@ -26,9 +25,9 @@ class LighthouseCore(object):
|
||||
# Plugin Metadata
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
PLUGIN_VERSION = "0.9.0"
|
||||
PLUGIN_VERSION = "0.9.4-DEV"
|
||||
AUTHORS = "Markus Gaasedelen"
|
||||
DATE = "2020"
|
||||
DATE = "2024"
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Initialization
|
||||
@@ -87,14 +86,10 @@ class LighthouseCore(object):
|
||||
|
||||
# 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
|
||||
@@ -195,7 +190,8 @@ class LighthouseCore(object):
|
||||
"""
|
||||
for lctx in self.lighthouse_contexts.values():
|
||||
lctx.director.refresh_theme()
|
||||
lctx.coverage_overview.refresh_theme()
|
||||
if lctx.coverage_overview:
|
||||
lctx.coverage_overview.refresh_theme()
|
||||
lctx.painter.force_repaint()
|
||||
|
||||
def open_coverage_overview(self, dctx=None):
|
||||
@@ -329,7 +325,7 @@ class LighthouseCore(object):
|
||||
self.open_coverage_overview(lctx.dctx)
|
||||
|
||||
# finally, emit any notable issues that occurred during load
|
||||
warn_errors(errors)
|
||||
warn_errors(errors, lctx.director.suppressed_errors)
|
||||
|
||||
def interactive_load_file(self, dctx=None):
|
||||
"""
|
||||
@@ -399,7 +395,7 @@ class LighthouseCore(object):
|
||||
self.open_coverage_overview(lctx.dctx)
|
||||
|
||||
# finally, emit any notable issues that occurred during load
|
||||
warn_errors(errors)
|
||||
warn_errors(errors, lctx.director.suppressed_errors)
|
||||
|
||||
def check_for_update(self):
|
||||
"""
|
||||
@@ -88,7 +88,7 @@ class LighthouseIDAPlugin(idaapi.plugin_t):
|
||||
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))
|
||||
|
||||
@@ -78,7 +78,7 @@ class DatabaseMetadata(object):
|
||||
# the cache of key database structures
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instructions = []
|
||||
self.instructions = set()
|
||||
|
||||
# internal members to help index & navigate the cached metadata
|
||||
self._name2func = {}
|
||||
@@ -152,14 +152,6 @@ class DatabaseMetadata(object):
|
||||
# Providers
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def get_instructions_slice(self, start_address, end_address):
|
||||
"""
|
||||
Get the instructions addresses that fall within a given range.
|
||||
"""
|
||||
index_start = bisect.bisect_left(self.instructions, start_address)
|
||||
index_end = bisect.bisect_left(self.instructions, end_address)
|
||||
return self.instructions[index_start:index_end]
|
||||
|
||||
def get_instruction_size(self, address):
|
||||
"""
|
||||
Get the size of an instruction at a given address.
|
||||
@@ -403,19 +395,6 @@ class DatabaseMetadata(object):
|
||||
if join:
|
||||
worker.join()
|
||||
|
||||
def _refresh_instructions(self):
|
||||
"""
|
||||
Refresh the list of database instructions (from function metadata).
|
||||
"""
|
||||
instructions = []
|
||||
for function_metadata in itervalues(self.functions):
|
||||
instructions.append(function_metadata.instructions)
|
||||
instructions = list(set(itertools.chain.from_iterable(instructions)))
|
||||
instructions.sort()
|
||||
|
||||
# commit the updated instruction list
|
||||
self.instructions = instructions
|
||||
|
||||
def _refresh_lookup(self):
|
||||
"""
|
||||
Refresh the internal fast lookup address lists.
|
||||
@@ -470,11 +449,11 @@ class DatabaseMetadata(object):
|
||||
|
||||
def _clear_cache(self):
|
||||
"""
|
||||
Cleare the metadata cache of all collected info.
|
||||
Clear the metadata cache of all collected info.
|
||||
"""
|
||||
self.nodes = {}
|
||||
self.functions = {}
|
||||
self.instructions = []
|
||||
self.instructions = set()
|
||||
self._node2func = collections.defaultdict(list)
|
||||
self._refresh_lookup()
|
||||
self.cached = False
|
||||
@@ -520,9 +499,6 @@ class DatabaseMetadata(object):
|
||||
end = time.time()
|
||||
logger.debug("Metadata collection took %s seconds" % (end - start))
|
||||
|
||||
# regenerate the instruction list from collected metadata
|
||||
self._refresh_instructions()
|
||||
|
||||
# refresh the internal function/node fast lookup lists
|
||||
self._refresh_lookup()
|
||||
|
||||
@@ -574,6 +550,8 @@ class DatabaseMetadata(object):
|
||||
completed += CHUNK_SIZE if function_addresses else len(addresses_chunk)
|
||||
progress_callback(completed, total)
|
||||
|
||||
self._cache_instructions()
|
||||
|
||||
@not_mainthread
|
||||
def _async_collect_metadata(self, function_addresses, progress_callback):
|
||||
"""
|
||||
@@ -658,6 +636,59 @@ class DatabaseMetadata(object):
|
||||
self.nodes.update(function_metadata.nodes)
|
||||
self.functions[address] = function_metadata
|
||||
|
||||
def _cache_instructions(self):
|
||||
"""
|
||||
This will be replaced with a disassembler-specific function at runtime.
|
||||
|
||||
NOTE: Read the 'MONKEY PATCHING' section at the end of this file.
|
||||
"""
|
||||
raise RuntimeError("This function should have been monkey patched...")
|
||||
|
||||
def _binja_cache_instructions(self):
|
||||
"""
|
||||
Cache the list of instructions by doing a full scrape of the Binary Ninja database.
|
||||
"""
|
||||
instructions = []
|
||||
|
||||
#
|
||||
# since 'code' does not exist outside of functions in binary ninja,
|
||||
# just scrape instructions from our existing cached nodes
|
||||
#
|
||||
|
||||
for function_metadata in itervalues(self.functions):
|
||||
instructions.append(function_metadata.instructions)
|
||||
|
||||
# commit the updated instruction list
|
||||
self.instructions = set(itertools.chain.from_iterable(instructions))
|
||||
|
||||
def _ida_cache_instructions(self):
|
||||
"""
|
||||
Cache the list of instructions by doing a full scrape of the IDA database.
|
||||
"""
|
||||
instructions = set()
|
||||
|
||||
# alias for speed
|
||||
ida_is_code = idaapi.is_code
|
||||
ida_get_flags = idaapi.get_flags
|
||||
ida_next_head = idaapi.next_head
|
||||
add_instruction = instructions.add
|
||||
|
||||
# scrape instruction addresses from the database
|
||||
for seg_address in idautils.Segments():
|
||||
seg = idaapi.getseg(seg_address)
|
||||
|
||||
current_address = seg_address
|
||||
end_address = seg.end_ea
|
||||
|
||||
# save the address of each defined instruction in the segment
|
||||
while current_address < end_address:
|
||||
if ida_is_code(ida_get_flags(current_address)):
|
||||
add_instruction(current_address)
|
||||
current_address = ida_next_head(current_address, end_address)
|
||||
|
||||
# commit the updated instruction set
|
||||
self.instructions = instructions
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Signal Handlers
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -905,7 +936,7 @@ class FunctionMetadata(object):
|
||||
|
||||
for i in range(0, count.value):
|
||||
if edges[i].target:
|
||||
function_metadata.edges[edge_src].append(node._create_instance(BNNewBasicBlockReference(edges[i].target), bv).start)
|
||||
function_metadata.edges[edge_src].append(node._create_instance(BNNewBasicBlockReference(edges[i].target)).start)
|
||||
core.BNFreeBasicBlockEdgeList(edges, count.value)
|
||||
|
||||
# NOTE/PERF ~28% of metadata collection time alone...
|
||||
@@ -959,6 +990,14 @@ class FunctionMetadata(object):
|
||||
|
||||
# update the map of confirmed (walked) edges
|
||||
confirmed_edges[current_src] = self.edges.pop(current_src)
|
||||
|
||||
#
|
||||
# retain only the 'confirmed' edges. this may differ from the
|
||||
# original edge map because we are only keeping edges that can be
|
||||
# walked from the function entry. (eg, no ida exception handlers)
|
||||
#
|
||||
|
||||
self.edges = confirmed_edges
|
||||
|
||||
# compute the final cyclomatic complexity for the function
|
||||
num_edges = sum(len(x) for x in itervalues(confirmed_edges))
|
||||
@@ -1152,6 +1191,7 @@ def metadata_progress(completed, total):
|
||||
if disassembler.NAME == "IDA":
|
||||
import idaapi
|
||||
import idautils
|
||||
DatabaseMetadata._cache_instructions = DatabaseMetadata._ida_cache_instructions
|
||||
FunctionMetadata._refresh_nodes = FunctionMetadata._ida_refresh_nodes
|
||||
NodeMetadata._cache_node = NodeMetadata._ida_cache_node
|
||||
|
||||
@@ -1162,6 +1202,7 @@ elif disassembler.NAME == "BINJA":
|
||||
import ctypes
|
||||
import binaryninja
|
||||
from binaryninja import core
|
||||
DatabaseMetadata._cache_instructions = DatabaseMetadata._binja_cache_instructions
|
||||
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
|
||||
NodeMetadata._cache_node = NodeMetadata._binja_cache_node
|
||||
|
||||
@@ -38,6 +38,8 @@ class BinjaPainter(DatabasePainter):
|
||||
|
||||
def _clear_instructions(self, instructions):
|
||||
bv = disassembler[self.lctx].bv
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
for address in instructions:
|
||||
for func in bv.get_functions_containing(address):
|
||||
func.set_auto_instr_highlight(address, HighlightStandardColor.NoHighlightColor)
|
||||
@@ -45,6 +47,11 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_instructions -= set(instructions)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _partial_paint(self, bv, instructions, color):
|
||||
for address in instructions:
|
||||
for func in bv.get_functions_containing(address):
|
||||
@@ -57,6 +64,8 @@ class BinjaPainter(DatabasePainter):
|
||||
db_coverage = self.director.coverage
|
||||
db_metadata = self.director.metadata
|
||||
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
r, g, b, _ = self.palette.coverage_paint.getRgb()
|
||||
color = HighlightColor(red=r, green=g, blue=b)
|
||||
|
||||
@@ -83,10 +92,17 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_nodes |= (set(node_addresses) - partial_nodes)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _clear_nodes(self, node_addresses):
|
||||
bv = disassembler[self.lctx].bv
|
||||
db_metadata = self.director.metadata
|
||||
|
||||
state = bv.begin_undo_actions()
|
||||
|
||||
for node_address in node_addresses:
|
||||
node_metadata = db_metadata.nodes.get(node_address, None)
|
||||
|
||||
@@ -102,6 +118,11 @@ class BinjaPainter(DatabasePainter):
|
||||
self._painted_nodes -= set(node_addresses)
|
||||
self._action_complete.set()
|
||||
|
||||
if hasattr(bv, "forget_undo_actions"):
|
||||
bv.forget_undo_actions(state)
|
||||
else:
|
||||
bv.commit_undo_actions(state)
|
||||
|
||||
def _refresh_ui(self):
|
||||
pass
|
||||
|
||||
@@ -9,7 +9,7 @@ from idaapi import clr_abits, set_abits, netnode, set_node_info
|
||||
|
||||
from lighthouse.util import *
|
||||
from lighthouse.util.disassembler import disassembler
|
||||
from lighthouse.util.disassembler.ida_api import map_line2citem, map_line2node, lex_citem_indexes
|
||||
from lighthouse.util.disassembler.ida_api import map_line2citem, map_line2node, lex_citem_indexes, hexrays_available
|
||||
from lighthouse.painting import DatabasePainter
|
||||
|
||||
logger = logging.getLogger("Lighthouse.Painting.IDA")
|
||||
@@ -153,11 +153,11 @@ class IDAPainter(DatabasePainter):
|
||||
# enable / disable hook based on the painter being enabled or disabled
|
||||
if status:
|
||||
self._idp_hooks.hook()
|
||||
if idaapi.init_hexrays_plugin():
|
||||
if hexrays_available():
|
||||
idaapi.install_hexrays_callback(self._hxe_callback)
|
||||
else:
|
||||
self._idp_hooks.unhook()
|
||||
if idaapi.init_hexrays_plugin():
|
||||
if hexrays_available():
|
||||
idaapi.remove_hexrays_callback(self._hxe_callback)
|
||||
|
||||
# send the status changed signal...
|
||||
@@ -216,41 +216,60 @@ class IDAPainter(DatabasePainter):
|
||||
|
||||
# retrieve all the necessary structures to paint this node
|
||||
node_coverage = db_coverage.nodes.get(node_address, None)
|
||||
node_metadata = db_metadata.nodes.get(node_address, None)
|
||||
functions = db_metadata.get_functions_by_node(node_address)
|
||||
|
||||
#
|
||||
# due to the fact that multiple functions may 'share' a node,
|
||||
# we need to go through and explicitly fetch the node metadata
|
||||
# from each function when performing a paint.
|
||||
#
|
||||
# this is because each function will have a unique node_id in
|
||||
# the target node_metadata(s)
|
||||
#
|
||||
|
||||
node_metadatas = {}
|
||||
for function in functions:
|
||||
|
||||
# attempt to safely fetch the node metadata from a function
|
||||
node_metadata = function.nodes.get(node_address, None)
|
||||
|
||||
#
|
||||
# this is possible if function is getting torn down. this is because
|
||||
# we don't use locks. this just means it is time for us to bail as
|
||||
# the metadata state is changing and the paint should be canceled
|
||||
#
|
||||
|
||||
if not node_metadata:
|
||||
node_metadatas = []
|
||||
break
|
||||
|
||||
node_metadatas[function.address] = node_metadata
|
||||
|
||||
#
|
||||
# if we did not get *everything* that we needed, then it is
|
||||
# possible the database changesd, or the coverage set changed...
|
||||
# possible the database changed, or the coverage set changed...
|
||||
#
|
||||
# this is kind of what we get for not using locks :D but that's
|
||||
# okay, just stop painting here and let the painter sort it out
|
||||
#
|
||||
|
||||
if not (node_coverage and node_metadata and functions):
|
||||
if not (node_coverage and node_metadatas):
|
||||
self._msg_queue.put(self.MSG_ABORT)
|
||||
node_addresses = node_addresses[:node_addresses.index(node_address)]
|
||||
break
|
||||
|
||||
#
|
||||
# get_functions_by_node() can return multiple functios (eg, a
|
||||
# shared node) but in IDA should only ever return one... so we
|
||||
# can pull it out now
|
||||
#
|
||||
|
||||
function_metadata = functions[0]
|
||||
|
||||
# ignore nodes that are only partially executed
|
||||
if node_coverage.instructions_executed != node_metadata.instruction_count:
|
||||
continue
|
||||
|
||||
# do the *actual* painting of a single node instance
|
||||
set_node_info(
|
||||
function_metadata.address,
|
||||
node_metadata.id,
|
||||
node_info,
|
||||
node_flags
|
||||
)
|
||||
# do the *actual* painting o;f a single node instance
|
||||
for function_address, node_metadata in iteritems(node_metadatas):
|
||||
set_node_info(
|
||||
function_address,
|
||||
node_metadata.id,
|
||||
node_info,
|
||||
node_flags
|
||||
)
|
||||
|
||||
self._painted_nodes |= set(node_addresses)
|
||||
self._action_complete.set()
|
||||
@@ -271,32 +290,37 @@ class IDAPainter(DatabasePainter):
|
||||
# loop through every node that we have metadata data for, clearing
|
||||
# their paint (color) in the IDA graph view as applicable.
|
||||
#
|
||||
# read self._paint_nodes() comments for more info, the code below
|
||||
# is very similar, sans the repetitive comments
|
||||
#
|
||||
|
||||
for node_address in node_addresses:
|
||||
|
||||
# retrieve all the necessary structures to paint this node
|
||||
node_metadata = db_metadata.nodes.get(node_address, None)
|
||||
functions = db_metadata.get_functions_by_node(node_address)
|
||||
|
||||
#
|
||||
# abort if something looks like it changed... read the comments in
|
||||
# self._paint_nodes for more verbose information
|
||||
#
|
||||
node_metadatas = {}
|
||||
for function in functions:
|
||||
node_metadata = function.nodes.get(node_address, None)
|
||||
|
||||
if not (node_metadata and functions):
|
||||
if not node_metadata:
|
||||
node_metadatas = {}
|
||||
break
|
||||
|
||||
node_metadatas[function.address] = node_metadata
|
||||
|
||||
# abort if something looks like it changed...
|
||||
if not node_metadatas:
|
||||
self._msg_queue.put(self.MSG_ABORT)
|
||||
node_addresses = node_addresses[:node_addresses.index(node_address)]
|
||||
break
|
||||
|
||||
function_metadata = functions[0]
|
||||
|
||||
# do the *actual* painting of a single node instance
|
||||
set_node_info(
|
||||
function_metadata.address,
|
||||
node_metadata.id,
|
||||
node_info,
|
||||
node_flags
|
||||
)
|
||||
for function_address, node_metadata in iteritems(node_metadatas):
|
||||
set_node_info(
|
||||
function_address,
|
||||
node_metadata.id,
|
||||
node_info,
|
||||
node_flags
|
||||
)
|
||||
|
||||
self._painted_nodes -= set(node_addresses)
|
||||
self._action_complete.set()
|
||||
@@ -537,8 +537,6 @@ class DatabasePainter(object):
|
||||
a rebase occurs while the painter is running.
|
||||
"""
|
||||
db_metadata = self.director.metadata
|
||||
instructions = db_metadata.instructions
|
||||
nodes = viewvalues(db_metadata.nodes)
|
||||
|
||||
# a rebase has not occurred
|
||||
if not db_metadata.cached or (db_metadata.imagebase == self._imagebase):
|
||||
@@ -100,7 +100,22 @@ class DrcovData(CoverageFile):
|
||||
mod_ids = [module.id for module in modules]
|
||||
|
||||
# loop through the coverage data and filter out data for the target ids
|
||||
coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
if self.version < 3:
|
||||
coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
|
||||
#
|
||||
# drcov version 3 does not include the 'preferred' / sub-module base
|
||||
# in the bb offset, so we must add that base offset before returning
|
||||
# the block offsets to correctly normalize things
|
||||
#
|
||||
# it's unclear if the preferred_base for given sub-module segments
|
||||
# will always be correct, so we opt to simply use the first segment
|
||||
# in a given module as the base to compute the known runtime offset
|
||||
#
|
||||
|
||||
else:
|
||||
mod_bases = dict([(module.id, module.start - modules[0].start) for module in modules])
|
||||
coverage_blocks = [(mod_bases[bb.mod_id] + bb.start, bb.size) for bb in self.bbs if bb.mod_id in mod_ids]
|
||||
|
||||
# return the filtered coverage blocks
|
||||
return coverage_blocks
|
||||
@@ -137,7 +152,7 @@ class DrcovData(CoverageFile):
|
||||
flavor_line = f.readline().decode('utf-8').strip()
|
||||
self.flavor = flavor_line.split(":")[1]
|
||||
|
||||
assert self.version == 2, "Only drcov version 2 log files supported"
|
||||
assert self.version == 2 or self.version == 3, "Only drcov versions 2 and 3 log files supported"
|
||||
|
||||
def _parse_module_table(self, f):
|
||||
"""
|
||||
@@ -192,7 +207,7 @@ class DrcovData(CoverageFile):
|
||||
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'
|
||||
@@ -227,6 +242,12 @@ class DrcovData(CoverageFile):
|
||||
Mac/Linux:
|
||||
'Columns: id, containing_id, start, end, entry, offset, path'
|
||||
|
||||
DynamoRIO v10.0.19734, table version 5:
|
||||
Windows:
|
||||
'Columns: id, containing_id, start, end, entry, offset, preferred_base, checksum, timestamp, path'
|
||||
Mac/Linux:
|
||||
'Columns: id, containing_id, start, end, entry, offset, preferred_base, path'
|
||||
|
||||
"""
|
||||
|
||||
# NOTE/COMPAT: there is no 'Columns' line for the v1 table...
|
||||
@@ -310,22 +331,30 @@ class DrcovData(CoverageFile):
|
||||
|
||||
# parse the plaintext basic block entries one by one
|
||||
else:
|
||||
self._parse_bb_table_text_entries(f)
|
||||
|
||||
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()
|
||||
|
||||
if table_header != "module id, start, size:":
|
||||
raise ValueError("Invalid BB header: %r" % table_header)
|
||||
|
||||
pattern = re.compile(r"^module\[\s*(?P<mod>[0-9]+)\]\:\s*(?P<start>0x[0-9a-fA-F]+)\,\s*(?P<size>[0-9]+)$")
|
||||
for i, bb in enumerate(self.bbs):
|
||||
text_entry = f.readline().decode('utf-8').strip()
|
||||
if not text_entry:
|
||||
continue
|
||||
|
||||
if text_entry != "module id, start, size:":
|
||||
raise ValueError("Invalid BB header: %r" % text_entry)
|
||||
match = pattern.match(text_entry)
|
||||
if not match:
|
||||
raise ValueError("Invalid BB entry: %r" % text_entry)
|
||||
|
||||
pattern = re.compile(r"^module\[\s*(?P<mod>[0-9]+)\]\:\s*(?P<start>0x[0-9a-fA-F]+)\,\s*(?P<size>[0-9]+)$")
|
||||
for bb in self.bbs:
|
||||
text_entry = f.readline().decode('utf-8').strip()
|
||||
|
||||
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 +405,8 @@ class DrcovModule(object):
|
||||
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 +467,25 @@ class DrcovModule(object):
|
||||
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
|
||||
#------------------------------------------------------------------------------
|
||||
@@ -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
|
||||
@@ -118,7 +118,7 @@ class CoverageComboBox(QtWidgets.QComboBox):
|
||||
|
||||
self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
|
||||
self.setMaximumHeight(self._font_metrics.height()*1.75)
|
||||
self.setMaximumHeight(int(self._font_metrics.height()*1.75))
|
||||
|
||||
# draw the QComboBox with a 'Windows'-esque style
|
||||
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
|
||||
@@ -437,10 +437,7 @@ class CoverageComboBoxView(QtWidgets.QTableView):
|
||||
#
|
||||
|
||||
hh.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
|
||||
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
|
||||
vh.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
hh.setMinimumSectionSize(0)
|
||||
vh.setMinimumSectionSize(0)
|
||||
|
||||
# get the column width hint from the model for the 'X' delete column
|
||||
@@ -451,7 +448,9 @@ class CoverageComboBoxView(QtWidgets.QTableView):
|
||||
)
|
||||
|
||||
# set the 'X' delete icon column width to a fixed size based on the hint
|
||||
hh.setMinimumSectionSize(icon_column_width)
|
||||
hh.resizeSection(COLUMN_DELETE, icon_column_width)
|
||||
hh.setSectionResizeMode(1, QtWidgets.QHeaderView.ResizeToContents)
|
||||
|
||||
# install a delegate to do some custom painting against the combobox
|
||||
self.setItemDelegate(ComboBoxDelegate(self))
|
||||
@@ -533,7 +532,7 @@ class CoverageComboBoxModel(QtCore.QAbstractTableModel):
|
||||
delete_icon = QtGui.QPixmap(plugin_resource("icons/delete_coverage.png"))
|
||||
|
||||
# compute the appropriate size for the deletion icon
|
||||
icon_height = self._font_metrics.height()*0.75
|
||||
icon_height = int(self._font_metrics.height()*0.75)
|
||||
icon_width = icon_height
|
||||
|
||||
# scale the icon as appropriate (very likely scaling it down)
|
||||
@@ -29,6 +29,7 @@ class CoverageOverview(object):
|
||||
|
||||
self.lctx.coverage_overview = self
|
||||
self.initialized = False
|
||||
self._refreshed_callback = None
|
||||
|
||||
# see the EventProxy class below for more details
|
||||
self._events = EventProxy(self)
|
||||
@@ -42,7 +43,7 @@ class CoverageOverview(object):
|
||||
self.refresh()
|
||||
|
||||
# register for cues from the director
|
||||
self.director.refreshed(self.refresh)
|
||||
self._refreshed_callback = self.director.refreshed(self.refresh)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Pseudo Widget Functions
|
||||
@@ -64,11 +65,27 @@ class CoverageOverview(object):
|
||||
"""
|
||||
The CoverageOverview is being hidden / deleted.
|
||||
"""
|
||||
if self.widget is None:
|
||||
return
|
||||
|
||||
if self._refreshed_callback:
|
||||
self.director.unregister_refreshed(self._refreshed_callback)
|
||||
self._refreshed_callback = None
|
||||
|
||||
if self.lctx.coverage_overview is self:
|
||||
self.lctx.coverage_overview = None
|
||||
|
||||
self.initialized = False
|
||||
self._combobox = None
|
||||
self._shell = None
|
||||
self._toolbar = None
|
||||
self._table_view = None
|
||||
self._table_controller = None
|
||||
self._table_model = None
|
||||
self._settings_button = None
|
||||
self._settings_menu = None
|
||||
self._shell_elements = None
|
||||
self._events = None
|
||||
self.widget = None
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -194,7 +211,7 @@ class CoverageOverview(object):
|
||||
|
||||
# layout the major elements of our widget
|
||||
layout = QtWidgets.QGridLayout()
|
||||
layout.setSpacing(get_dpi_scale()*5.0)
|
||||
layout.setSpacing(int(get_dpi_scale()*5))
|
||||
layout.addWidget(self._table_view)
|
||||
layout.addWidget(self._toolbar)
|
||||
|
||||
@@ -214,8 +231,8 @@ class CoverageOverview(object):
|
||||
-1*self._settings_menu.sizeHint().height()
|
||||
)
|
||||
center = QtCore.QPoint(
|
||||
self._settings_button.sizeHint().width()/2,
|
||||
self._settings_button.sizeHint().height()/2
|
||||
int(self._settings_button.sizeHint().width()/2),
|
||||
int(self._settings_button.sizeHint().height()/2)
|
||||
)
|
||||
where = self._settings_button.mapToGlobal(center+delta)
|
||||
self._settings_menu.popup(where)
|
||||
@@ -229,6 +246,9 @@ class CoverageOverview(object):
|
||||
"""
|
||||
Refresh the Coverage Overview.
|
||||
"""
|
||||
if not (self._table_model and self._shell and self._combobox):
|
||||
return
|
||||
|
||||
self._table_model.refresh()
|
||||
self._shell.refresh()
|
||||
self._combobox.refresh()
|
||||
@@ -238,6 +258,9 @@ class CoverageOverview(object):
|
||||
"""
|
||||
Update visual elements based on theme change.
|
||||
"""
|
||||
if not (self._table_view and self._table_model and self._shell and self._combobox):
|
||||
return
|
||||
|
||||
self._table_view.refresh_theme()
|
||||
self._table_model.refresh_theme()
|
||||
self._shell.refresh_theme()
|
||||
@@ -275,6 +298,22 @@ class EventProxy(QtCore.QObject):
|
||||
|
||||
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()
|
||||
|
||||
#
|
||||
@@ -68,10 +68,6 @@ class TableSettingsMenu(QtWidgets.QMenu):
|
||||
self._action_refresh_metadata.setToolTip("Refresh the database metadata and coverage mapping")
|
||||
self.addAction(self._action_refresh_metadata)
|
||||
|
||||
self._action_dump_unmapped = QtWidgets.QAction("Dump unmapped coverage", None)
|
||||
self._action_dump_unmapped.setToolTip("Print all coverage data not mapped to a function")
|
||||
self.addAction(self._action_dump_unmapped)
|
||||
|
||||
self._action_export_html = QtWidgets.QAction("Generate HTML report", None)
|
||||
self._action_export_html.setToolTip("Export the coverage table to HTML")
|
||||
self.addAction(self._action_export_html)
|
||||
@@ -91,7 +87,6 @@ class TableSettingsMenu(QtWidgets.QMenu):
|
||||
self._action_disable_paint.triggered[bool].connect(lambda x: lctx.painter.set_enabled(not x))
|
||||
self._action_force_clear.triggered.connect(lctx.painter.force_clear)
|
||||
self._action_export_html.triggered.connect(controller.export_to_html)
|
||||
self._action_dump_unmapped.triggered.connect(lctx.director.dump_unmapped)
|
||||
lctx.painter.status_changed(self._ui_painter_changed_status)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -145,7 +145,7 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
entry_rect = entry_fm.boundingRect(entry_text)
|
||||
|
||||
# select the larger of the two potential column widths
|
||||
column_width = max(title_rect.width(), entry_rect.width()*1.2)
|
||||
column_width = int(max(title_rect.width(), entry_rect.width()*1.2))
|
||||
|
||||
# save the final column width
|
||||
self.setColumnWidth(i, column_width)
|
||||
@@ -191,13 +191,17 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
# NOTE: don't ask too many questions about this voodoo math :D
|
||||
spacing = entry_fm.height() - entry_fm.xHeight()
|
||||
tweak = (17*get_dpi_scale() - spacing)/get_dpi_scale()
|
||||
vh.setDefaultSectionSize(entry_fm.height()+tweak)
|
||||
vh.setDefaultSectionSize(int(entry_fm.height()+tweak))
|
||||
|
||||
def _ui_init_table_ctx_menu_actions(self):
|
||||
"""
|
||||
Initialize the right click context menu actions for the table view.
|
||||
"""
|
||||
|
||||
# misc actions
|
||||
self._action_dump_orphan = QtWidgets.QAction("Dump orphan addresses", None)
|
||||
self._action_dump_internal = QtWidgets.QAction("Dump internal addresses (Debug)", None)
|
||||
|
||||
# function actions
|
||||
self._action_rename = QtWidgets.QAction("Rename", None)
|
||||
self._action_copy_name = QtWidgets.QAction("Copy name", None)
|
||||
@@ -261,7 +265,12 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
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 +290,11 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
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)
|
||||
@@ -298,8 +311,8 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
"""
|
||||
|
||||
# get the list rows currently selected in the coverage table
|
||||
selected_rows = self.selectionModel().selectedRows()
|
||||
if len(selected_rows) == 0:
|
||||
selected_row_indexes = self.selectionModel().selectedRows()
|
||||
if len(selected_row_indexes) == 0:
|
||||
return None
|
||||
|
||||
# the context menu we will dynamically populate
|
||||
@@ -311,13 +324,25 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
# copy function name, address, or renaming the function.
|
||||
#
|
||||
|
||||
if len(selected_rows) == 1:
|
||||
ctx_menu.addAction(self._action_rename)
|
||||
ctx_menu.addSeparator()
|
||||
ctx_menu.addAction(self._action_copy_name)
|
||||
ctx_menu.addAction(self._action_copy_address)
|
||||
ctx_menu.addAction(self._action_copy_name_and_address)
|
||||
ctx_menu.addSeparator()
|
||||
if len(selected_row_indexes) == 1:
|
||||
|
||||
row = selected_row_indexes[0].row()
|
||||
function_address = self._model.row2func[row]
|
||||
|
||||
# special handling for right click of orphan coverage row
|
||||
if function_address == BADADDR:
|
||||
ctx_menu.addAction(self._action_dump_orphan)
|
||||
ctx_menu.addAction(self._action_dump_internal)
|
||||
return ctx_menu
|
||||
|
||||
# normal right click of a function row
|
||||
else:
|
||||
ctx_menu.addAction(self._action_rename)
|
||||
ctx_menu.addSeparator()
|
||||
ctx_menu.addAction(self._action_copy_name)
|
||||
ctx_menu.addAction(self._action_copy_address)
|
||||
ctx_menu.addAction(self._action_copy_name_and_address)
|
||||
ctx_menu.addSeparator()
|
||||
|
||||
#
|
||||
# if multiple functions are selected then show actions available
|
||||
@@ -376,6 +401,14 @@ class CoverageTableView(QtWidgets.QTableView):
|
||||
elif action == self._action_clear_prefix:
|
||||
self._controller.clear_function_prefixes(rows)
|
||||
|
||||
# handle the 'Dump orphan addresses' action
|
||||
elif action == self._action_dump_orphan:
|
||||
self._controller.dump_orphan()
|
||||
|
||||
# handle the 'Dump internal addresses' action
|
||||
elif action == self._action_dump_internal:
|
||||
self._controller.dump_internal()
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Context Menu (Table Header)
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -429,6 +462,9 @@ class CoverageTableController(object):
|
||||
|
||||
# retrieve details about the function targeted for rename
|
||||
function_address = self._model.row2func[row]
|
||||
if function_address == BADADDR:
|
||||
return
|
||||
|
||||
original_name = disassembler[self.lctx].get_function_raw_name_at(function_address)
|
||||
|
||||
# prompt the user for a new function name
|
||||
@@ -527,6 +563,38 @@ class CoverageTableController(object):
|
||||
copy_to_clipboard(function_name_and_address.rstrip())
|
||||
return function_name_and_address
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Dumping
|
||||
#--------------------------------------------------------------------------
|
||||
|
||||
def dump_orphan(self):
|
||||
"""
|
||||
Dump the orphan coverage data.
|
||||
"""
|
||||
coverage = self.lctx.director.coverage
|
||||
lmsg("Orphan coverage addresses for %s:" % coverage.name)
|
||||
self._dump_addresses(coverage.orphan_addresses)
|
||||
|
||||
def dump_internal(self):
|
||||
"""
|
||||
Dump the internal coverage data.
|
||||
"""
|
||||
coverage = self.lctx.director.coverage
|
||||
lmsg("Internal coverage addresses for %s:" % coverage.name)
|
||||
self._dump_addresses(coverage.unmapped_addresses)
|
||||
|
||||
def _dump_addresses(self, coverage_addresses):
|
||||
"""
|
||||
Dump the given list of addresses to the terminal.
|
||||
"""
|
||||
coverage_addresses = sorted(coverage_addresses)
|
||||
if not coverage_addresses:
|
||||
lmsg(" * (there is no addresses to dump)")
|
||||
return
|
||||
|
||||
for address in coverage_addresses:
|
||||
lmsg(" * 0x%X" % address)
|
||||
|
||||
#---------------------------------------------------------------------------
|
||||
# Misc
|
||||
#---------------------------------------------------------------------------
|
||||
@@ -538,6 +606,8 @@ class CoverageTableController(object):
|
||||
|
||||
# get the clicked function address
|
||||
function_address = self._model.row2func[row]
|
||||
if function_address == BADADDR:
|
||||
return
|
||||
|
||||
#
|
||||
# if there is actually coverage in the function, attempt to locate the
|
||||
@@ -599,9 +669,13 @@ class CoverageTableController(object):
|
||||
{
|
||||
"filter": "HTML Files (*.html)",
|
||||
"caption": "Save HTML Report",
|
||||
"directory": suggested_filepath
|
||||
}
|
||||
|
||||
if USING_PYQT5:
|
||||
kwargs["directory"] = suggested_filepath
|
||||
else:
|
||||
kwargs["dir"] = suggested_filepath
|
||||
|
||||
# prompt the user with the file dialog, and await their chosen filename(s)
|
||||
filename, _ = file_dialog.getSaveFileName(**kwargs)
|
||||
if not filename:
|
||||
@@ -627,7 +701,8 @@ class CoverageTableController(object):
|
||||
function_addresses = []
|
||||
for row_number in rows:
|
||||
address = self._model.row2func[row_number]
|
||||
function_addresses.append(address)
|
||||
if address != BADADDR:
|
||||
function_addresses.append(address)
|
||||
return function_addresses
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
@@ -728,7 +803,9 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
|
||||
# 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
|
||||
@@ -824,6 +901,87 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# unhandeled header request
|
||||
return None
|
||||
|
||||
def _data_function_display(self, function_address, column):
|
||||
"""
|
||||
Return a string to diplay in the requested column of the given function.
|
||||
"""
|
||||
|
||||
# lookup the function info for the given function
|
||||
try:
|
||||
function_metadata = self.lctx.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
|
||||
# not have an entry in the coverage map. that means we should
|
||||
# yield a default, 'blank', coverage item in these instances
|
||||
#
|
||||
|
||||
function_coverage = self._director.coverage.functions.get(
|
||||
function_address,
|
||||
self._blank_coverage
|
||||
)
|
||||
|
||||
# Coverage % - (by instruction execution)
|
||||
if column == self.COV_PERCENT:
|
||||
return "%5.2f" % (function_coverage.instruction_percent*100)
|
||||
|
||||
# Function Name
|
||||
elif column == self.FUNC_NAME:
|
||||
return function_metadata.name
|
||||
|
||||
# Function Address
|
||||
elif column == self.FUNC_ADDR:
|
||||
return "0x%X" % function_metadata.address
|
||||
|
||||
# Basic Blocks
|
||||
elif column == self.BLOCKS_HIT:
|
||||
return "%3u / %-3u" % (function_coverage.nodes_executed,
|
||||
function_metadata.node_count)
|
||||
|
||||
# Instructions Hit
|
||||
elif column == self.INST_HIT:
|
||||
return "%4u / %-4u" % (function_coverage.instructions_executed,
|
||||
function_metadata.instruction_count)
|
||||
|
||||
# Function Size
|
||||
elif column == self.FUNC_SIZE:
|
||||
return "%u" % function_metadata.size
|
||||
|
||||
# Cyclomatic Complexity
|
||||
elif column == self.COMPLEXITY:
|
||||
return "%u" % function_metadata.cyclomatic_complexity
|
||||
|
||||
# unhandeled? maybe make this an assert?
|
||||
return None
|
||||
|
||||
def _data_orphan_display(self, column):
|
||||
"""
|
||||
Return a string to be displayed by the table
|
||||
"""
|
||||
if column == self.FUNC_NAME:
|
||||
return "Orphan Coverage"
|
||||
elif column == self.INST_HIT:
|
||||
return "%u" % len(self._director.coverage.orphan_addresses)
|
||||
return "N/A"
|
||||
|
||||
def data(self, index, role=QtCore.Qt.DisplayRole):
|
||||
"""
|
||||
Define how Qt should access the underlying model data.
|
||||
@@ -832,80 +990,31 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# a request has been made for what text to show in a table cell
|
||||
if role == QtCore.Qt.DisplayRole:
|
||||
|
||||
# alias the requested column number once, for readability & perf
|
||||
column = index.column()
|
||||
function_address = self.row2func[index.row()]
|
||||
|
||||
# lookup the function info for this row
|
||||
try:
|
||||
function_address = self.row2func[index.row()]
|
||||
function_metadata = self.lctx.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
|
||||
# not have an entry in the coverage map. that means we should
|
||||
# yield a default, 'blank', coverage item in these instances
|
||||
#
|
||||
|
||||
function_coverage = self._director.coverage.functions.get(
|
||||
function_address,
|
||||
self._blank_coverage
|
||||
)
|
||||
|
||||
# Coverage % - (by instruction execution)
|
||||
if column == self.COV_PERCENT:
|
||||
return "%5.2f" % (function_coverage.instruction_percent*100)
|
||||
|
||||
# Function Name
|
||||
elif column == self.FUNC_NAME:
|
||||
return function_metadata.name
|
||||
|
||||
# Function Address
|
||||
elif column == self.FUNC_ADDR:
|
||||
return "0x%X" % function_metadata.address
|
||||
|
||||
# Basic Blocks
|
||||
elif column == self.BLOCKS_HIT:
|
||||
return "%3u / %-3u" % (function_coverage.nodes_executed,
|
||||
function_metadata.node_count)
|
||||
|
||||
# Instructions Hit
|
||||
elif column == self.INST_HIT:
|
||||
return "%4u / %-4u" % (function_coverage.instructions_executed,
|
||||
function_metadata.instruction_count)
|
||||
|
||||
# Function Size
|
||||
elif column == self.FUNC_SIZE:
|
||||
return "%u" % function_metadata.size
|
||||
|
||||
# Cyclomatic Complexity
|
||||
elif column == self.COMPLEXITY:
|
||||
return "%u" % function_metadata.cyclomatic_complexity
|
||||
if function_address == BADADDR:
|
||||
return self._data_orphan_display(column)
|
||||
else:
|
||||
return self._data_function_display(function_address, column)
|
||||
|
||||
# cell background color request
|
||||
elif role == QtCore.Qt.BackgroundRole:
|
||||
function_address = self.row2func[index.row()]
|
||||
|
||||
# special handling for 'orphan' coverage
|
||||
if function_address == BADADDR:
|
||||
|
||||
# if there was *ANY* coverage, color the 'orphan' line red
|
||||
if self._director.coverage.orphan_addresses:
|
||||
return self.lctx.palette.table_coverage_bad
|
||||
|
||||
# normal handling
|
||||
function_coverage = self._director.coverage.functions.get(
|
||||
function_address,
|
||||
self._blank_coverage
|
||||
)
|
||||
|
||||
return function_coverage.coverage_color
|
||||
|
||||
# cell font style format request
|
||||
@@ -942,6 +1051,13 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
self.layoutChanged.emit()
|
||||
return
|
||||
|
||||
#
|
||||
# In PySide6 (eg. Binary Ninja) the Qt.SortOrder type does not convert
|
||||
# to a simple integer (unlike PyQt5) so we reduce the type for comapt
|
||||
#
|
||||
|
||||
direction = (sort_order == QtCore.Qt.SortOrder.DescendingOrder)
|
||||
|
||||
#
|
||||
# NOTE: attrgetter appears to profile ~8-12% faster than lambdas
|
||||
# accessing the member on the member, hence the strange paradigm
|
||||
@@ -952,7 +1068,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
sorted_functions = sorted(
|
||||
itervalues(self._visible_metadata),
|
||||
key=attrgetter(sort_field),
|
||||
reverse=sort_order
|
||||
reverse=direction
|
||||
)
|
||||
|
||||
# sort the table entries by a function coverage attribute
|
||||
@@ -960,7 +1076,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
sorted_functions = sorted(
|
||||
itervalues(self._visible_coverage),
|
||||
key=attrgetter(sort_field),
|
||||
reverse=sort_order
|
||||
reverse=direction
|
||||
)
|
||||
|
||||
#
|
||||
@@ -978,7 +1094,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
# items (0%) should be appended to the *end*
|
||||
#
|
||||
|
||||
if sort_order:
|
||||
if direction:
|
||||
sorted_functions += self._no_coverage
|
||||
|
||||
#
|
||||
@@ -994,6 +1110,7 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
|
||||
# finally, rebuild the row2func mapping and notify views of this change
|
||||
self.row2func = dict(zip(xrange(len(sorted_functions)), sorted_addresses))
|
||||
self.row2func[len(self.row2func)] = BADADDR
|
||||
self.func2row = {v: k for k, v in iteritems(self.row2func)}
|
||||
self.layoutChanged.emit()
|
||||
|
||||
@@ -1305,6 +1422,9 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
|
||||
self.row2func[row] = function_address
|
||||
row += 1
|
||||
|
||||
# add a special entry for 'orphan coverage'
|
||||
self.row2func[len(self.row2func)] = BADADDR
|
||||
|
||||
# build the inverse func --> row mapping
|
||||
self.func2row = {v: k for k, v in iteritems(self.row2func)}
|
||||
|
||||
@@ -116,7 +116,7 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
name_entry.setToolTip(coverage.filepath)
|
||||
self._table.setItem(i, 2, name_entry)
|
||||
date_entry = QtWidgets.QTableWidgetItem()
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(coverage.timestamp*1000))
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(int(coverage.timestamp*1000)))
|
||||
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(date_entry))
|
||||
|
||||
# filepaths
|
||||
@@ -135,7 +135,7 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
name_entry.setToolTip(filepath)
|
||||
self._table.setItem(i, 2, name_entry)
|
||||
date_entry = QtWidgets.QTableWidgetItem()
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(timestamp*1000))
|
||||
date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(int(timestamp*1000)))
|
||||
self._table.setItem(i, 3, date_entry)
|
||||
|
||||
self._table.resizeColumnsToContents()
|
||||
@@ -153,8 +153,8 @@ class CoverageXref(QtWidgets.QDialog):
|
||||
layout.addWidget(self._table)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = get_dpi_scale() * 250
|
||||
width = get_dpi_scale() * 600
|
||||
height = int(get_dpi_scale() * 250)
|
||||
width = int(get_dpi_scale() * 600)
|
||||
self.setMinimumHeight(height)
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
@@ -30,11 +30,18 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
|
||||
# dialog attributes
|
||||
self.selected_name = None
|
||||
self.remember_alias = False
|
||||
|
||||
# configure the widget for use
|
||||
self._ui_init()
|
||||
|
||||
@property
|
||||
def remember_alias(self):
|
||||
return self._checkbox_remember.isChecked()
|
||||
|
||||
@property
|
||||
def ignore_missing(self):
|
||||
return self._checkbox_ignore_missing.isChecked()
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Initialization - UI
|
||||
#--------------------------------------------------------------------------
|
||||
@@ -67,11 +74,11 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
description_text = \
|
||||
"Lighthouse could not automatically identify the target module in the given coverage file:<br />" \
|
||||
"<br />" \
|
||||
"-- <b>Target:</b> %s<br />" \
|
||||
"-- <b>Coverage File:</b> %s<br />" \
|
||||
"-- <b>Target:</b> {0}<br />" \
|
||||
"-- <b>Coverage File:</b> {1}<br />" \
|
||||
"<br />" \
|
||||
"Please double click the name of the module that matches this database, or close this dialog<br />" \
|
||||
"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)
|
||||
@@ -82,6 +89,10 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
self._checkbox_remember = QtWidgets.QCheckBox("Remember target module alias for this session")
|
||||
self._checkbox_remember.setFont(self._font)
|
||||
|
||||
# a checkbox to ignore future 'missing coverage' / select module warnings
|
||||
self._checkbox_ignore_missing = QtWidgets.QCheckBox("Suppress this dialog for the remaining coverage files")
|
||||
self._checkbox_ignore_missing.setFont(self._font)
|
||||
|
||||
def _ui_init_table(self):
|
||||
"""
|
||||
Initialize the module selector table UI elements.
|
||||
@@ -134,10 +145,11 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
layout.addWidget(self._label_description)
|
||||
layout.addWidget(self._table)
|
||||
layout.addWidget(self._checkbox_remember)
|
||||
layout.addWidget(self._checkbox_ignore_missing)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = get_dpi_scale() * 250
|
||||
width = get_dpi_scale() * 400
|
||||
height = int(get_dpi_scale() * 250)
|
||||
width = int(get_dpi_scale() * 400)
|
||||
self.setMinimumHeight(height)
|
||||
self.setMinimumWidth(width)
|
||||
|
||||
@@ -153,5 +165,4 @@ class ModuleSelector(QtWidgets.QDialog):
|
||||
A cell/row has been double clicked in the module table.
|
||||
"""
|
||||
self.selected_name = self._table.item(row, 0).text()
|
||||
self.remember_alias = self._checkbox_remember.isChecked()
|
||||
self.accept()
|
||||
self.accept()
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
import struct
|
||||
import glob
|
||||
import shutil
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
# NOTE: Py2/Py3 compat
|
||||
try:
|
||||
@@ -260,8 +259,13 @@ class LighthousePalette(object):
|
||||
user_theme_dir = self.get_user_theme_dir()
|
||||
makedirs(user_theme_dir)
|
||||
|
||||
# enumerate all in-box / default themes
|
||||
plugin_theme_dir = self.get_plugin_theme_dir()
|
||||
json_files = glob.glob(os.path.join(plugin_theme_dir, "*.json"))
|
||||
|
||||
# copy the default themes into the user directory if they don't exist
|
||||
for theme_name in self._default_themes.values():
|
||||
for default_theme_file in json_files:
|
||||
theme_name = os.path.basename(default_theme_file)
|
||||
|
||||
#
|
||||
# check if lighthouse has copied the default themes into the user
|
||||
@@ -274,8 +278,7 @@ class LighthousePalette(object):
|
||||
continue
|
||||
|
||||
# copy the in-box themes to the user theme directory
|
||||
plugin_theme_file = os.path.join(self.get_plugin_theme_dir(), theme_name)
|
||||
shutil.copy(plugin_theme_file, user_theme_file)
|
||||
shutil.copy(default_theme_file, user_theme_file)
|
||||
|
||||
#
|
||||
# if the user tries to switch themes, ensure the file dialog will start
|
||||
@@ -546,7 +549,11 @@ class LighthousePalette(object):
|
||||
# lmao, don't ask me why they forgot about this attribute from 5.0 - 5.6
|
||||
#
|
||||
|
||||
if disassembler.NAME == "BINJA":
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if disassembler.NAME == "BINJA" or (disassembler.NAME == "IDA"
|
||||
and disassembler._version_major == 9
|
||||
and disassembler._version_minor >= 2):
|
||||
test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
|
||||
else:
|
||||
test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
|
||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"name": "Long Night",
|
||||
"author": "https://github.com/ioncodes",
|
||||
|
||||
"colors":
|
||||
{
|
||||
"black": [33, 33, 33],
|
||||
"white": [241, 239, 236],
|
||||
|
||||
"darkGray": [20, 20, 20],
|
||||
"darkGray2": [30, 30, 30],
|
||||
"darkGray3": [54, 54, 54],
|
||||
|
||||
"gray": [100, 100, 100],
|
||||
"lightGray": [55, 55, 55],
|
||||
|
||||
"red": [188, 101, 141],
|
||||
"green": [64, 255, 64],
|
||||
"blue": [104, 134, 197],
|
||||
"lightBlue": [128, 200, 255],
|
||||
"darkBlue": [44, 44, 44],
|
||||
"purple": [121, 104, 197],
|
||||
|
||||
"focusRed": [255, 83, 112],
|
||||
"selection": [67, 67, 67]
|
||||
},
|
||||
|
||||
"fields":
|
||||
{
|
||||
"coverage_paint": ["darkBlue", "lightBlue"],
|
||||
|
||||
"table_text": "white",
|
||||
"table_grid": "black",
|
||||
"table_coverage_none": "black",
|
||||
"table_coverage_bad": "red",
|
||||
"table_coverage_good": "blue",
|
||||
"table_background": "black",
|
||||
"table_selection": "purple",
|
||||
|
||||
"html_summary_text": "white",
|
||||
"html_table_header": "white",
|
||||
"html_page_background": "black",
|
||||
|
||||
"shell_text": "white",
|
||||
"shell_text_valid": "lightBlue",
|
||||
"shell_text_invalid": "red",
|
||||
"shell_highlight_invalid": "red",
|
||||
|
||||
"shell_border": "lightGray",
|
||||
"shell_border_focus": "focusRed",
|
||||
"shell_background": "black",
|
||||
|
||||
"shell_hint_text": "white",
|
||||
"shell_hint_background": "black",
|
||||
|
||||
"logic_token": "red",
|
||||
"comma_token": "green",
|
||||
"paren_token": "green",
|
||||
"coverage_token": "lightBlue",
|
||||
|
||||
"combobox_text": "white",
|
||||
"combobox_selection_text": "white",
|
||||
"combobox_selection_background": "selection",
|
||||
|
||||
"combobox_border": "lightGray",
|
||||
"combobox_border_focus": "focusRed",
|
||||
"combobox_background": "black"
|
||||
}
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class BinjaCoreAPI(DisassemblerCoreAPI):
|
||||
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
|
||||
@@ -289,14 +289,18 @@ class BinjaContextAPI(DisassemblerContextAPI):
|
||||
|
||||
return vi.navigateToFunction(func, address)
|
||||
|
||||
@BinjaCoreAPI.execute_write
|
||||
def set_function_name_at(self, function_address, new_name):
|
||||
func = self.bv.get_function_at(function_address)
|
||||
|
||||
if not func:
|
||||
return
|
||||
|
||||
if new_name == "":
|
||||
new_name = None
|
||||
|
||||
state = self.bv.begin_undo_actions()
|
||||
func.name = new_name
|
||||
self.bv.commit_undo_actions(state)
|
||||
|
||||
#--------------------------------------------------------------------------
|
||||
# Hooks API
|
||||
@@ -322,9 +326,6 @@ class RenameHooks(binaryview.BinaryDataNotification):
|
||||
|
||||
def __init__(self, bv):
|
||||
self._bv = bv
|
||||
self.symbol_added = self.__symbol_handler
|
||||
self.symbol_updated = self.__symbol_handler
|
||||
self.symbol_removed = self.__symbol_handler
|
||||
|
||||
def hook(self):
|
||||
self._bv.register_notification(self)
|
||||
@@ -332,11 +333,25 @@ class RenameHooks(binaryview.BinaryDataNotification):
|
||||
def unhook(self):
|
||||
self._bv.unregister_notification(self)
|
||||
|
||||
def __symbol_handler(self, view, symbol):
|
||||
def symbol_added(self, *args):
|
||||
self.__symbol_handler(*args)
|
||||
|
||||
def symbol_updated(self, *args):
|
||||
self.__symbol_handler(*args)
|
||||
|
||||
def symbol_removed(self, *args):
|
||||
self.__symbol_handler(*args, True)
|
||||
|
||||
def __symbol_handler(self, view, symbol, removed=False):
|
||||
|
||||
func = self._bv.get_function_at(symbol.address)
|
||||
if not func.start == symbol.address:
|
||||
if not func or not func.start == symbol.address:
|
||||
return
|
||||
self.name_changed(symbol.address, symbol.name)
|
||||
|
||||
if removed:
|
||||
self.name_changed(symbol.address, "sub_%x" % symbol.address)
|
||||
else:
|
||||
self.name_changed(symbol.address, symbol.name)
|
||||
|
||||
def name_changed(self, address, name):
|
||||
"""
|
||||
@@ -380,7 +395,11 @@ if QT_AVAILABLE:
|
||||
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 +411,13 @@ if QT_AVAILABLE:
|
||||
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)
|
||||
@@ -150,14 +150,21 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
self._dockable_factory[dockable_name] = create_widget_callback
|
||||
|
||||
def create_dockable_widget(self, parent, dockable_name):
|
||||
import sip
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if USING_PYSIDE6:
|
||||
from shiboken6 import wrapInstance
|
||||
else:
|
||||
import sip
|
||||
def wrapInstance(ptr, base=None):
|
||||
return sip.wrapinstance(int(ptr), base)
|
||||
|
||||
# create a dockable widget, and save a reference to it for later use
|
||||
twidget = idaapi.create_empty_widget(dockable_name)
|
||||
self._dockable_widgets[dockable_name] = twidget
|
||||
|
||||
# cast the IDA 'twidget' as a Qt widget for use
|
||||
widget = sip.wrapinstance(int(twidget), QtWidgets.QWidget)
|
||||
widget = wrapInstance(int(twidget), QtWidgets.QWidget)
|
||||
widget.name = dockable_name
|
||||
widget.visible = False
|
||||
|
||||
@@ -226,7 +233,12 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
# attempt to generate an 'html' dump of the first 0x20 bytes (instructions)
|
||||
ida_fd = idaapi.fopenWT(path)
|
||||
idaapi.gen_file(idaapi.OFILE_LST, ida_fd, imagebase, imagebase+0x20, idaapi.GENFLG_GENHTML)
|
||||
idaapi.eclose(ida_fd)
|
||||
# IDA 9.x SDK fix: removed `idaapi.eclose`, added `ida_fpro.qfclose`
|
||||
if int(idaapi.get_kernel_version()[0]) >= 9:
|
||||
import ida_fpro
|
||||
ida_fpro.qfclose(ida_fd)
|
||||
else:
|
||||
idaapi.eclose(ida_fd)
|
||||
|
||||
# read the dumped text
|
||||
with open(path, "r") as fd:
|
||||
@@ -238,16 +250,30 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
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, '<body bgcolor="', '">')
|
||||
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...")
|
||||
@@ -276,9 +302,16 @@ class IDACoreAPI(DisassemblerCoreAPI):
|
||||
# touch the target form so we know it is populated
|
||||
self._touch_ida_window(twidget)
|
||||
|
||||
# locate the Qt Widget for a form and take 1px image slice of it
|
||||
import sip
|
||||
widget = sip.wrapinstance(int(twidget), QtWidgets.QWidget)
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
if USING_PYSIDE6:
|
||||
from shiboken6 import wrapInstance
|
||||
else:
|
||||
import sip
|
||||
def wrapInstance(ptr, base=None):
|
||||
return sip.wrapinstance(int(ptr), base)
|
||||
|
||||
widget = wrapInstance(int(twidget), QtWidgets.QWidget)
|
||||
pixmap = widget.grab(QtCore.QRect(0, 10, widget.width(), 1))
|
||||
|
||||
# convert the raw pixmap into an image (easier to interface with)
|
||||
@@ -411,6 +444,16 @@ class RenameHooks(idaapi.IDB_Hooks):
|
||||
# HexRays Util
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
def hexrays_available():
|
||||
"""
|
||||
Return True if an IDA decompiler is loaded and available for use.
|
||||
"""
|
||||
try:
|
||||
import ida_hexrays
|
||||
return ida_hexrays.init_hexrays_plugin()
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def map_line2citem(decompilation_text):
|
||||
"""
|
||||
Map decompilation line numbers to citems.
|
||||
@@ -568,4 +611,3 @@ def lex_citem_indexes(line):
|
||||
|
||||
# return all the citem indexes extracted from this line of text
|
||||
return indexes
|
||||
|
||||
@@ -110,12 +110,10 @@ def start_logging():
|
||||
# only enable logging if the LIGHTHOUSE_LOGGING environment variable is
|
||||
# present. we simply return a stub logger to sinkhole messages.
|
||||
#
|
||||
# NOTE / v0.9.0: logging is enabled by default for now...
|
||||
#
|
||||
|
||||
#if os.getenv("LIGHTHOUSE_LOGGING") == None:
|
||||
# logger.disabled = True
|
||||
# return logger
|
||||
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()
|
||||
@@ -146,6 +146,7 @@ def register_callback(callback_list, callback):
|
||||
|
||||
# 'register' the callback
|
||||
callback_list.append(callback_ref)
|
||||
return callback_ref
|
||||
|
||||
def notify_callback(callback_list, *args):
|
||||
"""
|
||||
@@ -208,32 +209,12 @@ def notify_callback(callback_list, *args):
|
||||
for callback_ref in cleanup:
|
||||
callback_list.remove(callback_ref)
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# Coverage Util
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
def build_hitmap(data):
|
||||
def unregister_callback(callback_list, callback_ref):
|
||||
"""
|
||||
Build a hitmap from the given list of address.
|
||||
|
||||
A hitmap is a map of address --> number of executions.
|
||||
|
||||
The list of input addresses can be any sort of runtime trace, coverage,
|
||||
or profiling data that one would like to build a hitmap for.
|
||||
Remove a previously-registered callback reference.
|
||||
"""
|
||||
output = collections.defaultdict(int)
|
||||
|
||||
# if there is no input data, simply return an empty hitmap
|
||||
if not data:
|
||||
return output
|
||||
|
||||
#
|
||||
# walk through the given list of given addresses and build a
|
||||
# corresponding hitmap for them
|
||||
#
|
||||
|
||||
for address in data:
|
||||
output[address] += 1
|
||||
|
||||
# return the hitmap
|
||||
return output
|
||||
try:
|
||||
callback_list.remove(callback_ref)
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
@@ -0,0 +1,118 @@
|
||||
|
||||
#
|
||||
# this global is used to indicate whether Qt bindings for python are present
|
||||
# and available for use by Lighthouse.
|
||||
#
|
||||
|
||||
QT_AVAILABLE = False
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PyQt5 <--> PySide2 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
#
|
||||
# we use this file to shim/re-alias a few Qt API's to ensure compatibility
|
||||
# between the popular Qt frameworks. these shims serve to reduce the number
|
||||
# of compatibility checks in the plugin code that consumes them.
|
||||
#
|
||||
# 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 (< 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...
|
||||
#
|
||||
|
||||
# IDA 9.2 SDK fix: migrate from PyQt5 to PySide6
|
||||
# https://docs.hex-rays.com/user-guide/plugins/migrating-pyqt5-code-to-pyside6
|
||||
try:
|
||||
import idaapi
|
||||
ver_major, ver_minor = map(int, idaapi.get_kernel_version().split("."))
|
||||
USING_NEW_IDA = ver_major == 9 and ver_minor >= 2
|
||||
USING_OLD_IDA = not(USING_NEW_IDA)
|
||||
except ImportError:
|
||||
USING_NEW_IDA = False
|
||||
USING_OLD_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 (IDA from 7.0 to 9.1)
|
||||
if USING_OLD_IDA:
|
||||
|
||||
try:
|
||||
import PyQt5.QtGui as QtGui
|
||||
import PyQt5.QtCore as QtCore
|
||||
import PyQt5.QtWidgets as QtWidgets
|
||||
|
||||
# importing went okay, PyQt5 must be available for use
|
||||
QT_AVAILABLE = True
|
||||
USING_PYQT5 = True
|
||||
|
||||
# import failed, PyQt5 is not available
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PySide2 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# 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
|
||||
import PySide2.QtWidgets as QtWidgets
|
||||
|
||||
# alias for less PySide2 <--> PyQt5 shimming
|
||||
QtCore.pyqtSignal = QtCore.Signal
|
||||
QtCore.pyqtSlot = QtCore.Slot
|
||||
|
||||
# importing went okay, PySide must be available for use
|
||||
QT_AVAILABLE = True
|
||||
USING_PYSIDE2 = True
|
||||
|
||||
# import failed. No Qt / UI bindings available...
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
# PySide6 Compatibility
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
# If all else fails, try to load PySide6 (New Binary Ninja and IDA)
|
||||
if not QT_AVAILABLE and (USING_NEW_BINJA or USING_NEW_IDA):
|
||||
|
||||
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
|
||||
@@ -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
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
@@ -37,8 +37,8 @@ def copy_to_clipboard(data):
|
||||
Copy the given data (a string) to the system clipboard.
|
||||
"""
|
||||
cb = QtWidgets.QApplication.clipboard()
|
||||
cb.clear(mode=cb.Clipboard)
|
||||
cb.setText(data, mode=cb.Clipboard)
|
||||
cb.clear(mode=QtGui.QClipboard.Mode.Clipboard)
|
||||
cb.setText(data, mode=QtGui.QClipboard.Mode.Clipboard)
|
||||
|
||||
def flush_qt_events():
|
||||
"""
|
||||
@@ -71,17 +71,17 @@ def get_dpi_scale():
|
||||
# xHeight is expected to be 40.0 at normal DPI
|
||||
return fm.height() / 173.0
|
||||
|
||||
def compute_color_on_gradiant(percent, color1, color2):
|
||||
def compute_color_on_gradient(percent, color1, color2):
|
||||
"""
|
||||
Compute the color specified by a percent between two colors.
|
||||
"""
|
||||
r1, g1, b1, _ = color1.getRgb()
|
||||
r2, g2, b2, _ = color2.getRgb()
|
||||
|
||||
# compute the new color across the gradiant of color1 -> color 2
|
||||
r = r1 + percent * (r2 - r1)
|
||||
g = g1 + percent * (g2 - g1)
|
||||
b = b1 + percent * (b2 - b1)
|
||||
# compute the new color across the gradient of color1 -> color 2
|
||||
r = r1 + int(percent * (r2 - r1))
|
||||
g = g1 + int(percent * (g2 - g1))
|
||||
b = b1 + int(percent * (b2 - b1))
|
||||
|
||||
# return the new color
|
||||
return QtGui.QColor(r,g,b)
|
||||
@@ -121,8 +121,8 @@ def prompt_string(label, title, default=""):
|
||||
dlg.setWindowTitle(title)
|
||||
dlg.setTextValue(default)
|
||||
dlg.resize(
|
||||
dpi_scale*400,
|
||||
dpi_scale*50
|
||||
int(dpi_scale*400),
|
||||
int(dpi_scale*50)
|
||||
)
|
||||
dlg.setModal(True)
|
||||
dlg.show()
|
||||
@@ -62,7 +62,7 @@ class WaitBox(QtWidgets.QDialog):
|
||||
# configure the main widget / form
|
||||
self.setSizeGripEnabled(False)
|
||||
self.setModal(True)
|
||||
self._dpi_scale = get_dpi_scale()*5.0
|
||||
self._dpi_scale = get_dpi_scale()*5
|
||||
|
||||
# initialize abort button
|
||||
self._abort_button = QtWidgets.QPushButton("Cancel")
|
||||
@@ -83,19 +83,19 @@ class WaitBox(QtWidgets.QDialog):
|
||||
v_layout.setAlignment(QtCore.Qt.AlignCenter)
|
||||
v_layout.addWidget(self._text_label)
|
||||
if self._abort:
|
||||
self._abort_button.clicked.connect(abort)
|
||||
self._abort_button.clicked.connect(self._abort)
|
||||
v_layout.addWidget(self._abort_button)
|
||||
|
||||
v_layout.setSpacing(self._dpi_scale*3)
|
||||
v_layout.setSpacing(int(self._dpi_scale*3))
|
||||
v_layout.setContentsMargins(
|
||||
self._dpi_scale*5,
|
||||
self._dpi_scale,
|
||||
self._dpi_scale*5,
|
||||
self._dpi_scale
|
||||
int(self._dpi_scale*5),
|
||||
int(self._dpi_scale),
|
||||
int(self._dpi_scale*5),
|
||||
int(self._dpi_scale)
|
||||
)
|
||||
|
||||
# scale widget dimensions based on DPI
|
||||
height = self._dpi_scale * 15
|
||||
height = int(self._dpi_scale * 15)
|
||||
self.setMinimumHeight(height)
|
||||
|
||||
# compute the dialog layout
|
||||
@@ -23,7 +23,7 @@ def check_for_update(current_version, callback):
|
||||
update_thread = threading.Thread(
|
||||
target=async_update_check,
|
||||
args=(current_version, callback,),
|
||||
name="UpdateChecker"
|
||||
name="Lighthouse UpdateChecker"
|
||||
)
|
||||
update_thread.start()
|
||||
|
||||
@@ -32,6 +32,7 @@ def async_update_check(current_version, callback):
|
||||
An async worker thread to check for an plugin update.
|
||||
"""
|
||||
logger.debug("Checking for update...")
|
||||
current_version = "v" + current_version
|
||||
|
||||
try:
|
||||
response = urlopen(UPDATE_URL, timeout=5.0)
|
||||
@@ -42,7 +43,7 @@ def async_update_check(current_version, callback):
|
||||
logger.debug(" - Failed to reach GitHub for update check...")
|
||||
return
|
||||
|
||||
# convert vesrion #'s to integer for easy compare...
|
||||
# convert version #'s to integer for easy compare...
|
||||
version_remote = int(''.join(re.findall('\d+', remote_version)))
|
||||
version_local = int(''.join(re.findall('\d+', current_version)))
|
||||
|
||||
@@ -54,8 +55,8 @@ def async_update_check(current_version, callback):
|
||||
|
||||
# notify the user if an update is available
|
||||
update_message = "An update is available for Lighthouse!\n\n" \
|
||||
" - Latest Version: %s\n" % (remote_version) + \
|
||||
" - Current Version: %s\n\n" % (current_version) + \
|
||||
" - Latest Version: %s\n" % (remote_version) + \
|
||||
" - Current Version: %s\n\n" % (current_version) + \
|
||||
"Please go download the update from GitHub."
|
||||
|
||||
callback(update_message)
|
||||