From 4b5ceabed17b260d4551e2ffe019bcb800ef26ee Mon Sep 17 00:00:00 2001
From: b
Date: Wed, 2 Jan 2019 16:06:27 +0100
Subject: [PATCH 001/154] Binja API: Getters -> Data members
---
plugin/lighthouse/util/disassembler/binja_api.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 96d0544a..460db2b1 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -22,7 +22,7 @@
#
DEPENDENCY_PATH = os.path.join(
- binaryninja.user_plugin_path(),
+ binaryninja.user_plugin_path,
"Lib",
"site-packages"
)
@@ -110,7 +110,7 @@ def __init__(self, bv=None):
self._python = _binja_get_scripting_instance()
def _init_version(self):
- version_string = binaryninja.core_version()
+ version_string = binaryninja.core_version
# retrieve Binja's version #
if "-" in version_string: # dev
@@ -155,7 +155,7 @@ def version_patch(self):
@property
def headless(self):
- return not binaryninja.core_ui_enabled()
+ return not binaryninja.core_ui_enabled
#--------------------------------------------------------------------------
# Synchronization Decorators
From f3fd77e8634a95b6e6adce5841a5c43f66d3e938 Mon Sep 17 00:00:00 2001
From: b
Date: Sun, 6 Jan 2019 08:55:50 +0100
Subject: [PATCH 002/154] Stable+Dev compatibility
---
.../lighthouse/util/disassembler/binja_api.py | 23 ++++++++++++++++---
1 file changed, 20 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 460db2b1..f97db0bb 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -21,8 +21,16 @@
# ship with PyQt5 bindings in-box.
#
+binja_user_plugin_path=None
+
+try:
+ binja_user_plugin_path=binaryninja.user_plugin_path()
+except TypeError:
+ print("[!] Running with stable API")
+ binja_user_plugin_path=binaryninja.user_plugin_path
+
DEPENDENCY_PATH = os.path.join(
- binaryninja.user_plugin_path,
+ binja_user_plugin_path,
"Lib",
"site-packages"
)
@@ -110,7 +118,11 @@ def __init__(self, bv=None):
self._python = _binja_get_scripting_instance()
def _init_version(self):
- version_string = binaryninja.core_version
+ version_string = None
+ try:
+ version_string = binaryninja.core_version()
+ except TypeError:
+ version_string = binaryninja.core_version
# retrieve Binja's version #
if "-" in version_string: # dev
@@ -155,7 +167,12 @@ def version_patch(self):
@property
def headless(self):
- return not binaryninja.core_ui_enabled
+ ret = None
+ try:
+ ret = binaryninja.core_ui_enabled()
+ except TypeError:
+ ret = binaryninja.core_ui_enabled
+ return not ret
#--------------------------------------------------------------------------
# Synchronization Decorators
From 0a5870a65a26817256fd6b59179c22ddd1549015 Mon Sep 17 00:00:00 2001
From: b
Date: Sun, 6 Jan 2019 09:36:18 +0100
Subject: [PATCH 003/154] Added comments
---
plugin/lighthouse/util/disassembler/binja_api.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index f97db0bb..95128607 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -22,11 +22,10 @@
#
binja_user_plugin_path=None
-
+# Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
try:
binja_user_plugin_path=binaryninja.user_plugin_path()
except TypeError:
- print("[!] Running with stable API")
binja_user_plugin_path=binaryninja.user_plugin_path
DEPENDENCY_PATH = os.path.join(
@@ -119,6 +118,7 @@ def __init__(self, bv=None):
def _init_version(self):
version_string = None
+ # Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
try:
version_string = binaryninja.core_version()
except TypeError:
@@ -168,6 +168,7 @@ def version_patch(self):
@property
def headless(self):
ret = None
+ # Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
try:
ret = binaryninja.core_ui_enabled()
except TypeError:
From 5423bbf7e992d38e47f804c06433068599d59e11 Mon Sep 17 00:00:00 2001
From: Andrew Fasano
Date: Fri, 25 Jan 2019 15:48:21 -0500
Subject: [PATCH 004/154] Add python3 support for drcov parser
And remain compatible with python2
---
plugin/lighthouse/parsers/drcov.py | 22 +++++++++++-----------
1 file changed, 11 insertions(+), 11 deletions(-)
diff --git a/plugin/lighthouse/parsers/drcov.py b/plugin/lighthouse/parsers/drcov.py
index df9c0bc0..a1b2e194 100644
--- a/plugin/lighthouse/parsers/drcov.py
+++ b/plugin/lighthouse/parsers/drcov.py
@@ -129,12 +129,12 @@ def _parse_drcov_header(self, f):
# parse drcov version from log
# eg: DRCOV VERSION: 2
- version_line = f.readline().strip()
+ version_line = f.readline().decode('utf-8').strip()
self.version = int(version_line.split(":")[1])
# parse drcov flavor from log
# eg: DRCOV FLAVOR: drcov
- flavor_line = f.readline().strip()
+ 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"
@@ -163,7 +163,7 @@ def _parse_module_table_header(self, f):
# parse module table 'header'
# eg: Module Table: version 2, count 11
- header_line = f.readline().strip()
+ header_line = f.readline().decode('utf-8').strip()
field_name, field_data = header_line.split(": ")
#assert field_name == "Module Table"
@@ -235,7 +235,7 @@ def _parse_module_table_columns(self, f):
# parse module table 'columns'
# eg: Columns: id, base, end, entry, checksum, timestamp, path
- column_line = f.readline().strip()
+ column_line = f.readline().decode('utf-8').strip()
field_name, field_data = column_line.split(": ")
#assert field_name == "Columns"
@@ -250,8 +250,8 @@ def _parse_module_table_modules(self, f):
"""
# loop through each *expected* line in the module table and parse it
- for i in xrange(self.module_table_count):
- module = DrcovModule(f.readline().strip(), self.module_table_version)
+ for i in range(self.module_table_count):
+ module = DrcovModule(f.readline().decode('utf-8').strip(), self.module_table_version)
self.modules.append(module)
def _parse_bb_table(self, f):
@@ -268,7 +268,7 @@ def _parse_bb_table_header(self, f):
# parse basic block table 'header'
# eg: BB Table: 2792 bbs
- header_line = f.readline().strip()
+ header_line = f.readline().decode('utf-8').strip()
field_name, field_data = header_line.split(": ")
#assert field_name == "BB Table"
@@ -305,14 +305,14 @@ def _parse_bb_table_entries(self, f):
f.readinto(self.basic_blocks)
else: # let's parse the text records
- text_entry = f.readline().strip()
+ text_entry = f.readline().decode('utf-8').strip()
if text_entry != "module id, start, size:":
raise ValueError("Invalid BB header: %r" % text_entry)
pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-f]+)\,\s*(?P[0-9]+)$")
for basic_block in self.basic_blocks:
- text_entry = f.readline().strip()
+ text_entry = f.readline().decode('utf-8').strip()
match = pattern.match(text_entry)
if not match:
@@ -468,10 +468,10 @@ class DrcovBasicBlock(Structure):
# base usage
if argc < 2:
- print "usage: %s " % os.path.basename(sys.argv[0])
+ print("usage: {} ".format(os.path.basename(sys.argv[0])))
sys.exit()
# attempt file parse
x = DrcovData(argv[1])
for bb in x.basic_blocks:
- print "0x%08x" % bb.start
+ print("0x{:08x}".format(bb.start))
From 03717b03a2a7c688efa371bdc9b4f6c8cb824982 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 10 Mar 2019 16:06:10 -0400
Subject: [PATCH 005/154] rough plumbing for additional coverage formats #41
---
plugin/lighthouse/core.py | 11 +-
plugin/lighthouse/director.py | 66 +-
plugin/lighthouse/metadata.py | 24 +
plugin/lighthouse/parsers/__init__.py | 1 -
plugin/lighthouse/reader/__init__.py | 1 +
plugin/lighthouse/reader/coverage_file.py | 43 +
plugin/lighthouse/reader/coverage_reader.py | 109 ++
plugin/lighthouse/reader/parsers/__init__.py | 0
.../lighthouse/{ => reader}/parsers/drcov.py | 129 +-
plugin/lighthouse/reader/parsers/modoff.py | 34 +
testcase/bombox-cov.modoff.txt | 1558 +++++++++++++++++
11 files changed, 1889 insertions(+), 87 deletions(-)
delete mode 100644 plugin/lighthouse/parsers/__init__.py
create mode 100644 plugin/lighthouse/reader/__init__.py
create mode 100644 plugin/lighthouse/reader/coverage_file.py
create mode 100644 plugin/lighthouse/reader/coverage_reader.py
create mode 100644 plugin/lighthouse/reader/parsers/__init__.py
rename plugin/lighthouse/{ => reader}/parsers/drcov.py (84%)
create mode 100644 plugin/lighthouse/reader/parsers/modoff.py
create mode 100644 testcase/bombox-cov.modoff.txt
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 100f010f..d24a458b 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -7,7 +7,7 @@
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
-from lighthouse.parsers import DrcovData
+from lighthouse.reader import CoverageReader
from lighthouse.palette import LighthousePalette
from lighthouse.painting import CoveragePainter
from lighthouse.director import CoverageDirector
@@ -20,9 +20,9 @@
# Plugin Metadata
#------------------------------------------------------------------------------
-PLUGIN_VERSION = "0.8.3"
+PLUGIN_VERSION = "0.8.4-DEV"
AUTHORS = "Markus Gaasedelen"
-DATE = "2018"
+DATE = "2019"
#------------------------------------------------------------------------------
# Lighthouse Plugin Core
@@ -318,7 +318,7 @@ def interactive_load_file(self):
await_future(future)
# insert the loaded drcov data objects into the director
- created_coverage, errors = self.director.create_coverage_from_drcov_list(drcov_list)
+ created_coverage, errors = self.director.create_coverage_from_files(drcov_list)
#
# if the director failed to map any coverage, the user probably
@@ -397,6 +397,7 @@ def load_coverage_files(filenames):
Load multiple code coverage files from disk.
"""
loaded_coverage = []
+ coverage_reader = CoverageReader()
#
# loop through each of the given filenames and attempt to load/parse
@@ -408,7 +409,7 @@ def load_coverage_files(filenames):
# attempt to load/parse a single coverage data file from disk
try:
- drcov_data = DrcovData(filename)
+ drcov_data = coverage_reader.open(filename)
# catch all for parse errors / bad input / malformed files
except Exception as e:
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index ff52461f..f73fe327 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -348,7 +348,7 @@ def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
"""
return self.update_coverage(coverage_name, coverage_data, coverage_filepath)
- def create_coverage_from_drcov_list(self, drcov_list):
+ def create_coverage_from_files(self, drcov_list):
"""
Create a number of database coverage mappings from a list of DrcovData.
@@ -417,7 +417,7 @@ def create_coverage_from_drcov_list(self, drcov_list):
# assign a suffix to the coverage name in the event of a collision
if coverage and coverage.filepath != drcov_data.filepath:
- for i in xrange(2,0x100000):
+ for i in xrange(2, 100000):
new_name = "%s_%u" % (coverage_name, i)
if not self.get_coverage(new_name):
break
@@ -456,6 +456,31 @@ def create_coverage_from_drcov_list(self, drcov_list):
# done
return (created_coverage, errors)
+ def _find_fuzzy_name(self, drcov_data, target_name):
+ """
+ TODO
+ """
+
+ # attempt lookup using case-insensitive filename
+ for module_name in drcov_data.modules:
+ if module_name.lower() in target_name.lower():
+ return module_name
+
+ #
+ # no hits yet... let's cleave the extension from the given module
+ # name (if present) and try again
+ #
+
+ if "." in target_name:
+ target_name = target_name.split(".")[0]
+
+ # attempt lookup using case-insensitive filename without extension
+ for module_name in drcov_data.modules:
+ if module_name.lower() in target_name.lower():
+ return module_name
+
+ return None
+
def _normalize_drcov_data(self, drcov_data):
"""
Extract and normalize relevant coverage data from a DrcovData object.
@@ -463,19 +488,40 @@ def _normalize_drcov_data(self, drcov_data):
Returns a list of executed instruction addresses for this database.
"""
+ # TODO all this is real rough draft right now
+
# extract the coverage relevant to this database (well, the root binary)
root_filename = self.metadata.filename
- coverage_blocks = drcov_data.get_blocks_by_module(root_filename)
+ module_name = self._find_fuzzy_name(drcov_data, root_filename) # TODO
+
+ try:
+ coverage_blocks = drcov_data.get_blocks(module_name)
- # rebase the coverage log's basic blocks to the database imagebase
- imagebase = self.metadata.imagebase
- rebased_blocks = rebase_blocks(imagebase, coverage_blocks)
+ # rebase the coverage log's basic blocks to the database imagebase
+ imagebase = self.metadata.imagebase
+ rebased_blocks = rebase_blocks(imagebase, coverage_blocks)
- # coalesce the blocks into larger contiguous blobs
- condensed_blocks = coalesce_blocks(rebased_blocks)
+ # coalesce the blocks into larger contiguous blobs
+ condensed_blocks = coalesce_blocks(rebased_blocks)
- # flatten the blobs into individual instruction addresses
- return self.metadata.flatten_blocks(condensed_blocks)
+ # flatten the blobs into individual instruction addresses
+ return self.metadata.flatten_blocks(condensed_blocks)
+
+ except NotImplementedError:
+ pass
+
+ try:
+ coverage_offsets = drcov_data.get_offsets(module_name)
+ rebased_offsets = map(lambda x: self.metadata.imagebase+x, coverage_offsets)
+ confidence = self.metadata.measure_block_confidence(rebased_offsets)
+ #print "Block confidence: %f" % confidence
+ if confidence > 0.90:
+ return rebased_offsets # inst trace
+ else:
+ return self.metadata.flatten_offsets() # bb trace
+ return rebased_offsets
+ except NotImplementedError:
+ pass
def aggregate_drcov_batch(self, drcov_list):
"""
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index ea0afcf3..40231363 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -211,6 +211,30 @@ def get_closest_function(self, address):
else:
return self.functions[before]
+ def measure_block_confidence(self, addresses):
+ """
+ TODO
+ """
+ if not addresses:
+ return 0
+ good = 0
+ for address in addresses:
+ if address in self.nodes:
+ good += 1
+ return float(good)/len(addresses)
+
+ def flatten_block_heads(self, addresses):
+ """
+ TODO this will probably get deleted
+ """
+ output = []
+ for address in addresses:
+ block = self.nodes.get(address, None)
+ if not block:
+ continue # lol
+ output.extend(block.instructions)
+ return output
+
def flatten_blocks(self, basic_blocks):
"""
Flatten a list of basic blocks (address, size) to instruction addresses.
diff --git a/plugin/lighthouse/parsers/__init__.py b/plugin/lighthouse/parsers/__init__.py
deleted file mode 100644
index 9689fa65..00000000
--- a/plugin/lighthouse/parsers/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from drcov import DrcovData
diff --git a/plugin/lighthouse/reader/__init__.py b/plugin/lighthouse/reader/__init__.py
new file mode 100644
index 00000000..c3deb519
--- /dev/null
+++ b/plugin/lighthouse/reader/__init__.py
@@ -0,0 +1 @@
+from coverage_reader import CoverageReader
diff --git a/plugin/lighthouse/reader/coverage_file.py b/plugin/lighthouse/reader/coverage_file.py
new file mode 100644
index 00000000..09d19cde
--- /dev/null
+++ b/plugin/lighthouse/reader/coverage_file.py
@@ -0,0 +1,43 @@
+import abc
+
+class CoverageFile(object):
+ """
+ Templated class for Lighthouse-compatible code coverage file reader.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def __init__(self, filepath=None):
+ self.filepath = filepath
+ self.modules = {}
+ self._parse()
+
+ #--------------------------------------------------------------------------
+ # Public
+ #--------------------------------------------------------------------------
+
+ def get_addresses(self, module_name=None):
+ """
+ Return coverage data for the named module as absolute addresses.
+ """
+ raise NotImplementedError("Absolute addresses not supported by this log format")
+
+ def get_offsets(self, module_name=None):
+ """
+ Return coverage data for the named module as relative offets.
+ """
+ raise NotImplementedError("Relative addresses not supported by this log format")
+
+ def get_blocks(self, module_name=None):
+ """
+ Return coverage data for the named module in block form (offset, size).
+ """
+ raise NotImplementedError("Block+Size not supported by this log format")
+
+ #--------------------------------------------------------------------------
+ # Parsing Routines - Top Level
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def _parse(self):
+ raise NotImplementedError("Coverage parser not implemented")
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
new file mode 100644
index 00000000..8b45b8f3
--- /dev/null
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -0,0 +1,109 @@
+import os
+import sys
+import inspect
+import logging
+import traceback
+
+from .coverage_file import CoverageFile
+
+logger = logging.getLogger("Lighthouse.Reader")
+
+MODULES_DIRECTORY = os.path.join(os.path.dirname(os.path.realpath(__file__)), "parsers")
+
+class CoverageReader(object):
+ """
+ TODO
+ """
+
+ def __init__(self):
+ self._installed_parsers = {}
+ self._import_parsers()
+
+ def open(self, filepath):
+ """
+ TODO
+ """
+
+ for name, parser in self._installed_parsers.iteritems():
+ try:
+ return parser(filepath)
+ except Exception as e:
+ #print traceback.format_exc()
+ pass
+
+ raise ValueError("No compatible coverage parser for %s" % filepath)
+
+ def _import_parsers(self):
+ """
+ Scan and import coverage file parsers.
+ """
+ target_subclass = CoverageFile
+ ignored_files = ["__init__.py"]
+
+ # loop through all the files in the parsers folder
+ for filename in os.listdir(MODULES_DIRECTORY):
+
+ # ignore specified files, and anything not *.py
+ if filename in ignored_files or filename.endswith(".py") == False:
+ continue
+
+ # attempt to load a CoverageFile format from the current *.py file
+ logger.debug("| Searching file %s" % filename)
+ parser_file = filename[:-3]
+ parser_class = self._locate_subclass(parser_file, target_subclass)
+
+ if not parser_class:
+ logger.warning("| - No object subclassing from %s found in %s..." \
+ % (target_subclass.__name__, parser_file))
+ continue
+
+ # instantiate and add the parser to our dict of imported parsers
+ logger.debug("| | Found %s" % parser_class.__name__)
+ self._installed_parsers[parser_class.__name__] = parser_class
+ logger.debug("+- Done dynamically importing parsers")
+
+ # return the number of modules successfully imported
+ return self._installed_parsers
+
+ def _locate_subclass(self, module_file, target_subclass):
+ """
+ Return the first matching target_subclass in module_file.
+
+ This function is used to scan a specific file (module_file)
+ in the Lighthouse parsers directory for class definitions that
+ subclass from target_subclass.
+
+ We use this to dynmically import, locate, and return objects
+ that are utilizing our CoverageFile abstraction.
+ """
+ module = None
+ module_class = None
+
+ # attempt to import the given filepath as a python module
+ try:
+ module = __import__("parsers." + module_file, globals(), locals(), ['object'], -1)
+ except Exception as e:
+ logger.exception("| - Parser import failed")
+ return None
+
+ #
+ # inspect the module for any classes that subclass from target_subclass
+ # eg: target_subclass == CoverageFile
+ #
+
+ class_members = inspect.getmembers(module, inspect.isclass)
+ for a_class in class_members:
+
+ # does the current class definition we're inspecting subclass
+ # from target_subclass? if so, it is a match
+ try:
+ if a_class[1].__bases__[0] == target_subclass:
+ module_class = a_class[1]
+ break
+
+ # this class does not subclass / etc / not interesting / ignore it
+ except IndexError as e:
+ pass
+
+ # return discovered parser or None
+ return module_class
diff --git a/plugin/lighthouse/reader/parsers/__init__.py b/plugin/lighthouse/reader/parsers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugin/lighthouse/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
similarity index 84%
rename from plugin/lighthouse/parsers/drcov.py
rename to plugin/lighthouse/reader/parsers/drcov.py
index a1b2e194..e9d34f50 100644
--- a/plugin/lighthouse/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -1,100 +1,81 @@
#!/usr/bin/python
-
import os
+import re
import sys
import mmap
import struct
-import re
from ctypes import *
+try:
+ from ..coverage_file import CoverageFile
+except ImportError as e:
+ CoverageFile = object
+
#------------------------------------------------------------------------------
-# drcov log parser
+# DynamoRIO Drcov Log Parser
#------------------------------------------------------------------------------
-class DrcovData(object):
+class DrcovData(CoverageFile):
"""
A drcov log parser.
"""
- def __init__(self, filepath=None):
- # original filepath
+ def __init__(self, filepath=None):
self.filepath = filepath
# drcov header attributes
self.version = 0
- self.flavor = None
+ self.flavor = None
# drcov module table
- self.module_table_count = 0
+ self.module_table_count = 0
self.module_table_version = 0
- self.modules = []
+ self.modules = {}
# drcov basic block data
- self.bb_table_count = 0
+ self.bbs = []
+ self.bb_table_count = 0
self.bb_table_is_binary = True
- self.basic_blocks = []
- # parse the given filepath
- self._parse_drcov_file(filepath)
+ # parse
+ super(DrcovData, self).__init__(filepath)
#--------------------------------------------------------------------------
# Public
#--------------------------------------------------------------------------
- def get_module(self, module_name, fuzzy=True):
+ def get_offsets(self, module_name):
"""
- Get a module by its name.
-
- Note that this is a 'fuzzy' lookup by default.
+ Return coverage data as basic block offsets for the named module.
"""
+ try:
+ module = self.modules[module_name]
+ except KeyError:
+ raise ValueError("No coverage for module '%s' in log" % module_name)
- # fuzzy module name lookup
- if fuzzy:
-
- # attempt lookup using case-insensitive filename
- for module in self.modules:
- if module_name.lower() in module.filename.lower():
- return module
-
- #
- # no hits yet... let's cleave the extension from the given module
- # name (if present) and try again
- #
-
- if "." in module_name:
- module_name = module_name.split(".")[0]
-
- # attempt lookup using case-insensitive filename without extension
- for module in self.modules:
- if module_name.lower() in module.filename.lower():
- return module
+ # extract module id for speed
+ mod_id = module.id
- # strict lookup
- else:
- for module in self.modules:
- if module_name == module.filename:
- return module
+ # loop through the coverage data and filter out data for only this module
+ coverage_blocks = [bb.start for bb in self.bbs if bb.mod_id == mod_id]
- # no matching module exists
- return None
+ # return the filtered coverage blocks
+ return coverage_blocks
- def get_blocks_by_module(self, module_name):
+ def get_blocks(self, module_name):
"""
- Extract coverage blocks pertaining to the named module.
+ Return coverage data as basic blocks (offset, size) for the named module.
"""
-
- # locate the coverage that matches the given module_name
- module = self.get_module(module_name)
-
- # if we fail to find a module that matches the given name, bail
- if not module:
+ try:
+ module = self.modules[module_name]
+ except KeyError:
raise ValueError("No coverage for module '%s' in log" % module_name)
# extract module id for speed
mod_id = module.id
# loop through the coverage data and filter out data for only this module
- coverage_blocks = [(bb.start, bb.size) for bb in self.basic_blocks if bb.mod_id == mod_id]
+ coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id == mod_id]
# return the filtered coverage blocks
return coverage_blocks
@@ -103,21 +84,15 @@ def get_blocks_by_module(self, module_name):
# Parsing Routines - Top Level
#--------------------------------------------------------------------------
- def _parse_drcov_file(self, filepath):
+ def _parse(self):
"""
Parse drcov coverage from the given log file.
"""
- with open(filepath, "rb") as f:
+ with open(self.filepath, "rb") as f:
self._parse_drcov_header(f)
self._parse_module_table(f)
self._parse_bb_table(f)
- def _parse_drcov_data(self, drcov_data):
- """
- Parse drcov coverage from the given data blob.
- """
- pass # TODO/DRCOV
-
#--------------------------------------------------------------------------
# Parsing Routines - Internals
#--------------------------------------------------------------------------
@@ -252,7 +227,17 @@ def _parse_module_table_modules(self, f):
# loop through each *expected* line in the module table and parse it
for i in range(self.module_table_count):
module = DrcovModule(f.readline().decode('utf-8').strip(), self.module_table_version)
- self.modules.append(module)
+
+ # try to handle module name collisions...
+ if module.filename in self.modules:
+ public_name = module.filename + "_" + str(i)
+ else:
+ public_name = module.filename
+
+ assert not (public_name in self.modules), "Stop doing weird stuff."
+
+ # save the parsed module
+ self.modules[public_name] = module
def _parse_bb_table(self, f):
"""
@@ -297,30 +282,32 @@ def _parse_bb_table_entries(self, f):
"""
Parse drcov log basic block table entries from filestream.
"""
+
# allocate the ctypes structure array of basic blocks
- self.basic_blocks = (DrcovBasicBlock * self.bb_table_count)()
+ self.bbs = (DrcovBasicBlock * self.bb_table_count)()
+ # read binary basic block entries directly into the newly allocated array
if self.bb_table_is_binary:
- # read the basic block entries directly into the newly allocated array
- f.readinto(self.basic_blocks)
+ f.readinto(self.bbs)
- else: # let's parse the text records
+ # parse the plaintext basic block entries one by one
+ else:
text_entry = f.readline().decode('utf-8').strip()
if text_entry != "module id, start, size:":
raise ValueError("Invalid BB header: %r" % text_entry)
pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-f]+)\,\s*(?P[0-9]+)$")
- for basic_block in self.basic_blocks:
+ 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)
- basic_block.start = int(match.group("start"), 16)
- basic_block.size = int(match.group("size"), 10)
- basic_block.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
@@ -473,5 +460,5 @@ class DrcovBasicBlock(Structure):
# attempt file parse
x = DrcovData(argv[1])
- for bb in x.basic_blocks:
+ for bb in x.bbs:
print("0x{:08x}".format(bb.start))
diff --git a/plugin/lighthouse/reader/parsers/modoff.py b/plugin/lighthouse/reader/parsers/modoff.py
new file mode 100644
index 00000000..513dc245
--- /dev/null
+++ b/plugin/lighthouse/reader/parsers/modoff.py
@@ -0,0 +1,34 @@
+import os
+import collections
+
+from ..coverage_file import CoverageFile
+
+class ModOffData(CoverageFile):
+ """
+ A module+offset log parser.
+ """
+
+ def __init__(self, filepath):
+ super(ModOffData, self).__init__(filepath)
+
+ #--------------------------------------------------------------------------
+ # Public
+ #--------------------------------------------------------------------------
+
+ def get_offsets(self, module_name):
+ return self.modules.get(module_name, {})
+
+ #--------------------------------------------------------------------------
+ # Parsing Routines - Top Level
+ #--------------------------------------------------------------------------
+
+ def _parse(self):
+ """
+ Parse modoff coverage from the given log file.
+ """
+ modules = collections.defaultdict(lambda: collections.defaultdict(int))
+ with open(self.filepath) as f:
+ for line in f:
+ module_name, bb_offset = line.rsplit("+", 1)
+ modules[module_name][int(bb_offset, 16)] += 1
+ self.modules = modules
diff --git a/testcase/bombox-cov.modoff.txt b/testcase/bombox-cov.modoff.txt
new file mode 100644
index 00000000..daf84009
--- /dev/null
+++ b/testcase/bombox-cov.modoff.txt
@@ -0,0 +1,1558 @@
+boombox+3a06
+boombox+3a09
+boombox+3a0f
+boombox+3a15
+boombox+3a1a
+boombox+3a1f
+boombox+3a21
+boombox+3a28
+boombox+3a2a
+boombox+3a31
+boombox+3a33
+boombox+3a37
+boombox+3a3c
+boombox+12b0
+boombox+12b2
+boombox+12b3
+boombox+12b5
+boombox+12b9
+boombox+12bb
+boombox+12be
+boombox+12c2
+boombox+12c4
+boombox+12c6
+boombox+12c9
+boombox+12dc
+boombox+12df
+boombox+1358
+boombox+135d
+boombox+1362
+boombox+1367
+boombox+136e
+boombox+1373
+boombox+1376
+boombox+137c
+boombox+1383
+boombox+1389
+boombox+1390
+boombox+1395
+boombox+139b
+boombox+13a2
+boombox+13a9
+boombox+13af
+boombox+13b5
+boombox+13b9
+boombox+13bf
+boombox+13c6
+boombox+13cd
+boombox+13d3
+boombox+13d7
+boombox+13dc
+boombox+13e1
+boombox+13e8
+boombox+13ed
+boombox+13f3
+boombox+13fa
+boombox+1400
+boombox+1407
+boombox+140c
+boombox+1412
+boombox+1419
+boombox+1420
+boombox+1426
+boombox+142c
+boombox+1430
+boombox+1436
+boombox+143d
+boombox+1444
+boombox+144a
+boombox+144f
+boombox+1456
+boombox+145b
+boombox+1461
+boombox+1467
+boombox+1469
+boombox+1150
+boombox+1154
+boombox+115a
+boombox+115d
+boombox+11d4
+boombox+11d8
+boombox+1060
+boombox+1065
+boombox+106a
+boombox+106b
+boombox+106f
+boombox+1072
+boombox+1074
+boombox+1077
+boombox+107a
+boombox+107f
+boombox+1085
+boombox+1087
+boombox+108a
+boombox+108d
+boombox+1093
+boombox+1095
+boombox+1097
+boombox+1099
+boombox+4928
+boombox+109b
+boombox+10a0
+boombox+10a4
+boombox+10a6
+boombox+10a9
+boombox+10ab
+boombox+10ae
+boombox+10de
+boombox+10e3
+boombox+10e8
+boombox+10eb
+boombox+10ef
+boombox+10f3
+boombox+10f4
+boombox+147e
+boombox+1483
+boombox+1487
+boombox+148b
+boombox+148f
+boombox+1495
+boombox+1498
+boombox+149b
+boombox+14a1
+boombox+14a6
+boombox+14ab
+boombox+14b0
+boombox+14b4
+boombox+14b9
+boombox+14be
+boombox+14c5
+boombox+14ca
+boombox+14cd
+boombox+14d3
+boombox+14da
+boombox+14e0
+boombox+14e7
+boombox+14ec
+boombox+14f2
+boombox+14f9
+boombox+1500
+boombox+1506
+boombox+150c
+boombox+1510
+boombox+1516
+boombox+151d
+boombox+1524
+boombox+152a
+boombox+152f
+boombox+1532
+boombox+1537
+boombox+153e
+boombox+1543
+boombox+1549
+boombox+1550
+boombox+1556
+boombox+155d
+boombox+1562
+boombox+1568
+boombox+156f
+boombox+1576
+boombox+157c
+boombox+1582
+boombox+1586
+boombox+158c
+boombox+1593
+boombox+159a
+boombox+15a0
+boombox+15a4
+boombox+15a9
+boombox+15ae
+boombox+15b2
+boombox+15b5
+boombox+15ba
+boombox+15bc
+boombox+15c0
+boombox+15c2
+boombox+15c5
+boombox+15c8
+boombox+15cc
+boombox+15d0
+boombox+15d6
+boombox+15db
+boombox+15e0
+boombox+15e5
+boombox+15ec
+boombox+15ef
+boombox+15f4
+boombox+15f9
+boombox+15ff
+boombox+1606
+boombox+160c
+boombox+1613
+boombox+1618
+boombox+161e
+boombox+1625
+boombox+162c
+boombox+1632
+boombox+1638
+boombox+163c
+boombox+1642
+boombox+1649
+boombox+1650
+boombox+1656
+boombox+165b
+boombox+165d
+boombox+1661
+boombox+1663
+boombox+1664
+boombox+1665
+boombox+3d57
+boombox+3d5e
+boombox+3d63
+boombox+3d69
+boombox+3d70
+boombox+3d76
+boombox+3d7d
+boombox+3d82
+boombox+3d88
+boombox+3d8f
+boombox+3d96
+boombox+3d9c
+boombox+3da2
+boombox+3da6
+boombox+3dac
+boombox+3db3
+boombox+3dba
+boombox+3dc0
+boombox+3dc6
+boombox+3dc8
+boombox+3960
+boombox+3962
+boombox+3964
+boombox+3320
+boombox+3324
+boombox+332b
+boombox+3331
+boombox+3338
+boombox+333e
+boombox+3345
+boombox+334a
+boombox+3350
+boombox+3357
+boombox+335d
+boombox+3364
+boombox+336b
+boombox+3371
+boombox+3378
+boombox+337e
+boombox+3385
+boombox+338b
+boombox+3392
+boombox+3398
+boombox+339f
+boombox+33a4
+boombox+33aa
+boombox+33b1
+boombox+33b7
+boombox+33be
+boombox+33c5
+boombox+33cb
+boombox+33d2
+boombox+33d8
+boombox+33df
+boombox+33e4
+boombox+33ea
+boombox+33f1
+boombox+33f7
+boombox+33fe
+boombox+3405
+boombox+340b
+boombox+3412
+boombox+3417
+boombox+341d
+boombox+3424
+boombox+342a
+boombox+3431
+boombox+3438
+boombox+343e
+boombox+3445
+boombox+344b
+boombox+3452
+boombox+3457
+boombox+345d
+boombox+3464
+boombox+346a
+boombox+3471
+boombox+3478
+boombox+347e
+boombox+3485
+boombox+348b
+boombox+3492
+boombox+3497
+boombox+349d
+boombox+34a4
+boombox+34aa
+boombox+34b1
+boombox+34b8
+boombox+34be
+boombox+34c5
+boombox+34cb
+boombox+34d2
+boombox+34d8
+boombox+34df
+boombox+34e4
+boombox+34ea
+boombox+34f1
+boombox+34f7
+boombox+34fe
+boombox+3505
+boombox+350b
+boombox+3512
+boombox+3518
+boombox+351f
+boombox+3524
+boombox+352a
+boombox+3531
+boombox+3537
+boombox+353e
+boombox+3545
+boombox+354b
+boombox+3552
+boombox+3558
+boombox+355f
+boombox+3564
+boombox+356a
+boombox+3571
+boombox+3577
+boombox+357e
+boombox+3585
+boombox+358b
+boombox+3592
+boombox+3598
+boombox+359f
+boombox+35a4
+boombox+35aa
+boombox+35b1
+boombox+35b7
+boombox+35be
+boombox+35c5
+boombox+35cb
+boombox+35d2
+boombox+35d7
+boombox+35dd
+boombox+35e4
+boombox+35ea
+boombox+35f1
+boombox+35f8
+boombox+35fe
+boombox+3605
+boombox+360b
+boombox+3612
+boombox+3617
+boombox+361d
+boombox+3624
+boombox+362a
+boombox+3631
+boombox+3638
+boombox+363e
+boombox+3645
+boombox+364b
+boombox+3652
+boombox+3657
+boombox+365d
+boombox+3664
+boombox+366a
+boombox+3671
+boombox+3678
+boombox+367e
+boombox+3685
+boombox+368b
+boombox+3692
+boombox+3698
+boombox+369f
+boombox+36a5
+boombox+36ac
+boombox+36b1
+boombox+36b7
+boombox+36be
+boombox+36c4
+boombox+36cb
+boombox+36d2
+boombox+36d8
+boombox+36df
+boombox+36e5
+boombox+36ec
+boombox+36f1
+boombox+36f7
+boombox+36fe
+boombox+3704
+boombox+370b
+boombox+3712
+boombox+3718
+boombox+371f
+boombox+3725
+boombox+372c
+boombox+3731
+boombox+3737
+boombox+373e
+boombox+3744
+boombox+374b
+boombox+3752
+boombox+3758
+boombox+375f
+boombox+3765
+boombox+376c
+boombox+3771
+boombox+3777
+boombox+377e
+boombox+3784
+boombox+378b
+boombox+3792
+boombox+3798
+boombox+379f
+boombox+37a5
+boombox+37ac
+boombox+37b1
+boombox+37b7
+boombox+37be
+boombox+37c4
+boombox+37cb
+boombox+37d2
+boombox+37d8
+boombox+37df
+boombox+37e5
+boombox+37ec
+boombox+37f2
+boombox+37f9
+boombox+37ff
+boombox+3805
+boombox+3809
+boombox+380d
+boombox+3969
+boombox+3970
+boombox+3972
+boombox+3977
+boombox+397b
+boombox+397f
+boombox+3982
+boombox+3984
+boombox+398a
+boombox+3991
+boombox+3997
+boombox+399e
+boombox+39a3
+boombox+39a9
+boombox+39b0
+boombox+39b7
+boombox+39bd
+boombox+39c3
+boombox+39c7
+boombox+39cd
+boombox+39d4
+boombox+39db
+boombox+39e1
+boombox+39e7
+boombox+39eb
+boombox+39ee
+boombox+39f3
+boombox+39f9
+boombox+39fd
+boombox+3a00
+boombox+3a41
+boombox+3a47
+boombox+3a49
+boombox+3a50
+boombox+3a52
+boombox+3a59
+boombox+3a5b
+boombox+3a5f
+boombox+3a64
+boombox+16f0
+boombox+16f2
+boombox+16f3
+boombox+16f7
+boombox+16fa
+boombox+16ff
+boombox+1706
+boombox+170b
+boombox+1711
+boombox+1718
+boombox+171e
+boombox+1725
+boombox+172a
+boombox+1730
+boombox+1737
+boombox+173e
+boombox+1744
+boombox+174a
+boombox+174e
+boombox+1754
+boombox+175b
+boombox+1762
+boombox+1768
+boombox+176d
+boombox+1774
+boombox+1776
+boombox+177b
+boombox+1781
+boombox+1787
+boombox+1789
+boombox+2590
+boombox+2595
+boombox+259a
+boombox+259b
+boombox+259f
+boombox+25a2
+boombox+25a9
+boombox+25ae
+boombox+25b4
+boombox+25bb
+boombox+25c1
+boombox+25c8
+boombox+25cd
+boombox+25d3
+boombox+25da
+boombox+25e1
+boombox+25e7
+boombox+25ed
+boombox+25f1
+boombox+25f7
+boombox+25fe
+boombox+2605
+boombox+260b
+boombox+2610
+boombox+2614
+boombox+2617
+boombox+2620
+boombox+2627
+boombox+262c
+boombox+2632
+boombox+2639
+boombox+263f
+boombox+2646
+boombox+264b
+boombox+2651
+boombox+2658
+boombox+265f
+boombox+2665
+boombox+266b
+boombox+266f
+boombox+2675
+boombox+267c
+boombox+2683
+boombox+2689
+boombox+268c
+boombox+268e
+boombox+2691
+boombox+26a2
+boombox+26a9
+boombox+26ad
+boombox+26b3
+boombox+26b5
+boombox+26b9
+boombox+26bc
+boombox+2693
+boombox+269a
+boombox+26a0
+boombox+26c2
+boombox+26c7
+boombox+26cc
+boombox+26d0
+boombox+26d1
+boombox+179e
+boombox+17a2
+boombox+17a4
+boombox+17a7
+boombox+181e
+boombox+1823
+boombox+1828
+boombox+182c
+boombox+18a8
+boombox+18ad
+boombox+18b0
+boombox+18b2
+boombox+18b9
+boombox+18bb
+boombox+18bf
+boombox+18c3
+boombox+18c5
+boombox+18c9
+boombox+18cb
+boombox+18d0
+boombox+18d4
+boombox+18d7
+boombox+18dd
+boombox+18df
+boombox+18e3
+boombox+18e6
+boombox+18ea
+boombox+18ec
+boombox+18f1
+boombox+18f4
+boombox+18fa
+boombox+1901
+boombox+1906
+boombox+190a
+boombox+190f
+boombox+1914
+boombox+1919
+boombox+191b
+boombox+191f
+boombox+1920
+boombox+1921
+boombox+2b50
+boombox+2b55
+boombox+2b56
+boombox+2b5a
+boombox+2b5d
+boombox+2b5f
+boombox+2b62
+boombox+2b67
+boombox+2b6a
+boombox+2b6d
+boombox+2b72
+boombox+2b79
+boombox+2b7c
+boombox+2b7f
+boombox+2b85
+boombox+2b8c
+boombox+2b92
+boombox+2b99
+boombox+2b9c
+boombox+2ba2
+boombox+2ba9
+boombox+2bac
+boombox+2bb2
+boombox+2bb8
+boombox+2bbc
+boombox+2bc2
+boombox+2bc9
+boombox+2bd0
+boombox+2bd5
+boombox+2bd9
+boombox+2bda
+boombox+29d0
+boombox+29d5
+boombox+29d8
+boombox+29da
+boombox+29e1
+boombox+29e4
+boombox+29ec
+boombox+29ef
+boombox+29fa
+boombox+29ff
+boombox+2a02
+boombox+2a60
+boombox+2a65
+boombox+2a68
+boombox+2a6a
+boombox+2a71
+boombox+2a74
+boombox+2a7c
+boombox+2a7f
+boombox+2a8a
+boombox+2a8f
+boombox+2a92
+boombox+3a69
+boombox+3a6f
+boombox+3a71
+boombox+3a78
+boombox+3a7a
+boombox+3a81
+boombox+3a83
+boombox+3a87
+boombox+3a8c
+boombox+1930
+boombox+1932
+boombox+1936
+boombox+1939
+boombox+193e
+boombox+1945
+boombox+194a
+boombox+1950
+boombox+1957
+boombox+195d
+boombox+1964
+boombox+1969
+boombox+196f
+boombox+1976
+boombox+197d
+boombox+1983
+boombox+1989
+boombox+198d
+boombox+1993
+boombox+199a
+boombox+19a1
+boombox+19a7
+boombox+19ac
+boombox+19b3
+boombox+19bc
+boombox+19c2
+boombox+19c8
+boombox+19ca
+boombox+19de
+boombox+19e2
+boombox+19e9
+boombox+19eb
+boombox+19ee
+boombox+1a5d
+boombox+1a63
+boombox+1a65
+boombox+1a6a
+boombox+1a70
+boombox+1a77
+boombox+1a7d
+boombox+1a84
+boombox+1a89
+boombox+1a8f
+boombox+1a96
+boombox+1a21
+boombox+1a28
+boombox+1a2e
+boombox+1a34
+boombox+1a38
+boombox+1a3e
+boombox+1a45
+boombox+1a4c
+boombox+1a52
+boombox+1a57
+boombox+1a5b
+boombox+1a5c
+boombox+1a98
+boombox+1a9b
+boombox+1aa0
+boombox+1aa6
+boombox+1aae
+boombox+1ab4
+boombox+1abb
+boombox+1ac1
+boombox+1ac8
+boombox+1acd
+boombox+1ad3
+boombox+1ada
+boombox+1ae1
+boombox+1ae7
+boombox+1aed
+boombox+1af1
+boombox+1af7
+boombox+1afe
+boombox+1b05
+boombox+1b0b
+boombox+1b0d
+boombox+1b11
+boombox+1b12
+boombox+3a91
+boombox+3a97
+boombox+3a99
+boombox+3aa0
+boombox+3aa2
+boombox+3aa9
+boombox+3aab
+boombox+3aaf
+boombox+3ab4
+boombox+1b20
+boombox+1b22
+boombox+1b29
+boombox+1b30
+boombox+1b33
+boombox+1b3b
+boombox+1b3d
+boombox+1b40
+boombox+1b45
+boombox+1b49
+boombox+1b4e
+boombox+1b53
+boombox+1b57
+boombox+1b5a
+boombox+1b60
+boombox+1b65
+boombox+1b68
+boombox+1b6e
+boombox+1b71
+boombox+1b75
+boombox+1b7b
+boombox+1b83
+boombox+1b88
+boombox+1b8c
+boombox+1b90
+boombox+1bc6
+boombox+1bce
+boombox+1bd4
+boombox+1bd8
+boombox+1bdc
+boombox+1be1
+boombox+1be3
+boombox+1be6
+boombox+1bea
+boombox+1bed
+boombox+1bf0
+boombox+1bf5
+boombox+1bfa
+boombox+1bff
+boombox+1c06
+boombox+1c0b
+boombox+1c10
+boombox+1c16
+boombox+1c18
+boombox+28d0
+boombox+28d2
+boombox+28d3
+boombox+28d7
+boombox+28dc
+boombox+28e1
+boombox+28e5
+boombox+28e7
+boombox+28ec
+boombox+28ef
+boombox+28f2
+boombox+28f5
+boombox+28f8
+boombox+29a6
+boombox+29ab
+boombox+29b0
+boombox+29b5
+boombox+29b8
+boombox+29bc
+boombox+29be
+boombox+29c0
+boombox+29c4
+boombox+29c5
+boombox+29c6
+boombox+3280
+boombox+3285
+boombox+3286
+boombox+328a
+boombox+328d
+boombox+3291
+boombox+3294
+boombox+3298
+boombox+329a
+boombox+329f
+boombox+32a6
+boombox+32ad
+boombox+32b0
+boombox+32b4
+boombox+32b7
+boombox+32bd
+boombox+32c4
+boombox+32cb
+boombox+32ce
+boombox+32d4
+boombox+32db
+boombox+32de
+boombox+32e4
+boombox+32eb
+boombox+32f2
+boombox+32f5
+boombox+32fb
+boombox+3301
+boombox+3305
+boombox+330a
+boombox+330e
+boombox+330f
+boombox+2a15
+boombox+2a1a
+boombox+2a1d
+boombox+2aa5
+boombox+2aaa
+boombox+2aad
+boombox+1c2e
+boombox+1c34
+boombox+1c36
+boombox+1c3b
+boombox+1c3d
+boombox+1c45
+boombox+1c47
+boombox+1c4b
+boombox+1c4e
+boombox+1c52
+boombox+1c55
+boombox+1c57
+boombox+1c5b
+boombox+1c60
+boombox+1c67
+boombox+1b9e
+boombox+1ba3
+boombox+1bab
+boombox+1bad
+boombox+1bb5
+boombox+1bb8
+boombox+1bbd
+boombox+1bc4
+boombox+1bc5
+boombox+3e20
+boombox+3e27
+boombox+3e29
+boombox+3e2d
+boombox+3e32
+boombox+3e34
+boombox+3ab9
+boombox+3abf
+boombox+3ac1
+boombox+3ac8
+boombox+3aca
+boombox+3ad1
+boombox+3ad3
+boombox+3ad7
+boombox+3adc
+boombox+1d10
+boombox+1d12
+boombox+1d16
+boombox+1d1a
+boombox+1d1d
+boombox+1d20
+boombox+1d26
+boombox+1d2b
+boombox+1d2e
+boombox+1d34
+boombox+1d37
+boombox+1d3b
+boombox+1d41
+boombox+1d46
+boombox+1d4b
+boombox+1d4f
+boombox+1d53
+boombox+1d55
+boombox+1d5c
+boombox+1d61
+boombox+1d66
+boombox+1d68
+boombox+1d6d
+boombox+1d71
+boombox+1d72
+boombox+1b92
+boombox+1b97
+boombox+3ae1
+boombox+3ae7
+boombox+3ae9
+boombox+3af0
+boombox+3af2
+boombox+3af9
+boombox+3afb
+boombox+3aff
+boombox+3b04
+boombox+1ed0
+boombox+1ed2
+boombox+1ed6
+boombox+1eda
+boombox+1edd
+boombox+1ee0
+boombox+1ee6
+boombox+1eeb
+boombox+1eee
+boombox+1ef4
+boombox+1ef7
+boombox+1efb
+boombox+1f19
+boombox+1f1e
+boombox+1f39
+boombox+1f3e
+boombox+1f44
+boombox+1f4b
+boombox+1f50
+boombox+1f52
+boombox+1f57
+boombox+1f5d
+boombox+1f5f
+boombox+1f6f
+boombox+1f75
+boombox+1f77
+boombox+1f7c
+boombox+1f7e
+boombox+1f83
+boombox+1f85
+boombox+1f88
+boombox+1f8b
+boombox+1f8d
+boombox+1f8f
+boombox+1f92
+boombox+1f94
+boombox+1f9b
+boombox+1fa0
+boombox+1fa8
+boombox+1fad
+boombox+1faf
+boombox+1fb3
+boombox+1fb4
+boombox+1d73
+boombox+1d78
+boombox+1d7e
+boombox+1d85
+boombox+1d8a
+boombox+1d8c
+boombox+1d91
+boombox+1d97
+boombox+1d99
+boombox+1dae
+boombox+1db4
+boombox+1db7
+boombox+1dbc
+boombox+1dbf
+boombox+1dc4
+boombox+1dc7
+boombox+1dca
+boombox+1dcd
+boombox+1dcf
+boombox+1dd1
+boombox+1dd5
+boombox+1dd9
+boombox+1ddb
+boombox+1dde
+boombox+1de1
+boombox+1de3
+boombox+1de7
+boombox+1dee
+boombox+1df3
+boombox+1df8
+boombox+1dfd
+boombox+1dff
+boombox+1e03
+boombox+1e04
+boombox+3b09
+boombox+3b0f
+boombox+3b11
+boombox+3b18
+boombox+3b1a
+boombox+3b21
+boombox+3b23
+boombox+3b27
+boombox+3b2c
+boombox+2060
+boombox+2064
+boombox+2068
+boombox+206b
+boombox+206d
+boombox+2072
+boombox+2075
+boombox+2077
+boombox+207a
+boombox+207e
+boombox+209b
+boombox+209e
+boombox+20a1
+boombox+20a5
+boombox+20a7
+boombox+20ae
+boombox+20b3
+boombox+20b8
+boombox+20bd
+boombox+20c1
+boombox+3b31
+boombox+3b37
+boombox+3b39
+boombox+3b40
+boombox+3b42
+boombox+3b49
+boombox+3b4b
+boombox+3b4f
+boombox+3b54
+boombox+2160
+boombox+2164
+boombox+2168
+boombox+216b
+boombox+216d
+boombox+2172
+boombox+2175
+boombox+2177
+boombox+217a
+boombox+217e
+boombox+219b
+boombox+219d
+boombox+219f
+boombox+21a6
+boombox+21ab
+boombox+21b0
+boombox+21b5
+boombox+21b9
+boombox+3b59
+boombox+3b5f
+boombox+3b61
+boombox+3b68
+boombox+3b6a
+boombox+3b71
+boombox+3b73
+boombox+3b77
+boombox+3b7c
+boombox+2260
+boombox+2263
+boombox+2264
+boombox+226b
+boombox+2272
+boombox+2275
+boombox+227d
+boombox+2281
+boombox+2284
+boombox+2287
+boombox+228d
+boombox+2292
+boombox+2295
+boombox+229b
+boombox+229e
+boombox+22a2
+boombox+22a8
+boombox+22ad
+boombox+22b1
+boombox+22b5
+boombox+22b9
+boombox+22bd
+boombox+22c1
+boombox+22c8
+boombox+22cd
+boombox+22d0
+boombox+22d5
+boombox+22da
+boombox+22dc
+boombox+22e2
+boombox+22e7
+boombox+22ed
+boombox+22f2
+boombox+22f5
+boombox+22fa
+boombox+2300
+boombox+2302
+boombox+2304
+boombox+2309
+boombox+2332
+boombox+2335
+boombox+233a
+boombox+233e
+boombox+2343
+boombox+2346
+boombox+234b
+boombox+2350
+boombox+2353
+boombox+2357
+boombox+235e
+boombox+2363
+boombox+236b
+boombox+2373
+boombox+2375
+boombox+27c0
+boombox+27c5
+boombox+27c6
+boombox+27c7
+boombox+27c9
+boombox+27cb
+boombox+27cd
+boombox+27d1
+boombox+27d4
+boombox+27d6
+boombox+27d9
+boombox+27dd
+boombox+27e0
+boombox+27e3
+boombox+27e7
+boombox+27e9
+boombox+27ec
+boombox+27ef
+boombox+28bb
+boombox+28bd
+boombox+28c1
+boombox+28c3
+boombox+28c5
+boombox+28c7
+boombox+28c8
+boombox+28c9
+boombox+23fb
+boombox+2403
+boombox+2406
+boombox+240b
+boombox+2412
+boombox+2413
+boombox+3b81
+boombox+3b83
+boombox+3b87
+boombox+3b90
+boombox+3b95
+boombox+3b98
+boombox+3b9d
+boombox+3bb3
+boombox+3bb5
+boombox+3bb9
+boombox+3bc0
+boombox+3bc5
+boombox+3bc8
+boombox+3bcd
+boombox+3bdf
+boombox+3be5
+boombox+3bf9
+boombox+3bfb
+boombox+3bff
+boombox+3c00
+boombox+3c04
+boombox+3c07
+boombox+3c0b
+boombox+3c1d
+boombox+3c21
+boombox+3c25
+boombox+3c28
+boombox+3c2c
+boombox+3c2e
+boombox+3c31
+boombox+3c33
+boombox+3c35
+boombox+3c39
+boombox+3c40
+boombox+3c44
+boombox+3c47
+boombox+3c4b
+boombox+3c7e
+boombox+3c85
+boombox+3c89
+boombox+3c8f
+boombox+3c95
+boombox+3c97
+boombox+3c99
+boombox+3ca0
+boombox+3ca4
+boombox+3caa
+boombox+3cb0
+boombox+3cb2
+boombox+3cb4
+boombox+3cbb
+boombox+3cbf
+boombox+3cc5
+boombox+3ccb
+boombox+3ccd
+boombox+3ccf
+boombox+3cd6
+boombox+3cda
+boombox+3ce0
+boombox+3ce6
+boombox+3ce8
+boombox+3cee
+boombox+3cf5
+boombox+3cfa
+boombox+3d00
+boombox+3d07
+boombox+3d0d
+boombox+3d14
+boombox+3d19
+boombox+3d1f
+boombox+3d26
+boombox+3d2d
+boombox+3d33
+boombox+3d39
+boombox+3d3d
+boombox+3d43
+boombox+3d4a
+boombox+3d51
+boombox+3be7
+boombox+3bee
+boombox+3c0d
+boombox+3c11
+boombox+3c13
+boombox+3c18
+boombox+2bf0
+boombox+2bf4
+boombox+2bfb
+boombox+2c00
+boombox+2c06
+boombox+2c0d
+boombox+2c13
+boombox+2c1a
+boombox+2c1f
+boombox+2c25
+boombox+2c2c
+boombox+2c33
+boombox+2c39
+boombox+2c3f
+boombox+2c43
+boombox+2c49
+boombox+2c50
+boombox+2c57
+boombox+2c5d
+boombox+2c64
+boombox+2c6a
+boombox+2c71
+boombox+2c76
+boombox+2c7c
+boombox+2c83
+boombox+2c89
+boombox+2c90
+boombox+2c95
+boombox+2c9b
+boombox+2ca2
+boombox+2ca9
+boombox+2caf
+boombox+2cb5
+boombox+2cb9
+boombox+2cbf
+boombox+2cc6
+boombox+2ccd
+boombox+2cd3
+boombox+2cda
+boombox+2ce0
+boombox+2ce7
+boombox+2cec
+boombox+2cf2
+boombox+2cf9
+boombox+2cff
+boombox+2d06
+boombox+2d0b
+boombox+2d11
+boombox+2d18
+boombox+2d1f
+boombox+2d25
+boombox+2d2b
+boombox+2d2f
+boombox+2d35
+boombox+2d3c
+boombox+2d43
+boombox+2d49
+boombox+2d50
+boombox+2d56
+boombox+2d5d
+boombox+2d62
+boombox+2d68
+boombox+2d6f
+boombox+2d75
+boombox+2d7c
+boombox+2d81
+boombox+2d87
+boombox+2d8e
+boombox+2d95
+boombox+2d9b
+boombox+2da1
+boombox+2da5
+boombox+2dab
+boombox+2db2
+boombox+2db9
+boombox+2dbf
+boombox+2dc6
+boombox+2dcc
+boombox+2dd3
+boombox+2dd8
+boombox+2dde
+boombox+2de5
+boombox+2deb
+boombox+2df2
+boombox+2df7
+boombox+2dfd
+boombox+2e04
+boombox+2e0b
+boombox+2e11
+boombox+2e17
+boombox+2e1b
+boombox+2e21
+boombox+2e28
+boombox+2e2f
+boombox+2e35
+boombox+2e3c
+boombox+2e42
+boombox+2e49
+boombox+2e4e
+boombox+2e54
+boombox+2e5b
+boombox+2e61
+boombox+2e68
+boombox+2e6d
+boombox+2e73
+boombox+2e7a
+boombox+2e81
+boombox+2e87
+boombox+2e8d
+boombox+2e91
+boombox+2e97
+boombox+2e9e
+boombox+2ea5
+boombox+2eab
+boombox+2eb2
+boombox+2eb8
+boombox+2ebf
+boombox+2ec4
+boombox+2eca
+boombox+2ed1
+boombox+2ed7
+boombox+2ede
+boombox+2ee3
+boombox+2ee9
+boombox+2ef0
+boombox+2ef7
+boombox+2efd
+boombox+2f03
+boombox+2f07
+boombox+2f0d
+boombox+2f14
+boombox+2f1b
+boombox+2f21
+boombox+2f28
+boombox+2f2e
+boombox+2f35
+boombox+2f3a
+boombox+2f40
+boombox+2f47
+boombox+2f4d
+boombox+2f54
+boombox+2f59
+boombox+2f5f
+boombox+2f66
+boombox+2f6d
+boombox+2f73
+boombox+2f79
+boombox+2f7d
+boombox+2f83
+boombox+2f8a
+boombox+2f91
+boombox+2f97
+boombox+2f9e
+boombox+2fa4
+boombox+2fab
+boombox+2fb0
+boombox+2fb6
+boombox+2fbd
+boombox+2fc3
+boombox+2fca
+boombox+2fcf
+boombox+2fd5
+boombox+2fdc
+boombox+2fe3
+boombox+2fe9
+boombox+2fef
+boombox+2ff3
+boombox+2ff9
+boombox+3000
+boombox+3007
+boombox+300d
+boombox+3014
+boombox+301a
+boombox+3021
+boombox+3026
+boombox+302c
+boombox+3033
+boombox+3039
+boombox+3040
+boombox+3045
+boombox+304b
+boombox+3052
+boombox+3059
+boombox+305f
+boombox+3065
+boombox+3069
+boombox+306f
+boombox+3076
+boombox+307d
+boombox+3083
+boombox+308a
+boombox+3090
+boombox+3097
+boombox+309c
+boombox+30a2
+boombox+30a9
+boombox+30af
+boombox+30b6
+boombox+30bb
+boombox+30c1
+boombox+30c8
+boombox+30cf
+boombox+30d5
+boombox+30db
+boombox+30df
+boombox+30e5
+boombox+30ec
+boombox+30f3
+boombox+30f9
+boombox+3100
+boombox+3106
+boombox+310d
+boombox+3112
+boombox+3118
+boombox+311f
+boombox+3125
+boombox+312c
+boombox+3131
+boombox+3137
+boombox+313e
+boombox+3145
+boombox+314b
+boombox+3151
+boombox+3155
+boombox+315b
+boombox+3162
+boombox+3169
+boombox+316f
+boombox+3176
+boombox+317c
+boombox+3183
+boombox+3188
+boombox+318e
+boombox+3195
+boombox+319b
+boombox+31a2
+boombox+31a7
+boombox+31ad
+boombox+31b4
+boombox+31bb
+boombox+31c1
+boombox+31c7
+boombox+31cb
+boombox+31d1
+boombox+31d8
+boombox+31df
+boombox+31e5
+boombox+31ec
+boombox+31f2
+boombox+31f9
+boombox+31fe
+boombox+3204
+boombox+320b
+boombox+3211
+boombox+3218
+boombox+321d
+boombox+3223
+boombox+322a
+boombox+3231
+boombox+3237
+boombox+323d
+boombox+3241
+boombox+3247
+boombox+324e
+boombox+3255
+boombox+325b
+boombox+3262
+boombox+3268
+boombox+326e
+boombox+3272
+boombox+3276
+boombox+3dce
+boombox+3dd0
+boombox+3dd2
+boombox+3dd8
+boombox+3dde
+boombox+3de0
+boombox+3de2
+boombox+3b9f
+boombox+3ba3
+boombox+3bcf
+boombox+3bd3
+boombox+3bd5
+boombox+3bda
+boombox+3c4d
+boombox+3c51
+boombox+3c53
+boombox+3c55
+boombox+3c59
+boombox+3c5c
+boombox+3c61
+boombox+3c69
+boombox+3c6d
+boombox+3c71
+boombox+3c75
+boombox+3c78
+boombox+3c7a
+boombox+3c7c
+boombox+3c7d
+boombox+40fe
+boombox+4100
+boombox+47dc
+boombox+47e1
+boombox+47e2
+boombox+47e6
+boombox+47ed
+boombox+47f4
+boombox+4804
+boombox+4807
+boombox+4809
+boombox+480e
+boombox+4812
+boombox+4813
\ No newline at end of file
From 50cfd522f5c824874a317d9d8e27e63b4b962e8c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 15 Mar 2019 19:42:17 -0400
Subject: [PATCH 006/154] fix bad code in recent coverage loading work
---
plugin/lighthouse/director.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index f73fe327..06740590 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -512,14 +512,15 @@ def _normalize_drcov_data(self, drcov_data):
try:
coverage_offsets = drcov_data.get_offsets(module_name)
- rebased_offsets = map(lambda x: self.metadata.imagebase+x, coverage_offsets)
- confidence = self.metadata.measure_block_confidence(rebased_offsets)
+ coverage_addresses = map(lambda x: self.metadata.imagebase+x, coverage_offsets)
+ confidence = self.metadata.measure_block_confidence(coverage_addresses)
+
#print "Block confidence: %f" % confidence
if confidence > 0.90:
- return rebased_offsets # inst trace
+ return self.metadata.flatten_block_heads(coverage_addresses) # bb trace
else:
- return self.metadata.flatten_offsets() # bb trace
- return rebased_offsets
+ return coverage_addresses # inst trace
+
except NotImplementedError:
pass
From 0f51554d8c30a284c946ec7b3da428346e7f5baf Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 15 Mar 2019 19:43:26 -0400
Subject: [PATCH 007/154] suppress some IDA warning messages during func
renames
---
plugin/lighthouse/metadata.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 40231363..c6f52623 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -584,11 +584,11 @@ def _name_changed(self, address, new_name, local_name=None):
#
if address != function.address:
- return
+ return 0
# if the name isn't actually changing (misfire?) nothing to do
if new_name == function.name:
- return
+ return 0
logger.debug("Name changing @ 0x%X" % address)
logger.debug(" Old name: %s" % function.name)
From a55ede77f99d972121d493036b2f5795289f803e Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 15 Mar 2019 19:47:26 -0400
Subject: [PATCH 008/154] bugfix: deleted functions were not properly removed
from metadata cache on refresh
---
plugin/lighthouse/metadata.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index c6f52623..e5dbd36b 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -387,6 +387,7 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback):
function_addresses = disassembler.execute_read(
disassembler.get_function_addresses
)()
+ function_addresses = list(set(function_addresses+self.functions.keys()))
# refresh database properties that we wish to cache
self._async_refresh_properties()
@@ -812,7 +813,7 @@ def _compute_complexity(self):
# current node (node_address) to walk the function graph
#
- to_walk = set([self.address])
+ to_walk = set([self.address]) if self.nodes else set()
while to_walk:
# this is the address of the node we will 'walk' from
From b27698520b5b0ad69472e83b8f2ad18b90d92100 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 15 Mar 2019 19:48:57 -0400
Subject: [PATCH 009/154] split metadata cache out of director
---
plugin/lighthouse/core.py | 16 ++--
plugin/lighthouse/director.py | 66 ++-------------
plugin/lighthouse/metadata.py | 113 ++++++++++++++++++-------
plugin/lighthouse/ui/coverage_table.py | 2 +-
4 files changed, 98 insertions(+), 99 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index d24a458b..fb84d660 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -51,11 +51,14 @@ def _init(self):
Initialize the core components of the plugin.
"""
- # the plugin's color palette
+ # the database metadata cache
+ self.metadata = DatabaseMetadata()
+
+ # the plugin color palette
self.palette = LighthousePalette()
# the coverage engine
- self.director = CoverageDirector(self.palette)
+ self.director = CoverageDirector(self.metadata, self.palette)
# the coverage painter
self.painter = CoveragePainter(self.director, self.palette)
@@ -102,6 +105,7 @@ def _cleanup(self):
"""
self.painter.terminate()
self.director.terminate()
+ self.metadata.terminate()
#--------------------------------------------------------------------------
# UI Integration (Internal)
@@ -195,9 +199,7 @@ def interactive_load_batch(self):
# background while the user is selecting which coverage files to load
#
- future = self.director.refresh_metadata(
- progress_callback=metadata_progress
- )
+ future = self.metadata.refresh_async(progress_callback=metadata_progress)
#
# we will now prompt the user with an interactive file dialog so they
@@ -283,9 +285,7 @@ def interactive_load_file(self):
# background while the user is selecting which coverage files to load
#
- future = self.director.refresh_metadata(
- progress_callback=metadata_progress
- )
+ future = self.metadata.refresh_async(progress_callback=metadata_progress)
#
# we will now prompt the user with an interactive file dialog so they
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 06740590..1b711b0e 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -49,14 +49,14 @@ class CoverageDirector(object):
ERROR_COVERAGE_ABSENT = 1
ERROR_COVERAGE_SUSPICIOUS = 2
- def __init__(self, palette):
+ def __init__(self, metadata, palette):
+
+ # the database metadata cache
+ self.metadata = metadata
# the plugin color palette
self._palette = palette
- # the central database metadata cache
- self.metadata = DatabaseMetadata()
-
#----------------------------------------------------------------------
# Coverage
#----------------------------------------------------------------------
@@ -194,21 +194,13 @@ def __init__(self, palette):
self._coverage_created_callbacks = []
self._coverage_deleted_callbacks = []
- # metadata callbacks
- self._metadata_modified_callbacks = []
-
def terminate(self):
"""
Cleanup & terminate the director.
"""
-
- # stop the composition worker
self._ast_queue.put(None)
self._composition_worker.join()
- # spin down the live metadata object
- self.metadata.terminate()
-
#--------------------------------------------------------------------------
# Properties
#--------------------------------------------------------------------------
@@ -304,18 +296,6 @@ def _notify_coverage_deleted(self):
"""
notify_callback(self._coverage_deleted_callbacks)
- def metadata_modified(self, callback):
- """
- Subscribe a callback for metadata modification events.
- """
- register_callback(self._metadata_modified_callbacks, callback)
-
- def _notify_metadata_modified(self):
- """
- Notify listeners of a metadata modification event.
- """
- notify_callback(self._metadata_modified_callbacks)
-
#----------------------------------------------------------------------
# Batch Loading
#----------------------------------------------------------------------
@@ -1174,47 +1154,11 @@ def refresh(self):
logger.debug("Refreshing the CoverageDirector")
# (re)build our metadata cache of the underlying database
- future = self.refresh_metadata(metadata_progress, True)
- await_future(future)
+ self.metadata.refresh()
# (re)map each set of loaded coverage data to the database
self._refresh_database_coverage()
- def refresh_metadata(self, progress_callback=None, force=False):
- """
- Refresh the database metadata cache utilized by the director.
-
- Returns a future (Queue) that will carry the completion message.
- """
-
- #
- # if this is the first time the director is going to use / populate
- # the database metadata, register the director for notifications of
- # metadata modification (this should only happen once)
- #
- # TODO/FUTURE: this is a little dirty, but it will suffice.
- #
-
- if not self.metadata.cached:
- self.metadata.function_renamed(self._notify_metadata_modified)
-
- #
- # if the lighthouse has collected metadata previously for this
- # disassembler session (eg, it is cached), ignore a request to refresh
- # it unless explicitly told to refresh via force=True
- #
-
- if self.metadata.cached and not force:
- fake_queue = Queue.Queue()
- fake_queue.put(False)
- return fake_queue
-
- # start the asynchronous metadata refresh
- result_queue = self.metadata.refresh(progress_callback=progress_callback)
-
- # return the queue that can be used to block for the async result
- return result_queue
-
def _refresh_database_coverage(self):
"""
Refresh all the database coverage mappings managed by the director.
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index e5dbd36b..aa3f8d1b 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -84,13 +84,17 @@ def __init__(self):
# placeholder attribute for disassembler event hooks
self._rename_hooks = None
- # metadata callbacks (see director for more info)
- self._function_renamed_callbacks = []
-
# asynchronous metadata collection thread
self._refresh_worker = None
self._stop_threads = False
+ #----------------------------------------------------------------------
+ # Callbacks
+ #----------------------------------------------------------------------
+
+ self._metadata_modified_callbacks = []
+ self._function_renamed_callbacks = []
+
def terminate(self):
"""
Cleanup & terminate the metadata object.
@@ -259,18 +263,33 @@ def is_big(self):
# Refresh
#--------------------------------------------------------------------------
- def refresh(self, function_addresses=None, progress_callback=None):
+ def refresh(self, function_addresses=None):
+ """
+ Refresh the database metadata cache.
+ """
+ self._core_refresh(function_addresses)
+
+ def refresh_async(self, function_addresses=None, progress_callback=None, force=False):
"""
- Request an asynchronous refresh of the database metadata.
+ Refresh the database metadata cache asynchronously.
- TODO/FUTURE: we should make a synchronous refresh available
+ Returns a future (Queue) that will carry the completion message.
"""
assert self._refresh_worker == None, 'Refresh already running'
result_queue = Queue.Queue()
#
- # reset the async abort/stop flag that can be used used to cancel the
- # ongoing refresh task
+ # if there is already metadata cached for this disassembler session,
+ # ignore a request to refresh it unless forced
+ #
+
+ if self.cached and not force:
+ result_queue.put(False)
+ return result_queue
+
+ #
+ # reset the async abort/stop flag so that it can be used to cancel our
+ # new refresh task if needed
#
self._stop_threads = False
@@ -281,7 +300,7 @@ def refresh(self, function_addresses=None, progress_callback=None):
self._refresh_worker = threading.Thread(
target=self._async_refresh,
- args=(result_queue, function_addresses, progress_callback,)
+ args=(function_addresses, progress_callback, result_queue,)
)
self._refresh_worker.start()
@@ -366,11 +385,26 @@ def _refresh_lookup(self):
#--------------------------------------------------------------------------
@not_mainthread
- def _async_refresh(self, result_queue, function_addresses, progress_callback):
+ def _async_refresh(self, function_addresses=None, progress_callback=None, result_queue=None):
"""
- The main routine for the asynchronous metadata refresh worker.
+ Internal thread worker routine to refresh the database metadata asynchronously.
+ """
+
+ # start an interruptable refresh
+ completed = self._core_refresh(function_addresses, progress_callback, True)
+
+ # clean up our thread's reference as it is basically done/dead
+ self._refresh_worker = None
+
+ # send the refresh result (good/bad) incase anyone is still listening
+ if result_queue:
+ result_queue.put(completed)
+
+ # exit thread...
- TODO/FUTURE: this should be cleaned up / refactored
+ def _core_refresh(self, function_addresses=None, progress_callback=None, is_async=False):
+ """
+ Internal routine that will update the database metadata cache.
"""
# pause our rename listening hooks (more performant collection)
@@ -389,14 +423,17 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback):
)()
function_addresses = list(set(function_addresses+self.functions.keys()))
- # refresh database properties that we wish to cache
- self._async_refresh_properties()
+ # refresh high level database properties that we wish to cache
+ self._sync_refresh_properties()
# refresh the core database metadata asynchronously
- completed = self._async_collect_metadata(
- function_addresses,
- progress_callback
- )
+ if is_async:
+ completed = self._async_collect_metadata(function_addresses, progress_callback)
+
+ # refresh the core database metadata synchronously
+ else:
+ self._sync_collect_metadata(function_addresses)
+ completed = True
# regenerate the instruction list from collected metadata
self._refresh_instructions()
@@ -431,31 +468,37 @@ def _async_refresh(self, result_queue, function_addresses, progress_callback):
# reinstall the rename listener hooks now that the refresh is done
self._rename_hooks.hook()
- # send the refresh result (good/bad) incase anyone is still listening
+ # the metadata refresh is effectively done, and the data is now 'cached'
if completed:
self.cached = True
- result_queue.put(True)
- else:
- result_queue.put(False)
- # clean up our thread's reference as it is basically done/dead
- self._refresh_worker = None
-
- # thread exit...
- return
+ # return true/false to indicates completion
+ return completed
@disassembler.execute_read
- def _async_refresh_properties(self):
+ def _sync_refresh_properties(self):
"""
Refresh a selection of interesting database properties.
"""
self.filename = disassembler.get_root_filename()
self.imagebase = disassembler.get_imagebase()
+ @disassembler.execute_read
+ def _sync_collect_metadata(self, function_addresses):
+ """
+ Collect metadata from the underlying database.
+ """
+ start = time.time()
+ #----------------------------------------------------------------------
+ self._update_functions({ ea: FunctionMetadata(ea) for ea in function_addresses })
+ #----------------------------------------------------------------------
+ end = time.time()
+ logger.debug("Synchronous metadata collection took %s seconds" % (end - start))
+
@not_mainthread
def _async_collect_metadata(self, function_addresses, progress_callback):
"""
- Collect metadata from the underlying database (interruptable).
+ Collect metadata from the underlying database asynchronously (interruptable).
"""
CHUNK_SIZE = 150
completed = 0
@@ -607,6 +650,18 @@ def _name_changed(self, address, new_name, local_name=None):
# Callbacks
#--------------------------------------------------------------------------
+ def metadata_modified(self, callback):
+ """
+ Subscribe a callback for metadata modification events.
+ """
+ register_callback(self._metadata_modified_callbacks, callback)
+
+ def _notify_metadata_modified(self):
+ """
+ Notify listeners of a metadata modification event.
+ """
+ notify_callback(self._metadata_modified_callbacks)
+
def function_renamed(self, callback):
"""
Subscribe a callback for function rename events.
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 672255bd..5982485b 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -730,7 +730,7 @@ def __init__(self, director, parent=None):
# register for cues from the director
self._director.coverage_switched(self._internal_refresh)
self._director.coverage_modified(self._internal_refresh)
- self._director.metadata_modified(self._data_changed)
+ self._director.metadata.function_renamed(self._data_changed)
#--------------------------------------------------------------------------
# QAbstractTableModel Overloads
From 7cab1c1db9f64583edc4420f18e9d96955757d99 Mon Sep 17 00:00:00 2001
From: xarkes
Date: Mon, 18 Mar 2019 01:56:28 +0100
Subject: [PATCH 010/154] Added python3 support (#62)
* Added Python3 support
* remove dependency of six library
* fixups based on Binja with Py3.6
---
plugin/lighthouse/coverage.py | 32 ++++-----
plugin/lighthouse/director.py | 8 +--
plugin/lighthouse/metadata.py | 27 ++++----
plugin/lighthouse/painting/painter.py | 15 ++--
plugin/lighthouse/reader/__init__.py | 2 +-
plugin/lighthouse/reader/coverage_reader.py | 5 +-
plugin/lighthouse/reader/parsers/drcov.py | 8 ++-
plugin/lighthouse/ui/coverage_table.py | 17 ++---
plugin/lighthouse/util/__init__.py | 1 +
.../lighthouse/util/disassembler/__init__.py | 4 +-
plugin/lighthouse/util/log.py | 2 +-
plugin/lighthouse/util/misc.py | 4 +-
plugin/lighthouse/util/python.py | 69 +++++++++++++++++++
plugin/lighthouse/util/qt/util.py | 4 +-
14 files changed, 137 insertions(+), 61 deletions(-)
create mode 100644 plugin/lighthouse/util/python.py
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index e6aae62e..aacb167f 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -182,7 +182,7 @@ def coverage(self):
"""
Return the instruction-level coverage bitmap/mask.
"""
- return self._hitmap.viewkeys()
+ return viewkeys(self._hitmap)
@property
def suspicious(self):
@@ -203,7 +203,7 @@ def suspicious(self):
# provided coverage data is malformed, or for a different binary
#
- for adddress, node_coverage in self.nodes.iteritems():
+ for adddress, node_coverage in iteritems(self.nodes):
if adddress in node_coverage.executed_instructions:
continue
bad += 1
@@ -263,14 +263,14 @@ def _finalize_nodes(self, dirty_nodes):
"""
Finalize the NodeCoverage objects statistics / data for use.
"""
- for node_coverage in dirty_nodes.itervalues():
+ for node_coverage in itervalues(dirty_nodes):
node_coverage.finalize()
def _finalize_functions(self, dirty_functions):
"""
Finalize the FunctionCoverage objects statistics / data for use.
"""
- for function_coverage in dirty_functions.itervalues():
+ for function_coverage in itervalues(dirty_functions):
function_coverage.finalize()
def _finalize_instruction_percent(self):
@@ -279,13 +279,13 @@ def _finalize_instruction_percent(self):
"""
# sum all the instructions in the database metadata
- total = sum(f.instruction_count for f in self._metadata.functions.itervalues())
+ total = sum(f.instruction_count for f in itervalues(self._metadata.functions))
if not total:
self.instruction_percent = 0.0
return
# sum the unique instructions executed across all functions
- executed = sum(f.instructions_executed for f in self.functions.itervalues())
+ executed = sum(f.instructions_executed for f in itervalues(self.functions))
# save the computed percentage of database instructions executed (0 to 1.0)
self.instruction_percent = float(executed) / total
@@ -300,7 +300,7 @@ def add_data(self, data, update=True):
"""
# add the given runtime data to our data source
- for address, hit_count in data.iteritems():
+ for address, hit_count in iteritems(data):
self._hitmap[address] += hit_count
# do not update other internal structures if requested
@@ -311,7 +311,7 @@ def add_data(self, data, update=True):
self._update_coverage_hash()
# mark these touched addresses as dirty
- self._unmapped_data |= data.viewkeys()
+ self._unmapped_data |= viewkeys(data)
def add_addresses(self, addresses, update=True):
"""
@@ -338,7 +338,7 @@ def subtract_data(self, data):
"""
# subtract the given hitmap from our existing hitmap
- for address, hit_count in data.iteritems():
+ for address, hit_count in iteritems(data):
self._hitmap[address] -= hit_count
#
@@ -381,7 +381,7 @@ def _update_coverage_hash(self):
Update the hash of the coverage mask.
"""
if self._hitmap:
- self.coverage_hash = hash(frozenset(self._hitmap.viewkeys()))
+ self.coverage_hash = hash(frozenset(viewkeys(self._hitmap)))
else:
self.coverage_hash = 0
@@ -529,7 +529,7 @@ def _map_functions(self, dirty_nodes):
# build or update the function level coverage metadata
#
- for node_coverage in dirty_nodes.itervalues():
+ for node_coverage in itervalues(dirty_nodes):
#
# using a given NodeCoverage object, we retrieve its underlying
@@ -617,7 +617,7 @@ def hits(self):
"""
Return the number of instruction executions in this function.
"""
- return sum(x.hits for x in self.nodes.itervalues())
+ return sum(x.hits for x in itervalues(self.nodes))
@property
def nodes_executed(self):
@@ -631,14 +631,14 @@ def instructions_executed(self):
"""
Return the number of unique instructions executed in this function.
"""
- return sum(x.instructions_executed for x in self.nodes.itervalues())
+ return sum(x.instructions_executed for x in itervalues(self.nodes))
@property
def instructions(self):
"""
Return the executed instruction addresses in this function.
"""
- return set([ea for node in self.nodes.itervalues() for ea in node.executed_instructions.keys()])
+ return set([ea for node in itervalues(self.nodes) for ea in node.executed_instructions.keys()])
#--------------------------------------------------------------------------
# Controls
@@ -664,7 +664,7 @@ def finalize(self):
float(self.instructions_executed) / function_metadata.instruction_count
# the sum of node executions in this function
- node_sum = sum(x.executions for x in self.nodes.itervalues())
+ node_sum = sum(x.executions for x in itervalues(self.nodes))
# the estimated number of executions this function has experienced
self.executions = float(node_sum) / function_metadata.node_count
@@ -699,7 +699,7 @@ def hits(self):
"""
Return the number of instruction executions in this node.
"""
- return sum(self.executed_instructions.itervalues())
+ return sum(itervalues(self.executed_instructions))
@property
def instructions_executed(self):
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 1b711b0e..0781347c 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1,5 +1,4 @@
import time
-import Queue
import string
import logging
import threading
@@ -7,6 +6,7 @@
from lighthouse.util import lmsg
from lighthouse.util.misc import *
+from lighthouse.util.python import *
from lighthouse.util.qt import await_future, await_lock, color_text
from lighthouse.util.disassembler import disassembler
from lighthouse.metadata import DatabaseMetadata, metadata_progress
@@ -164,7 +164,7 @@ def __init__(self, metadata, palette):
# to handle these computation requests.
#
- self._ast_queue = Queue.Queue()
+ self._ast_queue = queue.Queue()
self._composition_lock = threading.Lock()
self._composition_cache = CompositionCache()
@@ -224,14 +224,14 @@ def coverage_names(self):
"""
Return the list or loaded / composed database coverage names.
"""
- return self._database_coverage.keys()
+ return list(self._database_coverage)
@property
def special_names(self):
"""
Return the list of special (director maintained) coverage names.
"""
- return self._special_coverage.keys()
+ return list(self._special_coverage)
@property
def all_names(self):
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index aa3f8d1b..9ac4bbcd 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -1,12 +1,11 @@
import time
-import Queue
import bisect
import logging
import weakref
import threading
import collections
-
from lighthouse.util.misc import *
+from lighthouse.util.python import *
from lighthouse.util.disassembler import disassembler
logger = logging.getLogger("Lighthouse.Metadata")
@@ -276,7 +275,7 @@ def refresh_async(self, function_addresses=None, progress_callback=None, force=F
Returns a future (Queue) that will carry the completion message.
"""
assert self._refresh_worker == None, 'Refresh already running'
- result_queue = Queue.Queue()
+ result_queue = queue.Queue()
#
# if there is already metadata cached for this disassembler session,
@@ -352,7 +351,7 @@ def _refresh_instructions(self):
Refresh the list of database instructions (from function metadata).
"""
instructions = []
- for function_metadata in self.functions.itervalues():
+ for function_metadata in itervalues(self.functions):
instructions.extend(function_metadata.instructions)
instructions = list(set(instructions))
instructions.sort()
@@ -375,7 +374,7 @@ def _refresh_lookup(self):
"""
self._last_node = []
- self._name2func = { f.name: f.address for f in self.functions.itervalues() }
+ self._name2func = { f.name: f.address for f in itervalues(self.functions) }
self._node_addresses = sorted(self.nodes.keys())
self._function_addresses = sorted(self.functions.keys())
self._stale_lookup = False
@@ -421,7 +420,7 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
function_addresses = disassembler.execute_read(
disassembler.get_function_addresses
)()
- function_addresses = list(set(function_addresses+self.functions.keys()))
+ function_addresses = list(set(function_addresses+list(self.functions)))
# refresh high level database properties that we wish to cache
self._sync_refresh_properties()
@@ -552,7 +551,7 @@ def _update_functions(self, fresh_metadata):
# from any existing metadata we hold.
#
- for function_address, new_metadata in fresh_metadata.iteritems():
+ for function_address, new_metadata in iteritems(fresh_metadata):
# extract the 'old' metadata from the database metadata cache
old_metadata = self.functions.get(function_address, blank_function)
@@ -567,7 +566,7 @@ def _update_functions(self, fresh_metadata):
continue
# delete nodes that explicitly no longer exist
- old = old_metadata.nodes.viewkeys() - new_metadata.nodes.viewkeys()
+ old = viewkeys(old_metadata.nodes) - viewkeys(new_metadata.nodes)
for node_address in old:
del self.nodes[node_address]
@@ -713,7 +712,7 @@ def instructions(self):
"""
Return the instruction addresses in this function.
"""
- return set([ea for node in self.nodes.itervalues() for ea in node.instructions])
+ return set([ea for node in itervalues(self.nodes) for ea in node.instructions])
@property
def empty(self):
@@ -802,7 +801,7 @@ def _ida_refresh_nodes(self):
function_metadata.nodes[node_start] = node_metadata
# compute all of the edges between nodes in the current function
- for node_metadata in function_metadata.nodes.itervalues():
+ for node_metadata in itervalues(function_metadata.nodes):
edge_src = node_metadata.instructions[-1]
for edge_dst in idautils.CodeRefsFrom(edge_src, True):
if edge_dst in function_metadata.nodes:
@@ -895,7 +894,7 @@ def _compute_complexity(self):
confirmed_edges[current_src] = self.edges.pop(current_src)
# compute the final cyclomatic complexity for the function
- num_edges = sum(len(x) for x in confirmed_edges.itervalues())
+ num_edges = sum(len(x) for x in itervalues(confirmed_edges))
num_nodes = len(confirmed_nodes)
return num_edges - num_nodes + 2
@@ -903,10 +902,10 @@ def _finalize(self):
"""
Finalize function metadata for use.
"""
- self.size = sum(node.size for node in self.nodes.itervalues())
+ self.size = sum(node.size for node in itervalues(self.nodes))
self.node_count = len(self.nodes)
self.edge_count = len(self.edges)
- self.instruction_count = sum(node.instruction_count for node in self.nodes.itervalues())
+ self.instruction_count = sum(node.instruction_count for node in itervalues(self.nodes))
self.cyclomatic_complexity = self._compute_complexity()
#--------------------------------------------------------------------------
@@ -923,7 +922,7 @@ def __eq__(self, other):
result &= self.address == other.address
result &= self.node_count == other.node_count
result &= self.instruction_count == other.instruction_count
- result &= self.nodes.viewkeys() == other.nodes.viewkeys()
+ result &= viewkeys(self.nodes) == viewkeys(other.nodes)
return result
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index ebe7293a..d3a5398a 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -1,6 +1,5 @@
import abc
import time
-import Queue
import logging
import threading
@@ -53,7 +52,7 @@ def __init__(self, director, palette):
#
self._action_complete = threading.Event()
- self._msg_queue = Queue.Queue()
+ self._msg_queue = queue.Queue()
self._end_threads = False
#
@@ -227,15 +226,15 @@ def _paint_function(self, address):
stale_instructions = painted - function_coverage.instructions
# compute the painted nodes within this function
- painted = self._painted_nodes & function_metadata.nodes.viewkeys()
+ painted = self._painted_nodes & viewkeys(function_metadata.nodes)
# compute the painted nodes that will not get painted over
- stale_nodes_ea = painted - function_coverage.nodes.viewkeys()
+ stale_nodes_ea = painted - viewkeys(function_coverage.nodes)
stale_nodes = [function_metadata.nodes[ea] for ea in stale_nodes_ea]
# active instructions
instructions = function_coverage.instructions
- nodes = function_coverage.nodes.itervalues()
+ nodes = itervalues(function_coverage.nodes)
#
# ~ painting ~
@@ -266,7 +265,7 @@ def _clear_function(self, address):
"""
function_metadata = self._director.metadata.functions[address]
instructions = function_metadata.instructions
- nodes = function_metadata.nodes.itervalues()
+ nodes = itervalues(function_metadata.nodes)
# clear instructions
if not self._async_action(self._clear_instructions, instructions):
@@ -299,7 +298,7 @@ def _paint_database(self):
stale_inst = self._painted_instructions - db_coverage.coverage
# compute the painted nodes that will not get painted over
- stale_nodes_ea = self._painted_nodes - db_coverage.nodes.viewkeys()
+ stale_nodes_ea = self._painted_nodes - viewkeys(db_coverage.nodes)
stale_nodes = [db_metadata.nodes[ea] for ea in stale_nodes_ea]
# clear old instruction paint
@@ -315,7 +314,7 @@ def _paint_database(self):
return False # a repaint was requested
# paint new nodes
- if not self._async_action(self._paint_nodes, db_coverage.nodes.itervalues()):
+ if not self._async_action(self._paint_nodes, itervalues(db_coverage.nodes)):
return False # a repaint was requested
#------------------------------------------------------------------
diff --git a/plugin/lighthouse/reader/__init__.py b/plugin/lighthouse/reader/__init__.py
index c3deb519..a552f91d 100644
--- a/plugin/lighthouse/reader/__init__.py
+++ b/plugin/lighthouse/reader/__init__.py
@@ -1 +1 @@
-from coverage_reader import CoverageReader
+from .coverage_reader import CoverageReader
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 8b45b8f3..04a638cd 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -4,6 +4,7 @@
import logging
import traceback
+from lighthouse.util.python import iteritems
from .coverage_file import CoverageFile
logger = logging.getLogger("Lighthouse.Reader")
@@ -24,7 +25,7 @@ def open(self, filepath):
TODO
"""
- for name, parser in self._installed_parsers.iteritems():
+ for name, parser in iteritems(self._installed_parsers):
try:
return parser(filepath)
except Exception as e:
@@ -81,7 +82,7 @@ def _locate_subclass(self, module_file, target_subclass):
# attempt to import the given filepath as a python module
try:
- module = __import__("parsers." + module_file, globals(), locals(), ['object'], -1)
+ module = __import__("lighthouse.reader.parsers." + module_file, globals(), locals(), ['object'])
except Exception as e:
logger.exception("| - Parser import failed")
return None
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index e9d34f50..7f390684 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -11,6 +11,9 @@
except ImportError as e:
CoverageFile = object
+# Useful for python2 and python3 compatibility
+from builtins import bytes
+
#------------------------------------------------------------------------------
# DynamoRIO Drcov Log Parser
#------------------------------------------------------------------------------
@@ -264,11 +267,11 @@ def _parse_bb_table_header(self, f):
# peek at the next few bytes to determine if this is a binary bb table.
# An ascii bb table will have the line: 'module id, start, size:'
- token = "module id"
+ token = b"module id"
saved_position = f.tell()
# is this an ascii table?
- if f.read(len(token)) == token:
+ if bytes(f.read(len(token))) == token:
self.bb_table_is_binary = False
# nope! binary table
@@ -462,3 +465,4 @@ class DrcovBasicBlock(Structure):
x = DrcovData(argv[1])
for bb in x.bbs:
print("0x{:08x}".format(bb.start))
+
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 5982485b..de75721e 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -6,6 +6,7 @@
from lighthouse.util import lmsg
from lighthouse.util.qt import *
+from lighthouse.util.python import *
from lighthouse.util.misc import mainthread
from lighthouse.util.disassembler import disassembler
from lighthouse.coverage import FunctionCoverage, BADADDR
@@ -905,7 +906,7 @@ def sort(self, column, sort_order):
# sort the table entries by a function metadata attribute
if column in self.METADATA_ATTRIBUTES:
sorted_functions = sorted(
- self._visible_metadata.itervalues(),
+ itervalues(self._visible_metadata),
key=attrgetter(sort_field),
reverse=sort_order
)
@@ -913,7 +914,7 @@ def sort(self, column, sort_order):
# sort the table entries by a function coverage attribute
elif column in self.COVERAGE_ATTRIBUTES:
sorted_functions = sorted(
- self._visible_coverage.itervalues(),
+ itervalues(self._visible_coverage),
key=attrgetter(sort_field),
reverse=sort_order
)
@@ -949,7 +950,7 @@ def sort(self, column, sort_order):
# finally, rebuild the row2func mapping and notify views of this change
self.row2func = dict(zip(xrange(len(sorted_functions)), sorted_addresses))
- self.func2row = {v: k for k, v in self.row2func.iteritems()}
+ self.func2row = {v: k for k, v in iteritems(self.row2func)}
self.layoutChanged.emit()
# save the details of this sort event as they may be needed later
@@ -976,12 +977,12 @@ def get_modeled_coverage_percent(self):
# sum the # of instructions in all the visible functions
instruction_count = sum(
- meta.instruction_count for meta in self._visible_metadata.itervalues()
+ meta.instruction_count for meta in itervalues(self._visible_metadata)
)
# sum the # of instructions executed in all the visible functions
instructions_executed = sum(
- cov.instructions_executed for cov in self._visible_coverage.itervalues()
+ cov.instructions_executed for cov in itervalues(self._visible_coverage)
)
# compute coverage percentage of the visible functions
@@ -1205,7 +1206,7 @@ def _refresh_data(self):
normalize = lambda x: x
if not (set(self._search_string) & set(string.ascii_uppercase)):
- normalize = lambda x: string.lower(x)
+ normalize = lambda x: x.lower()
#
# it's time to rebuild the list of coverage items to make visible in
@@ -1214,7 +1215,7 @@ def _refresh_data(self):
#
# loop through *all* the functions as defined in the active metadata
- for function_address in metadata.functions.iterkeys():
+ for function_address in metadata.functions:
#------------------------------------------------------------------
# Filters - START
@@ -1248,7 +1249,7 @@ def _refresh_data(self):
row += 1
# build the inverse func --> row mapping
- self.func2row = {v: k for k, v in self.row2func.iteritems()}
+ self.func2row = {v: k for k, v in iteritems(self.row2func)}
# bake the final number of rows into the model
self._row_count = len(self.row2func)
diff --git a/plugin/lighthouse/util/__init__.py b/plugin/lighthouse/util/__init__.py
index 581f7fdf..c7de8360 100644
--- a/plugin/lighthouse/util/__init__.py
+++ b/plugin/lighthouse/util/__init__.py
@@ -1,3 +1,4 @@
+from .python import *
from .misc import *
from .debug import *
from .log import lmsg, logging_started, start_logging
diff --git a/plugin/lighthouse/util/disassembler/__init__.py b/plugin/lighthouse/util/disassembler/__init__.py
index 7bf0dab7..0d6eba94 100644
--- a/plugin/lighthouse/util/disassembler/__init__.py
+++ b/plugin/lighthouse/util/disassembler/__init__.py
@@ -16,7 +16,7 @@
if disassembler == None:
try:
- from ida_api import IDAAPI, DockableWindow
+ from .ida_api import IDAAPI, DockableWindow
disassembler = IDAAPI()
except ImportError:
pass
@@ -27,7 +27,7 @@
if disassembler == None:
try:
- from binja_api import BinjaAPI, DockableWindow
+ from .binja_api import BinjaAPI, DockableWindow
disassembler = BinjaAPI()
except ImportError:
pass
diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py
index bf8bcbdd..fb0db746 100644
--- a/plugin/lighthouse/util/log.py
+++ b/plugin/lighthouse/util/log.py
@@ -18,7 +18,7 @@ def lmsg(message):
# only print to disassembler if its output window is alive
if disassembler.is_msg_inited():
- print prefix_message
+ print(prefix_message)
else:
logger.info(message)
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index 99d9d927..edc560c9 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -3,6 +3,8 @@
import threading
import collections
+from .python import *
+
#------------------------------------------------------------------------------
# Plugin Util
#------------------------------------------------------------------------------
@@ -214,7 +216,7 @@ def rebase_blocks(base, basic_blocks):
"""
Rebase a list of basic block offsets (offset, size) to the given imagebase.
"""
- return map(lambda x: (base + x[0], x[1]), basic_blocks)
+ return list(map(lambda x: (base + x[0], x[1]), basic_blocks))
def build_hitmap(data):
"""
diff --git a/plugin/lighthouse/util/python.py b/plugin/lighthouse/util/python.py
new file mode 100644
index 00000000..789e4d12
--- /dev/null
+++ b/plugin/lighthouse/util/python.py
@@ -0,0 +1,69 @@
+import sys
+import operator
+
+#------------------------------------------------------------------------------
+# Python 2/3 Compatibilty Shims
+#------------------------------------------------------------------------------
+
+PY2 = sys.version_info[0] == 2
+PY3 = sys.version_info[0] == 3
+
+#
+# xrange shim
+#
+
+if PY3:
+ xrange = range # is this bad lol
+
+#
+# Queue --> queue shim
+#
+
+try:
+ import Queue as queue
+except:
+ import queue
+
+#
+# iter* shims by Benjamin Peterson, from https://github.com/benjaminp/six
+#
+
+if PY3:
+
+ def iterkeys(d, **kw):
+ return iter(d.keys(**kw))
+
+ def itervalues(d, **kw):
+ return iter(d.values(**kw))
+
+ def iteritems(d, **kw):
+ return iter(d.items(**kw))
+
+ def iterlists(d, **kw):
+ return iter(d.lists(**kw))
+
+ viewkeys = operator.methodcaller("keys")
+
+ viewvalues = operator.methodcaller("values")
+
+ viewitems = operator.methodcaller("items")
+
+else:
+
+ def iterkeys(d, **kw):
+ return d.iterkeys(**kw)
+
+ def itervalues(d, **kw):
+ return d.itervalues(**kw)
+
+ def iteritems(d, **kw):
+ return d.iteritems(**kw)
+
+ def iterlists(d, **kw):
+ return d.iterlists(**kw)
+
+ viewkeys = operator.methodcaller("viewkeys")
+
+ viewvalues = operator.methodcaller("viewvalues")
+
+ viewitems = operator.methodcaller("viewitems")
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 9d984011..17311839 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -1,10 +1,10 @@
import sys
import time
-import Queue
import logging
from .shim import *
from ..misc import is_mainthread
+from ..python import *
from ..disassembler import disassembler
logger = logging.getLogger("Lighthouse.Qt.Util")
@@ -216,7 +216,7 @@ def await_future(future):
# to the mainthread. flush the requests now and try again
#
- except Queue.Empty as e:
+ except queue.Empty as e:
pass
logger.debug("Awaiting future...")
From 72fe0f90e6b139c231796c9efc7f7494a18e2124 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 20 Mar 2019 13:14:36 -0400
Subject: [PATCH 011/154] add instruction sizes to metadata cache
---
plugin/lighthouse/metadata.py | 49 ++++++++++++++++++++++++++++++-----
1 file changed, 42 insertions(+), 7 deletions(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 9ac4bbcd..7116ab09 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -114,6 +114,33 @@ def get_instructions_slice(self, start_address, end_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.
+
+ Returns:
+ -1 if undefined address (not within a basic block)
+ 0 if within defined instruction
+ n if it is a defined instruction
+ """
+ node_metadata = self.get_node(address)
+
+ #
+ # if the given address does not fall within a node, we have no idea how
+ # big it really is. return -1
+ #
+
+ if not node_metadata:
+ return -1
+
+ #
+ # if the address falls within a node, attempt to return the size of the
+ # instruction at its address. if the address is misaligned / in the
+ # middle of an instruction, simply return 0
+ #
+
+ return node_metadata.instructions.get(address, 0)
+
def get_node(self, address):
"""
Get the node (basic block) metadata for a given address.
@@ -802,7 +829,7 @@ def _ida_refresh_nodes(self):
# compute all of the edges between nodes in the current function
for node_metadata in itervalues(function_metadata.nodes):
- edge_src = node_metadata.instructions[-1]
+ edge_src = node_metadata.edge_out
for edge_dst in idautils.CodeRefsFrom(edge_src, True):
if edge_dst in function_metadata.nodes:
function_metadata.edges[edge_src].append(edge_dst)
@@ -841,7 +868,7 @@ def _binja_refresh_nodes(self):
# destination that falls within this function.
#
- edge_src = node_metadata.instructions[-1]
+ edge_src = node_metadata.edge_out
for edge in node.outgoing_edges:
function_metadata.edges[edge_src].append(edge.target.start)
@@ -875,7 +902,7 @@ def _compute_complexity(self):
confirmed_nodes.add(node_address)
# now we loop through all edges that originate from this block
- current_src = self.nodes[node_address].instructions[-1]
+ current_src = self.nodes[node_address].edge_out
for current_dest in self.edges[current_src]:
# ignore nodes we have already visited
@@ -940,6 +967,7 @@ def __init__(self, start_ea, end_ea, node_id=None):
self.size = end_ea - start_ea
self.address = start_ea
self.instruction_count = 0
+ self.edge_out = -1
# flowchart node_id
self.id = node_id
@@ -948,7 +976,7 @@ def __init__(self, start_ea, end_ea, node_id=None):
self.function = None
# instruction addresses
- self.instructions = []
+ self.instructions = {}
#----------------------------------------------------------------------
@@ -982,9 +1010,12 @@ def _ida_build_metadata(self):
while current_address < node_end:
instruction_size = idaapi.get_item_end(current_address) - current_address
- self.instructions.append(current_address)
+ self.instructions[current_address] = instruction_size
current_address += instruction_size
+ # the source of the outward edge
+ self.edge_out = current_address - instruction_size
+
# save the number of instructions in this block
self.instruction_count = len(self.instructions)
@@ -1003,8 +1034,12 @@ def _binja_build_metadata(self):
#
while current_address < node_end:
- self.instructions.append(current_address)
- current_address += bv.get_instruction_length(current_address)
+ instruction_size = bv.get_instruction_length(current_address)
+ self.instructions[current_address] = instruction_size
+ current_address += instruction_size
+
+ # the source of the outward edge
+ self.edge_out = current_address - instruction_size
# save the number of instructions in this block
self.instruction_count = len(self.instructions)
From beb715cf9b27bc98cbc1b78d2d2a803dd53a4150 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 20 Mar 2019 13:17:24 -0400
Subject: [PATCH 012/154] tweaks to reader & cov file code
---
plugin/lighthouse/reader/coverage_file.py | 6 +++---
plugin/lighthouse/reader/coverage_reader.py | 14 ++++++++++----
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/plugin/lighthouse/reader/coverage_file.py b/plugin/lighthouse/reader/coverage_file.py
index 09d19cde..091d62bb 100644
--- a/plugin/lighthouse/reader/coverage_file.py
+++ b/plugin/lighthouse/reader/coverage_file.py
@@ -22,17 +22,17 @@ def get_addresses(self, module_name=None):
"""
raise NotImplementedError("Absolute addresses not supported by this log format")
- def get_offsets(self, module_name=None):
+ def get_offsets(self, module_name):
"""
Return coverage data for the named module as relative offets.
"""
raise NotImplementedError("Relative addresses not supported by this log format")
- def get_blocks(self, module_name=None):
+ def get_blocks(self, module_name):
"""
Return coverage data for the named module in block form (offset, size).
"""
- raise NotImplementedError("Block+Size not supported by this log format")
+ raise NotImplementedError("Block form not supported by this log format")
#--------------------------------------------------------------------------
# Parsing Routines - Top Level
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 04a638cd..ef1b74ad 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -24,15 +24,21 @@ def open(self, filepath):
"""
TODO
"""
+ coverage_file = None
for name, parser in iteritems(self._installed_parsers):
+ logger.debug("Attempting parse with '%s'" % name)
try:
- return parser(filepath)
+ coverage_file = parser(filepath)
+ break
except Exception as e:
- #print traceback.format_exc()
- pass
+ logger.debug("Parse failed...\n" + traceback.format_exc(e))
+
+ if not coverage_file:
+ raise ValueError("No compatible coverage parser for %s" % filepath)
- raise ValueError("No compatible coverage parser for %s" % filepath)
+ logger.debug("Parsed OKAY!")
+ return coverage_file
def _import_parsers(self):
"""
From 420c73560013f6222d6930f5a0a3088d20ade98f Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 20 Mar 2019 13:19:01 -0400
Subject: [PATCH 013/154] rename testcases
---
...mbox.exe.04936.0000.proc.log => drcov.log} | Bin
.../{bombox-cov.modoff.txt => modoff.log} | 0
testcase/trace.log | 14774 ++++++++++++++++
3 files changed, 14774 insertions(+)
rename testcase/{drcov.boombox.exe.04936.0000.proc.log => drcov.log} (100%)
rename testcase/{bombox-cov.modoff.txt => modoff.log} (100%)
create mode 100644 testcase/trace.log
diff --git a/testcase/drcov.boombox.exe.04936.0000.proc.log b/testcase/drcov.log
similarity index 100%
rename from testcase/drcov.boombox.exe.04936.0000.proc.log
rename to testcase/drcov.log
diff --git a/testcase/bombox-cov.modoff.txt b/testcase/modoff.log
similarity index 100%
rename from testcase/bombox-cov.modoff.txt
rename to testcase/modoff.log
diff --git a/testcase/trace.log b/testcase/trace.log
new file mode 100644
index 00000000..c90c332e
--- /dev/null
+++ b/testcase/trace.log
@@ -0,0 +1,14774 @@
+0x14000419c
+0x1400041a0
+0x1400045dc
+0x1400045e1
+0x1400045e2
+0x1400045e5
+0x1400045e9
+0x1400045f0
+0x1400045f5
+0x1400045ff
+0x140004602
+0x140004673
+0x140004678
+0x14000467b
+0x140004682
+0x140004686
+0x140004687
+0x1400041a5
+0x1400041a9
+0x140003fe0
+0x140003fe2
+0x140003fe6
+0x140003fef
+0x140003ff3
+0x140003ff5
+0x140003ff7
+0x140004000
+0x140004010
+0x140004016
+0x140004019
+0x140004025
+0x14000402b
+0x14000402d
+0x14000402f
+0x140004039
+0x140004040
+0x140004047
+0x140004820
+0x140003ea0
+0x140003ea4
+0x140003ea9
+0x140003eb0
+0x140003eb6
+0x140003ebd
+0x140003ec4
+0x140003ec7
+0x140003ecd
+0x140003ecf
+0x140003ed4
+0x140003ed8
+0x140003eda
+0x140003edc
+0x140003ee3
+0x140003ee5
+0x140003eeb
+0x140003eee
+0x140003ef4
+0x140003ef9
+0x140003eff
+0x140003f03
+0x140003f09
+0x140003f10
+0x140003f17
+0x140003f1e
+0x140003f24
+0x140003f26
+0x140003f2d
+0x140003f33
+0x140003f35
+0x1400047a4
+0x1400047a9
+0x1400047aa
+0x1400047ae
+0x1400047b5
+0x1400047bc
+0x1400047cc
+0x1400047cf
+0x1400047d1
+0x1400047d6
+0x1400047da
+0x1400047db
+0x140003f3a
+0x140004688
+0x14000468a
+0x140003f3f
+0x140003f46
+0x140003f55
+0x140003f5c
+0x140003f67
+0x140003f69
+0x140003f6d
+0x14000468c
+0x14000468e
+0x140004692
+0x14000469a
+0x1400046d2
+0x1400046d4
+0x1400046d8
+0x1400046d9
+0x1400044c8
+0x1400044cc
+0x1400044d3
+0x140004856
+0x1400044d8
+0x1400044da
+0x1400044de
+0x14000404c
+0x14000404e
+0x140004064
+0x14000406a
+0x14000406d
+0x14000406f
+0x140004076
+0x14000407d
+0x140004826
+0x140003f70
+0x140003f74
+0x140003f7b
+0x14000478c
+0x140004790
+0x1400046dc
+0x1400046de
+0x1400046e2
+0x1400046e5
+0x1400046ec
+0x1400046f2
+0x1400046f7
+0x1400046fb
+0x1400046fd
+0x140004700
+0x140004706
+0x140004786
+0x14000478a
+0x14000478b
+0x140004795
+0x140004798
+0x14000479a
+0x14000479c
+0x14000479e
+0x1400047a2
+0x140003f80
+0x140003f86
+0x140003f8d
+0x140003f93
+0x140003f9a
+0x140003fa1
+0x140003fa8
+0x140003faf
+0x140003fb4
+0x140003fba
+0x140003fc0
+0x140003fc2
+0x140003fce
+0x140003fd2
+0x140004082
+0x14000408c
+0x14000408e
+0x140004090
+0x140004092
+0x140004099
+0x1400040a1
+0x1400040c5
+0x1400040cc
+0x1400040d3
+0x1400040d6
+0x1400040dd
+0x1400040e4
+0x1400040ea
+0x1400038d0
+0x1400038d5
+0x1400038da
+0x1400038df
+0x1400038e0
+0x1400038e2
+0x1400038e4
+0x1400038e9
+0x1400038f0
+0x1400038f7
+0x1400038fa
+0x1400038fe
+0x140003900
+0x140003904
+0x14000390b
+0x14000390f
+0x140004928
+0x140003914
+0x140003919
+0x14000391b
+0x140003921
+0x140003925
+0x140003928
+0x14000392f
+0x140003935
+0x140003939
+0x140003940
+0x140003947
+0x14000394e
+0x140003955
+0x14000395c
+0x140003960
+0x140003962
+0x140003964
+0x140003320
+0x140003324
+0x14000332b
+0x140003331
+0x140003338
+0x14000333e
+0x140003345
+0x14000334a
+0x140003350
+0x140003357
+0x14000335d
+0x140003364
+0x14000336b
+0x140003371
+0x140003378
+0x14000337e
+0x140003385
+0x14000338b
+0x140003392
+0x140003398
+0x14000339f
+0x1400033a4
+0x1400033aa
+0x1400033b1
+0x1400033b7
+0x1400033be
+0x1400033c5
+0x1400033cb
+0x1400033d2
+0x1400033d8
+0x1400033df
+0x1400033e4
+0x1400033ea
+0x1400033f1
+0x1400033f7
+0x1400033fe
+0x140003405
+0x14000340b
+0x140003412
+0x140003417
+0x14000341d
+0x140003424
+0x14000342a
+0x140003431
+0x140003438
+0x14000343e
+0x140003445
+0x14000344b
+0x140003452
+0x140003457
+0x14000345d
+0x140003464
+0x14000346a
+0x140003471
+0x140003478
+0x14000347e
+0x140003485
+0x14000348b
+0x140003492
+0x140003497
+0x14000349d
+0x1400034a4
+0x1400034aa
+0x1400034b1
+0x1400034b8
+0x1400034be
+0x1400034c5
+0x1400034cb
+0x1400034d2
+0x1400034d8
+0x1400034df
+0x1400034e4
+0x1400034ea
+0x1400034f1
+0x1400034f7
+0x1400034fe
+0x140003505
+0x14000350b
+0x140003512
+0x140003518
+0x14000351f
+0x140003524
+0x14000352a
+0x140003531
+0x140003537
+0x14000353e
+0x140003545
+0x14000354b
+0x140003552
+0x140003558
+0x14000355f
+0x140003564
+0x14000356a
+0x140003571
+0x140003577
+0x14000357e
+0x140003585
+0x14000358b
+0x140003592
+0x140003598
+0x14000359f
+0x1400035a4
+0x1400035aa
+0x1400035b1
+0x1400035b7
+0x1400035be
+0x1400035c5
+0x1400035cb
+0x1400035d2
+0x1400035d7
+0x1400035dd
+0x1400035e4
+0x1400035ea
+0x1400035f1
+0x1400035f8
+0x1400035fe
+0x140003605
+0x14000360b
+0x140003612
+0x140003617
+0x14000361d
+0x140003624
+0x14000362a
+0x140003631
+0x140003638
+0x14000363e
+0x140003645
+0x14000364b
+0x140003652
+0x140003657
+0x14000365d
+0x140003664
+0x14000366a
+0x140003671
+0x140003678
+0x14000367e
+0x140003685
+0x14000368b
+0x140003692
+0x140003698
+0x14000369f
+0x1400036a5
+0x1400036ac
+0x1400036b1
+0x1400036b7
+0x1400036be
+0x1400036c4
+0x1400036cb
+0x1400036d2
+0x1400036d8
+0x1400036df
+0x1400036e5
+0x1400036ec
+0x1400036f1
+0x1400036f7
+0x1400036fe
+0x140003704
+0x14000370b
+0x140003712
+0x140003718
+0x14000371f
+0x140003725
+0x14000372c
+0x140003731
+0x140003737
+0x14000373e
+0x140003744
+0x14000374b
+0x140003752
+0x140003758
+0x14000375f
+0x140003765
+0x14000376c
+0x140003771
+0x140003777
+0x14000377e
+0x140003784
+0x14000378b
+0x140003792
+0x140003798
+0x14000379f
+0x1400037a5
+0x1400037ac
+0x1400037b1
+0x1400037b7
+0x1400037be
+0x1400037c4
+0x1400037cb
+0x1400037d2
+0x1400037d8
+0x1400037df
+0x1400037e5
+0x1400037ec
+0x1400037f2
+0x1400037f9
+0x1400037ff
+0x140003805
+0x140003809
+0x14000380d
+0x140003969
+0x140003970
+0x140003972
+0x140003977
+0x14000397b
+0x14000397f
+0x140003982
+0x140003984
+0x14000398a
+0x140003991
+0x140003997
+0x14000399e
+0x1400039a3
+0x1400039a9
+0x1400039b0
+0x1400039b7
+0x1400039bd
+0x1400039c3
+0x1400039c7
+0x1400039cd
+0x1400039d4
+0x1400039db
+0x1400039e1
+0x1400039e7
+0x1400039eb
+0x1400039ee
+0x1400039f3
+0x1400039f9
+0x1400039fd
+0x140003a00
+0x140003a06
+0x140003a09
+0x140003de7
+0x140003de9
+0x140003960
+0x140003962
+0x140003969
+0x140003970
+0x140003972
+0x140003977
+0x14000397b
+0x14000397f
+0x140003982
+0x140003984
+0x14000398a
+0x140003991
+0x140003997
+0x14000399e
+0x1400039a3
+0x1400039a9
+0x1400039b0
+0x1400039b7
+0x1400039bd
+0x1400039c3
+0x1400039c7
+0x1400039cd
+0x1400039d4
+0x1400039db
+0x1400039e1
+0x1400039e7
+0x1400039eb
+0x1400039ee
+0x1400039f3
+0x1400039f9
+0x1400039fd
+0x140003a00
+0x140003a06
+0x140003a09
+0x140003a0f
+0x140003a15
+0x140003a1a
+0x140003a1f
+0x140003a41
+0x140003a47
+0x140003a69
+0x140003a6f
+0x140003a91
+0x140003a97
+0x140003ab9
+0x140003abf
+0x140003ae1
+0x140003ae7
+0x140003b09
+0x140003b0f
+0x140003b31
+0x140003b37
+0x140003b59
+0x140003b5f
+0x140003b81
+0x140003b83
+0x140003b87
+0x140003b90
+0x140003b95
+0x140003b98
+0x140003b9d
+0x140003bb3
+0x140003bb5
+0x140003bb9
+0x140003bc0
+0x140003bc5
+0x140003bc8
+0x140003bcd
+0x140003bdf
+0x140003be5
+0x140003be7
+0x140003bee
+0x140003bf9
+0x140003bfb
+0x140003bff
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c13
+0x140002bf0
+0x140002bf4
+0x140002bfb
+0x140002c00
+0x140002c06
+0x140002c0d
+0x140002c13
+0x140002c1a
+0x140002c1f
+0x140002c25
+0x140002c2c
+0x140002c33
+0x140002c39
+0x140002c3f
+0x140002c43
+0x140002c49
+0x140002c50
+0x140002c57
+0x140002c5d
+0x140002c64
+0x140002c6a
+0x140002c71
+0x140002c76
+0x140002c7c
+0x140002c83
+0x140002c89
+0x140002c90
+0x140002c95
+0x140002c9b
+0x140002ca2
+0x140002ca9
+0x140002caf
+0x140002cb5
+0x140002cb9
+0x140002cbf
+0x140002cc6
+0x140002ccd
+0x140002cd3
+0x140002cda
+0x140002ce0
+0x140002ce7
+0x140002cec
+0x140002cf2
+0x140002cf9
+0x140002cff
+0x140002d06
+0x140002d0b
+0x140002d11
+0x140002d18
+0x140002d1f
+0x140002d25
+0x140002d2b
+0x140002d2f
+0x140002d35
+0x140002d3c
+0x140002d43
+0x140002d49
+0x140002d50
+0x140002d56
+0x140002d5d
+0x140002d62
+0x140002d68
+0x140002d6f
+0x140002d75
+0x140002d7c
+0x140002d81
+0x140002d87
+0x140002d8e
+0x140002d95
+0x140002d9b
+0x140002da1
+0x140002da5
+0x140002dab
+0x140002db2
+0x140002db9
+0x140002dbf
+0x140002dc6
+0x140002dcc
+0x140002dd3
+0x140002dd8
+0x140002dde
+0x140002de5
+0x140002deb
+0x140002df2
+0x140002df7
+0x140002dfd
+0x140002e04
+0x140002e0b
+0x140002e11
+0x140002e17
+0x140002e1b
+0x140002e21
+0x140002e28
+0x140002e2f
+0x140002e35
+0x140002e3c
+0x140002e42
+0x140002e49
+0x140002e4e
+0x140002e54
+0x140002e5b
+0x140002e61
+0x140002e68
+0x140002e6d
+0x140002e73
+0x140002e7a
+0x140002e81
+0x140002e87
+0x140002e8d
+0x140002e91
+0x140002e97
+0x140002e9e
+0x140002ea5
+0x140002eab
+0x140002eb2
+0x140002eb8
+0x140002ebf
+0x140002ec4
+0x140002eca
+0x140002ed1
+0x140002ed7
+0x140002ede
+0x140002ee3
+0x140002ee9
+0x140002ef0
+0x140002ef7
+0x140002efd
+0x140002f03
+0x140002f07
+0x140002f0d
+0x140002f14
+0x140002f1b
+0x140002f21
+0x140002f28
+0x140002f2e
+0x140002f35
+0x140002f3a
+0x140002f40
+0x140002f47
+0x140002f4d
+0x140002f54
+0x140002f59
+0x140002f5f
+0x140002f66
+0x140002f6d
+0x140002f73
+0x140002f79
+0x140002f7d
+0x140002f83
+0x140002f8a
+0x140002f91
+0x140002f97
+0x140002f9e
+0x140002fa4
+0x140002fab
+0x140002fb0
+0x140002fb6
+0x140002fbd
+0x140002fc3
+0x140002fca
+0x140002fcf
+0x140002fd5
+0x140002fdc
+0x140002fe3
+0x140002fe9
+0x140002fef
+0x140002ff3
+0x140002ff9
+0x140003000
+0x140003007
+0x14000300d
+0x140003014
+0x14000301a
+0x140003021
+0x140003026
+0x14000302c
+0x140003033
+0x140003039
+0x140003040
+0x140003045
+0x14000304b
+0x140003052
+0x140003059
+0x14000305f
+0x140003065
+0x140003069
+0x14000306f
+0x140003076
+0x14000307d
+0x140003083
+0x14000308a
+0x140003090
+0x140003097
+0x14000309c
+0x1400030a2
+0x1400030a9
+0x1400030af
+0x1400030b6
+0x1400030bb
+0x1400030c1
+0x1400030c8
+0x1400030cf
+0x1400030d5
+0x1400030db
+0x1400030df
+0x1400030e5
+0x1400030ec
+0x1400030f3
+0x1400030f9
+0x140003100
+0x140003106
+0x14000310d
+0x140003112
+0x140003118
+0x14000311f
+0x140003125
+0x14000312c
+0x140003131
+0x140003137
+0x14000313e
+0x140003145
+0x14000314b
+0x140003151
+0x140003155
+0x14000315b
+0x140003162
+0x140003169
+0x14000316f
+0x140003176
+0x14000317c
+0x140003183
+0x140003188
+0x14000318e
+0x140003195
+0x14000319b
+0x1400031a2
+0x1400031a7
+0x1400031ad
+0x1400031b4
+0x1400031bb
+0x1400031c1
+0x1400031c7
+0x1400031cb
+0x1400031d1
+0x1400031d8
+0x1400031df
+0x1400031e5
+0x1400031ec
+0x1400031f2
+0x1400031f9
+0x1400031fe
+0x140003204
+0x14000320b
+0x140003211
+0x140003218
+0x14000321d
+0x140003223
+0x14000322a
+0x140003231
+0x140003237
+0x14000323d
+0x140003241
+0x140003247
+0x14000324e
+0x140003255
+0x14000325b
+0x140003262
+0x140003268
+0x14000326e
+0x140003272
+0x140003276
+0x140003c18
+0x140003d57
+0x140003d5e
+0x140003d63
+0x140003d69
+0x140003d70
+0x140003d76
+0x140003d7d
+0x140003d82
+0x140003d88
+0x140003d8f
+0x140003d96
+0x140003d9c
+0x140003da2
+0x140003da6
+0x140003dac
+0x140003db3
+0x140003dba
+0x140003dc0
+0x140003dc6
+0x140003dc8
+0x140003960
+0x140003962
+0x140003964
+0x140003320
+0x140003324
+0x14000332b
+0x140003331
+0x140003338
+0x14000333e
+0x140003345
+0x14000334a
+0x140003350
+0x140003357
+0x14000335d
+0x140003364
+0x14000336b
+0x140003371
+0x140003378
+0x14000337e
+0x140003385
+0x14000338b
+0x140003392
+0x140003398
+0x14000339f
+0x1400033a4
+0x1400033aa
+0x1400033b1
+0x1400033b7
+0x1400033be
+0x1400033c5
+0x1400033cb
+0x1400033d2
+0x1400033d8
+0x1400033df
+0x1400033e4
+0x1400033ea
+0x1400033f1
+0x1400033f7
+0x1400033fe
+0x140003405
+0x14000340b
+0x140003412
+0x140003417
+0x14000341d
+0x140003424
+0x14000342a
+0x140003431
+0x140003438
+0x14000343e
+0x140003445
+0x14000344b
+0x140003452
+0x140003457
+0x14000345d
+0x140003464
+0x14000346a
+0x140003471
+0x140003478
+0x14000347e
+0x140003485
+0x14000348b
+0x140003492
+0x140003497
+0x14000349d
+0x1400034a4
+0x1400034aa
+0x1400034b1
+0x1400034b8
+0x1400034be
+0x1400034c5
+0x1400034cb
+0x1400034d2
+0x1400034d8
+0x1400034df
+0x1400034e4
+0x1400034ea
+0x1400034f1
+0x1400034f7
+0x1400034fe
+0x140003505
+0x14000350b
+0x140003512
+0x140003518
+0x14000351f
+0x140003524
+0x14000352a
+0x140003531
+0x140003537
+0x14000353e
+0x140003545
+0x14000354b
+0x140003552
+0x140003558
+0x14000355f
+0x140003564
+0x14000356a
+0x140003571
+0x140003577
+0x14000357e
+0x140003585
+0x14000358b
+0x140003592
+0x140003598
+0x14000359f
+0x1400035a4
+0x1400035aa
+0x1400035b1
+0x1400035b7
+0x1400035be
+0x1400035c5
+0x1400035cb
+0x1400035d2
+0x1400035d7
+0x1400035dd
+0x1400035e4
+0x1400035ea
+0x1400035f1
+0x1400035f8
+0x1400035fe
+0x140003605
+0x14000360b
+0x140003612
+0x140003617
+0x14000361d
+0x140003624
+0x14000362a
+0x140003631
+0x140003638
+0x14000363e
+0x140003645
+0x14000364b
+0x140003652
+0x140003657
+0x14000365d
+0x140003664
+0x14000366a
+0x140003671
+0x140003678
+0x14000367e
+0x140003685
+0x14000368b
+0x140003692
+0x140003698
+0x14000369f
+0x1400036a5
+0x1400036ac
+0x1400036b1
+0x1400036b7
+0x1400036be
+0x1400036c4
+0x1400036cb
+0x1400036d2
+0x1400036d8
+0x1400036df
+0x1400036e5
+0x1400036ec
+0x1400036f1
+0x1400036f7
+0x1400036fe
+0x140003704
+0x14000370b
+0x140003712
+0x140003718
+0x14000371f
+0x140003725
+0x14000372c
+0x140003731
+0x140003737
+0x14000373e
+0x140003744
+0x14000374b
+0x140003752
+0x140003758
+0x14000375f
+0x140003765
+0x14000376c
+0x140003771
+0x140003777
+0x14000377e
+0x140003784
+0x14000378b
+0x140003792
+0x140003798
+0x14000379f
+0x1400037a5
+0x1400037ac
+0x1400037b1
+0x1400037b7
+0x1400037be
+0x1400037c4
+0x1400037cb
+0x1400037d2
+0x1400037d8
+0x1400037df
+0x1400037e5
+0x1400037ec
+0x1400037f2
+0x1400037f9
+0x1400037ff
+0x140003805
+0x140003809
+0x14000380d
+0x140003969
+0x140003970
+0x140003972
+0x140003977
+0x14000397b
+0x14000397f
+0x140003982
+0x140003984
+0x14000398a
+0x140003991
+0x140003997
+0x14000399e
+0x1400039a3
+0x1400039a9
+0x1400039b0
+0x1400039b7
+0x1400039bd
+0x1400039c3
+0x1400039c7
+0x1400039cd
+0x1400039d4
+0x1400039db
+0x1400039e1
+0x1400039e7
+0x1400039eb
+0x1400039ee
+0x1400039f3
+0x1400039f9
+0x1400039fd
+0x140003a00
+0x140003a06
+0x140003a09
+0x140003a0f
+0x140003a15
+0x140003a1a
+0x140003a1f
+0x140003a21
+0x140003a28
+0x140003a2a
+0x140003a31
+0x140003a33
+0x140003a37
+0x1400012b0
+0x1400012b2
+0x1400012b3
+0x1400012b5
+0x1400012b9
+0x1400012bb
+0x1400012be
+0x1400012c2
+0x1400012c4
+0x1400012c6
+0x1400012c9
+0x1400012dc
+0x1400012df
+0x140001358
+0x14000135d
+0x140001362
+0x140001150
+0x140001154
+0x14000115a
+0x14000115d
+0x1400011d4
+0x1400011d8
+0x140001367
+0x14000136e
+0x140001373
+0x140001376
+0x14000137c
+0x140001383
+0x140001389
+0x140001390
+0x140001395
+0x14000139b
+0x1400013a2
+0x1400013a9
+0x1400013af
+0x1400013b5
+0x1400013b9
+0x1400013bf
+0x1400013c6
+0x1400013cd
+0x1400013d3
+0x1400013d7
+0x1400013dc
+0x140001060
+0x140001065
+0x14000106a
+0x14000106b
+0x14000106f
+0x140001072
+0x140001074
+0x140001077
+0x14000107a
+0x140004928
+0x14000107f
+0x140001085
+0x140001087
+0x14000108a
+0x14000108d
+0x140001093
+0x140001095
+0x140001097
+0x140001099
+0x14000109b
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010de
+0x1400010e3
+0x1400010e8
+0x1400010eb
+0x1400010ef
+0x1400010f3
+0x1400010f4
+0x1400013e1
+0x1400013e8
+0x1400013ed
+0x1400013f3
+0x1400013fa
+0x140001400
+0x140001407
+0x14000140c
+0x140001412
+0x140001419
+0x140001420
+0x140001426
+0x14000142c
+0x140001430
+0x140001436
+0x14000143d
+0x140001444
+0x14000144a
+0x14000144f
+0x140001456
+0x14000145b
+0x140001461
+0x140001467
+0x140001469
+0x14000147e
+0x140001483
+0x140001487
+0x14000148b
+0x14000148f
+0x140001495
+0x140001498
+0x14000149b
+0x1400014a1
+0x1400014a6
+0x1400014ab
+0x1400014b0
+0x1400014b4
+0x1400014b9
+0x140001150
+0x140001154
+0x14000115a
+0x14000115d
+0x1400011d4
+0x1400011d8
+0x1400014be
+0x1400014c5
+0x1400014ca
+0x1400014cd
+0x1400014d3
+0x1400014da
+0x1400014e0
+0x1400014e7
+0x1400014ec
+0x1400014f2
+0x1400014f9
+0x140001500
+0x140001506
+0x14000150c
+0x140001510
+0x140001516
+0x14000151d
+0x140001524
+0x14000152a
+0x14000152f
+0x140001532
+0x140001060
+0x140001065
+0x14000106a
+0x14000106b
+0x14000106f
+0x140001072
+0x140001074
+0x140001077
+0x14000107a
+0x140004928
+0x14000107f
+0x140001085
+0x140001087
+0x14000108a
+0x14000108d
+0x140001093
+0x140001095
+0x140001097
+0x140001099
+0x14000109b
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010de
+0x1400010e3
+0x1400010e8
+0x1400010eb
+0x1400010ef
+0x1400010f3
+0x1400010f4
+0x140001537
+0x14000153e
+0x140001543
+0x140001549
+0x140001550
+0x140001556
+0x14000155d
+0x140001562
+0x140001568
+0x14000156f
+0x140001576
+0x14000157c
+0x140001582
+0x140001586
+0x14000158c
+0x140001593
+0x14000159a
+0x1400015a0
+0x1400015a4
+0x1400015a9
+0x140001060
+0x140001065
+0x14000106a
+0x14000106b
+0x14000106f
+0x140001072
+0x140001074
+0x140001077
+0x14000107a
+0x140004928
+0x14000107f
+0x140001085
+0x140001087
+0x14000108a
+0x14000108d
+0x140001093
+0x140001095
+0x140001097
+0x140001099
+0x14000109b
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010a6
+0x1400010a9
+0x1400010ab
+0x1400010ae
+0x1400010a0
+0x1400010a4
+0x1400010de
+0x1400010e3
+0x1400010e8
+0x1400010eb
+0x1400010ef
+0x1400010f3
+0x1400010f4
+0x1400015ae
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015b2
+0x1400015b5
+0x1400015ba
+0x1400015bc
+0x1400015c0
+0x1400015c2
+0x1400015c5
+0x1400015c8
+0x1400015cc
+0x1400015d0
+0x1400015d6
+0x1400015db
+0x1400015e0
+0x1400015e5
+0x1400015ec
+0x1400015ef
+0x1400015f4
+0x1400015f9
+0x1400015ff
+0x140001606
+0x14000160c
+0x140001613
+0x140001618
+0x14000161e
+0x140001625
+0x14000162c
+0x140001632
+0x140001638
+0x14000163c
+0x140001642
+0x140001649
+0x140001650
+0x140001656
+0x14000165b
+0x14000165d
+0x140001661
+0x140001663
+0x140001664
+0x140001665
+0x140003a3c
+0x140003d57
+0x140003d5e
+0x140003d63
+0x140003d69
+0x140003d70
+0x140003d76
+0x140003d7d
+0x140003d82
+0x140003d88
+0x140003d8f
+0x140003d96
+0x140003d9c
+0x140003da2
+0x140003da6
+0x140003dac
+0x140003db3
+0x140003dba
+0x140003dc0
+0x140003dc6
+0x140003dc8
+0x140003960
+0x140003962
+0x140003964
+0x140003320
+0x140003324
+0x14000332b
+0x140003331
+0x140003338
+0x14000333e
+0x140003345
+0x14000334a
+0x140003350
+0x140003357
+0x14000335d
+0x140003364
+0x14000336b
+0x140003371
+0x140003378
+0x14000337e
+0x140003385
+0x14000338b
+0x140003392
+0x140003398
+0x14000339f
+0x1400033a4
+0x1400033aa
+0x1400033b1
+0x1400033b7
+0x1400033be
+0x1400033c5
+0x1400033cb
+0x1400033d2
+0x1400033d8
+0x1400033df
+0x1400033e4
+0x1400033ea
+0x1400033f1
+0x1400033f7
+0x1400033fe
+0x140003405
+0x14000340b
+0x140003412
+0x140003417
+0x14000341d
+0x140003424
+0x14000342a
+0x140003431
+0x140003438
+0x14000343e
+0x140003445
+0x14000344b
+0x140003452
+0x140003457
+0x14000345d
+0x140003464
+0x14000346a
+0x140003471
+0x140003478
+0x14000347e
+0x140003485
+0x14000348b
+0x140003492
+0x140003497
+0x14000349d
+0x1400034a4
+0x1400034aa
+0x1400034b1
+0x1400034b8
+0x1400034be
+0x1400034c5
+0x1400034cb
+0x1400034d2
+0x1400034d8
+0x1400034df
+0x1400034e4
+0x1400034ea
+0x1400034f1
+0x1400034f7
+0x1400034fe
+0x140003505
+0x14000350b
+0x140003512
+0x140003518
+0x14000351f
+0x140003524
+0x14000352a
+0x140003531
+0x140003537
+0x14000353e
+0x140003545
+0x14000354b
+0x140003552
+0x140003558
+0x14000355f
+0x140003564
+0x14000356a
+0x140003571
+0x140003577
+0x14000357e
+0x140003585
+0x14000358b
+0x140003592
+0x140003598
+0x14000359f
+0x1400035a4
+0x1400035aa
+0x1400035b1
+0x1400035b7
+0x1400035be
+0x1400035c5
+0x1400035cb
+0x1400035d2
+0x1400035d7
+0x1400035dd
+0x1400035e4
+0x1400035ea
+0x1400035f1
+0x1400035f8
+0x1400035fe
+0x140003605
+0x14000360b
+0x140003612
+0x140003617
+0x14000361d
+0x140003624
+0x14000362a
+0x140003631
+0x140003638
+0x14000363e
+0x140003645
+0x14000364b
+0x140003652
+0x140003657
+0x14000365d
+0x140003664
+0x14000366a
+0x140003671
+0x140003678
+0x14000367e
+0x140003685
+0x14000368b
+0x140003692
+0x140003698
+0x14000369f
+0x1400036a5
+0x1400036ac
+0x1400036b1
+0x1400036b7
+0x1400036be
+0x1400036c4
+0x1400036cb
+0x1400036d2
+0x1400036d8
+0x1400036df
+0x1400036e5
+0x1400036ec
+0x1400036f1
+0x1400036f7
+0x1400036fe
+0x140003704
+0x14000370b
+0x140003712
+0x140003718
+0x14000371f
+0x140003725
+0x14000372c
+0x140003731
+0x140003737
+0x14000373e
+0x140003744
+0x14000374b
+0x140003752
+0x140003758
+0x14000375f
+0x140003765
+0x14000376c
+0x140003771
+0x140003777
+0x14000377e
+0x140003784
+0x14000378b
+0x140003792
+0x140003798
+0x14000379f
+0x1400037a5
+0x1400037ac
+0x1400037b1
+0x1400037b7
+0x1400037be
+0x1400037c4
+0x1400037cb
+0x1400037d2
+0x1400037d8
+0x1400037df
+0x1400037e5
+0x1400037ec
+0x1400037f2
+0x1400037f9
+0x1400037ff
+0x140003805
+0x140003809
+0x14000380d
+0x140003969
+0x140003970
+0x140003972
+0x140003977
+0x14000397b
+0x14000397f
+0x140003982
+0x140003984
+0x14000398a
+0x140003991
+0x140003997
+0x14000399e
+0x1400039a3
+0x1400039a9
+0x1400039b0
+0x1400039b7
+0x1400039bd
+0x1400039c3
+0x1400039c7
+0x1400039cd
+0x1400039d4
+0x1400039db
+0x1400039e1
+0x1400039e7
+0x1400039eb
+0x1400039ee
+0x1400039f3
+0x1400039f9
+0x1400039fd
+0x140003a00
+0x140003a06
+0x140003a09
+0x140003a0f
+0x140003a15
+0x140003a1a
+0x140003a1f
+0x140003a41
+0x140003a47
+0x140003a69
+0x140003a6f
+0x140003a91
+0x140003a97
+0x140003ab9
+0x140003abf
+0x140003ae1
+0x140003ae7
+0x140003b09
+0x140003b0f
+0x140003b31
+0x140003b37
+0x140003b59
+0x140003b5f
+0x140003b81
+0x140003b83
+0x140003b87
+0x140003b90
+0x140003b95
+0x140003b98
+0x140003b9d
+0x140003bb3
+0x140003bb5
+0x140003bb9
+0x140003bc0
+0x140003bc5
+0x140003bc8
+0x140003bcd
+0x140003bdf
+0x140003be5
+0x140003be7
+0x140003bee
+0x140003bf9
+0x140003bfb
+0x140003bff
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c00
+0x140003c04
+0x140003c07
+0x140003c0b
+0x140003c0d
+0x140003c11
+0x140003c13
+0x140002bf0
+0x140002bf4
+0x140002bfb
+0x140002c00
+0x140002c06
+0x140002c0d
+0x140002c13
+0x140002c1a
+0x140002c1f
+0x140002c25
+0x140002c2c
+0x140002c33
+0x140002c39
+0x140002c3f
+0x140002c43
+0x140002c49
+0x140002c50
+0x140002c57
+0x140002c5d
+0x140002c64
+0x140002c6a
+0x140002c71
+0x140002c76
+0x140002c7c
+0x140002c83
+0x140002c89
+0x140002c90
+0x140002c95
+0x140002c9b
+0x140002ca2
+0x140002ca9
+0x140002caf
+0x140002cb5
+0x140002cb9
+0x140002cbf
+0x140002cc6
+0x140002ccd
+0x140002cd3
+0x140002cda
+0x140002ce0
+0x140002ce7
+0x140002cec
+0x140002cf2
+0x140002cf9
+0x140002cff
+0x140002d06
+0x140002d0b
+0x140002d11
+0x140002d18
+0x140002d1f
+0x140002d25
+0x140002d2b
+0x140002d2f
+0x140002d35
+0x140002d3c
+0x140002d43
+0x140002d49
+0x140002d50
+0x140002d56
+0x140002d5d
+0x140002d62
+0x140002d68
+0x140002d6f
+0x140002d75
+0x140002d7c
+0x140002d81
+0x140002d87
+0x140002d8e
+0x140002d95
+0x140002d9b
+0x140002da1
+0x140002da5
+0x140002dab
+0x140002db2
+0x140002db9
+0x140002dbf
+0x140002dc6
+0x140002dcc
+0x140002dd3
+0x140002dd8
+0x140002dde
+0x140002de5
+0x140002deb
+0x140002df2
+0x140002df7
+0x140002dfd
+0x140002e04
+0x140002e0b
+0x140002e11
+0x140002e17
+0x140002e1b
+0x140002e21
+0x140002e28
+0x140002e2f
+0x140002e35
+0x140002e3c
+0x140002e42
+0x140002e49
+0x140002e4e
+0x140002e54
+0x140002e5b
+0x140002e61
+0x140002e68
+0x140002e6d
+0x140002e73
+0x140002e7a
+0x140002e81
+0x140002e87
+0x140002e8d
+0x140002e91
+0x140002e97
+0x140002e9e
+0x140002ea5
+0x140002eab
+0x140002eb2
+0x140002eb8
+0x140002ebf
+0x140002ec4
+0x140002eca
+0x140002ed1
+0x140002ed7
+0x140002ede
+0x140002ee3
+0x140002ee9
+0x140002ef0
+0x140002ef7
+0x140002efd
+0x140002f03
+0x140002f07
+0x140002f0d
+0x140002f14
+0x140002f1b
+0x140002f21
+0x140002f28
+0x140002f2e
+0x140002f35
+0x140002f3a
+0x140002f40
+0x140002f47
+0x140002f4d
+0x140002f54
+0x140002f59
+0x140002f5f
+0x140002f66
+0x140002f6d
+0x140002f73
+0x140002f79
+0x140002f7d
+0x140002f83
+0x140002f8a
+0x140002f91
+0x140002f97
+0x140002f9e
+0x140002fa4
+0x140002fab
+0x140002fb0
+0x140002fb6
+0x140002fbd
+0x140002fc3
+0x140002fca
+0x140002fcf
+0x140002fd5
+0x140002fdc
+0x140002fe3
+0x140002fe9
+0x140002fef
+0x140002ff3
+0x140002ff9
+0x140003000
+0x140003007
+0x14000300d
+0x140003014
+0x14000301a
+0x140003021
+0x140003026
+0x14000302c
+0x140003033
+0x140003039
+0x140003040
+0x140003045
+0x14000304b
+0x140003052
+0x140003059
+0x14000305f
+0x140003065
+0x140003069
+0x14000306f
+0x140003076
+0x14000307d
+0x140003083
+0x14000308a
+0x140003090
+0x140003097
+0x14000309c
+0x1400030a2
+0x1400030a9
+0x1400030af
+0x1400030b6
+0x1400030bb
+0x1400030c1
+0x1400030c8
+0x1400030cf
+0x1400030d5
+0x1400030db
+0x1400030df
+0x1400030e5
+0x1400030ec
+0x1400030f3
+0x1400030f9
+0x140003100
+0x140003106
+0x14000310d
+0x140003112
+0x140003118
+0x14000311f
+0x140003125
+0x14000312c
+0x140003131
+0x140003137
+0x14000313e
+0x140003145
+0x14000314b
+0x140003151
+0x140003155
+0x14000315b
+0x140003162
+0x140003169
+0x14000316f
+0x140003176
+0x14000317c
+0x140003183
+0x140003188
+0x14000318e
+0x140003195
+0x14000319b
+0x1400031a2
+0x1400031a7
+0x1400031ad
+0x1400031b4
+0x1400031bb
+0x1400031c1
+0x1400031c7
+0x1400031cb
+0x1400031d1
+0x1400031d8
+0x1400031df
+0x1400031e5
+0x1400031ec
+0x1400031f2
+0x1400031f9
+0x1400031fe
+0x140003204
+0x14000320b
+0x140003211
+0x140003218
+0x14000321d
+0x140003223
+0x14000322a
+0x140003231
+0x140003237
+0x14000323d
+0x140003241
+0x140003247
+0x14000324e
+0x140003255
+0x14000325b
+0x140003262
+0x140003268
+0x14000326e
+0x140003272
+0x140003276
+0x140003c18
+0x140003d57
+0x140003d5e
+0x140003d63
+0x140003d69
+0x140003d70
+0x140003d76
+0x140003d7d
+0x140003d82
+0x140003d88
+0x140003d8f
+0x140003d96
+0x140003d9c
+0x140003da2
+0x140003da6
+0x140003dac
+0x140003db3
+0x140003dba
+0x140003dc0
+0x140003dc6
+0x140003dc8
+0x140003960
+0x140003962
+0x140003964
+0x140003320
+0x140003324
+0x14000332b
+0x140003331
+0x140003338
+0x14000333e
+0x140003345
+0x14000334a
+0x140003350
+0x140003357
+0x14000335d
+0x140003364
+0x14000336b
+0x140003371
+0x140003378
+0x14000337e
+0x140003385
+0x14000338b
+0x140003392
+0x140003398
+0x14000339f
+0x1400033a4
+0x1400033aa
+0x1400033b1
+0x1400033b7
+0x1400033be
+0x1400033c5
+0x1400033cb
+0x1400033d2
+0x1400033d8
+0x1400033df
+0x1400033e4
+0x1400033ea
+0x1400033f1
+0x1400033f7
+0x1400033fe
+0x140003405
+0x14000340b
+0x140003412
+0x140003417
+0x14000341d
+0x140003424
+0x14000342a
+0x140003431
+0x140003438
+0x14000343e
+0x140003445
+0x14000344b
+0x140003452
+0x140003457
+0x14000345d
+0x140003464
+0x14000346a
+0x140003471
+0x140003478
+0x14000347e
+0x140003485
+0x14000348b
+0x140003492
+0x140003497
+0x14000349d
+0x1400034a4
+0x1400034aa
+0x1400034b1
+0x1400034b8
+0x1400034be
+0x1400034c5
+0x1400034cb
+0x1400034d2
+0x1400034d8
+0x1400034df
+0x1400034e4
+0x1400034ea
+0x1400034f1
+0x1400034f7
+0x1400034fe
+0x140003505
+0x14000350b
+0x140003512
+0x140003518
+0x14000351f
+0x140003524
+0x14000352a
+0x140003531
+0x140003537
+0x14000353e
+0x140003545
+0x14000354b
+0x140003552
+0x140003558
+0x14000355f
+0x140003564
+0x14000356a
+0x140003571
+0x140003577
+0x14000357e
+0x140003585
+0x14000358b
+0x140003592
+0x140003598
+0x14000359f
+0x1400035a4
+0x1400035aa
+0x1400035b1
+0x1400035b7
+0x1400035be
+0x1400035c5
+0x1400035cb
+0x1400035d2
+0x1400035d7
+0x1400035dd
+0x1400035e4
+0x1400035ea
+0x1400035f1
+0x1400035f8
+0x1400035fe
+0x140003605
+0x14000360b
+0x140003612
+0x140003617
+0x14000361d
+0x140003624
+0x14000362a
+0x140003631
+0x140003638
+0x14000363e
+0x140003645
+0x14000364b
+0x140003652
+0x140003657
+0x14000365d
+0x140003664
+0x14000366a
+0x140003671
+0x140003678
+0x14000367e
+0x140003685
+0x14000368b
+0x140003692
+0x140003698
+0x14000369f
+0x1400036a5
+0x1400036ac
+0x1400036b1
+0x1400036b7
+0x1400036be
+0x1400036c4
+0x1400036cb
+0x1400036d2
+0x1400036d8
+0x1400036df
+0x1400036e5
+0x1400036ec
+0x1400036f1
+0x1400036f7
+0x1400036fe
+0x140003704
+0x14000370b
+0x140003712
+0x140003718
+0x14000371f
+0x140003725
+0x14000372c
+0x140003731
+0x140003737
+0x14000373e
+0x140003744
+0x14000374b
+0x140003752
+0x140003758
+0x14000375f
+0x140003765
+0x14000376c
+0x140003771
+0x140003777
+0x14000377e
+0x140003784
+0x14000378b
+0x140003792
+0x140003798
+0x14000379f
+0x1400037a5
+0x1400037ac
+0x1400037b1
+0x1400037b7
+0x1400037be
+0x1400037c4
+0x1400037cb
+0x1400037d2
+0x1400037d8
+0x1400037df
+0x1400037e5
+0x1400037ec
+0x1400037f2
+0x1400037f9
+0x1400037ff
+0x140003805
+0x140003809
+0x14000380d
+0x140003969
+0x140003970
+0x140003972
+0x140003977
+0x14000397b
+0x14000397f
+0x140003982
+0x140003984
+0x14000398a
+0x140003991
+0x140003997
+0x14000399e
+0x1400039a3
+0x1400039a9
+0x1400039b0
+0x1400039b7
+0x1400039bd
+0x1400039c3
+0x1400039c7
+0x1400039cd
+0x1400039d4
+0x1400039db
+0x1400039e1
+0x1400039e7
+0x1400039eb
+0x1400039ee
+0x1400039f3
+0x1400039f9
+0x1400039fd
+0x140003a00
+0x140003a06
+0x140003a09
+0x140003a0f
+0x140003a15
+0x140003a1a
+0x140003a1f
+0x140003a41
+0x140003a47
+0x140003a69
+0x140003a6f
+0x140003a91
+0x140003a97
+0x140003ab9
+0x140003abf
+0x140003ae1
+0x140003ae7
+0x140003b09
+0x140003b0f
+0x140003b31
+0x140003b37
+0x140003b59
+0x140003b5f
+0x140003b61
+0x140003b68
+0x140003b6a
+0x140003b71
+0x140003b73
+0x140003b77
+0x140002260
+0x140002263
+0x140002264
+0x14000226b
+0x140002272
+0x140002275
+0x14000227d
+0x140002281
+0x140002284
+0x140002287
+0x14000228d
+0x140002292
+0x140002295
+0x14000229b
+0x14000229e
+0x1400022a2
+0x1400022a8
+0x1400022ad
+0x1400022b1
+0x1400022b5
+0x1400022b9
+0x1400022bd
+0x1400022c1
+0x1400022c8
+0x1400022cd
+0x1400022d0
+0x140002b50
+0x140002b55
+0x140002b56
+0x140002b5a
+0x140002b5d
+0x140002b5f
+0x140002b62
+0x1400029d0
+0x1400029d5
+0x1400029d8
+0x1400029da
+0x1400029e1
+0x1400029e4
+0x1400029ec
+0x1400029ef
+0x140002a15
+0x140002a1a
+0x140002a1d
+0x140002b67
+0x140002b6a
+0x140002b6d
+0x140002a60
+0x140002a65
+0x140002a68
+0x140002a6a
+0x140002a71
+0x140002a74
+0x140002a7c
+0x140002a7f
+0x140002aa5
+0x140002aaa
+0x140002aad
+0x140002b72
+0x140002b79
+0x140002b7c
+0x140002b7f
+0x140002b85
+0x140002b8c
+0x140002b92
+0x140002b99
+0x140002b9c
+0x140002ba2
+0x140002ba9
+0x140002bac
+0x140002bb2
+0x140002bb8
+0x140002bbc
+0x140002bc2
+0x140002bc9
+0x140002bd0
+0x140002bd5
+0x140002bd9
+0x140002bda
+0x1400022d5
+0x1400022da
+0x1400022dc
+0x1400022e2
+0x140004928
+0x1400022e7
+0x1400022ed
+0x1400022f2
+0x1400022f5
+0x1400022fa
+0x140002300
+0x140002302
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002304
+0x140002309
+0x14000230b
+0x14000230e
+0x140002310
+0x140002316
+0x140002318
+0x1400047dc
+0x1400047e1
+0x1400047e2
+0x1400047e6
+0x1400047ed
+0x1400047f4
+0x140004804
+0x140004807
+0x140004809
+0x14000480e
+0x140004812
+0x140004813
From 5468bef8428e7901e525b63921ee41f0addf1a59 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 20 Mar 2019 13:21:43 -0400
Subject: [PATCH 014/154] refactoring some loading code, add 'trace' format
---
plugin/lighthouse/core.py | 20 +-
plugin/lighthouse/director.py | 330 +++++++++++++++-------
plugin/lighthouse/metadata.py | 38 ---
plugin/lighthouse/reader/parsers/trace.py | 34 +++
plugin/lighthouse/util/misc.py | 60 ----
5 files changed, 271 insertions(+), 211 deletions(-)
create mode 100644 plugin/lighthouse/reader/parsers/trace.py
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index fb84d660..dea1db30 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -7,7 +7,6 @@
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
-from lighthouse.reader import CoverageReader
from lighthouse.palette import LighthousePalette
from lighthouse.painting import CoveragePainter
from lighthouse.director import CoverageDirector
@@ -293,16 +292,7 @@ def interactive_load_file(self):
#
filenames = self._select_coverage_files()
-
- #
- # load the selected coverage files from disk (if any), returning a list
- # of loaded DrcovData objects (which contain coverage data)
- #
-
- disassembler.show_wait_box("Loading coverage from disk...")
- drcov_list = load_coverage_files(filenames)
- if not drcov_list:
- disassembler.hide_wait_box()
+ if not filenames:
self.director.metadata.abort_refresh()
return
@@ -317,8 +307,12 @@ def interactive_load_file(self):
disassembler.replace_wait_box("Building database metadata...")
await_future(future)
+ #
# insert the loaded drcov data objects into the director
- created_coverage, errors = self.director.create_coverage_from_files(drcov_list)
+ # TODO
+
+ disassembler.show_wait_box("Loading coverage from disk...")
+ created_coverage, errors = self.director.load_coverage_files(filenames)
#
# if the director failed to map any coverage, the user probably
@@ -337,7 +331,7 @@ def interactive_load_file(self):
#
disassembler.replace_wait_box("Selecting coverage...")
- self.director.select_coverage(created_coverage[0])
+ self.director.select_coverage(created_coverage[0].name)
# all done! pop the coverage overview to show the user their results
disassembler.hide_wait_box()
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 0781347c..82838996 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -2,13 +2,14 @@
import string
import logging
import threading
+import traceback
import collections
-from lighthouse.util import lmsg
from lighthouse.util.misc import *
from lighthouse.util.python import *
from lighthouse.util.qt import await_future, await_lock, color_text
from lighthouse.util.disassembler import disassembler
+from lighthouse.reader import CoverageReader
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.coverage import DatabaseCoverage
from lighthouse.composer.parser import *
@@ -46,8 +47,9 @@ class CoverageDirector(object):
between any number of coverage files.
"""
- ERROR_COVERAGE_ABSENT = 1
- ERROR_COVERAGE_SUSPICIOUS = 2
+ ERROR_COVERAGE_MALFORMED = 1
+ ERROR_COVERAGE_ABSENT = 2
+ ERROR_COVERAGE_SUSPICIOUS = 3
def __init__(self, metadata, palette):
@@ -61,6 +63,9 @@ def __init__(self, metadata, palette):
# Coverage
#----------------------------------------------------------------------
+ # the coverage file parser
+ self.reader = CoverageReader()
+
# the name of the active coverage
self.coverage_name = NEW_COMPOSITION
@@ -328,14 +333,27 @@ def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
"""
return self.update_coverage(coverage_name, coverage_data, coverage_filepath)
- def create_coverage_from_files(self, drcov_list):
+ #----------------------------------------------------------------------
+ # Coverage Loading
+ #----------------------------------------------------------------------
+
+ def load_coverage_file(self, filepath):
+ """
+ Create a new database coverage mapping from a coverage file.
+
+ Returns the created coverage object.
+ """
+ coverage, _ = self._load_coverage_internal(filepath, False)
+ return coverage
+
+ def load_coverage_files(self, filepaths, progress_callback=None):
"""
- Create a number of database coverage mappings from a list of DrcovData.
+ Create new database coverage mappings from a list of coverage files.
- Returns a tuple of (created_coverage, errors)
+ Returns a tuple of (created_coverage, all_errors)
"""
created_coverage = []
- errors = []
+ all_errors = []
#
# stop the director's aggregate from updating. this will prevent the
@@ -352,89 +370,191 @@ def create_coverage_from_files(self, drcov_list):
# into a generic format the director can consume (a list of addresses)
#
- for i, drcov_data in enumerate(drcov_list, 1):
+ for i, filepath in enumerate(filepaths, 1):
# keep the user informed about our progress while loading coverage
- disassembler.replace_wait_box(
- "Normalizing and mapping coverage %u/%u" % (i, len(drcov_list))
- )
+ if progress_callback:
+ progress_callback("Loading coverage %u/%u" % (i, len(filepaths)))
- #
- # translate the coverage data's basic block addresses to the
- # imagebase of the open database, and flatten the blocks to a
- # list of instruction addresses
- #
+ # load a single coverage file
+ coverage, errors = self._load_coverage_internal(filepath, True)
+ if coverage:
+ created_coverage.append(coverage)
- try:
- coverage_data = self._normalize_drcov_data(drcov_data)
- except ValueError as e:
- errors.append((self.ERROR_COVERAGE_ABSENT, drcov_data.filepath))
- lmsg("Failed to normalize coverage %s" % drcov_data.filepath)
- lmsg("- %s" % e)
- continue
+ # save any errors that were generated (suppressed)
+ all_errors.extend(errors)
- #
- # before injecting the new coverage data (now a list of instruction
- # addresses), we check to see if there is an existing coverage
- # object under the same name.
- #
- # if there is an existing coverage mapping, odds are that the user
- # is probably re-loading the same coverage file in which case we
- # simply overwrite the old DatabaseCoverage object.
- #
- # but we have to be careful for the case where the user loads a
- # coverage file from a different directory, but under the same name
- #
- # e.g:
- # - C:\coverage\foo.log
- # - C:\coverage\testing\foo.log
- #
- # in these cases, we will append a suffix to the new coverage file
- #
+ #
+ # resume the director's aggregation service, triggering an update to
+ # recompute the aggregate with the newly loaded coverage
+ #
- coverage_name = os.path.basename(drcov_data.filepath)
- coverage = self.get_coverage(coverage_name)
+ if progress_callback:
+ progress_callback("Recomputing coverage aggregate...")
- # assign a suffix to the coverage name in the event of a collision
- if coverage and coverage.filepath != drcov_data.filepath:
- for i in xrange(2, 100000):
- new_name = "%s_%u" % (coverage_name, i)
- if not self.get_coverage(new_name):
- break
- coverage_name = new_name
+ self.resume_aggregation()
- #
- # finally, we can ask the director to create a coverage mapping
- # from the data we have pre-processed for it
- #
+ # done
+ return (created_coverage, all_errors)
- coverage = self.create_coverage(
- coverage_name,
- coverage_data,
- drcov_data.filepath
- )
- created_coverage.append(coverage_name)
+ def _load_coverage_internal(self, filepath, suppress_errors):
+ """
+ Internal routine used to load coverage from disk.
+ """
+ errors = []
- # warn when loaded coverage appears to be poorly mapped (suspicious)
- if coverage.suspicious:
- errors.append((self.ERROR_COVERAGE_SUSPICIOUS, drcov_data.filepath))
- lmsg("Badly mapped coverage %s" % drcov_data.filepath)
+ #
+ # TODO
+ #
- # warn when loaded coverage (for this module) appears to be empty
- if not len(coverage.nodes):
- errors.append((self.ERROR_COVERAGE_ABSENT, drcov_data.filepath))
- lmsg("No relevant coverage data in %s" % drcov_data.filepath)
+ try:
+ coverage_file = self.reader.open(filepath)
+
+ # log if an error occurs when opening or parsing coverage file
+ except Exception as e:
+ error_msg = "Failed to parse coverage file '%s'" % filepath
+ logger.debug(error_msg)
+ logger.debug(traceback.format_exc(e))
+
+ # raise error
+ if not suppress_errors:
+ raise ValueError(error_msg)
+
+ # return as a suppressed error
+ errors.append((self.ERROR_COVERAGE_MALFORMED, error_msg, filepath))
+ return (None, errors)
#
- # resume the director's aggregation service, triggering an update to
- # recompute the aggregate with the newly loaded coverage
+ # attempt to extract coverage data from the coverage file that is
+ # relevant to this database. if successful, coverage_addresses should
+ # contain a list of addresses rebased to this database.
#
- disassembler.replace_wait_box("Recomputing coverage aggregate...")
- self.resume_aggregation()
+ coverage_addresses = self._extract_coverage_data(coverage_file)
+ if not coverage_addresses:
+ error_msg = "Failed to extract data from coverage file '%s'" % filepath
+ logger.debug(error_msg)
+
+ # raise error
+ if not suppress_errors:
+ raise ValueError(error_msg)
+
+ # return as a suppressed error
+ errors.append((self.ERROR_COVERAGE_ABSENT, error_msg, filepath))
+ return (None, errors)
+
+ #
+ # TODO: normalize coverage data to metadata format
+ #
+
+ coverage_data = self._optimize_coverage_data(coverage_addresses)
+
+ #
+ # before injecting the new coverage data (now a list of instruction
+ # addresses), we check to see if there is an existing coverage
+ # object under the same name.
+ #
+ # if there is an existing coverage mapping, odds are that the user
+ # is probably re-loading the same coverage file in which case we
+ # simply overwrite the old DatabaseCoverage object.
+ #
+ # but we have to be careful for the case where the user loads a
+ # coverage file from a different directory, but under the same name
+ #
+ # e.g:
+ # - C:\coverage\foo.log
+ # - C:\coverage\testing\foo.log
+ #
+ # in these cases, we will append a suffix to the new coverage file
+ #
+
+ coverage_name = os.path.basename(coverage_file.filepath)
+ coverage = self.get_coverage(coverage_name)
+
+ # assign a suffix to the coverage name in the event of a collision
+ if coverage and coverage.filepath != coverage_file.filepath:
+ for i in xrange(2, 100000):
+ new_name = "%s_%u" % (coverage_name, i)
+ if not self.get_coverage(new_name):
+ break
+ coverage_name = new_name
+
+ #
+ # finally, we can ask the director to create a coverage mapping
+ # from the data we have pre-processed for it
+ #
+
+ coverage = self.create_coverage(
+ coverage_name,
+ coverage_data,
+ coverage_file.filepath
+ )
# done
- return (created_coverage, errors)
+ return (coverage, errors)
+
+ def _optimize_coverage_data(self, coverage_addresses):
+ """
+ Internal routine to optimize raw coverage data to the current metadata.
+ """
+ logger.debug("Optimizing coverage data...")
+ addresses = set(coverage_addresses)
+
+ # bucketize coverage addresses
+ instructions = addresses & set(self.metadata.instructions)
+ basic_blocks = instructions & self.metadata.nodes.viewkeys()
+ unknown = addresses - instructions
+
+ # bucketize the uncategorized 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)
+
+ #
+ # TODO: what if there are no defined instructions?
+ # TODO: display undefined/misaligned data somehow
+ #
+
+ if not instructions:
+ logger.debug("No mappable instruction addresses in coverage data")
+ return None
+
+ #
+ # compute the number of basic blocks to TODO
+ #
+
+ 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 has been flattened already.
+ #
+
+ if block_ratio < block_trace_confidence:
+ logger.debug("Optimized as instruction trace...")
+ return list(instructions)
+
+ #
+ # take each basic block address, and expand it into a list of
+ # presumably executed instructions
+ #
+
+ block_instructions = []
+ for address in basic_blocks:
+ block_instructions.extend(list(self.metadata.nodes[address].instructions))
+
+ # DONE
+ logger.debug("Optimized as basic block trace...")
+ return list(block_instructions | instructions)
def _find_fuzzy_name(self, drcov_data, target_name):
"""
@@ -461,49 +581,59 @@ def _find_fuzzy_name(self, drcov_data, target_name):
return None
- def _normalize_drcov_data(self, drcov_data):
+ def _extract_coverage_data(self, coverage_file):
"""
- Extract and normalize relevant coverage data from a DrcovData object.
-
- Returns a list of executed instruction addresses for this database.
+ Internal routine to extract relevant coverage data from a CoverageFile.
"""
- # TODO all this is real rough draft right now
+ # more code-friendly, readable aliases
+ target_name = self.metadata.filename
+ imagebase = self.metadata.imagebase
- # extract the coverage relevant to this database (well, the root binary)
- root_filename = self.metadata.filename
- module_name = self._find_fuzzy_name(drcov_data, root_filename) # TODO
-
- try:
- coverage_blocks = drcov_data.get_blocks(module_name)
-
- # rebase the coverage log's basic blocks to the database imagebase
- imagebase = self.metadata.imagebase
- rebased_blocks = rebase_blocks(imagebase, coverage_blocks)
+ #
+ # inspect the coverage file and extract the module name that seems
+ # to match the executable loaded by the disassembler (fuzzy lookup)
+ #
- # coalesce the blocks into larger contiguous blobs
- condensed_blocks = coalesce_blocks(rebased_blocks)
+ module_name = self._find_fuzzy_name(coverage_file, target_name)
- # flatten the blobs into individual instruction addresses
- return self.metadata.flatten_blocks(condensed_blocks)
+ #
+ # (module, offset, size) style logs (eg, drcov)
+ #
+ try:
+ coverage_blocks = coverage_file.get_blocks(module_name)
+ coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
+ return coverage_addresses
except NotImplementedError:
pass
+ #
+ # (module, offset) style logs (eg, mod+off)
+ #
+
try:
- coverage_offsets = drcov_data.get_offsets(module_name)
- coverage_addresses = map(lambda x: self.metadata.imagebase+x, coverage_offsets)
- confidence = self.metadata.measure_block_confidence(coverage_addresses)
+ coverage_offsets = coverage_file.get_offsets(module_name)
+ coverage_addresses = map(lambda x: imagebase+x, coverage_offsets)
+ return coverage_addresses
+ except NotImplementedError:
+ pass
- #print "Block confidence: %f" % confidence
- if confidence > 0.90:
- return self.metadata.flatten_block_heads(coverage_addresses) # bb trace
- else:
- return coverage_addresses # inst trace
+ #
+ # (absolute address) style log (eg, instruction or bb trace)
+ #
+ # TODO: identify and remove irrelevant data from absolute traces
+ #
+ try:
+ coverage_addresses = coverage_file.get_addresses(module_name)
+ return coverage_addresses
except NotImplementedError:
pass
+ # well, this one is probably your fault
+ raise NotImplementedError("Incomplete CoverageFile implementation")
+
def aggregate_drcov_batch(self, drcov_list):
"""
Aggregate a given list of DrcovData into a single coverage mapping.
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 7116ab09..2e0a879d 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -241,44 +241,6 @@ def get_closest_function(self, address):
else:
return self.functions[before]
- def measure_block_confidence(self, addresses):
- """
- TODO
- """
- if not addresses:
- return 0
- good = 0
- for address in addresses:
- if address in self.nodes:
- good += 1
- return float(good)/len(addresses)
-
- def flatten_block_heads(self, addresses):
- """
- TODO this will probably get deleted
- """
- output = []
- for address in addresses:
- block = self.nodes.get(address, None)
- if not block:
- continue # lol
- output.extend(block.instructions)
- return output
-
- def flatten_blocks(self, basic_blocks):
- """
- Flatten a list of basic blocks (address, size) to instruction addresses.
-
- This function provides a way to convert a list of (address, size) basic
- block entries into a list of individual instruction (or byte) addresses
- based on the current metadata.
- """
- output = []
- for address, size in basic_blocks:
- instructions = self.get_instructions_slice(address, address+size)
- output.extend(instructions)
- return output
-
def is_big(self):
"""
Return a bool indicating whether we think the database is 'big'.
diff --git a/plugin/lighthouse/reader/parsers/trace.py b/plugin/lighthouse/reader/parsers/trace.py
new file mode 100644
index 00000000..95009eea
--- /dev/null
+++ b/plugin/lighthouse/reader/parsers/trace.py
@@ -0,0 +1,34 @@
+import collections
+from ..coverage_file import CoverageFile
+
+class TraceData(CoverageFile):
+ """
+ An instruction (or basic block) address trace log parser.
+ """
+
+ def __init__(self, filepath):
+ self._hitmap = {}
+ super(TraceData, self).__init__(filepath)
+
+ #--------------------------------------------------------------------------
+ # Public
+ #--------------------------------------------------------------------------
+
+ def get_addresses(self, module_name=None):
+ if module_name:
+ raise ValueError("No module mapping in this log format")
+ return self._hitmap.keys()
+
+ #--------------------------------------------------------------------------
+ # Parsing Routines - Top Level
+ #--------------------------------------------------------------------------
+
+ def _parse(self):
+ """
+ Parse absolute address coverage from the given log file.
+ """
+ hitmap = collections.defaultdict(int)
+ with open(self.filepath) as f:
+ for line in f:
+ hitmap[int(line, 16)] += 1
+ self._hitmap = hitmap
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index edc560c9..5766b6c3 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -158,66 +158,6 @@ def notify_callback(callback_list, *args):
# Coverage Util
#------------------------------------------------------------------------------
-def coalesce_blocks(blocks):
- """
- Coalesce a list of (address, size) blocks.
-
- eg:
- blocks = [
- (4100, 10),
- (4200, 100),
- (4300, 10),
- (4310, 20),
- (4400, 10),
- ]
-
- returns:
- coalesced = [(4100, 10), (4200, 130), (4400, 10)]
-
- """
-
- # nothing to do
- if not blocks:
- return []
- elif len(blocks) == 1:
- return blocks
-
- # before we can operate on the blocks, we must ensure they are sorted
- blocks = sorted(blocks)
-
- #
- # coalesce the list of given blocks
- #
-
- coalesced = [blocks.pop(0)]
- while blocks:
-
- block_start, block_size = blocks.pop(0)
-
- #
- # compute the end address of the current coalescing block. if the
- # blocks do not overlap, create a new block to start coalescing from
- #
-
- if sum(coalesced[-1]) < block_start:
- coalesced.append((block_start, block_size))
- continue
-
- #
- # the blocks overlap, so update the current coalescing block
- #
-
- coalesced[-1] = (coalesced[-1][0], (block_start+block_size) - coalesced[-1][0])
-
- # return the list of coalesced blocks
- return coalesced
-
-def rebase_blocks(base, basic_blocks):
- """
- Rebase a list of basic block offsets (offset, size) to the given imagebase.
- """
- return list(map(lambda x: (base + x[0], x[1]), basic_blocks))
-
def build_hitmap(data):
"""
Build a hitmap from the given list of address.
From e4ea2956e8266e5b23b7f95e5817ab6ec4a6a5f3 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 21 Mar 2019 12:41:23 -0400
Subject: [PATCH 015/154] improve batch loading, refactor loading in general
---
plugin/lighthouse/core.py | 45 ++-
plugin/lighthouse/director.py | 341 +++++++++-----------
plugin/lighthouse/exceptions.py | 21 ++
plugin/lighthouse/reader/coverage_reader.py | 24 +-
4 files changed, 208 insertions(+), 223 deletions(-)
create mode 100644 plugin/lighthouse/exceptions.py
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index dea1db30..1a2d990c 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -205,21 +205,14 @@ def interactive_load_batch(self):
# can select the coverage files they would like to load from disk
#
- filenames = self._select_coverage_files()
-
- #
- # load the selected coverage files from disk (if any), returning a list
- # of loaded DrcovData objects (which contain coverage data)
- #
-
- drcov_list = load_coverage_files(filenames)
- if not drcov_list:
+ filepaths = self._select_coverage_files()
+ if not filepaths:
self.director.metadata.abort_refresh()
return
# prompt the user to name the new coverage aggregate
default_name = "BATCH_%s" % self.director.peek_shorthand()
- ok, coverage_name = prompt_string(
+ ok, batch_name = prompt_string(
"Batch Name:",
"Please enter a name for this coverage",
default_name
@@ -230,8 +223,8 @@ def interactive_load_batch(self):
# abort the loading process...
#
- if not (ok and coverage_name):
- lmsg("User failed to enter a name for the loaded batch...")
+ if not (ok and batch_name):
+ lmsg("User failed to enter a name for the batch coverage...")
self.director.metadata.abort_refresh()
return
@@ -251,23 +244,20 @@ def interactive_load_batch(self):
# to normalize and condense (aggregate) all the coverage data
#
- new_coverage, errors = self.director.aggregate_drcov_batch(drcov_list)
-
- #
- # finally, we can inject the aggregated coverage data into the
- # director under the user specified batch name
- #
-
- disassembler.replace_wait_box("Mapping coverage...")
- self.director.create_coverage(coverage_name, new_coverage.data)
+ disassembler.replace_wait_box("Loading coverage from disk...")
+ batch_coverage, errors = self.director.load_coverage_batch(
+ filepaths,
+ batch_name,
+ disassembler.replace_wait_box
+ )
# select the newly created batch coverage
disassembler.replace_wait_box("Selecting coverage...")
- self.director.select_coverage(coverage_name)
+ self.director.select_coverage(batch_name)
# all done! pop the coverage overview to show the user their results
disassembler.hide_wait_box()
- lmsg("Successfully loaded batch %s..." % coverage_name)
+ lmsg("Successfully loaded batch %s..." % batch_name)
self.open_coverage_overview()
# finally, emit any notable issues that occurred during load
@@ -304,15 +294,16 @@ def interactive_load_file(self):
# a progress dialog depicts the work remaining in the refresh
#
- disassembler.replace_wait_box("Building database metadata...")
+ disassembler.show_wait_box("Building database metadata...")
await_future(future)
#
# insert the loaded drcov data objects into the director
- # TODO
+ # TODO/COMMENT
+ #
- disassembler.show_wait_box("Loading coverage from disk...")
- created_coverage, errors = self.director.load_coverage_files(filenames)
+ disassembler.replace_wait_box("Loading coverage from disk...")
+ created_coverage, errors = self.director.load_coverage_files(filenames, disassembler.replace_wait_box)
#
# if the director failed to map any coverage, the user probably
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 82838996..0d4e95ee 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -9,9 +9,11 @@
from lighthouse.util.python import *
from lighthouse.util.qt import await_future, await_lock, color_text
from lighthouse.util.disassembler import disassembler
+
from lighthouse.reader import CoverageReader
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.coverage import DatabaseCoverage
+from lighthouse.exceptions import CoverageParseError
from lighthouse.composer.parser import *
logger = logging.getLogger("Lighthouse.Director")
@@ -337,161 +339,161 @@ def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
# Coverage Loading
#----------------------------------------------------------------------
+ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.debug):
+ """
+ Create a new database coverage mapping from a list of coverage files.
+
+ Returns a tuple of (coverage, errors)
+ """
+ errors = []
+ aggregate_addresses = set()
+
+ for i, filepath in enumerate(filepaths, 1):
+ logger.debug("-"*50)
+ progress_callback("Aggregating batch data %u/%u" % (i, len(filepaths)))
+
+ # attempt to load coverage data from disk
+ try:
+ coverage_file = self.reader.open(filepath)
+ coverage_addresses = self._extract_coverage_data(coverage_file)
+
+ # save and suppress warnings generated from loading coverage files
+ except (CoverageParseError, CoverageExtractionError) as e:
+ errors.append(e)
+ continue
+
+ # aggregate all coverage data into a single set of addresses
+ aggregate_addresses.update(coverage_addresses)
+
+ # 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)
+
+ # return the created coverage name
+ return (coverage, errors)
+
def load_coverage_file(self, filepath):
"""
Create a new database coverage mapping from a coverage file.
Returns the created coverage object.
"""
- coverage, _ = self._load_coverage_internal(filepath, False)
- return coverage
+ raise NotImplementedError("TODO/HEADLESS")
- def load_coverage_files(self, filepaths, progress_callback=None):
+ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
"""
Create new database coverage mappings from a list of coverage files.
- Returns a tuple of (created_coverage, all_errors)
+ Returns a tuple of (created_coverage, errors)
"""
- created_coverage = []
- all_errors = []
+ errors = []
+ all_coverage = []
#
- # stop the director's aggregate from updating. this will prevent the
- # aggregate from recomputing after each individual mapping is created.
- # instead, we will wait till *all* have been created, computing the
- # new aggregate at the very end. this is far more performant.
+ # stop the director's aggregate set from recomputing after each new
+ # coverage mapping is created. instead, we want to wait till *all* new
+ # files have been loaded and mapped, computing the new aggregate only
+ # at very end. this is far more performant.
#
self.suspend_aggregation()
#
- # loop through the coverage data we been given (drcov_list), and begin
- # the normalization process to translate / filter / flatten its blocks
- # into a generic format the director can consume (a list of addresses)
+ # loop through the list of filepaths we have been given and begin
+ # the process of loading the coverage data from disk, and normalizing
+ # it for the director to consume
#
for i, filepath in enumerate(filepaths, 1):
+ logger.debug("-"*50)
+ progress_callback("Loading coverage %u/%u" % (i, len(filepaths)))
+
+ # attempt to load coverage data from disk
+ 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 (CoverageParseError, CoverageExtractionError) as e:
+ errors.append(e)
+ continue
- # keep the user informed about our progress while loading coverage
- if progress_callback:
- progress_callback("Loading coverage %u/%u" % (i, len(filepaths)))
+ #
+ # request a name for the new coverage mapping that the director will
+ # generate from the loaded coverage data
+ #
- # load a single coverage file
- coverage, errors = self._load_coverage_internal(filepath, True)
- if coverage:
- created_coverage.append(coverage)
+ coverage_name = self._suggest_coverage_name(filepath)
+ coverage = self.create_coverage(coverage_name, coverage_data, filepath)
- # save any errors that were generated (suppressed)
- all_errors.extend(errors)
+ # add the newly created coverage to the list of coverage to be returned
+ all_coverage.append(coverage)
#
# resume the director's aggregation service, triggering an update to
- # recompute the aggregate with the newly loaded coverage
+ # recompute the aggregate set with the newly loaded coverage
#
- if progress_callback:
- progress_callback("Recomputing coverage aggregate...")
-
+ progress_callback("Recomputing coverage aggregate...")
self.resume_aggregation()
- # done
- return (created_coverage, all_errors)
+ # all done
+ return (all_coverage, errors)
- def _load_coverage_internal(self, filepath, suppress_errors):
+ def _extract_coverage_data(self, coverage_file):
"""
- Internal routine used to load coverage from disk.
+ Internal routine to extract relevant coverage data from a CoverageFile.
"""
- errors = []
+
+ # more code-friendly, readable aliases
+ target_name = self.metadata.filename
+ imagebase = self.metadata.imagebase
#
- # TODO
+ # inspect the coverage file and extract the module name that seems
+ # to match the executable loaded by the disassembler (fuzzy lookup)
#
- try:
- coverage_file = self.reader.open(filepath)
-
- # log if an error occurs when opening or parsing coverage file
- except Exception as e:
- error_msg = "Failed to parse coverage file '%s'" % filepath
- logger.debug(error_msg)
- logger.debug(traceback.format_exc(e))
-
- # raise error
- if not suppress_errors:
- raise ValueError(error_msg)
-
- # return as a suppressed error
- errors.append((self.ERROR_COVERAGE_MALFORMED, error_msg, filepath))
- return (None, errors)
+ module_name = self._find_fuzzy_name(coverage_file, target_name)
#
- # attempt to extract coverage data from the coverage file that is
- # relevant to this database. if successful, coverage_addresses should
- # contain a list of addresses rebased to this database.
+ # (module, offset, size) style logs (eg, drcov)
#
- coverage_addresses = self._extract_coverage_data(coverage_file)
- if not coverage_addresses:
- error_msg = "Failed to extract data from coverage file '%s'" % filepath
- logger.debug(error_msg)
-
- # raise error
- if not suppress_errors:
- raise ValueError(error_msg)
-
- # return as a suppressed error
- errors.append((self.ERROR_COVERAGE_ABSENT, error_msg, filepath))
- return (None, errors)
+ try:
+ coverage_blocks = coverage_file.get_blocks(module_name)
+ coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
+ return coverage_addresses
+ except NotImplementedError:
+ pass
#
- # TODO: normalize coverage data to metadata format
+ # (module, offset) style logs (eg, mod+off)
#
- coverage_data = self._optimize_coverage_data(coverage_addresses)
+ try:
+ coverage_offsets = coverage_file.get_offsets(module_name)
+ coverage_addresses = map(lambda x: imagebase+x, coverage_offsets)
+ return coverage_addresses
+ except NotImplementedError:
+ pass
#
- # before injecting the new coverage data (now a list of instruction
- # addresses), we check to see if there is an existing coverage
- # object under the same name.
- #
- # if there is an existing coverage mapping, odds are that the user
- # is probably re-loading the same coverage file in which case we
- # simply overwrite the old DatabaseCoverage object.
- #
- # but we have to be careful for the case where the user loads a
- # coverage file from a different directory, but under the same name
- #
- # e.g:
- # - C:\coverage\foo.log
- # - C:\coverage\testing\foo.log
+ # (absolute address) style log (eg, instruction or bb trace)
#
- # in these cases, we will append a suffix to the new coverage file
+ # TODO: identify and remove irrelevant data from absolute traces
#
- coverage_name = os.path.basename(coverage_file.filepath)
- coverage = self.get_coverage(coverage_name)
-
- # assign a suffix to the coverage name in the event of a collision
- if coverage and coverage.filepath != coverage_file.filepath:
- for i in xrange(2, 100000):
- new_name = "%s_%u" % (coverage_name, i)
- if not self.get_coverage(new_name):
- break
- coverage_name = new_name
-
- #
- # finally, we can ask the director to create a coverage mapping
- # from the data we have pre-processed for it
- #
+ try:
+ coverage_addresses = coverage_file.get_addresses(module_name)
+ return coverage_addresses
+ except NotImplementedError:
+ pass
- coverage = self.create_coverage(
- coverage_name,
- coverage_data,
- coverage_file.filepath
- )
+ # well, this one is probably your fault
+ raise NotImplementedError("Incomplete CoverageFile implementation")
- # done
- return (coverage, errors)
def _optimize_coverage_data(self, coverage_addresses):
"""
@@ -527,7 +529,7 @@ def _optimize_coverage_data(self, coverage_addresses):
return None
#
- # compute the number of basic blocks to TODO
+ # TODO/COMMENT
#
block_ratio = len(basic_blocks) / float(len(instructions))
@@ -556,116 +558,71 @@ def _optimize_coverage_data(self, coverage_addresses):
logger.debug("Optimized as basic block trace...")
return list(block_instructions | instructions)
- def _find_fuzzy_name(self, drcov_data, target_name):
+ def _suggest_coverage_name(self, filepath):
"""
- TODO
- """
-
- # attempt lookup using case-insensitive filename
- for module_name in drcov_data.modules:
- if module_name.lower() in target_name.lower():
- return module_name
-
- #
- # no hits yet... let's cleave the extension from the given module
- # name (if present) and try again
- #
-
- if "." in target_name:
- target_name = target_name.split(".")[0]
-
- # attempt lookup using case-insensitive filename without extension
- for module_name in drcov_data.modules:
- if module_name.lower() in target_name.lower():
- return module_name
-
- return None
-
- def _extract_coverage_data(self, coverage_file):
- """
- Internal routine to extract relevant coverage data from a CoverageFile.
+ Return a suggested coverage name for the given filepath.
"""
+ coverage_name = os.path.basename(filepath)
+ coverage = self.get_coverage(coverage_name)
- # more code-friendly, readable aliases
- target_name = self.metadata.filename
- imagebase = self.metadata.imagebase
+ # no internal conflict, the filename is a unique enough coverage name
+ if not coverage:
+ return coverage_name
#
- # inspect the coverage file and extract the module name that seems
- # to match the executable loaded by the disassembler (fuzzy lookup)
+ # if there is an existing coverage mapping under this name, odds are
+ # that the user is re-loading the same coverage file in which case the
+ # director will overwrite the old DatabaseCoverage object.
#
-
- module_name = self._find_fuzzy_name(coverage_file, target_name)
-
+ # however, we have to be careful for the case where the user loads a
+ # coverage file from a different directory under the same name
#
- # (module, offset, size) style logs (eg, drcov)
- #
-
- try:
- coverage_blocks = coverage_file.get_blocks(module_name)
- coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
- return coverage_addresses
- except NotImplementedError:
- pass
-
+ # e.g:
+ # - C:\coverage\foo.log
+ # - C:\coverage\testing\foo.log
#
- # (module, offset) style logs (eg, mod+off)
+ # in these cases, we will append a suffix to the new coverage file
#
- try:
- coverage_offsets = coverage_file.get_offsets(module_name)
- coverage_addresses = map(lambda x: imagebase+x, coverage_offsets)
- return coverage_addresses
- except NotImplementedError:
- pass
+ # assign a suffix to the coverage_name in the event of a collision
+ if coverage.filepath != filepath:
- #
- # (absolute address) style log (eg, instruction or bb trace)
- #
- # TODO: identify and remove irrelevant data from absolute traces
- #
+ # find a suitable suffix
+ for i in xrange(2, 1000000):
+ new_name = "%s_%u" % (coverage_name, i)
+ if not self.get_coverage(new_name):
+ break
- try:
- coverage_addresses = coverage_file.get_addresses(module_name)
- return coverage_addresses
- except NotImplementedError:
- pass
+ # save the suffixed name to the return value
+ coverage_name = new_name
- # well, this one is probably your fault
- raise NotImplementedError("Incomplete CoverageFile implementation")
+ # return the suggested coverage name for the given filepath
+ return coverage_name
- def aggregate_drcov_batch(self, drcov_list):
+ def _find_fuzzy_name(self, coverage_file, target_name):
"""
- Aggregate a given list of DrcovData into a single coverage mapping.
-
- See create_coverage_from_drcov_list(...) for more verbose comments.
+ Return the closest matching module name in the given coverage file.
"""
- errors = []
-
- # create a new coverage set to manually aggregate data into
- coverage = DatabaseCoverage(self._palette)
- for i, drcov_data in enumerate(drcov_list, 1):
+ # attempt lookup using case-insensitive filename
+ for module_name in coverage_file.modules:
+ if module_name.lower() in target_name.lower():
+ return module_name
- # keep the user informed about our progress while aggregating
- disassembler.replace_wait_box(
- "Aggregating batch data %u/%u" % (i, len(drcov_list))
- )
+ #
+ # no hits yet... let's cleave the extension from the given module
+ # name (if present) and try again
+ #
- # normalize coverage data to the open database
- try:
- addresses = self._normalize_drcov_data(drcov_data)
- except Exception as e:
- errors.append((self.ERROR_COVERAGE_ABSENT, drcov_data.filepath))
- lmsg("Failed to normalize coverage %s" % drcov_data.filepath)
- lmsg("- %s" % e)
- continue
+ if "." in target_name:
+ target_name = target_name.split(".")[0]
- # aggregate the addresses into the output coverage mapping
- coverage.add_addresses(addresses, False)
+ # attempt lookup using case-insensitive filename without extension
+ for module_name in coverage_file.modules:
+ if module_name.lower() in target_name.lower():
+ return module_name
- # return the created coverage name
- return (coverage, errors)
+ return None
#----------------------------------------------------------------------
# Coverage Management
diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py
new file mode 100644
index 00000000..7f1e2894
--- /dev/null
+++ b/plugin/lighthouse/exceptions.py
@@ -0,0 +1,21 @@
+class LighthouseError(Exception):
+ """
+ An error generated by Lighthouse.
+ """
+ def __init__(self, *args, **kwargs):
+ super(LighthouseError, self).__init__(*args, **kwargs)
+
+class CoverageParseError(LighthouseError):
+ """
+ An error generated by the CoverageReader.
+ """
+ def __init__(self, filepath, tracebacks):
+ super(CoverageParseError, self).__init__("Failed to parse coverage file")
+ self.filepath = filepath
+ self.tracebacks = tracebacks
+
+class CoverageExtractionError(LighthouseError):
+ """
+ An error generated when extracting data from a coverage file fails.
+ """
+ pass
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index ef1b74ad..00e63762 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -6,6 +6,7 @@
from lighthouse.util.python import iteritems
from .coverage_file import CoverageFile
+from lighthouse.exceptions import CoverageParseError
logger = logging.getLogger("Lighthouse.Reader")
@@ -22,22 +23,37 @@ def __init__(self):
def open(self, filepath):
"""
- TODO
+ Open and parse a coverage file from disk.
+
+ Returns a CoverageFile on success, or raises CoverageParseError on failure.
"""
coverage_file = None
+ parse_failures = {}
+ # attempt to parse the given coverage file with each available parser
for name, parser in iteritems(self._installed_parsers):
logger.debug("Attempting parse with '%s'" % name)
+
+ # attempt to open/parse the coverage file with the given parser
try:
coverage_file = parser(filepath)
break
+
+ # log the exceptions for each parse failure
except Exception as e:
- logger.debug("Parse failed...\n" + traceback.format_exc(e))
+ parse_failures[name] = traceback.format_exc(e)
+ logger.debug("| Parse FAILED")
+
+ #
+ # if *all* the coverage file parsers failed, raise an exception with
+ # information for each failure (for debugging)
+ #
if not coverage_file:
- raise ValueError("No compatible coverage parser for %s" % filepath)
+ raise CoverageParseError(filepath, parse_failures)
- logger.debug("Parsed OKAY!")
+ # successful parse
+ logger.debug("| Parse OKAY")
return coverage_file
def _import_parsers(self):
From ffc45f86a833f2a13a905bbe49bec0549dba3684 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 30 Mar 2019 14:06:28 -0400
Subject: [PATCH 016/154] further refactor some loading code
---
plugin/lighthouse/core.py | 122 ++----------------
plugin/lighthouse/director.py | 81 +++++++-----
plugin/lighthouse/exceptions.py | 135 +++++++++++++++++++-
plugin/lighthouse/reader/coverage_file.py | 2 +-
plugin/lighthouse/reader/coverage_reader.py | 6 +-
plugin/lighthouse/reader/parsers/drcov.py | 9 +-
plugin/lighthouse/reader/parsers/modoff.py | 2 +-
plugin/lighthouse/reader/parsers/trace.py | 2 -
8 files changed, 193 insertions(+), 166 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 1a2d990c..06b1f863 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -12,6 +12,7 @@
from lighthouse.director import CoverageDirector
from lighthouse.coverage import DatabaseCoverage
from lighthouse.metadata import DatabaseMetadata, metadata_progress
+from lighthouse.exceptions import *
logger = logging.getLogger("Lighthouse.Core")
@@ -251,6 +252,13 @@ def interactive_load_batch(self):
disassembler.replace_wait_box
)
+ # if batch creation fails...
+ if not batch_coverage:
+ lmsg("Creation of batch '%s' failed..." % batch_name)
+ disassembler.hide_wait_box()
+ warn_errors(errors)
+ return
+
# select the newly created batch coverage
disassembler.replace_wait_box("Selecting coverage...")
self.director.select_coverage(batch_name)
@@ -332,10 +340,6 @@ def interactive_load_file(self):
# finally, emit any notable issues that occurred during load
warn_errors(errors)
- #--------------------------------------------------------------------------
- # Internal
- #--------------------------------------------------------------------------
-
def _select_coverage_files(self):
"""
Prompt a file selection dialog, returning file selections.
@@ -372,113 +376,3 @@ def _select_coverage_files(self):
# return the captured filenames
return filenames
-
-#------------------------------------------------------------------------------
-# Util
-#------------------------------------------------------------------------------
-
-def load_coverage_files(filenames):
- """
- Load multiple code coverage files from disk.
- """
- loaded_coverage = []
- coverage_reader = CoverageReader()
-
- #
- # loop through each of the given filenames and attempt to load/parse
- # their coverage data from disk
- #
-
- load_failure = False
- for filename in filenames:
-
- # attempt to load/parse a single coverage data file from disk
- try:
- drcov_data = coverage_reader.open(filename)
-
- # catch all for parse errors / bad input / malformed files
- except Exception as e:
- lmsg("Failed to load coverage %s" % filename)
- lmsg(" - Error: %s" % str(e))
- logger.exception(" - Traceback:")
- load_failure = True
- continue
-
- # save the loaded coverage data to the output list
- loaded_coverage.append(drcov_data)
-
- # warn if we encountered malformed files...
- if load_failure:
- warn_drcov_malformed()
-
- # return all the successfully loaded coverage files
- return loaded_coverage
-
-#------------------------------------------------------------------------------
-# Warnings
-#------------------------------------------------------------------------------
-
-def warn_errors(errors):
- """
- Warn the user of any encountered errors with a messagebox.
- """
- seen = []
-
- for error in errors:
- error_type = error[0]
-
- # only emit an error once
- if error_type in seen:
- continue
-
- # emit relevant error messages
- if error_type == CoverageDirector.ERROR_COVERAGE_ABSENT:
- warn_module_missing()
- elif error_type == CoverageDirector.ERROR_COVERAGE_SUSPICIOUS:
- warn_bad_mapping()
- else:
- raise NotImplementedError("UNKNOWN ERROR OCCURRED")
-
- seen.append(error_type)
-
-def warn_drcov_malformed():
- """
- Display a warning for malformed/unreadable coverage files.
- """
- disassembler.warning(
- "Failed to parse one or more of the selected coverage files!\n\n"
- " Possible reasons:\n"
- " - You selected a file that was *not* a coverage file.\n"
- " - The selected coverage file is malformed or unreadable.\n\n"
- "Please see the disassembler console for more info..."
- )
-
-def warn_module_missing():
- """
- Display a warning for missing coverage data.
- """
- disassembler.warning(
- "No coverage data was extracted from one of the selected files.\n\n"
- " Possible reasons:\n"
- " - You selected a coverage file for the wrong binary.\n"
- " - The name of the executable file used to generate this database\n"
- " is different than the one you collected coverage against.\n"
- " - Your DBI failed to collect any coverage for this binary.\n\n"
- "Please see the disassembler console for more info..."
- )
-
-def warn_bad_mapping():
- """
- Display a warning for badly mapped coverage data.
- """
- disassembler.warning(
- "One or more of the loaded coverage files appears to be badly mapped.\n\n"
- " Possible reasons:\n"
- " - You selected a coverage file that was collected against a\n"
- " slightly different version of the binary.\n"
- " - You recorded an application with very abnormal control flow.\n"
- " - The coverage file might be malformed.\n\n"
- "This means that any coverage displayed by Lighthouse is probably\n"
- "wrong, and should be used at your own discretion."
- )
-
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 0d4e95ee..c6c86934 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -13,7 +13,7 @@
from lighthouse.reader import CoverageReader
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.coverage import DatabaseCoverage
-from lighthouse.exceptions import CoverageParseError
+from lighthouse.exceptions import *
from lighthouse.composer.parser import *
logger = logging.getLogger("Lighthouse.Director")
@@ -49,10 +49,6 @@ class CoverageDirector(object):
between any number of coverage files.
"""
- ERROR_COVERAGE_MALFORMED = 1
- ERROR_COVERAGE_ABSENT = 2
- ERROR_COVERAGE_SUSPICIOUS = 3
-
def __init__(self, metadata, palette):
# the database metadata cache
@@ -325,16 +321,6 @@ def suspend_aggregation(self):
"""
self._aggregation_suspended = True
- #----------------------------------------------------------------------
- # Coverage Creation
- #----------------------------------------------------------------------
-
- def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
- """
- Create a new database coverage mapping from the given data.
- """
- return self.update_coverage(coverage_name, coverage_data, coverage_filepath)
-
#----------------------------------------------------------------------
# Coverage Loading
#----------------------------------------------------------------------
@@ -358,28 +344,34 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
coverage_addresses = self._extract_coverage_data(coverage_file)
# save and suppress warnings generated from loading coverage files
- except (CoverageParseError, CoverageExtractionError) as e:
+ except CoverageParsingError as e:
errors.append(e)
continue
+ # ensure some data was actually extracted from the log
+ if not coverage_addresses:
+ errors.append(CoverageMissingError(filepath))
+ continue
+
# aggregate all coverage data into a single set of addresses
aggregate_addresses.update(coverage_addresses)
+ 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)
+ # evaluate coverage
+ if not coverage.nodes:
+ errors.append(CoverageMappingAbsent(coverage))
+ elif coverage.suspicious:
+ errors.append(CoverageMappingSuspicious(coverage))
+
# return the created coverage name
return (coverage, errors)
- def load_coverage_file(self, filepath):
- """
- Create a new database coverage mapping from a coverage file.
-
- Returns the created coverage object.
- """
- raise NotImplementedError("TODO/HEADLESS")
-
def load_coverage_files(self, filepaths, progress_callback=logger.debug):
"""
Create new database coverage mappings from a list of coverage files.
@@ -415,10 +407,15 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
coverage_data = self._optimize_coverage_data(coverage_addresses)
# save and suppress warnings generated from loading coverage files
- except (CoverageParseError, CoverageExtractionError) as e:
+ except CoverageParsingError as e:
errors.append(e)
continue
+ # ensure some data was actually extracted from the log
+ if not coverage_addresses:
+ errors.append(CoverageMissingError(filepath))
+ continue
+
#
# request a name for the new coverage mapping that the director will
# generate from the loaded coverage data
@@ -427,6 +424,12 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
coverage_name = self._suggest_coverage_name(filepath)
coverage = self.create_coverage(coverage_name, coverage_data, filepath)
+ # evaluate coverage
+ if not coverage.nodes:
+ errors.append(CoverageMappingAbsent(coverage))
+ elif coverage.suspicious:
+ errors.append(CoverageMappingSuspicious(coverage))
+
# add the newly created coverage to the list of coverage to be returned
all_coverage.append(coverage)
@@ -445,9 +448,6 @@ def _extract_coverage_data(self, coverage_file):
"""
Internal routine to extract relevant coverage data from a CoverageFile.
"""
-
- # more code-friendly, readable aliases
- target_name = self.metadata.filename
imagebase = self.metadata.imagebase
#
@@ -455,14 +455,22 @@ def _extract_coverage_data(self, coverage_file):
# to match the executable loaded by the disassembler (fuzzy lookup)
#
- module_name = self._find_fuzzy_name(coverage_file, target_name)
+ module_name = self._find_fuzzy_name(coverage_file, self.metadata.filename)
+
+ #
+ # TODO/BAILOUT
+ #
+
+ if not module_name and coverage_file.modules:
+ logger.debug("TODO/BAILOUT DIALOG")
+ return []
#
# (module, offset, size) style logs (eg, drcov)
#
try:
- coverage_blocks = coverage_file.get_blocks(module_name)
+ coverage_blocks = coverage_file.get_offset_blocks(module_name)
coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
return coverage_addresses
except NotImplementedError:
@@ -480,9 +488,7 @@ def _extract_coverage_data(self, coverage_file):
pass
#
- # (absolute address) style log (eg, instruction or bb trace)
- #
- # TODO: identify and remove irrelevant data from absolute traces
+ # (absolute address) style log (eg, instruction/bb trace)
#
try:
@@ -491,10 +497,9 @@ def _extract_coverage_data(self, coverage_file):
except NotImplementedError:
pass
- # well, this one is probably your fault
+ # well, this one is probably the fault of the CoverageFile author...
raise NotImplementedError("Incomplete CoverageFile implementation")
-
def _optimize_coverage_data(self, coverage_addresses):
"""
Internal routine to optimize raw coverage data to the current metadata.
@@ -628,6 +633,12 @@ def _find_fuzzy_name(self, coverage_file, target_name):
# Coverage Management
#----------------------------------------------------------------------
+ def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
+ """
+ Create a new database coverage mapping from the given data.
+ """
+ return self.update_coverage(coverage_name, coverage_data, coverage_filepath)
+
def select_coverage(self, coverage_name):
"""
Activate a loaded coverage mapping by name.
diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py
index 7f1e2894..6aadd791 100644
--- a/plugin/lighthouse/exceptions.py
+++ b/plugin/lighthouse/exceptions.py
@@ -1,3 +1,10 @@
+from lighthouse.util.log import lmsg
+from lighthouse.util.disassembler import disassembler
+
+#------------------------------------------------------------------------------
+# Exception Definitions
+#------------------------------------------------------------------------------
+
class LighthouseError(Exception):
"""
An error generated by Lighthouse.
@@ -5,17 +12,133 @@ class LighthouseError(Exception):
def __init__(self, *args, **kwargs):
super(LighthouseError, self).__init__(*args, **kwargs)
-class CoverageParseError(LighthouseError):
+class CoverageParsingError(LighthouseError):
"""
- An error generated by the CoverageReader.
+ An error generated by the CoverageReader when all parsers fail.
"""
def __init__(self, filepath, tracebacks):
- super(CoverageParseError, self).__init__("Failed to parse coverage file")
+ super(CoverageParsingError, self).__init__("Failed to parse coverage file")
self.filepath = filepath
self.tracebacks = tracebacks
-class CoverageExtractionError(LighthouseError):
+ def __str__(self):
+ return self.message + " '%s'" % self.filepath
+
+class CoverageMissingError(LighthouseError):
+ """
+ An error generated when no data was extracted from a CoverageFile.
+ """
+ def __init__(self, filepath):
+ super(CoverageMissingError, self).__init__("No coverage extracted from file")
+ self.filepath = filepath
+
+ def __str__(self):
+ return self.message + " '%s'" % self.filepath
+
+class CoverageMappingSuspicious(LighthouseError):
+ """
+ A warning generated when coverage data does not appear to match the database.
+ """
+ def __init__(self, coverage):
+ super(CoverageMappingSuspicious, self).__init__("Coverage data appears badly mapped")
+ self.coverage = coverage
+
+ def __str__(self):
+ return self.message + " for coverage '%s'" % self.coverage.name
+
+class CoverageMappingAbsent(LighthouseError):
+ """
+ A warning generated when coverage data cannot be mapped.
+ """
+ def __init__(self, coverage):
+ super(CoverageMappingAbsent, self).__init__("No coverage data could be mapped")
+ self.coverage = coverage
+
+ def __str__(self):
+ return self.message + " for coverage '%s'" % self.coverage.name
+
+#------------------------------------------------------------------------------
+# UI Warnings
+#------------------------------------------------------------------------------
+
+def warn_errors(errors):
+ """
+ Warn the user of any encountered errors with a messagebox.
+ """
+ seen = []
+ error_map = \
+ {
+ CoverageParsingError: warn_coverage_parsing,
+ CoverageMissingError: warn_coverage_missing,
+ CoverageMappingAbsent: warn_mapping_absent,
+ CoverageMappingSuspicious: warn_mapping_suspicious,
+ }
+
+ for error in errors:
+ error_type = type(error)
+
+ lmsg(error)
+ if error_type in seen:
+ return
+
+ try:
+ error_map[error_type](error)
+ except KeyError:
+ raise NotImplementedError("UNKNOWN ERROR OCCURRED")
+
+ seen.append(error_type)
+
+def warn_coverage_parsing(error):
+ """
+ Display a warning for malformed/unreadable coverage files.
+ """
+ disassembler.warning(
+ "Failed to parse one or more of the selected coverage files!\n\n"
+ " Possible reasons:\n"
+ " - You selected a file that was *not* a coverage file.\n"
+ " - The selected coverage file is malformed or unreadable.\n"
+ " - A suitable parser for the coverage file is not installed.\n\n"
+ "Please see the disassembler console for more info..."
+ )
+
+def warn_coverage_missing(error):
+ """
+ Display a warning for missing coverage data.
+ """
+ disassembler.warning(
+ "No usable coverage data was extracted from one of the selected files.\n\n"
+ " Possible reasons:\n"
+ " - You selected a coverage file for the wrong binary.\n"
+ " - The name of the executable file used to generate this database\n"
+ " is different than the one you collected coverage against.\n"
+ " - Your DBI failed to collect any coverage for this binary.\n\n"
+ "Please see the disassembler console for more info..."
+ )
+
+def warn_mapping_absent(error):
+ """
+ Display a warning when no coverage data gets mapped.
+ """
+ disassembler.warning(
+ "One or more of the loaded coverage files has no visibly mapped data.\n\n"
+ " Possible reasons:\n"
+ " - The loaded coverage data does not fall within defined functions.\n"
+ " - You loaded an absolute address trace with a different imagebase.\n"
+ " - The coverage file might be corrupt.\n\n"
+ "Please see the disassembler console for more info..."
+ )
+
+def warn_mapping_suspicious(error):
"""
- An error generated when extracting data from a coverage file fails.
+ Display a warning for badly mapped coverage data.
"""
- pass
+ disassembler.warning(
+ "One or more of the loaded coverage files appears to be badly mapped.\n\n"
+ " Possible reasons:\n"
+ " - You selected a coverage file that was collected against a\n"
+ " slightly different version of the binary.\n"
+ " - You recorded an application with very abnormal control flow.\n"
+ " - The coverage file might be corrupt.\n\n"
+ "This means that any coverage displayed by Lighthouse is probably\n"
+ "wrong, and should be used at your own discretion."
+ )
diff --git a/plugin/lighthouse/reader/coverage_file.py b/plugin/lighthouse/reader/coverage_file.py
index 091d62bb..7d441532 100644
--- a/plugin/lighthouse/reader/coverage_file.py
+++ b/plugin/lighthouse/reader/coverage_file.py
@@ -28,7 +28,7 @@ def get_offsets(self, module_name):
"""
raise NotImplementedError("Relative addresses not supported by this log format")
- def get_blocks(self, module_name):
+ def get_offset_blocks(self, module_name):
"""
Return coverage data for the named module in block form (offset, size).
"""
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 00e63762..c949447f 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -6,7 +6,7 @@
from lighthouse.util.python import iteritems
from .coverage_file import CoverageFile
-from lighthouse.exceptions import CoverageParseError
+from lighthouse.exceptions import CoverageParsingError
logger = logging.getLogger("Lighthouse.Reader")
@@ -25,7 +25,7 @@ def open(self, filepath):
"""
Open and parse a coverage file from disk.
- Returns a CoverageFile on success, or raises CoverageParseError on failure.
+ Returns a CoverageFile on success, or raises CoverageParsingError on failure.
"""
coverage_file = None
parse_failures = {}
@@ -50,7 +50,7 @@ def open(self, filepath):
#
if not coverage_file:
- raise CoverageParseError(filepath, parse_failures)
+ raise CoverageParsingError(filepath, parse_failures)
# successful parse
logger.debug("| Parse OKAY")
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 7f390684..4b098ede 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -7,7 +7,8 @@
from ctypes import *
try:
- from ..coverage_file import CoverageFile
+ from lighthouse.exceptions import CoverageMissingError
+ from lighthouse.reader.coverage_file import CoverageFile
except ImportError as e:
CoverageFile = object
@@ -54,7 +55,7 @@ def get_offsets(self, module_name):
try:
module = self.modules[module_name]
except KeyError:
- raise ValueError("No coverage for module '%s' in log" % module_name)
+ return []
# extract module id for speed
mod_id = module.id
@@ -65,14 +66,14 @@ def get_offsets(self, module_name):
# return the filtered coverage blocks
return coverage_blocks
- def get_blocks(self, module_name):
+ def get_offset_blocks(self, module_name):
"""
Return coverage data as basic blocks (offset, size) for the named module.
"""
try:
module = self.modules[module_name]
except KeyError:
- raise ValueError("No coverage for module '%s' in log" % module_name)
+ return []
# extract module id for speed
mod_id = module.id
diff --git a/plugin/lighthouse/reader/parsers/modoff.py b/plugin/lighthouse/reader/parsers/modoff.py
index 513dc245..aca4d209 100644
--- a/plugin/lighthouse/reader/parsers/modoff.py
+++ b/plugin/lighthouse/reader/parsers/modoff.py
@@ -16,7 +16,7 @@ def __init__(self, filepath):
#--------------------------------------------------------------------------
def get_offsets(self, module_name):
- return self.modules.get(module_name, {})
+ return self.modules.get(module_name, {}).keys()
#--------------------------------------------------------------------------
# Parsing Routines - Top Level
diff --git a/plugin/lighthouse/reader/parsers/trace.py b/plugin/lighthouse/reader/parsers/trace.py
index 95009eea..ee438f9a 100644
--- a/plugin/lighthouse/reader/parsers/trace.py
+++ b/plugin/lighthouse/reader/parsers/trace.py
@@ -15,8 +15,6 @@ def __init__(self, filepath):
#--------------------------------------------------------------------------
def get_addresses(self, module_name=None):
- if module_name:
- raise ValueError("No module mapping in this log format")
return self._hitmap.keys()
#--------------------------------------------------------------------------
From dd50a3a8c8621a78cdadd27cf59353ad1394d077 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 30 Mar 2019 17:52:23 -0400
Subject: [PATCH 017/154] rough draft of coverage xref for ida #8
---
plugin/lighthouse/core.py | 12 ++++
plugin/lighthouse/director.py | 12 ++++
plugin/lighthouse/ida_integration.py | 93 +++++++++++++++++++++++++++-
3 files changed, 116 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 06b1f863..085de505 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -117,6 +117,7 @@ def _install_ui(self):
"""
self._install_load_file()
self._install_load_batch()
+ self._install_open_coverage_xref()
self._install_open_coverage_overview()
def _uninstall_ui(self):
@@ -124,6 +125,7 @@ def _uninstall_ui(self):
Cleanup & remove all plugin UI integrations.
"""
self._uninstall_open_coverage_overview()
+ self._uninstall_open_coverage_xref()
self._uninstall_load_batch()
self._uninstall_load_file()
@@ -188,6 +190,16 @@ def open_coverage_overview(self):
self._ui_coverage_overview = CoverageOverview(self)
self._ui_coverage_overview.show()
+ def open_coverage_xref(self, address):
+ """
+ TODO
+ """
+ xrefs = self.director.xref_coverage(address)
+
+ lmsg("Printing coverage xrefs for 0x%08X..." % address)
+ for coverage in xrefs:
+ lmsg(" - " + self.director.get_coverage_string(coverage.name))
+
def interactive_load_batch(self):
"""
Perform the user-interactive loading of a coverage batch.
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index c6c86934..95140302 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -633,6 +633,18 @@ def _find_fuzzy_name(self, coverage_file, target_name):
# Coverage Management
#----------------------------------------------------------------------
+ def xref_coverage(self, address):
+ """
+ Return a list of coverage object containing the given address.
+ """
+ found = []
+
+ for name, db_coverage in iteritems(self._database_coverage):
+ if address in db_coverage.coverage:
+ found.append(db_coverage)
+
+ return found
+
def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
"""
Create a new database coverage mapping from the given data.
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/ida_integration.py
index b8ecd026..2e299286 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/ida_integration.py
@@ -19,10 +19,14 @@ class LighthouseIDA(Lighthouse):
def __init__(self):
# menu entry icons
+ self._icon_id_xref = idaapi.BADADDR
self._icon_id_file = idaapi.BADADDR
self._icon_id_batch = idaapi.BADADDR
self._icon_id_overview = idaapi.BADADDR
+ # IDA ui hooks
+ self._ui_hooks = UIHooks(self)
+
# run initialization
super(LighthouseIDA, self).__init__()
@@ -32,6 +36,7 @@ def __init__(self):
ACTION_LOAD_FILE = "lighthouse:load_file"
ACTION_LOAD_BATCH = "lighthouse:load_batch"
+ ACTION_COVERAGE_XREF = "lighthouse:coverage_xref"
ACTION_COVERAGE_OVERVIEW = "lighthouse:coverage_overview"
def _install_load_file(self):
@@ -106,6 +111,54 @@ def _install_load_batch(self):
logger.info("Installed the 'Code coverage batch' menu entry")
+ def _install_open_coverage_xref(self):
+ """
+ TODO
+ """
+
+ # create a custom IDA icon
+ icon_path = plugin_resource(os.path.join("icons", "batch.png"))
+ icon_data = str(open(icon_path, "rb").read())
+ self._icon_id_xref = idaapi.load_custom_icon(data=icon_data)
+
+ # describe a custom IDA UI action
+ action_desc = idaapi.action_desc_t(
+ self.ACTION_COVERAGE_XREF, # The action name
+ "Xrefs coverage sets...", # The action text
+ IDACtxEntry(self._pre_open_coverage_xref),# The action handler
+ None, # Optional: action shortcut
+ "List coverage sets containing this address", # Optional: tooltip
+ self._icon_id_xref # Optional: the action icon
+ )
+
+ # register the action with IDA
+ result = idaapi.register_action(action_desc)
+ if not result:
+ RuntimeError("Failed to register coverage_xref action with IDA")
+
+ self._ui_hooks.hook()
+ logger.info("Installed the 'Code coverage batch' menu entry")
+
+ def _inject_ctx_actions(self, view, popup, view_type):
+ """
+ TODO
+ """
+
+ if view_type == idaapi.BWN_DISASMS:
+ idaapi.attach_action_to_popup(
+ view,
+ popup,
+ self.ACTION_COVERAGE_XREF, # The action ID (see above)
+ "Xrefs graph from...", # Relative path of where to add the action
+ idaapi.SETMENU_APP # We want to append the action after ^
+ )
+
+ def _pre_open_coverage_xref(self):
+ """
+ TODO
+ """
+ self.open_coverage_xref(idaapi.get_screen_ea())
+
def _install_open_coverage_overview(self):
"""
Install the 'View->Open subviews->Coverage Overview' menu entry.
@@ -190,6 +243,23 @@ def _uninstall_load_batch(self):
logger.info("Uninstalled the 'Code coverage batch' menu entry")
+ def _uninstall_open_coverage_xref(self):
+ """
+ TODO
+ """
+ self._ui_hooks.unhook()
+
+ # unregister the action
+ result = idaapi.unregister_action(self.ACTION_COVERAGE_XREF)
+ if not result:
+ return False
+
+ # delete the entry's icon
+ idaapi.free_custom_icon(self._icon_id_xref)
+ self._icon_id_xref = idaapi.BADADDR
+
+ logger.info("Uninstalled the 'Coverage Xref' menu entry")
+
def _uninstall_open_coverage_overview(self):
"""
Remove the 'View->Open subviews->Coverage Overview' menu entry.
@@ -215,7 +285,7 @@ def _uninstall_open_coverage_overview(self):
logger.info("Uninstalled the 'Coverage Overview' menu entry")
#------------------------------------------------------------------------------
-# IDA Action Handler Stub
+# IDA UI Helpers
#------------------------------------------------------------------------------
class IDACtxEntry(idaapi.action_handler_t):
@@ -239,3 +309,24 @@ def update(self, ctx):
Ensure the context menu is always available in IDA.
"""
return idaapi.AST_ENABLE_ALWAYS
+
+class UIHooks(idaapi.UI_Hooks):
+
+ def __init__(self, integration):
+ self.integration = integration
+ super(UIHooks, self).__init__()
+
+ def finish_populating_widget_popup(self, widget, popup):
+ """
+ A right click menu is about to be shown. (IDA 7)
+ """
+ self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
+ return 0
+
+ def finish_populating_tform_popup(self, form, popup):
+ """
+ A right click menu is about to be shown. (IDA 6.x)
+ """
+ self.integration._inject_ctx_actions(form, popup, idaapi.get_tform_type(form))
+ return 0
+
From 44cb1c8113ceee44b69c5cb7df892a13cc5bfd96 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 31 Mar 2019 17:57:57 -0400
Subject: [PATCH 018/154] rough coverage xref dialog
---
plugin/lighthouse/core.py | 11 ++-
plugin/lighthouse/ui/__init__.py | 1 +
plugin/lighthouse/ui/coverage_xref.py | 119 ++++++++++++++++++++++++++
3 files changed, 125 insertions(+), 6 deletions(-)
create mode 100644 plugin/lighthouse/ui/coverage_xref.py
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 085de505..3d914689 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -2,7 +2,7 @@
import abc
import logging
-from lighthouse.ui import CoverageOverview
+from lighthouse.ui import CoverageOverview, CoverageXref
from lighthouse.util import lmsg
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
@@ -194,11 +194,10 @@ def open_coverage_xref(self, address):
"""
TODO
"""
- xrefs = self.director.xref_coverage(address)
-
- lmsg("Printing coverage xrefs for 0x%08X..." % address)
- for coverage in xrefs:
- lmsg(" - " + self.director.get_coverage_string(coverage.name))
+ dialog = CoverageXref(self.director, address)
+ if not dialog.exec_():
+ return
+ self.director.select_coverage(dialog.selected_name)
def interactive_load_batch(self):
"""
diff --git a/plugin/lighthouse/ui/__init__.py b/plugin/lighthouse/ui/__init__.py
index dd62d3d0..5ab3dafc 100644
--- a/plugin/lighthouse/ui/__init__.py
+++ b/plugin/lighthouse/ui/__init__.py
@@ -1 +1,2 @@
+from .coverage_xref import CoverageXref
from .coverage_overview import CoverageOverview
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
new file mode 100644
index 00000000..b580f96f
--- /dev/null
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -0,0 +1,119 @@
+import os
+import time
+import string
+import logging
+
+from lighthouse.util import lmsg
+from lighthouse.util.qt import *
+from lighthouse.util.python import *
+from lighthouse.util.misc import mainthread
+from lighthouse.util.disassembler import disassembler
+
+logger = logging.getLogger("Lighthouse.UI.Xref")
+
+#------------------------------------------------------------------------------
+# Coverage Xref Dialog
+#------------------------------------------------------------------------------
+
+class CoverageXref(QtWidgets.QDialog):
+ def __init__(self, director, address):
+ super(CoverageXref, self).__init__()
+ self.director = director
+ self.address = address
+ self.selected_name = None
+ self._ui_init()
+
+ #--------------------------------------------------------------------------
+ # Initialization - UI
+ #--------------------------------------------------------------------------
+
+ def _ui_init(self):
+ """
+ Initialize UI elements.
+ """
+ self.setWindowTitle("Coverage xrefs to 0x%X" % self.address)
+ self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
+ #self.setWindowFlags(self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint)
+
+ # configure the main widget / form
+ #self.setSizeGripEnabled(False)
+ self.setModal(True)
+ self._dpi_scale = get_dpi_scale()*5.0
+
+ # initialize coverage xref table
+ self._build_table()
+
+ # layout the populated UI just before showing it
+ self._ui_layout()
+
+ def _build_table(self):
+
+ xrefs = self.director.xref_coverage(self.address)
+
+ self._table = QtWidgets.QTableWidget(self)
+ self._table.verticalHeader().setVisible(False)
+
+ # symbol, cov %, name, time
+ self._table.setColumnCount(4)
+ self._table.setHorizontalHeaderLabels(["Sym", "Cov %", "Coverage Name", "Time"])
+ self._table.setColumnWidth(0, 40)
+ self._table.setColumnWidth(1, 50)
+ self._table.setColumnWidth(2, 300)
+ self._table.setColumnWidth(3, 200)
+
+ # align text in table headers to the left
+ for i in range(4):
+ self._table.horizontalHeaderItem(i).setTextAlignment(QtCore.Qt.AlignLeft)
+
+ # disable bolding of table headers when selected
+ self._table.horizontalHeader().setHighlightSections(False)
+
+ # stretch the last column of the table (aesthetics)
+ self._table.horizontalHeader().setStretchLastSection(True)
+
+ # make table read only, select a full row by default
+ self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+ # populate table with coverage details
+ self._table.setSortingEnabled(False)
+ self._table.setRowCount(len(xrefs))
+ for i, coverage in enumerate(xrefs, 0):
+ self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self.director.get_shorthand(coverage.name)))
+ self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("%5.2f" % (coverage.instruction_percent*100)))
+ self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(coverage.name))
+ self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("TODO"))
+ self._table.setSortingEnabled(True)
+
+ # signals
+ self._table.cellDoubleClicked.connect(self._ui_cell_double_click)
+
+ def _ui_layout(self):
+ """
+ Layout the major UI elements of the widget.
+ """
+ layout = QtWidgets.QVBoxLayout()
+ layout.setContentsMargins(0,0,0,0)
+
+ # layout child widgets
+ layout.addWidget(self._table)
+
+ # scale widget dimensions based on DPI
+ height = self._dpi_scale * 50
+ self.setMinimumHeight(height)
+ width = self._dpi_scale * 120
+ self.setMinimumWidth(width)
+
+ # apply the widget layout
+ self.setLayout(layout)
+
+ #--------------------------------------------------------------------------
+ # Signal Handlers
+ #--------------------------------------------------------------------------
+
+ def _ui_cell_double_click(self, row, column):
+ """
+ TODO
+ """
+ self.selected_name = self._table.item(row, 2).text()
+ self.accept()
From 02d52fce73196e632ae409737f73f41a94130aba Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 31 Mar 2019 18:55:24 -0400
Subject: [PATCH 019/154] add timestamps to coverage
---
plugin/lighthouse/coverage.py | 17 +++++++++++++++++
plugin/lighthouse/ui/coverage_xref.py | 2 +-
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index aacb167f..c03e7b37 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -1,5 +1,8 @@
+import os
+import time
import logging
import weakref
+import datetime
import collections
from lighthouse.util import *
@@ -51,6 +54,12 @@ def __init__(self, palette, name="", filepath=None, data=None):
# the filepath this coverage data was sourced from
self.filepath = filepath
+ # the timestamp of the coverage file on disk
+ try:
+ self.timestamp = os.path.getmtime(filepath)
+ except (OSError, TypeError):
+ self.timestamp = time.time()
+
#
# this is the coverage mapping's reference to the underlying database
# metadata. it will use this for all its mapping operations.
@@ -177,6 +186,14 @@ def data(self):
"""
return self._hitmap
+ @property
+ def human_timestamp(self):
+ """
+ Return the backing coverage data (a hitmap).
+ """
+ dt = datetime.datetime.fromtimestamp(self.timestamp)
+ return dt.strftime("%b %d %Y %H:%M:%S")
+
@property
def coverage(self):
"""
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index b580f96f..900590cc 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -82,7 +82,7 @@ def _build_table(self):
self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self.director.get_shorthand(coverage.name)))
self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("%5.2f" % (coverage.instruction_percent*100)))
self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(coverage.name))
- self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("TODO"))
+ self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, coverage.human_timestamp)))
self._table.setSortingEnabled(True)
# signals
From f89f3609f9734fef0d51e16df1b6b1163d5f0427 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 31 Mar 2019 19:06:57 -0400
Subject: [PATCH 020/154] fix issue where coverage names are not saved to
composed sets
---
plugin/lighthouse/director.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 95140302..fb95777b 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -989,6 +989,7 @@ def add_composition(self, composite_name, ast):
# evaluate the last AST into a coverage set
composite_coverage = self._evaluate_composition(ast)
+ composite_coverage.name = composite_name
# save the evaluated coverage under the given name
self._commit_coverage(composite_name, composite_coverage)
From 8052798c23944eaf8662666b57cea40c1e0122cd Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 31 Mar 2019 19:17:47 -0400
Subject: [PATCH 021/154] cleanup the xref dialog a bit
---
plugin/lighthouse/ui/coverage_xref.py | 63 +++++++++++++++------------
1 file changed, 36 insertions(+), 27 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 900590cc..9f6d410c 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -1,13 +1,8 @@
-import os
-import time
-import string
import logging
from lighthouse.util import lmsg
from lighthouse.util.qt import *
from lighthouse.util.python import *
-from lighthouse.util.misc import mainthread
-from lighthouse.util.disassembler import disassembler
logger = logging.getLogger("Lighthouse.UI.Xref")
@@ -16,11 +11,23 @@
#------------------------------------------------------------------------------
class CoverageXref(QtWidgets.QDialog):
+ """
+ A Qt Dialog to list other coverage sets that contain a given address.
+
+ This class makes up a rudimentary xref dialog. It does not follow Qt
+ 'best practices' because it does not need to be super flashy, nor does
+ it demand much facetime.
+ """
+
def __init__(self, director, address):
super(CoverageXref, self).__init__()
- self.director = director
+ self._director = director
+
+ # dialog attributes
self.address = address
self.selected_name = None
+
+ # configure the widget for use
self._ui_init()
#--------------------------------------------------------------------------
@@ -33,39 +40,35 @@ def _ui_init(self):
"""
self.setWindowTitle("Coverage xrefs to 0x%X" % self.address)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
- #self.setWindowFlags(self.windowFlags() | QtCore.Qt.MSWindowsFixedSizeDialogHint)
-
- # configure the main widget / form
- #self.setSizeGripEnabled(False)
self.setModal(True)
- self._dpi_scale = get_dpi_scale()*5.0
# initialize coverage xref table
- self._build_table()
+ self._ui_init_table()
+ self._populate_table()
# layout the populated UI just before showing it
self._ui_layout()
- def _build_table(self):
-
- xrefs = self.director.xref_coverage(self.address)
-
- self._table = QtWidgets.QTableWidget(self)
+ def _ui_init_table(self):
+ """
+ Initialize the coverage xref table UI elements.
+ """
+ self._table = QtWidgets.QTableWidget()
self._table.verticalHeader().setVisible(False)
# symbol, cov %, name, time
self._table.setColumnCount(4)
- self._table.setHorizontalHeaderLabels(["Sym", "Cov %", "Coverage Name", "Time"])
+ self._table.setHorizontalHeaderLabels(["Sym", "Cov %", "Coverage Name", "Timestamp"])
self._table.setColumnWidth(0, 40)
self._table.setColumnWidth(1, 50)
self._table.setColumnWidth(2, 300)
self._table.setColumnWidth(3, 200)
- # align text in table headers to the left
+ # left align text in column headers
for i in range(4):
self._table.horizontalHeaderItem(i).setTextAlignment(QtCore.Qt.AlignLeft)
- # disable bolding of table headers when selected
+ # disable bolding of column headers when selected
self._table.horizontalHeader().setHighlightSections(False)
# stretch the last column of the table (aesthetics)
@@ -75,19 +78,25 @@ def _build_table(self):
self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+ # catch double click events on table rows
+ self._table.cellDoubleClicked.connect(self._ui_cell_double_click)
+
+ def _populate_table(self):
+ """
+ Populate the xref table with data from the coverage director.
+ """
+ xrefs = self._director.xref_coverage(self.address)
+
# populate table with coverage details
self._table.setSortingEnabled(False)
self._table.setRowCount(len(xrefs))
for i, coverage in enumerate(xrefs, 0):
- self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self.director.get_shorthand(coverage.name)))
+ self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self._director.get_shorthand(coverage.name)))
self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("%5.2f" % (coverage.instruction_percent*100)))
self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(coverage.name))
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, coverage.human_timestamp)))
self._table.setSortingEnabled(True)
- # signals
- self._table.cellDoubleClicked.connect(self._ui_cell_double_click)
-
def _ui_layout(self):
"""
Layout the major UI elements of the widget.
@@ -99,9 +108,9 @@ def _ui_layout(self):
layout.addWidget(self._table)
# scale widget dimensions based on DPI
- height = self._dpi_scale * 50
+ height = get_dpi_scale() * 250
+ width = get_dpi_scale() * 600
self.setMinimumHeight(height)
- width = self._dpi_scale * 120
self.setMinimumWidth(width)
# apply the widget layout
@@ -113,7 +122,7 @@ def _ui_layout(self):
def _ui_cell_double_click(self, row, column):
"""
- TODO
+ A cell/row has been double clicked in the xref table.
"""
self.selected_name = self._table.item(row, 2).text()
self.accept()
From b6bf203b8c9915caa6951f647b6f0e63d053920e Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 31 Mar 2019 19:48:30 -0400
Subject: [PATCH 022/154] cleanup IDA integration of xrefs
---
plugin/lighthouse/core.py | 24 +++++++--
plugin/lighthouse/ida_integration.py | 62 ++++++++++++++---------
plugin/lighthouse/reader/parsers/drcov.py | 5 +-
3 files changed, 60 insertions(+), 31 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 3d914689..ef16aba5 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -143,6 +143,13 @@ def _install_load_batch(self):
"""
pass
+ @abc.abstractmethod
+ def _install_open_coverage_xref(self):
+ """
+ Install the right click 'Coverage Xref' context menu entry.
+ """
+ pass
+
@abc.abstractmethod
def _install_open_coverage_overview(self):
"""
@@ -164,6 +171,13 @@ def _uninstall_load_batch(self):
"""
pass
+ @abc.abstractmethod
+ def _uninstall_open_coverage_xref(self):
+ """
+ Remove the right click 'Coverage Xref' context menu entry.
+ """
+ pass
+
@abc.abstractmethod
def _uninstall_open_coverage_overview(self):
"""
@@ -192,11 +206,15 @@ def open_coverage_overview(self):
def open_coverage_xref(self, address):
"""
- TODO
+ Open the 'Coverage Xref' dialog for a given address.
"""
+
+ # show the coverage xref dialog
dialog = CoverageXref(self.director, address)
if not dialog.exec_():
return
+
+ # activate the user selected xref (if one was double clicked)
self.director.select_coverage(dialog.selected_name)
def interactive_load_batch(self):
@@ -317,8 +335,8 @@ def interactive_load_file(self):
await_future(future)
#
- # insert the loaded drcov data objects into the director
- # TODO/COMMENT
+ # now that the database metadata is available, we can use the director
+ # to load and normalize the selected coverage files
#
disassembler.replace_wait_box("Loading coverage from disk...")
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/ida_integration.py
index 2e299286..1d1ee78a 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/ida_integration.py
@@ -113,7 +113,7 @@ def _install_load_batch(self):
def _install_open_coverage_xref(self):
"""
- TODO
+ Install the right click 'Coverage Xref' context menu entry.
"""
# create a custom IDA icon
@@ -139,26 +139,6 @@ def _install_open_coverage_xref(self):
self._ui_hooks.hook()
logger.info("Installed the 'Code coverage batch' menu entry")
- def _inject_ctx_actions(self, view, popup, view_type):
- """
- TODO
- """
-
- if view_type == idaapi.BWN_DISASMS:
- idaapi.attach_action_to_popup(
- view,
- popup,
- self.ACTION_COVERAGE_XREF, # The action ID (see above)
- "Xrefs graph from...", # Relative path of where to add the action
- idaapi.SETMENU_APP # We want to append the action after ^
- )
-
- def _pre_open_coverage_xref(self):
- """
- TODO
- """
- self.open_coverage_xref(idaapi.get_screen_ea())
-
def _install_open_coverage_overview(self):
"""
Install the 'View->Open subviews->Coverage Overview' menu entry.
@@ -245,7 +225,7 @@ def _uninstall_load_batch(self):
def _uninstall_open_coverage_xref(self):
"""
- TODO
+ Remove the right click 'Coverage Xref' context menu entry.
"""
self._ui_hooks.unhook()
@@ -284,6 +264,35 @@ def _uninstall_open_coverage_overview(self):
logger.info("Uninstalled the 'Coverage Overview' menu entry")
+ #--------------------------------------------------------------------------
+ # Helpers
+ #--------------------------------------------------------------------------
+
+ def _inject_ctx_actions(self, view, popup, view_type):
+ """
+ Inject context menu entries into IDA's right click menus.
+
+ NOTE: This is only being used for coverage xref at this time, but
+ may host additional actions in the future.
+
+ """
+
+ if view_type == idaapi.BWN_DISASMS:
+
+ idaapi.attach_action_to_popup(
+ view,
+ popup,
+ self.ACTION_COVERAGE_XREF, # The action ID (see above)
+ "Xrefs graph from...", # Relative path of where to add the action
+ idaapi.SETMENU_APP # We want to append the action after ^
+ )
+
+ def _pre_open_coverage_xref(self):
+ """
+ Grab a contextual address before opening the coverage xref dialog.
+ """
+ self.open_coverage_xref(idaapi.get_screen_ea())
+
#------------------------------------------------------------------------------
# IDA UI Helpers
#------------------------------------------------------------------------------
@@ -311,6 +320,12 @@ def update(self, ctx):
return idaapi.AST_ENABLE_ALWAYS
class UIHooks(idaapi.UI_Hooks):
+ """
+ Hooks for IDA's UI subsystem.
+
+ At the moment, we are only using these to inject into IDA's right click
+ context menus (eg, coverage xrefs)
+ """
def __init__(self, integration):
self.integration = integration
@@ -325,8 +340,7 @@ def finish_populating_widget_popup(self, widget, popup):
def finish_populating_tform_popup(self, form, popup):
"""
- A right click menu is about to be shown. (IDA 6.x)
+ A right click menu is about to be shown. (IDA 6.x) / COMPAT
"""
self.integration._inject_ctx_actions(form, popup, idaapi.get_tform_type(form))
return 0
-
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 4b098ede..9c48eff2 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -12,9 +12,6 @@
except ImportError as e:
CoverageFile = object
-# Useful for python2 and python3 compatibility
-from builtins import bytes
-
#------------------------------------------------------------------------------
# DynamoRIO Drcov Log Parser
#------------------------------------------------------------------------------
@@ -272,7 +269,7 @@ def _parse_bb_table_header(self, f):
saved_position = f.tell()
# is this an ascii table?
- if bytes(f.read(len(token))) == token:
+ if f.read(len(token)) == token:
self.bb_table_is_binary = False
# nope! binary table
From d6d0fbc7dddfc417c02d45935920d672d35ee43c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 1 Apr 2019 13:54:42 -0400
Subject: [PATCH 023/154] allow for smooth horizontal scrolling in coverage
table
---
plugin/lighthouse/ui/coverage_table.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index de75721e..e14b4118 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -86,6 +86,7 @@ def _ui_init_table(self):
"""
palette = self._model._director._palette
self.setFocusPolicy(QtCore.Qt.StrongFocus)
+ self.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
# widget style
self.setStyleSheet(
From fa3a13a085d175d6575663055ad871df683404d4 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 2 Apr 2019 12:26:42 -0400
Subject: [PATCH 024/154] allow for xrefing a batch
---
plugin/lighthouse/core.py | 22 +++++++++++++-
plugin/lighthouse/coverage.py | 8 -----
plugin/lighthouse/director.py | 42 +++++++++++++++++++++++++-
plugin/lighthouse/ui/coverage_xref.py | 43 +++++++++++++++++++++++----
plugin/lighthouse/util/misc.py | 8 +++++
5 files changed, 107 insertions(+), 16 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index ef16aba5..71cb8f3c 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -215,7 +215,27 @@ def open_coverage_xref(self, address):
return
# activate the user selected xref (if one was double clicked)
- self.director.select_coverage(dialog.selected_name)
+ if dialog.selected_coverage:
+ self.director.select_coverage(dialog.selected_coverage)
+ return
+
+ # load a coverage file from disk
+ disassembler.show_wait_box("Loading coverage from disk...")
+ created_coverage, errors = self.director.load_coverage_files(
+ [dialog.selected_filepath],
+ disassembler.replace_wait_box
+ )
+
+ # TODO rough...
+ if not created_coverage:
+ lmsg("No coverage files could be loaded...")
+ disassembler.hide_wait_box()
+ warn_errors(errors)
+ return
+
+ disassembler.replace_wait_box("Selecting coverage...")
+ self.director.select_coverage(created_coverage[0].name)
+ disassembler.hide_wait_box()
def interactive_load_batch(self):
"""
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index c03e7b37..857224e6 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -186,14 +186,6 @@ def data(self):
"""
return self._hitmap
- @property
- def human_timestamp(self):
- """
- Return the backing coverage data (a hitmap).
- """
- dt = datetime.datetime.fromtimestamp(self.timestamp)
- return dt.strftime("%b %d %Y %H:%M:%S")
-
@property
def coverage(self):
"""
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index fb95777b..56553833 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -70,6 +70,9 @@ def __init__(self, metadata, palette):
# a map of loaded or composed database coverages
self._database_coverage = collections.OrderedDict()
+ # TODO
+ self.owners = collections.defaultdict(set)
+
#
# the director automatically maintains / generates a few coverage sets
# of its own. these are not directly modifiable by the user, but may
@@ -333,6 +336,10 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
"""
errors = []
aggregate_addresses = set()
+ aggregate_owners = collections.defaultdict(list)
+
+ start = time.time()
+ #----------------------------------------------------------------------
for i, filepath in enumerate(filepaths, 1):
logger.debug("-"*50)
@@ -353,6 +360,10 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
errors.append(CoverageMissingError(filepath))
continue
+ # save the attribution data for this coverage data
+ for address in coverage_addresses:
+ aggregate_owners[address].append(filepath)
+
# aggregate all coverage data into a single set of addresses
aggregate_addresses.update(coverage_addresses)
@@ -363,12 +374,24 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
coverage_data = self._optimize_coverage_data(aggregate_addresses)
coverage = self.create_coverage(batch_name, coverage_data)
+ #
+ # transfer the aggregated coverage owners lists to the global owners
+ # map one address at a time.
+ #
+
+ for address in coverage_data:
+ self.owners[address].update(aggregate_owners[address])
+
# evaluate coverage
if not coverage.nodes:
errors.append(CoverageMappingAbsent(coverage))
elif coverage.suspicious:
errors.append(CoverageMappingSuspicious(coverage))
+ #----------------------------------------------------------------------
+ end = time.time()
+ logger.debug("Batch loading took %f seconds" % (end-start))
+
# return the created coverage name
return (coverage, errors)
@@ -381,6 +404,9 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
errors = []
all_coverage = []
+ start = time.time()
+ #----------------------------------------------------------------------
+
#
# stop the director's aggregate set from recomputing after each new
# coverage mapping is created. instead, we want to wait till *all* new
@@ -416,6 +442,10 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
errors.append(CoverageMissingError(filepath))
continue
+ # save the attribution data for this coverage data
+ for address in coverage_data:
+ self.owners[address].add(filepath)
+
#
# request a name for the new coverage mapping that the director will
# generate from the loaded coverage data
@@ -441,6 +471,10 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
progress_callback("Recomputing coverage aggregate...")
self.resume_aggregation()
+ #----------------------------------------------------------------------
+ end = time.time()
+ logger.debug("File loading took %f seconds" % (end-start))
+
# all done
return (all_coverage, errors)
@@ -633,7 +667,7 @@ def _find_fuzzy_name(self, coverage_file, target_name):
# Coverage Management
#----------------------------------------------------------------------
- def xref_coverage(self, address):
+ def get_address_coverage(self, address):
"""
Return a list of coverage object containing the given address.
"""
@@ -645,6 +679,12 @@ def xref_coverage(self, address):
return found
+ def get_address_file(self, address):
+ """
+ Return a list of coverage filepaths containing the given address.
+ """
+ return list(self.owners.get(address, []))
+
def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
"""
Create a new database coverage mapping from the given data.
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 9f6d410c..862f7872 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -1,7 +1,9 @@
+import os
import logging
from lighthouse.util import lmsg
from lighthouse.util.qt import *
+from lighthouse.util.misc import human_timestamp
from lighthouse.util.python import *
logger = logging.getLogger("Lighthouse.UI.Xref")
@@ -25,7 +27,8 @@ def __init__(self, director, address):
# dialog attributes
self.address = address
- self.selected_name = None
+ self.selected_coverage = None
+ self.selected_filepath = None
# configure the widget for use
self._ui_init()
@@ -85,16 +88,41 @@ def _populate_table(self):
"""
Populate the xref table with data from the coverage director.
"""
- xrefs = self._director.xref_coverage(self.address)
+ cov_xrefs = self._director.get_address_coverage(self.address)
+ file_xrefs = self._director.get_address_file(self.address)
+
+ # dedupe
+ for coverage in cov_xrefs:
+ if coverage.filepath in file_xrefs:
+ file_xrefs.remove(coverage.filepath)
# populate table with coverage details
self._table.setSortingEnabled(False)
- self._table.setRowCount(len(xrefs))
- for i, coverage in enumerate(xrefs, 0):
+ self._table.setRowCount(len(cov_xrefs) + len(file_xrefs))
+
+ # coverage objects
+ for i, coverage in enumerate(cov_xrefs, 0):
self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self._director.get_shorthand(coverage.name)))
self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("%5.2f" % (coverage.instruction_percent*100)))
self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(coverage.name))
- self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, coverage.human_timestamp)))
+ self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, human_timestamp(coverage.timestamp))))
+
+ # filepaths
+ for i, filepath in enumerate(file_xrefs, len(cov_xrefs)):
+
+ # try to read timestamp of the file on disk (if it exists)
+ try:
+ timestamp = os.path.getmtime(filepath)
+ timestamp = "%u (%s)" % (timestamp, human_timestamp(timestamp))
+ except (OSError, TypeError):
+ timestamp = "(unknown)"
+
+ # populate table entry
+ self._table.setItem(i, 0, QtWidgets.QTableWidgetItem("-"))
+ self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("-"))
+ self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(filepath))
+ self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(timestamp))
+
self._table.setSortingEnabled(True)
def _ui_layout(self):
@@ -124,5 +152,8 @@ def _ui_cell_double_click(self, row, column):
"""
A cell/row has been double clicked in the xref table.
"""
- self.selected_name = self._table.item(row, 2).text()
+ if self._table.item(row, 0).text() == "-":
+ self.selected_filepath = self._table.item(row, 2).text()
+ else:
+ self.selected_coverage = self._table.item(row, 2).text()
self.accept()
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index 5766b6c3..6da0ba4b 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -1,5 +1,6 @@
import os
import weakref
+import datetime
import threading
import collections
@@ -71,6 +72,13 @@ def hex_list(items):
"""
return '[{}]'.format(', '.join('0x%X' % x for x in items))
+def human_timestamp(timestamp):
+ """
+ Return a human readable timestamp for a given epoch.
+ """
+ dt = datetime.datetime.fromtimestamp(timestamp)
+ return dt.strftime("%b %d %Y %H:%M:%S")
+
#------------------------------------------------------------------------------
# Python Callback / Signals
#------------------------------------------------------------------------------
From 8316012782ec034bc51f2c380d8870ebf9d304c3 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 2 Apr 2019 17:03:15 -0400
Subject: [PATCH 025/154] switch xref attribution to bb granularity
---
plugin/lighthouse/director.py | 20 ++++++++------------
1 file changed, 8 insertions(+), 12 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 56553833..b971d8bd 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -336,7 +336,6 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
"""
errors = []
aggregate_addresses = set()
- aggregate_owners = collections.defaultdict(list)
start = time.time()
#----------------------------------------------------------------------
@@ -362,7 +361,8 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
# save the attribution data for this coverage data
for address in coverage_addresses:
- aggregate_owners[address].append(filepath)
+ if address in self.metadata.nodes:
+ self.owners[address].add(filepath)
# aggregate all coverage data into a single set of addresses
aggregate_addresses.update(coverage_addresses)
@@ -374,14 +374,6 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
coverage_data = self._optimize_coverage_data(aggregate_addresses)
coverage = self.create_coverage(batch_name, coverage_data)
- #
- # transfer the aggregated coverage owners lists to the global owners
- # map one address at a time.
- #
-
- for address in coverage_data:
- self.owners[address].update(aggregate_owners[address])
-
# evaluate coverage
if not coverage.nodes:
errors.append(CoverageMappingAbsent(coverage))
@@ -444,7 +436,8 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
# save the attribution data for this coverage data
for address in coverage_data:
- self.owners[address].add(filepath)
+ if address in self.metadata.nodes:
+ self.owners[address].add(filepath)
#
# request a name for the new coverage mapping that the director will
@@ -683,7 +676,10 @@ def get_address_file(self, address):
"""
Return a list of coverage filepaths containing the given address.
"""
- return list(self.owners.get(address, []))
+ node = self.metadata.get_node(address)
+ if not node:
+ return []
+ return list(self.owners.get(node.address, []))
def create_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
"""
From e6f02ab873d2ede761bc8ff4364073a28909be80 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 2 Apr 2019 17:20:57 -0400
Subject: [PATCH 026/154] allow smooth horizontal scrolling in xref window
---
plugin/lighthouse/ui/coverage_xref.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 862f7872..49592210 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -58,6 +58,7 @@ def _ui_init_table(self):
"""
self._table = QtWidgets.QTableWidget()
self._table.verticalHeader().setVisible(False)
+ self._table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
# symbol, cov %, name, time
self._table.setColumnCount(4)
From b82dba0d60600c41c614afc759a443b6b6664659 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 16 Apr 2019 14:52:32 -0400
Subject: [PATCH 027/154] tweak: removes unnecessary composer shell border
imposed by some Qt configs
---
plugin/lighthouse/ui/coverage_overview.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 19863c61..0ce40c09 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -145,6 +145,11 @@ def _ui_init_toolbar_elements(self):
self._shell_elements = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
self._shell_elements.setStyleSheet(
"""
+ QSplitter
+ {
+ border: none;
+ }
+
QSplitter::handle
{
background-color: #909090;
From 9558763e52610defe187c7313c1f01896c50085a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 16 Apr 2019 18:24:40 -0400
Subject: [PATCH 028/154] bugfix: collapse the coverage combobox when its head
is clicked in the expanded state
---
plugin/lighthouse/ui/coverage_combobox.py | 72 ++++++++++++++++++-----
1 file changed, 57 insertions(+), 15 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 252c0f66..cd44b901 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -46,35 +46,77 @@ def __init__(self, director, parent=None):
# QComboBox Overloads
#--------------------------------------------------------------------------
+ def showPopup(self):
+ """
+ Show the QComboBox dropdown/popup.
+ """
+ super(CoverageComboBox, self).showPopup()
+
+ #
+ # the next line of code will prevent the combobox 'head' from getting
+ # any mouse actions now that the popup/dropdown is visible.
+ #
+ # this is pretty aggressive, but it will allow the user to 'collapse'
+ # the combobox dropdown while it is in an expanded state by simply
+ # clicking the combobox head as one can do to expand it.
+ #
+ # the reason this dirty trick is able to simulate a 'collapsing click'
+ # is because the user clicks 'outside' the popup/dropdown which
+ # automatically closes it. if the click was on the combobox head, it
+ # is simply ignored because we set this attribute!
+ #
+ # when the popup is closing, we undo this action in hidePopup()
+ #
+ # we have to use this workaround because we are using an 'editable' Qt
+ # combobox which behaves differently to clicks than a normal combobox.
+ #
+
+ self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
+
+ def hidePopup(self):
+ """
+ Hide the QComboBox dropdown/popup.
+ """
+ super(CoverageComboBox, self).hidePopup()
+
+ #
+ # the combobox popup is now hidden / collapsed. the combobox head needs
+ # to be re-enlightened to direct mouse clicks (eg, to expand it). this
+ # undos the setAttribute action in showPopup() above.
+ #
+ # we use a short timer of 100ms to ensure the 'hiding' of the dropdown
+ # and its associated click are processed first. aftwards, it is safe to
+ # begin accepting clicks again.
+ #
+
+ QtCore.QTimer.singleShot(100, lambda: self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False))
+
def mousePressEvent(self, e):
"""
Capture mouse click events to the QComboBox.
"""
- # get the widget currently beneath the given mouse event
+ # get the widget currently beneath the mouse event being handled
hovering = self.childAt(e.pos())
#
- # if the hovered widget is the 'head' of the QComboBox, we want to
- # move any mouse clicks to appear like it was on the dropdown arrow
- # box on the right side.
+ # if the hovered widget is the 'head' of the QComboBox, we assume
+ # the user has clicked it and should show the dropwdown 'popup'
#
- # this is to satisfy some internal QComboBox Qt logic, allowing us
- # to collapse and expand an 'editable' QComboBox by clicking anywhere
- # on the 'head' (the read-only QLineEdit)
+ # we must showPopup() ourselves because internal Qt logic for
+ # 'editable' comboboxes try to enter an editing mode for the field
+ # rather than expanding the dropdown.
#
- # this is basically dirty-hax
+ # if you don't remember, our combobox is marked 'editable' to satisfy
+ # some internal Qt logic so that our 'Windows' draw style is used
#
if hovering == self.lineEdit():
- new_pos = QtCore.QPoint(
- self.rect().right() - 10,
- self.rect().height()/2
- )
- logger.debug("Moved click %s --> %s" % (e.pos(), new_pos))
- e = move_mouse_event(e, new_pos)
+ self.showPopup()
+ e.accept()
+ return
- # handle the event (possibly moved) as it normally would be
+ # handle any other events as they normally should be
super(CoverageComboBox, self).mousePressEvent(e)
#--------------------------------------------------------------------------
From 8a03f035a107c4a590b8caaaf7d9689063f0fc02 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 17 Apr 2019 17:12:00 -0400
Subject: [PATCH 029/154] improve shell / combobox styling
---
plugin/lighthouse/composer/shell.py | 20 +++++++++----
plugin/lighthouse/palette.py | 11 +++++++
plugin/lighthouse/ui/coverage_combobox.py | 36 +++++++++++++++--------
3 files changed, 50 insertions(+), 17 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 7f3e93f6..16ca4f6c 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -94,11 +94,21 @@ def _ui_init_shell(self):
self._line = ComposingLine()
# configure the shell background & default text color
- palette = self._line.palette()
- palette.setColor(QtGui.QPalette.Base, self._palette.overview_bg)
- palette.setColor(QtGui.QPalette.Text, self._palette.composer_fg)
- palette.setColor(QtGui.QPalette.WindowText, self._palette.composer_fg)
- self._line.setPalette(palette)
+ qpal = self._line.palette()
+ #qpal.setColor(QtGui.QPalette.Base, self._palette.overview_bg)
+ qpal.setColor(QtGui.QPalette.Text, self._palette.composer_fg)
+ qpal.setColor(QtGui.QPalette.WindowText, self._palette.composer_fg)
+ self._line.setPalette(qpal)
+
+ self._line.setStyleSheet(
+ "QPlainTextEdit {"
+ " background-color: %s;" % self._palette.overview_bg.name() +
+ " border: 1px solid %s;" % self._palette.border.name() +
+ "} "
+ "QPlainTextEdit:hover, QPlainTextEdit:focus {"
+ " border: 1px solid %s;" % self._palette.focus.name() +
+ "}"
+ )
def _ui_init_completer(self):
"""
diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/palette.py
index def7929a..143f108b 100644
--- a/plugin/lighthouse/palette.py
+++ b/plugin/lighthouse/palette.py
@@ -71,6 +71,9 @@ def __init__(self):
self._combobox_selection_bg = [QtGui.QColor(51, 153, 255), QtGui.QColor(51, 153, 255)]
self._combobox_selection_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)]
+ self._border = [QtGui.QColor(100, 100, 100), QtGui.QColor(100, 100, 100)]
+ self._focus = [QtGui.QColor(160, 160, 160), QtGui.QColor(160, 160, 160)]
+
#
# Composition Grammar
#
@@ -279,6 +282,14 @@ def combobox_selection_bg(self):
def combobox_selection_fg(self):
return self._combobox_selection_fg[self.qt_theme]
+ @property
+ def border(self):
+ return self._border[self.qt_theme]
+
+ @property
+ def focus(self):
+ return self._focus[self.qt_theme]
+
#--------------------------------------------------------------------------
# Composition Grammar
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index cd44b901..e128fe0a 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -127,6 +127,7 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
+ palette = self._director._palette
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
@@ -153,6 +154,21 @@ def _ui_init(self):
self.lineEdit().setEnabled(False) # text can't be selected
self.setMaximumHeight(self._font_metrics.height()*1.75)
+ #
+ # the purpose of the padding in this stylesheet is to pad the visible
+ # selection text in the combobox 'head' on first show. The reason being
+ # is that without this, the text for the selected coverage will lapse
+ # behind the combobox dropdown arrow (which is Qt by design???)
+ #
+
+ self.lineEdit().setStyleSheet(
+ "QLineEdit { "
+ " color: %s;" % palette.combobox_fg.name() +
+ " padding: 0 2ex 0 2ex;"
+ " background-color: %s;" % palette.overview_bg.name() +
+ "}"
+ )
+
#
# the combobox will pick a size based on its contents when it is first
# made visible, but we also make it is arbitrarily resizable for the
@@ -162,20 +178,16 @@ def _ui_init(self):
self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
- #
- # the purpose of this stylesheet is to pad the visible selection text
- # in the combobox 'head' on first show. The reason being is that
- # without this, the text for the selected coverage will lapse behind
- # the combobox dropdown arrow (which is Qt by design???)
- #
- # I don't like the tail of the text disappearing behind this silly
- # dropdown arrow, therefore we pad the right side of the combobox.
- #
-
- self.setStyleSheet("QComboBox { padding: 0 2ex 0 2ex; }")
-
# draw the QComboBox with a 'Windows'-esque style
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
+ self.setStyleSheet(
+ "QComboBox {"
+ " border: 1px solid %s;" % palette.border.name() +
+ "} "
+ "QComboBox:hover, QComboBox:focus {"
+ " border: 1px solid %s;" % palette.focus.name() +
+ "}"
+ )
# connect relevant signals
self._ui_init_signals()
From 80d65fd5847c131aa2abc8f0044cc9c587389e97 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 19 Apr 2019 19:32:57 -0400
Subject: [PATCH 030/154] styling consistency improvements
---
plugin/lighthouse/composer/shell.py | 2 +-
plugin/lighthouse/ui/coverage_combobox.py | 25 +++++++++++++----------
plugin/lighthouse/ui/coverage_table.py | 2 +-
3 files changed, 16 insertions(+), 13 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 16ca4f6c..5a069f70 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -86,7 +86,7 @@ def _ui_init_shell(self):
# the composer label at the head of the shell
self._line_label = QtWidgets.QLabel("Composer")
self._line_label.setStyleSheet("QLabel { margin: 0 1ex 0 1ex }")
- self._line_label.setAlignment(QtCore.Qt.AlignVCenter | QtCore.Qt.AlignHCenter)
+ self._line_label.setAlignment(QtCore.Qt.AlignCenter)
self._line_label.setFont(self._font)
self._line_label.setFixedWidth(self._line_label.sizeHint().width())
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index e128fe0a..dae955e6 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -152,19 +152,13 @@ def _ui_init(self):
self.lineEdit().setFont(self._font)
self.lineEdit().setReadOnly(True) # text can't be edited
self.lineEdit().setEnabled(False) # text can't be selected
- self.setMaximumHeight(self._font_metrics.height()*1.75)
-
- #
- # the purpose of the padding in this stylesheet is to pad the visible
- # selection text in the combobox 'head' on first show. The reason being
- # is that without this, the text for the selected coverage will lapse
- # behind the combobox dropdown arrow (which is Qt by design???)
- #
+ # configure the combobox style
self.lineEdit().setStyleSheet(
"QLineEdit { "
- " color: %s;" % palette.combobox_fg.name() +
- " padding: 0 2ex 0 2ex;"
+ " border: none;"
+ " padding: 0 0 0 2ex;"
+ " margin: 0;"
" background-color: %s;" % palette.overview_bg.name() +
"}"
)
@@ -177,12 +171,15 @@ def _ui_init(self):
self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow)
self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored)
+ self.setMaximumHeight(self._font_metrics.height()*1.75)
# draw the QComboBox with a 'Windows'-esque style
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
self.setStyleSheet(
"QComboBox {"
+ " color: %s;" % palette.combobox_fg.name() +
" border: 1px solid %s;" % palette.border.name() +
+ " padding: 0;"
"} "
"QComboBox:hover, QComboBox:focus {"
" border: 1px solid %s;" % palette.focus.name() +
@@ -430,6 +427,7 @@ def _ui_init(self):
hh.setResizeMode(1, QtWidgets.QHeaderView.Fixed)
vh.setResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+ hh.setMinimumSectionSize(0)
vh.setMinimumSectionSize(0)
# get the column width hint from the model for the 'X' delete column
@@ -598,8 +596,13 @@ def data(self, index, role=QtCore.Qt.DisplayRole):
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignVCenter | QtCore.Qt.AlignLeft
+ # combobox header, padded with " " to account for dropdown arrow overlap
+ elif role == QtCore.Qt.EditRole:
+ if index.column() == COLUMN_COVERAGE_STRING and index.row() != self._seperator_index:
+ return self._director.get_coverage_string(self._entries[index.row()]) + " "
+
# data display request
- elif role in [QtCore.Qt.DisplayRole, QtCore.Qt.EditRole]:
+ elif role == QtCore.Qt.DisplayRole:
if index.column() == COLUMN_COVERAGE_STRING and index.row() != self._seperator_index:
return self._director.get_coverage_string(self._entries[index.row()])
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index e14b4118..93f22de5 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -542,7 +542,7 @@ def toggle_column_alignment(self, column):
# toggle the column alignment between center (default) and left
if alignment == QtCore.Qt.AlignCenter:
- new_alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter
+ new_alignment = QtCore.Qt.AlignVCenter
else:
new_alignment = QtCore.Qt.AlignCenter
From 8a656b10d3845acc47c072c9846a0420a078581e Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 19 Apr 2019 19:53:03 -0400
Subject: [PATCH 031/154] tweak: make the function name column left aligned by
default
---
plugin/lighthouse/ui/coverage_table.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 93f22de5..3c1da003 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -698,6 +698,9 @@ def __init__(self, director, parent=None):
self._default_alignment for x in self.COLUMN_HEADERS
]
+ # make the function name column left aligned by default
+ self.set_column_alignment(self.FUNC_NAME, QtCore.Qt.AlignVCenter)
+
# initialize a monospace font to use for table row / cell text
self._entry_font = MonospaceFont()
self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics)
From 7303d72fa2593744bb0c47537a54f0f280ee320d Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 19 Apr 2019 19:58:29 -0400
Subject: [PATCH 032/154] python 3 compat
---
plugin/lighthouse/util/log.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py
index fb0db746..2bb197fe 100644
--- a/plugin/lighthouse/util/log.py
+++ b/plugin/lighthouse/util/log.py
@@ -80,7 +80,7 @@ def cleanup_log_directory(log_directory):
filetimes[os.path.getmtime(filepath)] = filepath
# get the filetimes and check if there's enough to warrant cleanup
- times = filetimes.keys()
+ times = list(filetimes.keys())
if len(times) < MAX_LOGS:
return
From 366df2f5d88541007633f1c5064a5c1d5d96e950 Mon Sep 17 00:00:00 2001
From: volokitinss
Date: Fri, 10 May 2019 18:29:52 +0200
Subject: [PATCH 033/154] Match upper case hex symbols as well (#67)
---
plugin/lighthouse/reader/parsers/drcov.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 9c48eff2..7573dc9a 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -298,7 +298,7 @@ def _parse_bb_table_entries(self, f):
if text_entry != "module id, start, size:":
raise ValueError("Invalid BB header: %r" % text_entry)
- pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-f]+)\,\s*(?P[0-9]+)$")
+ pattern = re.compile(r"^module\[\s*(?P[0-9]+)\]\:\s*(?P0x[0-9a-fA-F]+)\,\s*(?P[0-9]+)$")
for bb in self.bbs:
text_entry = f.readline().decode('utf-8').strip()
From 80404a9f57751a5ea588d51fdb5dc158cb84a569 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 May 2019 14:08:25 -0400
Subject: [PATCH 034/154] make disassembler version fields less annoying
---
plugin/lighthouse/util/disassembler/api.py | 21 ++++++++++++-------
.../lighthouse/util/disassembler/binja_api.py | 12 -----------
.../lighthouse/util/disassembler/ida_api.py | 12 -----------
3 files changed, 13 insertions(+), 32 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index a239cb72..26182269 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -30,6 +30,11 @@ class DisassemblerAPI(object):
def __init__(self):
self._waitbox = None
+ # required version fields
+ self._version_major = NotImplemented
+ self._version_minor = NotImplemented
+ self._version_patch = NotImplemented
+
if not self.headless and QT_AVAILABLE:
from ..qt import WaitBox
self._waitbox = WaitBox("Please wait...")
@@ -38,31 +43,31 @@ def __init__(self):
# Properties
#--------------------------------------------------------------------------
- @abc.abstractproperty
def version_major(self):
"""
Return the major version number of the disassembler framework.
"""
- pass
+ assert self._version_major != NotImplemented
+ return self._version_major
- @abc.abstractproperty
def version_minor(self):
"""
Return the minor version number of the disassembler framework.
"""
- pass
+ assert self._version_patch != NotImplemented
+ return self._version_patch
- @abc.abstractproperty
- def version_minor(self):
+ def version_patch(self):
"""
Return the patch version number of the disassembler framework.
"""
- pass
+ assert self._version_patch != NotImplemented
+ return self._version_patch
@abc.abstractproperty
def headless(self):
"""
- Return a bool indicating if the disassembler is running headlessly.
+ Return a bool indicating if the disassembler is running without a GUI.
"""
pass
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 95128607..86200510 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -153,18 +153,6 @@ def bv(self, bv):
raise ValueError("BinaryView cannot be changed once set...")
self._bv = bv
- @property
- def version_major(self):
- return self._version_major
-
- @property
- def version_minor(self):
- return self._version_minor
-
- @property
- def version_patch(self):
- return self._version_patch
-
@property
def headless(self):
ret = None
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index fa6fbcc0..d966d107 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -86,18 +86,6 @@ def _init_version(self):
# Properties
#--------------------------------------------------------------------------
- @property
- def version_major(self):
- return self._version_major
-
- @property
- def version_minor(self):
- return self._version_minor
-
- @property
- def version_patch(self):
- return self._version_patch
-
@property
def headless(self):
return False
From f6932bd8d0371df403c57b3a646bb930f84189c5 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 May 2019 19:17:39 -0400
Subject: [PATCH 035/154] adds qt_mainthread global
---
plugin/lighthouse/util/qt/util.py | 80 +++++++++++++++++++++++++++++++
1 file changed, 80 insertions(+)
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 17311839..20088aae 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -1,6 +1,7 @@
import sys
import time
import logging
+import threading
from .shim import *
from ..misc import is_mainthread
@@ -275,3 +276,82 @@ def await_lock(lock):
#
raise RuntimeError("Failed to acquire lock after %f seconds!" % timeout)
+
+class QMainthread(QtCore.QObject):
+ """
+ A Qt object whose sole purpose is to execute code on the mainthread.
+ """
+ toMainthread = QtCore.pyqtSignal(object)
+ toMainthreadFast = QtCore.pyqtSignal(object)
+
+ def __init__(self):
+ super(QMainthread, self).__init__()
+
+ # helpers used to ensure thread safety
+ self._lock = threading.Lock()
+ self._fast_refs = []
+ self._result_queue = queue.Queue()
+
+ # signals used to communicate with the Qt mainthread
+ self.toMainthread.connect(self._execute_with_result)
+ self.toMainthreadFast.connect(self._execute_fast)
+
+ #--------------------------------------------------------------------------
+ # Public
+ #--------------------------------------------------------------------------
+
+ def execute(self, function):
+ """
+ Execute a function on the mainthread and wait for its return value.
+
+ This function is safe to call from any thread, at any time.
+ """
+
+ # if we are already on the mainthread, execute the callable inline
+ if is_mainthread():
+ return function()
+
+ # execute the callable on the mainthread and wait for it to complete
+ with self._lock:
+ self.toMainthread.emit(function)
+ result = self._result_queue.get()
+
+ # return the result of executing on the mainthread
+ return result
+
+ def execute_fast(self, function):
+ """
+ Execute a function on the mainthread without waiting for completion.
+ """
+
+ #
+ # append the given function to a reference list.
+ #
+ # I do this because I am not confident python / qt will guarantee the
+ # lifetime of the callable (function) as we cross threads and the
+ # callee scope/callstack dissolves away from beneath us
+ #
+ # this callable will be deleted from the ref list in _excute_fast()
+ #
+
+ self._fast_refs.append(function)
+
+ # signal to the mainthread that a new function is ready to execute
+ self.toMainthreadFast.emit(function)
+
+ #--------------------------------------------------------------------------
+ # Internal
+ #--------------------------------------------------------------------------
+
+ def _execute_with_result(self, function):
+ try:
+ self._result_queue.put(function())
+ except Exception as e:
+ logger.exception("QMainthread Exception")
+ self._result_queue.put(None)
+
+ def _execute_fast(self, function):
+ function()
+ self._fast_refs.remove(function)
+
+qt_mainthread = QMainthread()
From 9b1d579d3d9f64421a0e5cd332d8fd80a0ab7d58 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 May 2019 16:56:11 -0400
Subject: [PATCH 036/154] improve disassembler logging compatibility
---
plugin/lighthouse/util/disassembler/api.py | 7 +++++++
plugin/lighthouse/util/disassembler/binja_api.py | 3 +++
plugin/lighthouse/util/disassembler/ida_api.py | 3 +++
plugin/lighthouse/util/log.py | 5 +++--
4 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index 26182269..fd00b1e1 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -174,6 +174,13 @@ def set_function_name_at(self, function_address, new_name):
"""
pass
+ @abc.abstractmethod
+ def message(self, function_address, new_name):
+ """
+ Print a message to the disassembler console.
+ """
+ pass
+
#--------------------------------------------------------------------------
# UI API Shims
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 86200510..e3e9614d 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -273,6 +273,9 @@ def set_function_name_at(self, function_address, new_name):
self.bv.write(function_address, self.bv.read(function_address, 1))
+ def message(self, message):
+ print(message)
+
#--------------------------------------------------------------------------
# UI API Shims
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index d966d107..55e82ad7 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -148,6 +148,9 @@ def navigate(self, address):
def set_function_name_at(self, function_address, new_name):
idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN)
+ def message(self, message):
+ print(message)
+
#--------------------------------------------------------------------------
# UI API Shims
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py
index 2bb197fe..9d8de8fc 100644
--- a/plugin/lighthouse/util/log.py
+++ b/plugin/lighthouse/util/log.py
@@ -18,7 +18,7 @@ def lmsg(message):
# only print to disassembler if its output window is alive
if disassembler.is_msg_inited():
- print(prefix_message)
+ disassembler.message(prefix_message)
else:
logger.info(message)
@@ -54,7 +54,8 @@ def __init__(self, logger, stream, log_level=logging.INFO):
def write(self, buf):
for line in buf.rstrip().splitlines():
self._logger.log(self._log_level, line.rstrip())
- self._stream.write(buf)
+ if self._stream:
+ self._stream.write(buf)
def flush(self):
pass
From 7caa33df2ea2e850e360e0e829b523f61450aebc Mon Sep 17 00:00:00 2001
From: Dominik Maier
Date: Tue, 22 Oct 2019 21:03:17 +0200
Subject: [PATCH 037/154] Fixed python3 bytesstrings (#71)
---
coverage/frida/frida-drcov.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/coverage/frida/frida-drcov.py b/coverage/frida/frida-drcov.py
index 5a7c3f67..bb060559 100755
--- a/coverage/frida/frida-drcov.py
+++ b/coverage/frida/frida-drcov.py
@@ -227,12 +227,12 @@ def create_header(mods):
header_modules = '\n'.join(entries)
- return header + header_modules + '\n'
+ return ("%s%s\n" % (header, header_modules)).encode("utf-8")
# take the recv'd basic blocks, finish the header, and append the coverage
def create_coverage(data):
- bb_header = 'BB Table: %d bbs\n' % len(data)
- return bb_header + ''.join(data)
+ bb_header = b'BB Table: %d bbs\n' % len(data)
+ return bb_header + b''.join(data)
def on_message(msg, data):
#print(msg)
@@ -323,7 +323,7 @@ def main():
script.on('message', on_message)
script.load()
- print('[*] Now collecting info, control-D to terminate....')
+ print('[*] Now collecting info, control-C or control-D to terminate....')
sys.stdin.read()
From 7578faea828e6d0861ce6a0c120277c4fd4085b4 Mon Sep 17 00:00:00 2001
From: yrp
Date: Mon, 20 May 2019 21:53:06 -0700
Subject: [PATCH 038/154] Fix directory types
Tbh I didn't analyze this in depth -- it was a type error causing
lighthouse to bail out so I just mashed everything into a set. It seems
to work tho, so #shipit
---
plugin/lighthouse/director.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index b971d8bd..a9b3412b 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -582,9 +582,9 @@ def _optimize_coverage_data(self, coverage_addresses):
# presumably executed instructions
#
- block_instructions = []
+ block_instructions = set([])
for address in basic_blocks:
- block_instructions.extend(list(self.metadata.nodes[address].instructions))
+ block_instructions |= set(self.metadata.nodes[address].instructions)
# DONE
logger.debug("Optimized as basic block trace...")
From b505af995682901450542a82a671419f84ea9393 Mon Sep 17 00:00:00 2001
From: yrp
Date: Mon, 20 May 2019 21:50:08 -0700
Subject: [PATCH 039/154] Allow blank and comments in modoff
---
plugin/lighthouse/reader/parsers/modoff.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/plugin/lighthouse/reader/parsers/modoff.py b/plugin/lighthouse/reader/parsers/modoff.py
index aca4d209..63a629df 100644
--- a/plugin/lighthouse/reader/parsers/modoff.py
+++ b/plugin/lighthouse/reader/parsers/modoff.py
@@ -29,6 +29,14 @@ def _parse(self):
modules = collections.defaultdict(lambda: collections.defaultdict(int))
with open(self.filepath) as f:
for line in f:
+ trimmed = line.trim()
+
+ # skip empty lines
+ if not len(trimmed): continue
+
+ # comments can start with ';' or '#'
+ if trimmed[0] in [';', '#']: continue
+
module_name, bb_offset = line.rsplit("+", 1)
modules[module_name][int(bb_offset, 16)] += 1
self.modules = modules
From 8593e976d1448c25dae09bbee555eb379a8d2204 Mon Sep 17 00:00:00 2001
From: yrp
Date: Fri, 12 Jul 2019 23:18:12 -0700
Subject: [PATCH 040/154] Derp, syntax...
---
plugin/lighthouse/reader/parsers/modoff.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/reader/parsers/modoff.py b/plugin/lighthouse/reader/parsers/modoff.py
index 63a629df..4549a2b3 100644
--- a/plugin/lighthouse/reader/parsers/modoff.py
+++ b/plugin/lighthouse/reader/parsers/modoff.py
@@ -29,7 +29,7 @@ def _parse(self):
modules = collections.defaultdict(lambda: collections.defaultdict(int))
with open(self.filepath) as f:
for line in f:
- trimmed = line.trim()
+ trimmed = line.strip()
# skip empty lines
if not len(trimmed): continue
From ac12ef74db593e1bf2fea9a800c0f62ecdd85930 Mon Sep 17 00:00:00 2001
From: Jakob Pearson <40262817+Jakob6174@users.noreply.github.com>
Date: Tue, 22 Oct 2019 20:07:44 +0100
Subject: [PATCH 041/154] Minor changes, to compile on Windows with VS2017
(#69)
* Update build-x64.bat
* Update build-x86.bat
* Update ImageManager.cpp
* Update CodeCoverage.cpp
* Update CodeCoverage.cpp
* Update ImageManager.cpp
---
coverage/pin/CodeCoverage.cpp | 20 ++++++++++----------
coverage/pin/ImageManager.cpp | 4 ++--
coverage/pin/build-x64.bat | 3 ++-
coverage/pin/build-x86.bat | 1 +
4 files changed, 15 insertions(+), 13 deletions(-)
diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp
index 71e94ee3..270c8311 100644
--- a/coverage/pin/CodeCoverage.cpp
+++ b/coverage/pin/CodeCoverage.cpp
@@ -25,22 +25,22 @@ using unordered_map = std::tr1::unordered_map;
}
// Tool's arguments.
-static KNOB KnobModuleWhitelist(KNOB_MODE_APPEND, "pintool", "w", "",
+static KNOB KnobModuleWhitelist(KNOB_MODE_APPEND, "pintool", "w", "",
"Add a module to the white list. If none is specified, everymodule is white-listed. Example: libTIFF.dylib");
-static KNOB KnobLogFile(KNOB_MODE_WRITEONCE, "pintool", "l", "trace.log",
+static KNOB KnobLogFile(KNOB_MODE_WRITEONCE, "pintool", "l", "trace.log",
"Name of the output file. If none is specified, trace.log is used.");
// Return the file/directory name of a path.
-static string base_name(const string& path)
+static std::string base_name(const std::string& path)
{
#if defined(TARGET_WINDOWS)
#define PATH_SEPARATOR "\\"
#else
#define PATH_SEPARATOR "/"
#endif
- string::size_type idx = path.rfind(PATH_SEPARATOR);
- string name = (idx == string::npos) ? path : path.substr(idx + 1);
+ std::string::size_type idx = path.rfind(PATH_SEPARATOR);
+ std::string name = (idx == std::string::npos) ? path : path.substr(idx + 1);
return name;
}
@@ -129,7 +129,7 @@ static VOID OnThreadFini(THREADID tid, const CONTEXT* ctxt, INT32 c, VOID* v)
static VOID OnImageLoad(IMG img, VOID* v)
{
auto& context = *reinterpret_cast(v);
- string img_name = base_name(IMG_Name(img));
+ std::string img_name = base_name(IMG_Name(img));
ADDRINT low = IMG_LowAddress(img);
ADDRINT high = IMG_HighAddress(img);
@@ -251,14 +251,14 @@ static VOID OnFini(INT32 code, VOID* v)
int main(int argc, char* argv[])
{
- cout << "CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com)" << endl;
+ std::cout << "CodeCoverage tool by Agustin Gianni (agustingianni@gmail.com)" << std::endl;
// Initialize symbol processing
PIN_InitSymbols();
// Initialize PIN.
if (PIN_Init(argc, argv)) {
- cerr << "Error initializing PIN, PIN_Init failed!" << endl;
+ std::cerr << "Error initializing PIN, PIN_Init failed!" << std::endl;
return -1;
}
@@ -268,7 +268,7 @@ int main(int argc, char* argv[])
// Create a an image manager that keeps track of the loaded/unloaded images.
context->m_images = new ImageManager();
for (unsigned i = 0; i < KnobModuleWhitelist.NumberOfValues(); ++i) {
- cout << "White-listing image: " << KnobModuleWhitelist.Value(i) << endl;
+ std::cout << "White-listing image: " << KnobModuleWhitelist.Value(i) << std::endl;
context->m_images->addWhiteListedImage(KnobModuleWhitelist.Value(i));
// We will only enable tracing when any of the whitelisted images gets loaded.
@@ -276,7 +276,7 @@ int main(int argc, char* argv[])
}
// Create a trace file.
- cout << "Logging code coverage information to: " << KnobLogFile.ValueString() << endl;
+ std::cout << "Logging code coverage information to: " << KnobLogFile.ValueString() << std::endl;
context->m_trace = new TraceFile(KnobLogFile.ValueString());
// Handlers for thread creation and destruction.
diff --git a/coverage/pin/ImageManager.cpp b/coverage/pin/ImageManager.cpp
index b0225a1c..79aba679 100644
--- a/coverage/pin/ImageManager.cpp
+++ b/coverage/pin/ImageManager.cpp
@@ -11,7 +11,7 @@ ImageManager::~ImageManager()
PIN_RWMutexFini(&images_lock);
}
-VOID ImageManager::addImage(string image_name, ADDRINT lo_addr,
+VOID ImageManager::addImage(std::string image_name, ADDRINT lo_addr,
ADDRINT hi_addr)
{
PIN_RWMutexWriteLock(&images_lock);
@@ -25,7 +25,7 @@ VOID ImageManager::removeImage(ADDRINT low)
{
PIN_RWMutexWriteLock(&images_lock);
{
- set::iterator i = images.find(LoadedImage("", low));
+ std::set::iterator i = images.find(LoadedImage("", low));
if (i != images.end()) {
LoadedImage li = *i;
images.erase(i);
diff --git a/coverage/pin/build-x64.bat b/coverage/pin/build-x64.bat
index c80bef16..765602f8 100644
--- a/coverage/pin/build-x64.bat
+++ b/coverage/pin/build-x64.bat
@@ -45,6 +45,7 @@ link ^
/ignore:4049 ^
/ignore:4210 ^
/ignore:4217 ^
+ /ignore:4281 ^
/DLL CodeCoverage.obj ImageManager.obj
-del *.obj *.pdb *.exp *.lib
\ No newline at end of file
+del *.obj *.pdb *.exp *.lib
diff --git a/coverage/pin/build-x86.bat b/coverage/pin/build-x86.bat
index f279bbd1..ac89ddd0 100644
--- a/coverage/pin/build-x86.bat
+++ b/coverage/pin/build-x86.bat
@@ -44,6 +44,7 @@ link ^
/ignore:4049 ^
/ignore:4210 ^
/ignore:4217 ^
+ /ignore:4281 ^
/DLL CodeCoverage.obj ImageManager.obj
del *.obj *.pdb *.exp *.lib
From e4ecc0dafd6ad59706c595cea4106164868eaf49 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 23 Feb 2020 17:34:29 -0500
Subject: [PATCH 042/154] fixes minor Qt warning for IDA 7.4
---
plugin/lighthouse/ui/coverage_combobox.py | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index dae955e6..febb6842 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -84,6 +84,23 @@ def hidePopup(self):
# to be re-enlightened to direct mouse clicks (eg, to expand it). this
# undos the setAttribute action in showPopup() above.
#
+ # if the coverage combobox is *not* visible, the coverage window is
+ # probably being closed / deleted. but just in case, we should attempt
+ # to restore the combobox's ability to accept clicks before bailing.
+ #
+ # this fixes a bug / Qt warning first printed in IDA 7.4 where 'self'
+ # (the comobobox) would be deleted by the time the 100ms timer in the
+ # 'normal' case fires below
+ #
+
+ if not self.isVisible():
+ self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
+ return
+
+ #
+ # in the more normal case, the comobobox is simply being collapsed
+ # by the user clicking it, or clicking away from it.
+ #
# we use a short timer of 100ms to ensure the 'hiding' of the dropdown
# and its associated click are processed first. aftwards, it is safe to
# begin accepting clicks again.
From 3fd3640517d5b547e1e9c2fcd82e4f9c373ba611 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 23 Feb 2020 22:40:12 -0500
Subject: [PATCH 043/154] Improves coverage exception warning code
---
plugin/lighthouse/director.py | 20 ++--
plugin/lighthouse/exceptions.py | 189 ++++++++++++++++----------------
2 files changed, 102 insertions(+), 107 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index a9b3412b..f6514c6b 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -334,7 +334,7 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
Returns a tuple of (coverage, errors)
"""
- errors = []
+ errors = collections.defaultdict(list)
aggregate_addresses = set()
start = time.time()
@@ -351,12 +351,12 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
# save and suppress warnings generated from loading coverage files
except CoverageParsingError as e:
- errors.append(e)
+ errors[CoverageParsingError].append(e)
continue
# ensure some data was actually extracted from the log
if not coverage_addresses:
- errors.append(CoverageMissingError(filepath))
+ errors[CoverageMissingError].append(CoverageMissingError(filepath))
continue
# save the attribution data for this coverage data
@@ -376,9 +376,9 @@ def load_coverage_batch(self, filepaths, batch_name, progress_callback=logger.de
# evaluate coverage
if not coverage.nodes:
- errors.append(CoverageMappingAbsent(coverage))
+ errors[CoverageMappingAbsent].append(CoverageMappingAbsent(coverage))
elif coverage.suspicious:
- errors.append(CoverageMappingSuspicious(coverage))
+ errors[CoverageMappingSuspicious].append(CoverageMappingSuspicious(coverage))
#----------------------------------------------------------------------
end = time.time()
@@ -393,7 +393,7 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
Returns a tuple of (created_coverage, errors)
"""
- errors = []
+ errors = collections.defaultdict(list)
all_coverage = []
start = time.time()
@@ -426,12 +426,12 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
# save and suppress warnings generated from loading coverage files
except CoverageParsingError as e:
- errors.append(e)
+ errors[CoverageParsingError].append(e)
continue
# ensure some data was actually extracted from the log
if not coverage_addresses:
- errors.append(CoverageMissingError(filepath))
+ errors[CoverageMissingError].append(CoverageMissingError(filepath))
continue
# save the attribution data for this coverage data
@@ -449,9 +449,9 @@ def load_coverage_files(self, filepaths, progress_callback=logger.debug):
# evaluate coverage
if not coverage.nodes:
- errors.append(CoverageMappingAbsent(coverage))
+ errors[CoverageMappingAbsent].append(CoverageMappingAbsent(coverage))
elif coverage.suspicious:
- errors.append(CoverageMappingSuspicious(coverage))
+ errors[CoverageMappingSuspicious].append(CoverageMappingSuspicious(coverage))
# add the newly created coverage to the list of coverage to be returned
all_coverage.append(coverage)
diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py
index 6aadd791..2a61a080 100644
--- a/plugin/lighthouse/exceptions.py
+++ b/plugin/lighthouse/exceptions.py
@@ -1,4 +1,5 @@
from lighthouse.util.log import lmsg
+from lighthouse.util.misc import iteritems
from lighthouse.util.disassembler import disassembler
#------------------------------------------------------------------------------
@@ -12,50 +13,99 @@ class LighthouseError(Exception):
def __init__(self, *args, **kwargs):
super(LighthouseError, self).__init__(*args, **kwargs)
-class CoverageParsingError(LighthouseError):
+#------------------------------------------------------------------------------
+# Coverage File Exceptions
+#------------------------------------------------------------------------------
+
+class CoverageException(LighthouseError):
"""
- An error generated by the CoverageReader when all parsers fail.
+ A class of errors pertaining to loading & mapping coverage files.
"""
- def __init__(self, filepath, tracebacks):
- super(CoverageParsingError, self).__init__("Failed to parse coverage file")
+ name = NotImplementedError
+ description = NotImplementedError
+
+ def __init__(self, message, filepath):
+ super(CoverageException, self).__init__(message)
self.filepath = filepath
- self.tracebacks = tracebacks
+
+ @property
+ def verbose(self):
+ return "Error: %s\n\n%s" % (self.name, self.description)
def __str__(self):
return self.message + " '%s'" % self.filepath
-class CoverageMissingError(LighthouseError):
+class CoverageParsingError(CoverageException):
"""
- An error generated when no data was extracted from a CoverageFile.
+ An error generated by the CoverageReader when all parsers fail.
"""
- def __init__(self, filepath):
- super(CoverageMissingError, self).__init__("No coverage extracted from file")
- self.filepath = filepath
+ name = "PARSE_FAILURE"
+ description = \
+ "Failed to parse one or more of the selected coverage files!\n\n" \
+ " Possible reasons:\n" \
+ " - You selected a file that was *not* a coverage file.\n" \
+ " - The selected coverage file is malformed or unreadable.\n" \
+ " - A suitable parser for the coverage file is not installed.\n\n" \
+ "Please see the disassembler console for more info..."
- def __str__(self):
- return self.message + " '%s'" % self.filepath
+ def __init__(self, filepath, tracebacks):
+ super(CoverageParsingError, self).__init__("Failed to parse coverage file", filepath)
+ self.tracebacks = tracebacks
-class CoverageMappingSuspicious(LighthouseError):
+class CoverageMissingError(CoverageException):
"""
- A warning generated when coverage data does not appear to match the database.
+ An error generated when no data was extracted from a CoverageFile.
"""
- def __init__(self, coverage):
- super(CoverageMappingSuspicious, self).__init__("Coverage data appears badly mapped")
- self.coverage = coverage
+ name = "NO_COVERAGE_ERROR"
+ description = \
+ "No usable coverage data was extracted from one of the selected files.\n\n" \
+ " Possible reasons:\n" \
+ " - You selected a coverage file for the wrong binary.\n" \
+ " - The name of the executable file used to generate this database\n" \
+ " is different than the one you collected coverage against.\n" \
+ " - Your DBI failed to collect any coverage for this binary.\n\n" \
+ "Please see the disassembler console for more info..."
- def __str__(self):
- return self.message + " for coverage '%s'" % self.coverage.name
+ def __init__(self, filepath):
+ super(CoverageMissingError, self).__init__("No coverage extracted from file", filepath)
-class CoverageMappingAbsent(LighthouseError):
+class CoverageMappingAbsent(CoverageException):
"""
A warning generated when coverage data cannot be mapped.
"""
+ name = "NO_COVERAGE_MAPPED"
+ description = \
+ "One or more of the loaded coverage files has no visibly mapped data.\n\n" \
+ " Possible reasons:\n" \
+ " - The loaded coverage data does not fall within defined functions.\n" \
+ " - You loaded an absolute address trace with a different imagebase.\n" \
+ " - The coverage file might be corrupt or malformed.\n\n" \
+ "Please see the disassembler console for more info..."
+
def __init__(self, coverage):
- super(CoverageMappingAbsent, self).__init__("No coverage data could be mapped")
+ super(CoverageMappingAbsent, self).__init__("No coverage data could be mapped", coverage.filepath)
self.coverage = coverage
- def __str__(self):
- return self.message + " for coverage '%s'" % self.coverage.name
+class CoverageMappingSuspicious(CoverageException):
+ """
+ A warning generated when coverage data does not appear to match the database.
+ """
+ name = "BAD_COVERAGE_MAPPING"
+ description = \
+ "One or more of the loaded coverage files appears to be badly mapped.\n\n" \
+ " Possible reasons:\n" \
+ " - You selected a coverage file that was collected against a\n" \
+ " slightly different version of the binary.\n" \
+ " - You recorded self-modifying code or something with very\n" \
+ " abnormal control flow (obfuscated code, malware, packers).\n" \
+ " - The coverage file might be corrupt or malformed.\n\n" \
+ "This means that any coverage displayed by Lighthouse is PROBABLY\n" \
+ "WRONG and is not be trusted because the coverage data does not\n." \
+ "appear to match the disassembled binary."
+
+ def __init__(self, coverage):
+ super(CoverageMappingSuspicious, self).__init__("Coverage data appears badly mapped", coverage.filepath)
+ self.coverage = coverage
#------------------------------------------------------------------------------
# UI Warnings
@@ -65,80 +115,25 @@ def warn_errors(errors):
"""
Warn the user of any encountered errors with a messagebox.
"""
- seen = []
- error_map = \
- {
- CoverageParsingError: warn_coverage_parsing,
- CoverageMissingError: warn_coverage_missing,
- CoverageMappingAbsent: warn_mapping_absent,
- CoverageMappingSuspicious: warn_mapping_suspicious,
- }
-
- for error in errors:
- error_type = type(error)
-
- lmsg(error)
- if error_type in seen:
- return
-
- try:
- error_map[error_type](error)
- except KeyError:
- raise NotImplementedError("UNKNOWN ERROR OCCURRED")
-
- seen.append(error_type)
-
-def warn_coverage_parsing(error):
- """
- Display a warning for malformed/unreadable coverage files.
- """
- disassembler.warning(
- "Failed to parse one or more of the selected coverage files!\n\n"
- " Possible reasons:\n"
- " - You selected a file that was *not* a coverage file.\n"
- " - The selected coverage file is malformed or unreadable.\n"
- " - A suitable parser for the coverage file is not installed.\n\n"
- "Please see the disassembler console for more info..."
- )
-def warn_coverage_missing(error):
- """
- Display a warning for missing coverage data.
- """
- disassembler.warning(
- "No usable coverage data was extracted from one of the selected files.\n\n"
- " Possible reasons:\n"
- " - You selected a coverage file for the wrong binary.\n"
- " - The name of the executable file used to generate this database\n"
- " is different than the one you collected coverage against.\n"
- " - Your DBI failed to collect any coverage for this binary.\n\n"
- "Please see the disassembler console for more info..."
- )
+ for error_type, error_list in iteritems(errors):
-def warn_mapping_absent(error):
- """
- Display a warning when no coverage data gets mapped.
- """
- disassembler.warning(
- "One or more of the loaded coverage files has no visibly mapped data.\n\n"
- " Possible reasons:\n"
- " - The loaded coverage data does not fall within defined functions.\n"
- " - You loaded an absolute address trace with a different imagebase.\n"
- " - The coverage file might be corrupt.\n\n"
- "Please see the disassembler console for more info..."
- )
+ #
+ # loop through the individual instances/files that caused this error
+ # and dump the results to the disassembler console...
+ #
-def warn_mapping_suspicious(error):
- """
- Display a warning for badly mapped coverage data.
- """
- disassembler.warning(
- "One or more of the loaded coverage files appears to be badly mapped.\n\n"
- " Possible reasons:\n"
- " - You selected a coverage file that was collected against a\n"
- " slightly different version of the binary.\n"
- " - You recorded an application with very abnormal control flow.\n"
- " - The coverage file might be corrupt.\n\n"
- "This means that any coverage displayed by Lighthouse is probably\n"
- "wrong, and should be used at your own discretion."
- )
+ lmsg("-"*50)
+ lmsg("Files reporting %s:" % error_type.name)
+ for error in error_list:
+ lmsg(" - %s" % error.filepath)
+
+ #
+ # popup a more verbose error messagebox for the user to read regarding
+ # this class of error they encountered
+ #
+
+ disassembler.warning(error.verbose)
+
+ # done ...
+ lmsg("-"*50)
From c0afc3b0c1bae488793cfe6e04093cdf98fe4b29 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 27 Feb 2020 20:30:00 -0500
Subject: [PATCH 044/154] create 'is disassembelr busy' api
---
plugin/lighthouse/util/disassembler/api.py | 7 +++++++
plugin/lighthouse/util/disassembler/binja_api.py | 4 ++++
plugin/lighthouse/util/disassembler/ida_api.py | 4 ++++
3 files changed, 15 insertions(+)
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index fd00b1e1..c3a25db7 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -71,6 +71,13 @@ def headless(self):
"""
pass
+ @abc.abstractproperty
+ def busy(self):
+ """
+ Return a bool indicating if the disassembler is busy / processing.
+ """
+ pass
+
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index e3e9614d..2fd0c4c7 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -163,6 +163,10 @@ def headless(self):
ret = binaryninja.core_ui_enabled
return not ret
+ @propery
+ def busy(self):
+ return False # TODO
+
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 55e82ad7..ec8c38f1 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -90,6 +90,10 @@ def _init_version(self):
def headless(self):
return False
+ @property
+ def busy(self):
+ return not(idaapi.auto_is_ok())
+
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
From e70c24663791e7b0449eb777d76ef071fcf344cb Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 27 Feb 2020 20:32:58 -0500
Subject: [PATCH 045/154] make BADADDR more 'universal'
---
plugin/lighthouse/coverage.py | 2 --
plugin/lighthouse/metadata.py | 2 +-
plugin/lighthouse/util/misc.py | 2 ++
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index 857224e6..4859c084 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -32,8 +32,6 @@
# get updated or refreshed by the user.
#
-BADADDR = 0xFFFFFFFFFFFFFFFF
-
#------------------------------------------------------------------------------
# Database Coverage
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 2e0a879d..29a89673 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -63,7 +63,7 @@ def __init__(self):
# name & imagebase of the executable this metadata is based on
self.filename = ""
- self.imagebase = -1
+ self.imagebase = BADADDR
# database metadata cache status
self.cached = False
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index 6da0ba4b..d3803b84 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -6,6 +6,8 @@
from .python import *
+BADADDR = 0xFFFFFFFFFFFFFFFF
+
#------------------------------------------------------------------------------
# Plugin Util
#------------------------------------------------------------------------------
From 1ddffa5a19715970cde91d662819600c5c8a96ae Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 21 Mar 2020 00:27:53 -0400
Subject: [PATCH 046/154] most of the plumbing for supporting an image rebase,
barring more testin...
---
plugin/lighthouse/core.py | 26 +++++++++++
plugin/lighthouse/coverage.py | 53 +++++++++++++++++++++--
plugin/lighthouse/director.py | 34 ++++++++++++++-
plugin/lighthouse/metadata.py | 39 +++++++++++++++--
plugin/lighthouse/painting/painter.py | 48 ++++++++++++++++++++
plugin/lighthouse/ui/coverage_overview.py | 5 ++-
plugin/lighthouse/ui/coverage_settings.py | 2 +-
plugin/lighthouse/ui/coverage_table.py | 2 +
8 files changed, 199 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 71cb8f3c..d88ecf08 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -69,6 +69,11 @@ def _init(self):
# the directory to start the coverage file dialog in
self._last_directory = None
+ # a timed callback for lighthouse to check for certain state changes
+ self._scheduled = QtCore.QTimer()
+ self._scheduled.timeout.connect(self.scheduled)
+ #self._scheduled.start(1000) # TODO: re-enable once more testing is done...
+
def print_banner(self):
"""
Print the plugin banner.
@@ -103,6 +108,7 @@ def _cleanup(self):
"""
Spin down any lingering core components before plugin unload.
"""
+ self._scheduled.stop()
self.painter.terminate()
self.director.terminate()
self.metadata.terminate()
@@ -425,3 +431,23 @@ def _select_coverage_files(self):
# return the captured filenames
return filenames
+
+ #--------------------------------------------------------------------------
+ # Scheduled
+ #--------------------------------------------------------------------------
+
+ @disassembler.execute_read
+ def scheduled(self):
+ metadata = self.director.metadata
+
+ # get current imagebase
+ base = disassembler.get_imagebase()
+ lmsg("Imagebase: 0x%08x" % base)
+
+ # detect an image rebase
+ if (metadata.cached and base != metadata.imagebase) and not disassembler.busy:
+ lmsg("Image rebase detected, rebasing Lighthouse metadata...")
+ self.director.refresh()
+
+ # schedule the next update
+ self._scheduled.start(1000)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index 4859c084..b3bd66b9 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -97,6 +97,7 @@ def __init__(self, palette, name="", filepath=None, data=None):
#
self._hitmap = build_hitmap(data)
+ self._imagebase = BADADDR
#
# the coverage hash is a simple hash of the coverage mask (hitmap keys)
@@ -239,6 +240,48 @@ def update_metadata(self, metadata, delta=None):
Install a new databasee metadata object.
"""
self._metadata = weakref.proxy(metadata)
+
+ #
+ # if the underlying database / metadata gets rebased, we will need to
+ # rebase our coverage data. the 'raw' coverage data stored in the
+ # hitmap is stored as absolute addresses for performance reasons
+ #
+ # here we compute the offset that we will need to rebase the coverage
+ # data by should a rebase have occurred
+ #
+
+ rebase_offset = self._metadata.imagebase - self._imagebase
+
+ #
+ # if the coverage's imagebase is still BADADDR, that means that this
+ # coverage object hasn't yet been mapped onto a given metadata cache.
+ #
+ # that's fine, we just need to initialize our imagebase which should
+ # (hopefully!) match the imagebase originally used when baking the
+ # coverage data into an absolute address form.
+ #
+
+ if self._imagebase == BADADDR:
+ self._imagebase = self._metadata.imagebase
+
+ #
+ # if the imagebase for this coverage exists, then it is susceptible to
+ # being rebased by a metadata update. if rebase_offset is non-zero,
+ # this is an indicator that a rebase has occurred.
+ #
+ # when a rebase occurs in the metadata, we must also rebase our
+ # coverage data (stored in the hitmap)
+ #
+
+ elif rebase_offset:
+ self._hitmap = { (address + rebase_offset): hits for address, hits in iteritems(self._hitmap) }
+ self._imagebase = self._metadata.imagebase
+
+ #
+ # since the metadata has been updated in one form or another, we need
+ # to trash our existing coverage mapping, and rebuild it from the data.
+ #
+
self.unmap_all()
def refresh(self):
@@ -574,11 +617,15 @@ def unmap_all(self):
"""
Unmap all mapped coverage data.
"""
- self._unmapped_data = set(self._hitmap.keys())
- self._unmapped_data.add(BADADDR)
- self._misaligned_data = set()
+
+ # clear out the processed / computed coverage data structures
self.nodes = {}
self.functions = {}
+ self._misaligned_data = set()
+
+ # dump the source coverage data back into an 'unmapped' state
+ self._unmapped_data = set(self._hitmap.keys())
+ self._unmapped_data.add(BADADDR)
#--------------------------------------------------------------------------
# Debug
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index f6514c6b..19581c66 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -5,6 +5,7 @@
import traceback
import collections
+from lighthouse.util.qt import flush_qt_events
from lighthouse.util.misc import *
from lighthouse.util.python import *
from lighthouse.util.qt import await_future, await_lock, color_text
@@ -200,6 +201,9 @@ def __init__(self, metadata, palette):
self._coverage_created_callbacks = []
self._coverage_deleted_callbacks = []
+ # director callbacks
+ self._refreshed_callbacks = []
+
def terminate(self):
"""
Cleanup & terminate the director.
@@ -302,6 +306,18 @@ def _notify_coverage_deleted(self):
"""
notify_callback(self._coverage_deleted_callbacks)
+ def refreshed(self, callback):
+ """
+ Subscribe a callback for director refresh events.
+ """
+ register_callback(self._refreshed_callbacks, callback)
+
+ def _notify_refreshed(self):
+ """
+ Notify listeners of a director refresh event.
+ """
+ notify_callback(self._refreshed_callbacks)
+
#----------------------------------------------------------------------
# Batch Loading
#----------------------------------------------------------------------
@@ -1298,14 +1314,28 @@ def refresh(self):
"""
Complete refresh of the director and mapped coverage.
"""
- logger.debug("Refreshing the CoverageDirector")
+ lmsg("Refreshing Lighthouse...")
- # (re)build our metadata cache of the underlying database
+ #
+ # refreshing might take awhile, so pop a waitbox that should update
+ # with status messages as the refresh runs...
+ #
+
+ disassembler.show_wait_box("Refreshing Lighthouse...")
+ flush_qt_events()
+
+ # (re) build our metadata cache of the underlying database
self.metadata.refresh()
# (re)map each set of loaded coverage data to the database
self._refresh_database_coverage()
+ # notify of full-refresh
+ self._notify_refreshed()
+
+ # all done ...
+ disassembler.hide_wait_box()
+
def _refresh_database_coverage(self):
"""
Refresh all the database coverage mappings managed by the director.
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 29a89673..82894214 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -93,6 +93,7 @@ def __init__(self):
self._metadata_modified_callbacks = []
self._function_renamed_callbacks = []
+ self._rebased_callbacks = []
def terminate(self):
"""
@@ -399,6 +400,23 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
if self._rename_hooks:
self._rename_hooks.unhook()
+ # grab the cached imagebase as it might have changed
+ prev_imagebase = self.imagebase
+ was_cached = self.cached
+
+ # refresh high level database properties that we wish to cache
+ self._sync_refresh_properties()
+
+ #
+ # if a rebase occured, trash all present metadata as its easier to
+ # rebuild the cache from scratch...
+ #
+
+ if prev_imagebase != self.imagebase:
+ self.nodes = {}
+ self.functions = {}
+ self.instructions = []
+
#
# if the caller provided no function addresses to target for refresh,
# we will perform a complete metadata refresh of all database defined
@@ -411,9 +429,6 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
)()
function_addresses = list(set(function_addresses+list(self.functions)))
- # refresh high level database properties that we wish to cache
- self._sync_refresh_properties()
-
# refresh the core database metadata asynchronously
if is_async:
completed = self._async_collect_metadata(function_addresses, progress_callback)
@@ -460,6 +475,10 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
if completed:
self.cached = True
+ # detect & notify of a rebase event
+ if was_cached and (prev_imagebase != self.imagebase):
+ self._notify_rebased(prev_imagebase, self.imagebase)
+
# return true/false to indicates completion
return completed
@@ -662,6 +681,20 @@ def _notify_function_renamed(self):
"""
notify_callback(self._function_renamed_callbacks)
+ def rebased(self, callback):
+ """
+ Subscribe a callback for director rebasing events.
+ """
+ register_callback(self._rebased_callbacks, callback)
+
+ def _notify_rebased(self, old_imagebase, new_imagebase):
+ """
+ Notify listeners of a database rebasing event.
+
+ TODO/FUTURE: send old / new imagebases
+ """
+ notify_callback(self._rebased_callbacks)
+
#------------------------------------------------------------------------------
# Function Metadata
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index d3a5398a..c5ef82a3 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -18,6 +18,7 @@ class DatabasePainter(object):
MSG_TERMINATE = 0
MSG_REPAINT = 1
MSG_CLEAR = 2
+ MSG_REBASE = 3
def __init__(self, director, palette):
@@ -38,6 +39,7 @@ def __init__(self, director, palette):
# instruction addresses and graph nodes it has painted.
#
+ self._imagebase = BADADDR
self._painted_nodes = set()
self._painted_instructions = set()
@@ -75,6 +77,7 @@ def __init__(self, director, palette):
# register for cues from the director
self._director.coverage_switched(self.repaint)
self._director.coverage_modified(self.repaint)
+ self._director.refreshed(self.check_rebase)
#--------------------------------------------------------------------------
# Status
@@ -140,6 +143,13 @@ def clear_paint(self):
# trigger the database clear
self._msg_queue.put(self.MSG_CLEAR)
+ def check_rebase(self):
+ """
+ Perform a rebase on the painted data cache (if necessary).
+ """
+ self._msg_queue.put(self.MSG_REBASE)
+ self._msg_queue.put(self.MSG_REPAINT)
+
#--------------------------------------------------------------------------
# Commands
#--------------------------------------------------------------------------
@@ -290,6 +300,14 @@ def _paint_database(self):
start = time.time()
#------------------------------------------------------------------
+ # initialize imagebase if it hasn't been already...
+ if self._imagebase == BADADDR:
+ self._imagebase = db_metadata.imagebase
+
+ # abandon painting early if it appears a rebase has occurred
+ elif self._imagebase != db_metadata.imagebase:
+ return False
+
# immediately paint user-visible regions of the database
if not self._priority_paint():
return False # a repaint was requested
@@ -347,6 +365,32 @@ def _clear_database(self):
# paint finished successfully
return True
+ def _rebase_database(self):
+ """
+ Rebase the active database paint.
+
+ TODO/XXX: there may be some edgecases where painting can be wrong if
+ a rebase occurs while the painter is running.
+ """
+ db_metadata = self._director.metadata
+ instructions = db_metadata.instructions
+ nodes = db_metadata.nodes.viewvalues()
+
+ # a rebase has not occurred
+ if not db_metadata.cached or (db_metadata.imagebase == self._imagebase):
+ return False
+
+ # compute the offset of the rebase
+ rebase_offset = db_metadata.imagebase - self._imagebase
+
+ # rebase the cached addresses of what we have painted
+ self._painted_nodes = set([address+rebase_offset for address in self._painted_nodes])
+ self._painted_instructions = set([address+rebase_offset for address in self._painted_instructions])
+ self._imagebase = db_metadata.imagebase
+
+ # a rebase has been observed
+ return True
+
#--------------------------------------------------------------------------
# Priority Painting
#--------------------------------------------------------------------------
@@ -402,6 +446,10 @@ def _async_database_painter(self):
elif action == self.MSG_CLEAR:
result = self._clear_database()
+ # check for a rebase of the painted data
+ elif action == self.MSG_REBASE:
+ result = self._rebase_database()
+
# spin down the painting thread (this thread)
elif action == self.MSG_TERMINATE:
break
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 0ce40c09..fbd640a0 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -39,6 +39,9 @@ def __init__(self, core):
# refresh the data UI such that it reflects the most recent data
self.refresh()
+ # register for cues from the director
+ self._core.director.refreshed(self.refresh)
+
#--------------------------------------------------------------------------
# Pseudo Widget Functions
#--------------------------------------------------------------------------
@@ -58,7 +61,7 @@ def show(self):
#
if not self._core.director.metadata.cached:
- self._table_controller.refresh_metadata()
+ self._core.director.refresh()
def terminate(self):
"""
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 4d119ffd..8c1605a0 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -95,7 +95,7 @@ def connect_signals(self, controller, core):
"""
Connect UI signals.
"""
- self._action_refresh_metadata.triggered.connect(controller.refresh_metadata)
+ self._action_refresh_metadata.triggered.connect(core.director.refresh)
self._action_hide_zero.triggered[bool].connect(controller._model.filter_zero_coverage)
self._action_pause_paint.triggered[bool].connect(lambda x: core.painter.set_enabled(not x))
self._action_clear_paint.triggered.connect(core.painter.clear_paint)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 3c1da003..970cfe7a 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -552,6 +552,8 @@ def toggle_column_alignment(self, column):
def refresh_metadata(self):
"""
Hard refresh of the director and table metadata layers.
+
+ TODO: remove
"""
disassembler.show_wait_box("Building database metadata...")
self._model._director.refresh()
From 2eab6d902c5967fa1f1dff5d194bdc5cc17b479c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 21 Mar 2020 04:34:08 -0400
Subject: [PATCH 047/154] python 3 compat tweaks
---
plugin/lighthouse/director.py | 2 +-
plugin/lighthouse/painting/painter.py | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 19581c66..d37f71f4 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -552,7 +552,7 @@ def _optimize_coverage_data(self, coverage_addresses):
# bucketize coverage addresses
instructions = addresses & set(self.metadata.instructions)
- basic_blocks = instructions & self.metadata.nodes.viewkeys()
+ basic_blocks = instructions & viewkeys(self.metadata.nodes)
unknown = addresses - instructions
# bucketize the uncategorized addresses
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index c5ef82a3..f2cb7c6f 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -352,7 +352,7 @@ def _clear_database(self):
"""
db_metadata = self._director.metadata
instructions = db_metadata.instructions
- nodes = db_metadata.nodes.viewvalues()
+ nodes = viewvalues(db_metadata.nodes)
# clear all instructions
if not self._async_action(self._clear_instructions, instructions):
@@ -374,7 +374,7 @@ def _rebase_database(self):
"""
db_metadata = self._director.metadata
instructions = db_metadata.instructions
- nodes = db_metadata.nodes.viewvalues()
+ nodes = viewvalues(db_metadata.nodes)
# a rebase has not occurred
if not db_metadata.cached or (db_metadata.imagebase == self._imagebase):
From b9d514823ba8bd945b608fc58f527051a01879e1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 21 Mar 2020 04:35:53 -0400
Subject: [PATCH 048/154] shady temp fix to make lighthouse mostly work with
current binja
---
.../lighthouse/util/disassembler/binja_api.py | 76 +++++--------------
plugin/lighthouse/util/qt/shim.py | 13 ++--
plugin/lighthouse/util/qt/util.py | 7 --
3 files changed, 25 insertions(+), 71 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 2fd0c4c7..ffa5000d 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -7,36 +7,9 @@
import binaryninja
from binaryninja import PythonScriptingInstance, binaryview
+from binaryninjaui import DockHandler, DockContextHandler, UIContext
from binaryninja.plugin import BackgroundTaskThread
-#------------------------------------------------------------------------------
-# External PyQt5 Dependency
-#------------------------------------------------------------------------------
-#
-# amend the Python import path with a Libs folder for additional pip
-# packages required by Lighthouse (at least on Windows, and maybe macOS)
-#
-# TODO/FUTURE: it is kind of dirty that we have to do this here. maybe it
-# can be moved with a later refactor. in the long run, binary ninja will
-# ship with PyQt5 bindings in-box.
-#
-
-binja_user_plugin_path=None
-# Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
-try:
- binja_user_plugin_path=binaryninja.user_plugin_path()
-except TypeError:
- binja_user_plugin_path=binaryninja.user_plugin_path
-
-DEPENDENCY_PATH = os.path.join(
- binja_user_plugin_path,
- "Lib",
- "site-packages"
-)
-sys.path.append(DEPENDENCY_PATH)
-
-#------------------------------------------------------------------------------
-
from .api import DisassemblerAPI, DockableShim
from ..qt import *
from ..misc import is_mainthread, not_mainthread
@@ -163,7 +136,7 @@ def headless(self):
ret = binaryninja.core_ui_enabled
return not ret
- @propery
+ @property
def busy(self):
return False # TODO
@@ -395,43 +368,28 @@ def __init__(self, window_title, icon_path):
super(DockableWindow, self).__init__(window_title, icon_path)
# configure dockable widget container
- self._main_window = get_qt_main_window()
- self._widget = QtWidgets.QWidget()
- self._dockable = QtWidgets.QDockWidget(window_title, self._main_window)
- self._dockable.setWidget(self._widget)
- self._dockable.setWindowIcon(self._window_icon)
- self._dockable.setAttribute(QtCore.Qt.WA_DeleteOnClose)
- self._dockable.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding,
- QtWidgets.QSizePolicy.Expanding
- )
+ self._active_context = UIContext.allContexts()[0]
+ self._main_window = self._active_context.mainWindow()
+ self._dock_handler = self._main_window.findChild(DockHandler, '__DockHandler')
+ self._widget = QtWidgets.QWidget(self._dock_handler.parent())
+ self._dock_contxet = DockContextHandler(self._widget, self._window_title)
+
self._widget.setSizePolicy(
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding
)
# dock the widget on the right side of Binja
- self._main_window.addDockWidget(
- QtCore.Qt.RightDockWidgetArea,
- self._dockable
- )
-
- def show(self):
-
- #
- # NOTE/HACK:
- # this is a little dirty, but is used to set the default width
- # of the coverage overview / dockable widget when it is first shown
- #
-
- default_width = self._widget.sizeHint().width()
- self._dockable.setMinimumWidth(default_width)
-
- # show the widget
- self._dockable.show()
+ self._dock_handler.addDockWidget(self._widget, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, True, False)
+ self._dockable = self._dock_handler.getDockWidget(self._window_title)
- # undo the HACK
- self._dockable.setMinimumWidth(0)
+ self._dockable = QtWidgets.QDockWidget(window_title, self._main_window)
+ self._dockable.setWindowIcon(self._window_icon)
+ self._dockable.setAttribute(QtCore.Qt.WA_DeleteOnClose)
+ self._dockable.setSizePolicy(
+ QtWidgets.QSizePolicy.Expanding,
+ QtWidgets.QSizePolicy.Expanding
+ )
#------------------------------------------------------------------------------
# Binary Ninja Hacks XXX / TODO / V35
diff --git a/plugin/lighthouse/util/qt/shim.py b/plugin/lighthouse/util/qt/shim.py
index 20517d92..afa350ab 100644
--- a/plugin/lighthouse/util/qt/shim.py
+++ b/plugin/lighthouse/util/qt/shim.py
@@ -24,6 +24,7 @@
#
USING_PYQT5 = False
+USING_PYSIDE2 = False
#------------------------------------------------------------------------------
# PyQt5 Compatibility
@@ -45,22 +46,24 @@
pass
#------------------------------------------------------------------------------
-# PySide Compatibility
+# PySide2 Compatibility
#------------------------------------------------------------------------------
# if PyQt5 did not import, try to load PySide
if QT_AVAILABLE == False:
try:
- import PySide.QtGui as QtGui
- import PySide.QtCore as QtCore
+ import PySide2.QtGui as QtGui
+ import PySide2.QtCore as QtCore
+ import PySide2.QtWidgets as QtWidgets
- # alias for less PySide <--> PyQt5 shimming
- QtWidgets = QtGui
+ # 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
+ USING_PYQT5 = True # TODO: remove once PySide v1 is fully ripped out...
# import failed. No Qt / UI bindings available...
except ImportError:
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 20088aae..d19525d3 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -54,13 +54,6 @@ def get_qt_icon(name):
icon_type = getattr(QtWidgets.QStyle, name)
return QtWidgets.QApplication.style().standardIcon(icon_type)
-def get_qt_main_window():
- """
- Get the QMainWindow instance for the current Qt runtime.
- """
- app = QtCore.QCoreApplication.instance()
- return [x for x in app.allWidgets() if x.__class__ is QtWidgets.QMainWindow][0]
-
def get_default_font_size():
"""
Get the default font size for this QApplication.
From 29ecafd885ba78c7bc073ce3a6969791d559274a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 22 Mar 2020 03:10:27 -0400
Subject: [PATCH 049/154] fixes bug where binja could hang while building
metadata
---
plugin/lighthouse/metadata.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 82894214..1b64a049 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -1030,6 +1030,7 @@ def _binja_build_metadata(self):
while current_address < node_end:
instruction_size = bv.get_instruction_length(current_address)
+ instruction_size = instruction_size if instruction_size else 1 # TODO/HACK: binja can return 0 for undef/bad inst
self.instructions[current_address] = instruction_size
current_address += instruction_size
From c9788723bacca88b23a8d73fcc1658c4dc7197ba Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 25 Mar 2020 03:11:04 -0400
Subject: [PATCH 050/154] remove unecessary use of map, can cause ambiguity if
left unevaluated
---
plugin/lighthouse/director.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index d37f71f4..e0e57f98 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -525,7 +525,7 @@ def _extract_coverage_data(self, coverage_file):
try:
coverage_offsets = coverage_file.get_offsets(module_name)
- coverage_addresses = map(lambda x: imagebase+x, coverage_offsets)
+ coverage_addresses = [imagebase+x for x in coverage_offsets]
return coverage_addresses
except NotImplementedError:
pass
From c57296e649233144473fa7c6e047da276e8ab4b1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 25 Mar 2020 03:11:58 -0400
Subject: [PATCH 051/154] fix improper usage of traceback
---
plugin/lighthouse/reader/coverage_reader.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index c949447f..68888391 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -41,7 +41,7 @@ def open(self, filepath):
# log the exceptions for each parse failure
except Exception as e:
- parse_failures[name] = traceback.format_exc(e)
+ parse_failures[name] = traceback.format_exc()
logger.debug("| Parse FAILED")
#
From e0d309025fe558b18f25eb52423653c812e0de3b Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 25 Mar 2020 06:22:45 -0400
Subject: [PATCH 052/154] improve lighthouse's accuracy on interleaved
instructions
---
plugin/lighthouse/coverage.py | 27 +++++++++++++--------------
plugin/lighthouse/metadata.py | 12 ++++++++----
2 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index b3bd66b9..e1c18772 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -511,9 +511,6 @@ def _map_nodes(self):
node_coverage = NodeCoverage(node_metadata.address, self._weak_self)
self.nodes[node_metadata.address] = node_coverage
- # compute the end address of the current basic block
- node_end = node_metadata.address + 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
@@ -531,18 +528,20 @@ def _map_nodes(self):
# discarding its address from the unmapped data list
#
- if address in node_metadata.instructions:
- node_coverage.executed_instructions[address] = self._hitmap[address]
- self._unmapped_data.discard(address)
+ node_coverage.executed_instructions[address] = self._hitmap[address]
+ self._unmapped_data.discard(address)
- #
- # if the given address allegedly falls within this node's
- # address range, but doesn't line up with the known
- # instructions, log it as 'misaligned' / suspicious
- #
+ ##
+ ## if the given address allegedly falls within this node's
+ ## address range, but doesn't line up with the known
+ ## instructions, log it as 'misaligned' / suspicious
+ ##
+ ## TODO / XXX: This will need to be moved as instruction to
+ ## node mapping is now guaranteed
+ ##
- else:
- self._misaligned_data.add(address)
+ #else:
+ # self._misaligned_data.add(address)
# get the next address to attempt mapping on
try:
@@ -557,7 +556,7 @@ def _map_nodes(self):
# of this loop and send it through the full node lookup path
#
- if not (node_metadata.address <= address < node_end):
+ if not (address in node_metadata.instructions):
coverage_addresses.appendleft(address)
break
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 1b64a049..a533dddf 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -76,10 +76,13 @@ def __init__(self):
# internal members to help index & navigate the cached metadata
self._stale_lookup = False
self._name2func = {}
- self._last_node = [] # HACK: blank iterable for now
self._node_addresses = []
self._function_addresses = []
+ # HACK: dirty hack since we can't create a blank node easily
+ self._last_node = lambda: None
+ self._last_node.instructions = []
+
# placeholder attribute for disassembler event hooks
self._rename_hooks = None
@@ -149,7 +152,7 @@ def get_node(self, address):
assert not self._stale_lookup, "Stale metadata is unsafe to use..."
# fast path, effectively a LRU cache of 1 ;P
- if address in self._last_node:
+ if address in self._last_node.instructions:
return self._last_node
#
@@ -165,7 +168,7 @@ def get_node(self, address):
# node simply does not exist), then we have no match/metadata to return
#
- if not (node_metadata and address in node_metadata):
+ if not (node_metadata and address in node_metadata.instructions):
return None
#
@@ -363,7 +366,8 @@ def _refresh_lookup(self):
- get_function(ea)
"""
- self._last_node = []
+ self._last_node = lambda: None # XXX blank node hack, see other ref to _last_node
+ self._last_node.instructions = []
self._name2func = { f.name: f.address for f in itervalues(self.functions) }
self._node_addresses = sorted(self.nodes.keys())
self._function_addresses = sorted(self.functions.keys())
From d93b52354f2a5b5dab6bb0ce5280284b36d21437 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 28 Mar 2020 23:03:24 -0400
Subject: [PATCH 053/154] make it so that partially executed nodes do not have
their whole graph node colored
---
plugin/lighthouse/coverage.py | 22 ++++++++++++++--------
plugin/lighthouse/painting/ida_painter.py | 4 ++++
plugin/lighthouse/painting/painter.py | 2 ++
3 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index e1c18772..bc133132 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -165,6 +165,7 @@ def __init__(self, palette, name="", filepath=None, data=None):
self.nodes = {}
self.functions = {}
self.instruction_percent = 0.0
+ self.partial_nodes = set()
#
# we instantiate a single weakref of ourself (the DatbaseCoverage
@@ -313,9 +314,16 @@ def _finalize_nodes(self, dirty_nodes):
"""
Finalize the NodeCoverage objects statistics / data for use.
"""
- for node_coverage in itervalues(dirty_nodes):
+ metadata = self._metadata
+ for address, node_coverage in iteritems(dirty_nodes):
node_coverage.finalize()
+ # save off a reference to partially executed nodes
+ if node_coverage.instructions_executed != metadata.nodes[address].instruction_count:
+ self.partial_nodes.add(address)
+ else:
+ self.partial_nodes.discard(address)
+
def _finalize_functions(self, dirty_functions):
"""
Finalize the FunctionCoverage objects statistics / data for use.
@@ -620,6 +628,7 @@ def unmap_all(self):
# clear out the processed / computed coverage data structures
self.nodes = {}
self.functions = {}
+ self.partial_nodes = set()
self._misaligned_data = set()
# dump the source coverage data back into an 'unmapped' state
@@ -742,6 +751,7 @@ def __init__(self, node_address, database=None):
self.database = database
self.address = node_address
self.executed_instructions = {}
+ self.instructions_executed = 0
#--------------------------------------------------------------------------
# Properties
@@ -754,13 +764,6 @@ def hits(self):
"""
return sum(itervalues(self.executed_instructions))
- @property
- def instructions_executed(self):
- """
- Return the number of unique instructions executed in this node.
- """
- return len(self.executed_instructions)
-
#--------------------------------------------------------------------------
# Controls
#--------------------------------------------------------------------------
@@ -773,3 +776,6 @@ def finalize(self):
# the estimated number of executions this node has experienced.
self.executions = float(self.hits) / node_metadata.instruction_count
+
+ # the number of unique instructions executed
+ self.instructions_executed = len(self.executed_instructions)
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 3cde882d..a80c452d 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -237,6 +237,10 @@ def paint_nodes(self, nodes_coverage):
for node_coverage in nodes_coverage:
node_metadata = db_metadata.nodes[node_coverage.address]
+ # ignore nodes that are only partially executed
+ if node_coverage.instructions_executed != node_metadata.instruction_count:
+ continue
+
# assign the background color we would like to paint to this node
node_info.bg_color = self.palette.coverage_paint
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index f2cb7c6f..a27e9a8f 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -240,6 +240,7 @@ def _paint_function(self, address):
# compute the painted nodes that will not get painted over
stale_nodes_ea = painted - viewkeys(function_coverage.nodes)
+ stale_nodes_ea |= (painted & function_coverage.database.partial_nodes)
stale_nodes = [function_metadata.nodes[ea] for ea in stale_nodes_ea]
# active instructions
@@ -317,6 +318,7 @@ def _paint_database(self):
# compute the painted nodes that will not get painted over
stale_nodes_ea = self._painted_nodes - viewkeys(db_coverage.nodes)
+ stale_nodes_ea |= db_coverage.partial_nodes
stale_nodes = [db_metadata.nodes[ea] for ea in stale_nodes_ea]
# clear old instruction paint
From 28ea6b88217eb6a75fbcb9a85ff76f3b06b6f8ef Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 29 Mar 2020 02:43:28 -0400
Subject: [PATCH 054/154] make drcov.py work natively outside of lighthouse
---
plugin/lighthouse/reader/parsers/drcov.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 7573dc9a..0be49298 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -6,11 +6,18 @@
import struct
from ctypes import *
+#
+# I know people like to use this parser in their own projects, so this
+# if/def makes it compatible with being imported or used outside Lighthouse
+#
+
try:
from lighthouse.exceptions import CoverageMissingError
from lighthouse.reader.coverage_file import CoverageFile
+ g_lighthouse = True
except ImportError as e:
CoverageFile = object
+ g_lighthouse = False
#------------------------------------------------------------------------------
# DynamoRIO Drcov Log Parser
@@ -39,7 +46,10 @@ def __init__(self, filepath=None):
self.bb_table_is_binary = True
# parse
- super(DrcovData, self).__init__(filepath)
+ if g_lighthouse:
+ super(DrcovData, self).__init__(filepath)
+ else:
+ self._parse()
#--------------------------------------------------------------------------
# Public
From 257d69594e19e0ebd636c3b3fa4683c88796b47b Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 29 Mar 2020 03:43:04 -0400
Subject: [PATCH 055/154] fully deprecates IDA 6.X
---
dev_scripts/reload_IDA_8.bat | 18 ---
dev_scripts/reload_IDA_8_big.bat | 18 ---
dev_scripts/reload_IDA_8_ida.bat | 18 ---
dev_scripts/reload_IDA_95.bat | 18 ---
plugin/lighthouse/composer/shell.py | 7 +-
plugin/lighthouse/ida_integration.py | 8 +-
plugin/lighthouse/metadata.py | 14 +-
plugin/lighthouse/painting/ida_painter.py | 18 +--
plugin/lighthouse/palette.py | 5 +-
plugin/lighthouse/ui/coverage_combobox.py | 28 +---
plugin/lighthouse/ui/coverage_overview.py | 6 +-
plugin/lighthouse/ui/coverage_settings.py | 16 +-
plugin/lighthouse/ui/coverage_table.py | 7 +-
.../lighthouse/util/disassembler/ida_api.py | 145 ++++--------------
plugin/lighthouse/util/qt/shim.py | 19 +--
15 files changed, 54 insertions(+), 291 deletions(-)
delete mode 100644 dev_scripts/reload_IDA_8.bat
delete mode 100644 dev_scripts/reload_IDA_8_big.bat
delete mode 100644 dev_scripts/reload_IDA_8_ida.bat
delete mode 100644 dev_scripts/reload_IDA_95.bat
diff --git a/dev_scripts/reload_IDA_8.bat b/dev_scripts/reload_IDA_8.bat
deleted file mode 100644
index fd1324f9..00000000
--- a/dev_scripts/reload_IDA_8.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-set LIGHTHOUSE_LOGGING=1
-REM - Close any running instances of IDA
-call close_IDA.bat
-
-REM - Purge old lighthouse log files
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
-
-REM - Delete the old plugin bits
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\*lighthouse_plugin.py"
-rmdir "C:\tools\disassemblers\IDA 6.8\plugins\lighthouse" /s /q
-
-REM - Copy over the new plugin bits
-xcopy /s/y "..\plugin\*" "C:\tools\disassemblers\IDA 6.8\plugins\"
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\.#lighthouse_plugin.py"
-
-REM - Relaunch two IDA sessions
-start "" "C:\tools\disassemblers\IDA 6.8\idaq64.exe" "..\..\testcase\boombox.i64"
-
diff --git a/dev_scripts/reload_IDA_8_big.bat b/dev_scripts/reload_IDA_8_big.bat
deleted file mode 100644
index aa4f7b46..00000000
--- a/dev_scripts/reload_IDA_8_big.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-set LIGHTHOUSE_LOGGING=1
-REM - Close any running instances of IDA
-call close_IDA.bat
-
-REM - Purge old lighthouse log files
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
-
-REM - Delete the old plugin bits
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\*lighthouse_plugin.py"
-rmdir "C:\tools\disassemblers\IDA 6.8\plugins\lighthouse" /s /q
-
-REM - Copy over the new plugin bits
-xcopy /s/y "..\plugin\*" "C:\tools\disassemblers\IDA 6.8\plugins\"
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\.#lighthouse_plugin.py"
-
-REM - Relaunch two IDA sessions
-start "" "C:\tools\disassemblers\IDA 6.8\idaq.exe" "..\..\testcase\harness_ufs_pdf.instr.idb"
-
diff --git a/dev_scripts/reload_IDA_8_ida.bat b/dev_scripts/reload_IDA_8_ida.bat
deleted file mode 100644
index acf99b00..00000000
--- a/dev_scripts/reload_IDA_8_ida.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-set LIGHTHOUSE_LOGGING=1
-REM - Close any running instances of IDA
-call close_IDA.bat
-
-REM - Purge old lighthouse log files
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
-
-REM - Delete the old plugin bits
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\*lighthouse_plugin.py"
-rmdir "C:\tools\disassemblers\IDA 6.8\plugins\lighthouse" /s /q
-
-REM - Copy over the new plugin bits
-xcopy /s/y "..\plugin\*" "C:\tools\disassemblers\IDA 6.8\plugins\"
-del /F /Q "C:\tools\disassemblers\IDA 6.8\plugins\.#lighthouse_plugin.py"
-
-REM - Relaunch two IDA sessions
-start "" "C:\tools\disassemblers\IDA 6.8\idaq.exe" "..\..\testcase\idaq.idb"
-
diff --git a/dev_scripts/reload_IDA_95.bat b/dev_scripts/reload_IDA_95.bat
deleted file mode 100644
index 0b47d72d..00000000
--- a/dev_scripts/reload_IDA_95.bat
+++ /dev/null
@@ -1,18 +0,0 @@
-set LIGHTHOUSE_LOGGING=1
-REM - Close any running instances of IDA
-call close_IDA.bat
-
-REM - Purge old lighthouse log files
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
-
-REM - Delete the old plugin bits
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py"
-rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q
-
-REM - Copy over the new plugin bits
-xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\"
-del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py"
-
-REM - Relaunch two IDA sessions
-start "" "C:\tools\disassemblers\IDA 6.95\idaq64.exe" "..\..\testcase\boombox95.i64"
-
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 5a069f70..4fe4ed74 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -114,12 +114,7 @@ def _ui_init_completer(self):
"""
Initialize the coverage hint UI elements.
"""
-
- # NOTE/COMPAT:
- if USING_PYQT5:
- self._completer_model = QtCore.QStringListModel([])
- else:
- self._completer_model = QtGui.QStringListModel([])
+ self._completer_model = QtCore.QStringListModel([])
self._completer = QtWidgets.QCompleter(self)
self._completer.setCompletionMode(QtWidgets.QCompleter.PopupCompletion)
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/ida_integration.py
index 1d1ee78a..82c588d7 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/ida_integration.py
@@ -333,14 +333,8 @@ def __init__(self, integration):
def finish_populating_widget_popup(self, widget, popup):
"""
- A right click menu is about to be shown. (IDA 7)
+ A right click menu is about to be shown. (IDA 7.0+)
"""
self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
return 0
- def finish_populating_tform_popup(self, form, popup):
- """
- A right click menu is about to be shown. (IDA 6.x) / COMPAT
- """
- self.integration._inject_ctx_actions(form, popup, idaapi.get_tform_type(form))
- return 0
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index a533dddf..238fd646 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -798,25 +798,17 @@ def _ida_refresh_nodes(self):
for node_id in xrange(flowchart.size()):
node = flowchart[node_id]
- # NOTE/COMPAT
- if disassembler.USING_IDA7API:
- node_start = node.start_ea
- node_end = node.end_ea
- else:
- node_start = node.startEA
- node_end = node.endEA
-
#
# the node current node appears to have a size of zero. This means
# that another flowchart / function owns this node so we can just
# ignore it...
#
- if node_start == node_end:
+ if node.start_ea == node.end_ea:
continue
# create a new metadata object for this node
- node_metadata = NodeMetadata(node_start, node_end, node_id)
+ node_metadata = NodeMetadata(node.start_ea, node.end_ea, node_id)
#
# establish a relationship between this node (basic block) and
@@ -824,7 +816,7 @@ def _ida_refresh_nodes(self):
#
node_metadata.function = function_metadata
- function_metadata.nodes[node_start] = node_metadata
+ function_metadata.nodes[node.start_ea] = node_metadata
# compute all of the edges between nodes in the current function
for node_metadata in itervalues(function_metadata.nodes):
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index a80c452d..f395e0b5 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -39,7 +39,7 @@
# this section of code constitutes some of the most fragile, convoluted,
# and regression prone code in lighthouse. through some miraculous feats
# of engineering, the solution below appears to safely resolve both of
-# these problems for downlevel versions (IDA 6.8 --> 7.0)
+# these problems for downlevel versions (IDA 6.8 --> 7.x)
#
from lighthouse.util.qt import QtCore
@@ -223,12 +223,6 @@ def paint_nodes(self, nodes_coverage):
# create a node info object as our vehicle for setting the node color
node_info = idaapi.node_info_t()
- # NOTE/COMPAT:
- if disassembler.USING_IDA7API:
- set_node_info = idaapi.set_node_info
- else:
- set_node_info = idaapi.set_node_info2
-
#
# loop through every node that we have coverage data for, painting them
# in the IDA graph view as applicable.
@@ -245,7 +239,7 @@ def paint_nodes(self, nodes_coverage):
node_info.bg_color = self.palette.coverage_paint
# do the *actual* painting of a single node instance
- set_node_info(
+ idaapi.set_node_info(
node_metadata.function.address,
node_metadata.id,
node_info,
@@ -263,12 +257,6 @@ def clear_nodes(self, nodes_metadata):
node_info = idaapi.node_info_t()
node_info.bg_color = idc.DEFCOLOR
- # NOTE/COMPAT:
- if disassembler.USING_IDA7API:
- set_node_info = idaapi.set_node_info
- else:
- set_node_info = idaapi.set_node_info2
-
#
# loop through every node that we have metadata data for, clearing
# their paint (color) in the IDA graph view as applicable.
@@ -277,7 +265,7 @@ def clear_nodes(self, nodes_metadata):
for node_metadata in nodes_metadata:
# do the *actual* painting of a single node instance
- set_node_info(
+ idaapi.set_node_info(
node_metadata.function.address,
node_metadata.id,
node_info,
diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/palette.py
index 143f108b..c373038e 100644
--- a/plugin/lighthouse/palette.py
+++ b/plugin/lighthouse/palette.py
@@ -180,10 +180,7 @@ def _qt_theme_hint(self):
# lmao, don't ask me why they forgot about this attribute from 5.0 - 5.6
#
- if USING_PYQT5:
- test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
- else:
- test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
+ test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
# render the (invisible) widget
test_widget.show()
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index febb6842..e17b881a 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -272,18 +272,10 @@ def _ui_clicked_delete(self, index):
# event, (it looks weird) so clear the table/dropdown highlights now
#
- # NOTE/COMPAT
- if USING_PYQT5:
- self.view().selectionModel().setCurrentIndex(
- QtCore.QModelIndex(),
- QtCore.QItemSelectionModel.ClearAndSelect
- )
- else:
- self.view().selectionModel().setCurrentIndex(
- QtCore.QModelIndex(),
- QtGui.QItemSelectionModel.ClearAndSelect
- )
-
+ self.view().selectionModel().setCurrentIndex(
+ QtCore.QModelIndex(),
+ QtCore.QItemSelectionModel.ClearAndSelect
+ )
#
# the deletion of an entry will shift all the entries beneath it up
@@ -430,19 +422,13 @@ def _ui_init(self):
hh = self.horizontalHeader()
#
- # NOTE/COMPAT:
# - set the coverage name column to be stretchy and as tall as the text
# - make the 'X' icon column fixed width
#
- if USING_PYQT5:
- hh.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
- hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
- vh.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
- else:
- hh.setResizeMode(0, QtWidgets.QHeaderView.Stretch)
- hh.setResizeMode(1, QtWidgets.QHeaderView.Fixed)
- vh.setResizeMode(QtWidgets.QHeaderView.ResizeToContents)
+ hh.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch)
+ hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed)
+ vh.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents)
hh.setMinimumSectionSize(0)
vh.setMinimumSectionSize(0)
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index fbd640a0..23dccd4b 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -283,11 +283,7 @@ def eventFilter(self, source, event):
# that the user has probably started debugging.
#
- # NOTE / COMPAT:
- if disassembler.USING_IDA7API:
- debug_mode = bool(idaapi.find_widget("General registers"))
- else:
- debug_mode = bool(idaapi.find_tform("General registers"))
+ debug_mode = bool(idaapi.find_widget("General registers"))
#
# if this is the first time the user has started debugging, dock
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 8c1605a0..84c86c72 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -14,8 +14,7 @@ def __init__(self, parent=None):
self._visible_action = None
self._ui_init_actions()
- if USING_PYQT5:
- self.setToolTipsVisible(True)
+ self.setToolTipsVisible(True)
#--------------------------------------------------------------------------
# QMenu Overloads
@@ -34,19 +33,6 @@ def event(self, event):
event.accept()
return True
- # show action tooltips (for Qt < 5.1)
- elif event.type() == QtCore.QEvent.ToolTip and not USING_PYQT5:
- if action and self._visible_action != action:
- QtWidgets.QToolTip.showText(event.globalPos(), action.toolTip())
- self._visible_action = action
- event.accept()
- return True
-
- # clear tooltips (for Qt < 5.1)
- if not (action or USING_PYQT5):
- QtWidgets.QToolTip.hideText()
- self._visible_action = None
-
# handle any other events as wee normally should
return super(TableSettingsMenu, self).event(event)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 970cfe7a..bc98c483 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -174,10 +174,7 @@ def _ui_init_table(self):
#
# force the table row heights to be fixed height
- if USING_PYQT5:
- vh.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
- else:
- vh.setResizeMode(QtWidgets.QHeaderView.Fixed)
+ vh.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
# specify the fixed pixel height for the table headers
spacing = title_fm.height() - title_fm.xHeight()
@@ -586,7 +583,7 @@ def export_to_html(self):
{
"filter": "HTML Files (*.html)",
"caption": "Save HTML Report",
- "directory" if USING_PYQT5 else "dir": suggested_filepath
+ "directory": suggested_filepath
}
# prompt the user with the file dialog, and await their chosen filename(s)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index ec8c38f1..055fb957 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -7,6 +7,10 @@
import idaapi
import idautils
+if int(idaapi.get_kernel_version()[0]) < 7:
+ idaapi.warning("Lighthouse has deprecated support for IDA 6, please upgrade.")
+ raise ImportError
+
from .api import DisassemblerAPI, DockableShim
from ..qt import *
from ..misc import is_mainthread
@@ -57,16 +61,6 @@ class IDAAPI(DisassemblerAPI):
"""
NAME = "IDA"
- #
- # in IDA 7.0, Hex-Rays refactored the IDA API quite a bit. This
- # impacts Lighthouse in a few places, so we use version checks at
- # these junctions to determine which API's to use (v7.x or v6.x)
- #
- # search 'USING_IDA7API' in the codebase for example cases
- #
-
- USING_IDA7API = bool(idaapi.IDA_SDK_VERSION >= 700)
-
def __init__(self):
super(IDAAPI, self).__init__()
self._init_version()
@@ -115,12 +109,8 @@ def execute_ui(function):
#--------------------------------------------------------------------------
def create_rename_hooks(self):
- if self.USING_IDA7API:
- class RenameHooks(idaapi.IDB_Hooks):
- pass
- else:
- class RenameHooks(idaapi.IDP_Hooks):
- pass
+ class RenameHooks(idaapi.IDB_Hooks):
+ pass
return RenameHooks()
def get_database_directory(self):
@@ -136,9 +126,7 @@ def get_function_name_at(self, address):
return idaapi.get_short_name(address)
def get_function_raw_name_at(self, function_address):
- if self.USING_IDA7API:
- return idaapi.get_name(function_address)
- return idaapi.get_true_name(idaapi.BADADDR, function_address)
+ return idaapi.get_name(function_address)
def get_imagebase(self):
return idaapi.get_imagebase()
@@ -168,10 +156,7 @@ def get_disassembly_background_color(self):
disassembly view, and take a screenshot of said widget. It will then
attempt to extract the color of a single background pixel (hopefully).
"""
- if self.USING_IDA7API:
- return self._get_ida_bg_color_ida7()
- else:
- return self._get_ida_bg_color_ida6()
+ return self._get_ida_bg_color_ida7()
def is_msg_inited(self):
return idaapi.is_msg_inited()
@@ -219,40 +204,6 @@ def _get_ida_bg_color_ida7(self):
# return the predicted background color
return QtGui.QColor(predict_bg_color(image))
- def _get_ida_bg_color_ida6(self):
- """
- Get the background color of the IDA disassembly view. (IDA 6.x)
- """
- names = ["Enums", "Structures"]
- names += ["Hex View-%u" % i for i in range(5)]
- names += ["IDA View-%c" % chr(ord('A') + i) for i in range(5)]
-
- # find a form (eg, IDA view) to analyze colors from
- for window_name in names:
- form = idaapi.find_tform(window_name)
- if form:
- break
- else:
- raise RuntimeError("Failed to find donor View")
-
- # touch the target form so we know it is populated
- self._touch_ida_window(form)
-
- # locate the Qt Widget for a form and take 1px image slice of it
- if USING_PYQT5:
- widget = idaapi.PluginForm.FormToPyQtWidget(form, sys.modules[__name__])
- pixmap = widget.grab(QtCore.QRect(0, 10, widget.width(), 1))
- else:
- widget = idaapi.PluginForm.FormToPySideWidget(form, sys.modules[__name__])
- region = QtCore.QRect(0, 10, widget.width(), 1)
- pixmap = QtGui.QPixmap.grabWidget(widget, region)
-
- # convert the raw pixmap into an image (easier to interface with)
- image = QtGui.QImage(pixmap.toImage())
-
- # return the predicted background color
- return QtGui.QColor(predict_bg_color(image))
-
def _touch_ida_window(self, target):
"""
Touch a window/widget/form to ensure it gets drawn by IDA.
@@ -269,39 +220,19 @@ def _touch_ida_window(self, target):
"""
# get the currently active widget/form title (the form itself seems transient...)
- if self.USING_IDA7API:
- twidget = idaapi.get_current_widget()
- title = idaapi.get_widget_title(twidget)
- else:
- form = idaapi.get_current_tform()
- title = idaapi.get_tform_title(form)
+ twidget = idaapi.get_current_widget()
+ title = idaapi.get_widget_title(twidget)
- # touch/draw the widget by playing musical chairs
- if self.USING_IDA7API:
+ # touch the target window by switching to it
+ idaapi.activate_widget(target, True)
+ flush_qt_events()
- # touch the target window by switching to it
- idaapi.activate_widget(target, True)
- flush_qt_events()
+ # locate our previous selection
+ previous_twidget = idaapi.find_widget(title)
- # locate our previous selection
- previous_twidget = idaapi.find_widget(title)
-
- # return us to our previous selection
- idaapi.activate_widget(previous_twidget, True)
- flush_qt_events()
-
- else:
-
- # touch the target window by switching to it
- idaapi.switchto_tform(target, True)
- flush_qt_events()
-
- # locate our previous selection
- previous_form = idaapi.find_tform(title)
-
- # lookup our original form and switch back to it
- idaapi.switchto_tform(previous_form, True)
- flush_qt_events()
+ # return us to our previous selection
+ idaapi.activate_widget(previous_twidget, True)
+ flush_qt_events()
#------------------------------------------------------------------------------
# Dockable Window
@@ -309,25 +240,15 @@ def _touch_ida_window(self, target):
class DockableWindow(DockableShim):
"""
- A Dockable Qt widget, compatible with IDA 6.8 --> 7.x.
+ A Dockable Qt widget for IDA 7.0 and above.
"""
def __init__(self, window_title, icon_path):
super(DockableWindow, self).__init__(window_title, icon_path)
- # IDA 7+ Widgets
- if IDAAPI.USING_IDA7API:
- import sip
- self._form = idaapi.create_empty_widget(self._window_title)
- self._widget = sip.wrapinstance(long(self._form), QtWidgets.QWidget)
-
- # legacy IDA PluginForm's
- else:
- self._form = idaapi.create_tform(self._window_title, None)
- if USING_PYQT5:
- self._widget = idaapi.PluginForm.FormToPyQtWidget(self._form, sys.modules[__name__])
- else:
- self._widget = idaapi.PluginForm.FormToPySideWidget(self._form, sys.modules[__name__])
+ import sip
+ self._form = idaapi.create_empty_widget(self._window_title)
+ self._widget = sip.wrapinstance(long(self._form), QtWidgets.QWidget)
# set the window icon
self._widget.setWindowIcon(self._window_icon)
@@ -336,23 +257,11 @@ def show(self):
"""
Show the dockable widget.
"""
-
- # IDA 7+ Widgets
- if IDAAPI.USING_IDA7API:
- flags = idaapi.PluginForm.WOPN_TAB | \
- idaapi.PluginForm.WOPN_MENU | \
- idaapi.PluginForm.WOPN_RESTORE | \
- idaapi.PluginForm.WOPN_PERSIST
- idaapi.display_widget(self._form, flags)
-
- # legacy IDA PluginForm's
- else:
- flags = idaapi.PluginForm.FORM_TAB | \
- idaapi.PluginForm.FORM_MENU | \
- idaapi.PluginForm.FORM_RESTORE | \
- idaapi.PluginForm.FORM_PERSIST | \
- 0x80 #idaapi.PluginForm.FORM_QWIDGET
- idaapi.open_tform(self._form, flags)
+ flags = idaapi.PluginForm.WOPN_TAB | \
+ idaapi.PluginForm.WOPN_MENU | \
+ idaapi.PluginForm.WOPN_RESTORE | \
+ idaapi.PluginForm.WOPN_PERSIST
+ idaapi.display_widget(self._form, flags)
#------------------------------------------------------------------------------
# HexRays Util
diff --git a/plugin/lighthouse/util/qt/shim.py b/plugin/lighthouse/util/qt/shim.py
index afa350ab..2ac1a431 100644
--- a/plugin/lighthouse/util/qt/shim.py
+++ b/plugin/lighthouse/util/qt/shim.py
@@ -7,20 +7,16 @@
QT_AVAILABLE = False
#------------------------------------------------------------------------------
-# PyQt5 <--> PySide (Qt4) Interoperability
+# PyQt5 <--> PySide2 Compatibility
#------------------------------------------------------------------------------
#
-# from Qt4 --> Qt5, a number of objects / modules have changed places
-# within the Qt codebase. we use this file to shim/re-alias a few of these
-# changes to reduce the number of compatibility checks / code churn in the
-# plugin code that consumes them.
+# 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 makes the plugin codebase compatible with both PySide & PyQt5, a
-# necessary requirement to maintain compatibility with IDA 6.8 --> 7.x
-#
-# additionally, the 'USING_PYQT5' global can be used to check if we are
-# running in a PyQt5 context (versus PySide/Qt4). This may be used in a few
-# places throughout the project that could not be covered by our shims.
+# 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
@@ -63,7 +59,6 @@
# importing went okay, PySide must be available for use
QT_AVAILABLE = True
USING_PYSIDE2 = True
- USING_PYQT5 = True # TODO: remove once PySide v1 is fully ripped out...
# import failed. No Qt / UI bindings available...
except ImportError:
From e5b9f341931dbec4be4eee9b06e5b1ecf89339f6 Mon Sep 17 00:00:00 2001
From: Alexandre Maloteaux
Date: Sun, 29 Mar 2020 10:59:29 +0200
Subject: [PATCH 056/154] fix for ida 7.4 with python3.7 (#79)
* fix for ida 7.4 with python3.7
* fix html export too
* fix painter
* tweakd to use our own dict shims
* a few more minor fixes
Co-authored-by: gaasedelen
---
plugin/lighthouse/ida_integration.py | 8 ++++----
plugin/lighthouse/painting/ida_painter.py | 4 ++--
plugin/lighthouse/ui/coverage_combobox.py | 5 ++++-
plugin/lighthouse/ui/coverage_table.py | 2 +-
plugin/lighthouse/util/disassembler/ida_api.py | 10 ++++++----
5 files changed, 17 insertions(+), 12 deletions(-)
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/ida_integration.py
index 82c588d7..f52233be 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/ida_integration.py
@@ -46,7 +46,7 @@ def _install_load_file(self):
# create a custom IDA icon
icon_path = plugin_resource(os.path.join("icons", "load.png"))
- icon_data = str(open(icon_path, "rb").read())
+ icon_data = open(icon_path, "rb").read()
self._icon_id_file = idaapi.load_custom_icon(data=icon_data)
# describe a custom IDA UI action
@@ -82,7 +82,7 @@ def _install_load_batch(self):
# create a custom IDA icon
icon_path = plugin_resource(os.path.join("icons", "batch.png"))
- icon_data = str(open(icon_path, "rb").read())
+ icon_data = open(icon_path, "rb").read()
self._icon_id_batch = idaapi.load_custom_icon(data=icon_data)
# describe a custom IDA UI action
@@ -118,7 +118,7 @@ def _install_open_coverage_xref(self):
# create a custom IDA icon
icon_path = plugin_resource(os.path.join("icons", "batch.png"))
- icon_data = str(open(icon_path, "rb").read())
+ icon_data = open(icon_path, "rb").read()
self._icon_id_xref = idaapi.load_custom_icon(data=icon_data)
# describe a custom IDA UI action
@@ -146,7 +146,7 @@ def _install_open_coverage_overview(self):
# create a custom IDA icon
icon_path = plugin_resource(os.path.join("icons", "overview.png"))
- icon_data = str(open(icon_path, "rb").read())
+ icon_data = open(icon_path, "rb").read()
self._icon_id_overview = idaapi.load_custom_icon(data=icon_data)
# describe a custom IDA UI action
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index f395e0b5..cfbd500e 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -333,7 +333,7 @@ def paint_hexrays(self, cfunc, db_coverage):
lines_painted = 0
# extract the node addresses that have been hit by our function's mapping data
- executed_nodes = set(db_coverage.functions[cfunc.entry_ea].nodes.iterkeys())
+ executed_nodes = set(viewkeys(db_coverage.functions[cfunc.entry_ea].nodes))
#
# now we loop through every line_number of the decompiled text that claims
@@ -341,7 +341,7 @@ def paint_hexrays(self, cfunc, db_coverage):
# if it contains a node our coverage has marked as executed
#
- for line_number, line_nodes in line2node.iteritems():
+ for line_number, line_nodes in iteritems(line2node):
#
# if there is any intersection of nodes on this line and the coverage
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index e17b881a..97c17aa3 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -106,7 +106,10 @@ def hidePopup(self):
# begin accepting clicks again.
#
- QtCore.QTimer.singleShot(100, lambda: self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False))
+ QtCore.QTimer.singleShot(100, self.__hidePopup_setattr)
+
+ def __hidePopup_setattr(self):
+ self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
def mousePressEvent(self, e):
"""
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index bc98c483..7366d9a9 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -595,7 +595,7 @@ def export_to_html(self):
self._last_directory = os.path.dirname(filename) + os.sep
# write the generated HTML report to disk
- with open(filename, "wb") as fd:
+ with open(filename, "w") as fd:
fd.write(self._model.to_html())
lmsg("Saved HTML report to %s" % filename)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 055fb957..b1e11495 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -110,7 +110,8 @@ def execute_ui(function):
def create_rename_hooks(self):
class RenameHooks(idaapi.IDB_Hooks):
- pass
+ def renamed(self, a, b, c): # temporary, required for IDA 7.3/py3?
+ return 0
return RenameHooks()
def get_database_directory(self):
@@ -195,7 +196,7 @@ def _get_ida_bg_color_ida7(self):
# locate the Qt Widget for a form and take 1px image slice of it
import sip
- widget = sip.wrapinstance(long(twidget), QtWidgets.QWidget)
+ widget = sip.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)
@@ -248,7 +249,8 @@ def __init__(self, window_title, icon_path):
import sip
self._form = idaapi.create_empty_widget(self._window_title)
- self._widget = sip.wrapinstance(long(self._form), QtWidgets.QWidget)
+ self._widget = sip.wrapinstance(int(self._form), QtWidgets.QWidget)
+
# set the window icon
self._widget.setWindowIcon(self._window_icon)
@@ -328,7 +330,7 @@ def map_line2node(cfunc, metadata, line2citem):
# an effort to resolve the set of graph nodes associated with its citems.
#
- for line_number, citem_indexes in line2citem.iteritems():
+ for line_number, citem_indexes in iteritems(line2citem):
nodes = set()
#
From b8a996b5f1414dac68fcaddfe59f074c50858f8c Mon Sep 17 00:00:00 2001
From: lucasg
Date: Sun, 29 Mar 2020 11:13:27 +0200
Subject: [PATCH 057/154] Update README.md to provide more details on finding
IDA plugin dir w/ API (#72)
---
README.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/README.md b/README.md
index dea8f79c..6cb0dae9 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,8 @@ Lighthouse is a cross-platform (Windows, macOS, Linux) python plugin, supporting
- On macOS, the folder is at `/Applications/IDA\ Pro\ 6.8/idaq.app/Contents/MacOS/plugins`
- On Linux, the folder may be at `/opt/IDA/plugins/`
+(If you need to locate the plugin directory for your setup, just type `idaapi.idadir(idaapi.PLG_SUBDIR)` in IDAPython console)
+
It has been primarily developed and tested on Windows, so that is where we expect the best experience.
# Binary Ninja Installation (Experimental)
From b1488c308670952c4574fb709a31516b2d6d70f9 Mon Sep 17 00:00:00 2001
From: Jan Beck
Date: Sun, 29 Mar 2020 09:36:04 +0000
Subject: [PATCH 058/154] Add compatibility with PIN 3.11 (#77)
* Add compatibility with PIN 3.11
Co-authored-by: gaasedelen
---
coverage/pin/CodeCoverage.cpp | 1 +
coverage/pin/ImageManager.cpp | 1 +
2 files changed, 2 insertions(+)
diff --git a/coverage/pin/CodeCoverage.cpp b/coverage/pin/CodeCoverage.cpp
index 270c8311..8136f44a 100644
--- a/coverage/pin/CodeCoverage.cpp
+++ b/coverage/pin/CodeCoverage.cpp
@@ -1,3 +1,4 @@
+using namespace std;
#include
#include
#include
diff --git a/coverage/pin/ImageManager.cpp b/coverage/pin/ImageManager.cpp
index 79aba679..7e6938db 100644
--- a/coverage/pin/ImageManager.cpp
+++ b/coverage/pin/ImageManager.cpp
@@ -1,3 +1,4 @@
+using namespace std;
#include "ImageManager.h"
#include "pin.H"
From 0ef5c9d9e13fb66623ab1b1cbc2b8110aa5e7982 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 29 Mar 2020 17:31:07 -0400
Subject: [PATCH 059/154] expose the active CoverageDirector object instance
for the disassembler interpreter, or other scripts to use
---
plugin/lighthouse/__init__.py | 1 +
plugin/lighthouse/core.py | 10 ++++++++++
plugin/lighthouse_plugin.py | 2 ++
3 files changed, 13 insertions(+)
diff --git a/plugin/lighthouse/__init__.py b/plugin/lighthouse/__init__.py
index e69de29b..8879317c 100644
--- a/plugin/lighthouse/__init__.py
+++ b/plugin/lighthouse/__init__.py
@@ -0,0 +1 @@
+coverage_director = None
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index d88ecf08..6ebdf137 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -2,6 +2,8 @@
import abc
import logging
+import lighthouse
+
from lighthouse.ui import CoverageOverview, CoverageXref
from lighthouse.util import lmsg
from lighthouse.util.qt import *
@@ -74,6 +76,9 @@ def _init(self):
self._scheduled.timeout.connect(self.scheduled)
#self._scheduled.start(1000) # TODO: re-enable once more testing is done...
+ # expose the live CoverageDirector object instance for external scripts
+ lighthouse.coverage_director = self.director
+
def print_banner(self):
"""
Print the plugin banner.
@@ -108,6 +113,11 @@ def _cleanup(self):
"""
Spin down any lingering core components before plugin unload.
"""
+
+ # remove access to the exposed CoverageDirector
+ lighthouse.coverage_director = None
+
+ # spin down the rest of the core subsystems
self._scheduled.stop()
self.painter.terminate()
self.director.terminate()
diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py
index a96be38a..973a10be 100644
--- a/plugin/lighthouse_plugin.py
+++ b/plugin/lighthouse_plugin.py
@@ -17,10 +17,12 @@
elif disassembler.NAME == "IDA":
logger.info("Selecting IDA loader...")
from lighthouse.ida_loader import *
+ from lighthouse import coverage_director
elif disassembler.NAME == "BINJA":
logger.info("Selecting Binary Ninja loader...")
from lighthouse.binja_loader import *
+ from lighthouse import coverage_director
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
From da5942466afd8b4788d664bb744d97ba1f2bfe80 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 29 Mar 2020 17:34:21 -0400
Subject: [PATCH 060/154] update dates and version number, since there won't be
an v0.8.4 release...
---
LICENSE | 2 +-
README.md | 6 +++---
plugin/lighthouse/core.py | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/LICENSE b/LICENSE
index 70bc713f..610373a5 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017-2018 Markus Gaasedelen
+Copyright (c) 2017-2020 Markus Gaasedelen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 6cb0dae9..6f2381fe 100644
--- a/README.md
+++ b/README.md
@@ -225,11 +225,11 @@ Time and motivation permitting, future work may include:
* Coverage & profiling treemaps
* ~~Additional coverage sources, trace formats, etc~~
* Improved pseudocode painting
-* Lighthouse console access, headless usage
+* ~~Lighthouse console access~~, headless usage
* Custom themes
-* Python 3 support
+* ~~Python 3 support~~
-I welcome external contributions, issues, and feature requests.
+I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repo.
# Authors
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 6ebdf137..8df3f43a 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -22,9 +22,9 @@
# Plugin Metadata
#------------------------------------------------------------------------------
-PLUGIN_VERSION = "0.8.4-DEV"
+PLUGIN_VERSION = "0.9.0-DEV"
AUTHORS = "Markus Gaasedelen"
-DATE = "2019"
+DATE = "2020"
#------------------------------------------------------------------------------
# Lighthouse Plugin Core
From d53e5032d0b43514947280f3ac976a7c2b1d3207 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 31 Mar 2020 00:02:37 -0400
Subject: [PATCH 061/154] initial theme/palette refactoring
---
plugin/lighthouse/composer/shell.py | 24 +-
plugin/lighthouse/core.py | 3 +-
plugin/lighthouse/coverage.py | 6 +-
plugin/lighthouse/ui/__init__.py | 1 +
plugin/lighthouse/ui/coverage_combobox.py | 16 +-
plugin/lighthouse/ui/coverage_settings.py | 1 +
plugin/lighthouse/ui/coverage_table.py | 8 +-
plugin/lighthouse/{ => ui}/palette.py | 280 ++++++------------
.../ui/resources/themes/classic.json | 59 ++++
9 files changed, 182 insertions(+), 216 deletions(-)
rename plugin/lighthouse/{ => ui}/palette.py (54%)
create mode 100644 plugin/lighthouse/ui/resources/themes/classic.json
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 4fe4ed74..2afad64b 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -95,18 +95,18 @@ def _ui_init_shell(self):
# configure the shell background & default text color
qpal = self._line.palette()
- #qpal.setColor(QtGui.QPalette.Base, self._palette.overview_bg)
- qpal.setColor(QtGui.QPalette.Text, self._palette.composer_fg)
- qpal.setColor(QtGui.QPalette.WindowText, self._palette.composer_fg)
+ #qpal.setColor(QtGui.QPalette.Base, self._palette.shell_background)
+ qpal.setColor(QtGui.QPalette.Text, self._palette.shell_text)
+ qpal.setColor(QtGui.QPalette.WindowText, self._palette.shell_text)
self._line.setPalette(qpal)
self._line.setStyleSheet(
"QPlainTextEdit {"
- " background-color: %s;" % self._palette.overview_bg.name() +
- " border: 1px solid %s;" % self._palette.border.name() +
+ " background-color: %s;" % self._palette.shell_background.name() +
+ " border: 1px solid %s;" % self._palette.shell_border.name() +
"} "
"QPlainTextEdit:hover, QPlainTextEdit:focus {"
- " border: 1px solid %s;" % self._palette.focus.name() +
+ " border: 1px solid %s;" % self._palette.shell_border_focus.name() +
"}"
)
@@ -124,8 +124,8 @@ def _ui_init_completer(self):
self._completer.setWrapAround(False)
self._completer.popup().setFont(self._font)
self._completer.popup().setStyleSheet(
- "background: %s;" % self._palette.shell_hint_bg.name() +
- "color: %s;" % self._palette.shell_hint_fg.name()
+ "background: %s;" % self._palette.shell_hint_background.name() +
+ "color: %s;" % self._palette.shell_hint_text.name()
)
self._completer.setWidget(self._line)
@@ -388,9 +388,9 @@ def _highlight_search(self):
# color search based on if there are any matching results
if self._table_model.rowCount():
- self._color_text(self._palette.valid_text, start=1)
+ self._color_text(self._palette.shell_text_valid, start=1)
else:
- self._color_text(self._palette.invalid_text, start=1)
+ self._color_text(self._palette.shell_text_invalid, start=1)
################# UPDATES ENABLED #################
self._line.setUpdatesEnabled(True)
@@ -503,7 +503,7 @@ def _highlight_jump(self):
self._color_clear()
# color jump
- self._color_text(self._palette.valid_text)
+ self._color_text(self._palette.shell_text_valid)
################# UPDATES ENABLED #################
self._line.setUpdatesEnabled(True)
@@ -855,7 +855,7 @@ def _color_invalid(self):
cursor_position = cursor.position()
# setup the invalid text highlighter
- invalid_color = self._palette.invalid_highlight
+ invalid_color = self._palette.shell_highlight_invalid
highlight = QtGui.QTextCharFormat()
highlight.setFontWeight(QtGui.QFont.Bold)
highlight.setBackground(QtGui.QBrush(QtGui.QColor(invalid_color)))
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 8df3f43a..1954701c 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -4,12 +4,11 @@
import lighthouse
-from lighthouse.ui import CoverageOverview, CoverageXref
+from lighthouse.ui import *
from lighthouse.util import lmsg
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
-from lighthouse.palette import LighthousePalette
from lighthouse.painting import CoveragePainter
from lighthouse.director import CoverageDirector
from lighthouse.coverage import DatabaseCoverage
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index bc133132..be6ac2d4 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -6,8 +6,8 @@
import collections
from lighthouse.util import *
-from lighthouse.palette import compute_color_on_gradiant
from lighthouse.metadata import DatabaseMetadata
+from lighthouse.ui.palette import compute_color_on_gradiant
logger = logging.getLogger("Lighthouse.Coverage")
@@ -734,8 +734,8 @@ def finalize(self):
# bake colors
self.coverage_color = compute_color_on_gradiant(
self.instruction_percent,
- self.database.palette.coverage_bad,
- self.database.palette.coverage_good
+ self.database.palette.table_coverage_bad,
+ self.database.palette.table_coverage_good
)
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/__init__.py b/plugin/lighthouse/ui/__init__.py
index 5ab3dafc..0963495b 100644
--- a/plugin/lighthouse/ui/__init__.py
+++ b/plugin/lighthouse/ui/__init__.py
@@ -1,2 +1,3 @@
+from .palette import LighthousePalette
from .coverage_xref import CoverageXref
from .coverage_overview import CoverageOverview
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 97c17aa3..008bacfe 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -179,7 +179,7 @@ def _ui_init(self):
" border: none;"
" padding: 0 0 0 2ex;"
" margin: 0;"
- " background-color: %s;" % palette.overview_bg.name() +
+ " background-color: %s;" % palette.combobox_background.name() +
"}"
)
@@ -197,12 +197,12 @@ def _ui_init(self):
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
self.setStyleSheet(
"QComboBox {"
- " color: %s;" % palette.combobox_fg.name() +
- " border: 1px solid %s;" % palette.border.name() +
+ " color: %s;" % palette.combobox_text.name() +
+ " border: 1px solid %s;" % palette.combobox_border.name() +
" padding: 0;"
"} "
"QComboBox:hover, QComboBox:focus {"
- " border: 1px solid %s;" % palette.focus.name() +
+ " border: 1px solid %s;" % palette.combobox_border_focus.name() +
"}"
)
@@ -400,10 +400,10 @@ def _ui_init(self):
# widget style
self.setStyleSheet(
"QTableView {"
- " background-color: %s;" % palette.combobox_bg.name() +
- " color: %s;" % palette.combobox_fg.name() +
- " selection-background-color: %s;" % palette.combobox_selection_bg.name() +
- " selection-color: %s;" % palette.combobox_selection_fg.name() +
+ " background-color: %s;" % palette.combobox_background.name() +
+ " color: %s;" % palette.combobox_text.name() +
+ " selection-background-color: %s;" % palette.combobox_selection_background.name() +
+ " selection-color: %s;" % palette.combobox_selection_text.name() +
" margin: 0; outline: none;"
"} "
"QTableView::item{ padding: 0.5ex; } "
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 84c86c72..5f0291ee 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -1,4 +1,5 @@
import logging
+
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 7366d9a9..68077ffe 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -92,13 +92,13 @@ def _ui_init_table(self):
self.setStyleSheet(
"QTableView {"
" gridline-color: black;"
- " background-color: %s;" % palette.overview_bg.name() +
+ " background-color: %s;" % palette.table_background.name() +
#" color: %s;" % palette.combobox_fg.name() +
" outline: none; "
"} " +
"QTableView::item:selected {"
" color: white; "
- " background-color: %s;" % palette.selection.name() +
+ " background-color: %s;" % palette.table_selection.name() +
"}"
)
@@ -689,7 +689,7 @@ def __init__(self, director, parent=None):
# a fallback coverage object for functions with no coverage
self._blank_coverage = FunctionCoverage(BADADDR)
- self._blank_coverage.coverage_color = director._palette.coverage_none
+ self._blank_coverage.coverage_color = director._palette.table_coverage_none
# set the default column text alignment for each column (centered)
self._default_alignment = QtCore.Qt.AlignCenter
@@ -1127,7 +1127,7 @@ def _generate_html_table(self):
padding: 1ex 1em 1ex 1em;
}}
""".format(
- table_bg=palette.overview_bg.name(),
+ table_bg=palette.table_background.name(),
table_fg="white"
)
diff --git a/plugin/lighthouse/palette.py b/plugin/lighthouse/ui/palette.py
similarity index 54%
rename from plugin/lighthouse/palette.py
rename to plugin/lighthouse/ui/palette.py
index c373038e..08feedf9 100644
--- a/plugin/lighthouse/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -1,11 +1,61 @@
+import os
+import json
+import struct
+import logging
from lighthouse.util.qt import *
+from lighthouse.util.misc import plugin_resource
from lighthouse.util.disassembler import disassembler
-#
-# TODO/FUTURE: this file is a huge mess, and will probably be refactored
-# whenever I add external theme customization/controls (v0.9?)
-#
+logger = logging.getLogger("Lighthouse.UI.Palette")
+
+#------------------------------------------------------------------------------
+# Theme Util
+#------------------------------------------------------------------------------
+
+def swap_rgb(i):
+ return struct.unpack("I", i))[0] >> 8
+
+def to_rgb(color):
+ return ((color >> 16 & 0xFF), (color >> 8 & 0xFF), (color & 0xFF))
+
+def test_color_brightness(color):
+ """
+ Test the brightness of a color.
+ """
+ if color.lightness() > 255.0/2:
+ return "Light"
+ else:
+ return "Dark"
+
+def compute_color_on_gradiant(percent, color1, color2):
+ """
+ Compute the color specified by a percent between two colors.
+
+ TODO/PERF: This is silly, heavy, and can be refactored.
+ """
+
+ # dump the rgb values from QColor objects
+ 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)
+
+ # return the new color
+ return QtGui.QColor(r,g,b)
+
+def get_theme_dir():
+ """
+ Return the Lighthouse theme directory.
+ """
+ #theme_directory = os.path.join(
+ # disassembler.get_disassembler_user_directory(),
+ # "lighthouse_themes"
+ #)
+ return plugin_resource("themes")
#------------------------------------------------------------------------------
# Plugin Color Palette
@@ -35,53 +85,55 @@ def __init__(self):
"Light": 1,
}
- #
- # Coverage Overview
- #
-
- self._selection = [QtGui.QColor(100, 0, 130), QtGui.QColor(226, 143, 0)]
- self._coverage_none = [QtGui.QColor(30, 30, 30), QtGui.QColor(30, 30, 30)]
- self._coverage_bad = [QtGui.QColor(221, 0, 0), QtGui.QColor(207, 31, 0)]
- self._coverage_okay = [QtGui.QColor("#bf7ae7"), QtGui.QColor(207, 31, 0)]
- self._coverage_good = [QtGui.QColor(51, 153, 255), QtGui.QColor(75, 209, 42)]
-
- #
- # IDA Views / HexRays
- #
+ theme_path = os.path.join(get_theme_dir(), "classic.json")
+ theme = self.read_theme(theme_path)
+ self.apply_theme(theme)
- self._coverage_paint = [0x990000, 0xFFE2A8] # NOTE: IDA uses BBGGRR
+ #--------------------------------------------------------------------------
+ # Theme Loading
+ #--------------------------------------------------------------------------
- #
- # Composing Shell
- #
+ def read_theme(self, filepath):
+ """
+ Load a Lighthouse theme file from the given filepath
+ """
+ logging.debug("Opening theme '%s'..." % filepath)
- self._overview_bg = [QtGui.QColor(20, 20, 20), QtGui.QColor(20, 20, 20)]
- self._composer_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)]
+ # attempt to load the theme file contents from disk
+ try:
+ raw_theme = open(filepath, "r").read()
+ except Exception as e:
+ logger.debug("Could not open theme from '%s'" % filepath)
+ return None
- self._valid_text = [0x80F0FF, 0x0000FF]
- self._invalid_text = [0xF02070, 0xFF0000]
- self._invalid_highlight = [0x990000, 0xFF0000]
+ # convert the theme file contents to a json object/dict
+ try:
+ theme = json.loads(raw_theme)
+ except Exception as e:
+ logger.exception("Could not convert thme '%s' to json" % filepath)
+ return None
- self._shell_hint_bg = [QtGui.QColor(45, 45, 45), QtGui.QColor(45, 45, 45)]
- self._shell_hint_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)]
+ return theme
- self._combobox_bg = [QtGui.QColor(45, 45, 45), QtGui.QColor(45, 45, 45)]
- self._combobox_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)]
+ def apply_theme(self, theme):
+ """
+ Apply a given theme to Lighthouse.
+ """
+ logging.debug("Applying theme '%s'..." % theme["name"])
+ colors = theme["colors"]
- self._combobox_selection_bg = [QtGui.QColor(51, 153, 255), QtGui.QColor(51, 153, 255)]
- self._combobox_selection_fg = [QtGui.QColor(255, 255, 255), QtGui.QColor(255, 255, 255)]
+ for field_name, color_name in theme["fields"].items():
- self._border = [QtGui.QColor(100, 100, 100), QtGui.QColor(100, 100, 100)]
- self._focus = [QtGui.QColor(160, 160, 160), QtGui.QColor(160, 160, 160)]
+ # load the color
+ color_value = colors[color_name]
+ color = QtGui.QColor(*color_value)
- #
- # Composition Grammar
- #
+ # set theme self.[field_name] = color
+ setattr(self, field_name, color)
- self._logic_token = [QtGui.QColor("#F02070"), QtGui.QColor("#FF0000")]
- self._comma_token = [QtGui.QColor("#00FF00"), QtGui.QColor("#0000FF")]
- self._paren_token = [QtGui.QColor("#40FF40"), QtGui.QColor("#0000FF")]
- self._coverage_token = [QtGui.QColor("#80F0FF"), QtGui.QColor("#000000")]
+ # patchup the theme...
+ rgb = int(self.coverage_paint.name()[1:], 16)
+ self.coverage_paint = swap_rgb(rgb)
#--------------------------------------------------------------------------
# Theme Management
@@ -195,118 +247,6 @@ def _qt_theme_hint(self):
# return 'Dark' or 'Light'
return test_color_brightness(bg_color)
- #--------------------------------------------------------------------------
- # Coverage Overview
- #--------------------------------------------------------------------------
-
- @property
- def selection(self):
- return self._selection[self.qt_theme]
-
- @property
- def coverage_none(self):
- return self._coverage_none[self.qt_theme]
-
- @property
- def coverage_bad(self):
- return self._coverage_bad[self.qt_theme]
-
- @property
- def coverage_okay(self):
- return self._coverage_okay[self.qt_theme]
-
- @property
- def coverage_good(self):
- return self._coverage_good[self.qt_theme]
-
- #--------------------------------------------------------------------------
- # IDA Views / HexRays
- #--------------------------------------------------------------------------
-
- @property
- def coverage_paint(self):
- return self._coverage_paint[self.disassembly_theme]
-
- #--------------------------------------------------------------------------
- # Composing Shell
- #--------------------------------------------------------------------------
-
- @property
- def overview_bg(self):
- return self._overview_bg[self.qt_theme]
-
- @property
- def composer_fg(self):
- return self._composer_fg[self.qt_theme]
-
- @property
- def valid_text(self):
- return self._valid_text[self.qt_theme]
-
- @property
- def invalid_text(self):
- return self._invalid_text[self.qt_theme]
-
- @property
- def invalid_highlight(self):
- return self._invalid_highlight[self.qt_theme]
-
- @property
- def shell_hint_bg(self):
- return self._shell_hint_bg[self.qt_theme]
-
- @property
- def shell_hint_fg(self):
- return self._shell_hint_fg[self.qt_theme]
-
- #--------------------------------------------------------------------------
- # Coverage Combobox
- #--------------------------------------------------------------------------
-
- @property
- def combobox_bg(self):
- return self._combobox_bg[self.qt_theme]
-
- @property
- def combobox_fg(self):
- return self._combobox_fg[self.qt_theme]
-
- @property
- def combobox_selection_bg(self):
- return self._combobox_selection_bg[self.qt_theme]
-
- @property
- def combobox_selection_fg(self):
- return self._combobox_selection_fg[self.qt_theme]
-
- @property
- def border(self):
- return self._border[self.qt_theme]
-
- @property
- def focus(self):
- return self._focus[self.qt_theme]
-
- #--------------------------------------------------------------------------
- # Composition Grammar
- #--------------------------------------------------------------------------
-
- @property
- def logic_token(self):
- return self._logic_token[self.qt_theme]
-
- @property
- def comma_token(self):
- return self._comma_token[self.qt_theme]
-
- @property
- def paren_token(self):
- return self._paren_token[self.qt_theme]
-
- @property
- def coverage_token(self):
- return self._coverage_token[self.qt_theme]
-
@property
def TOKEN_COLORS(self):
"""
@@ -333,37 +273,3 @@ def TOKEN_COLORS(self):
"COVERAGE_TOKEN": self.coverage_token,
}
-#------------------------------------------------------------------------------
-# Palette Util
-#------------------------------------------------------------------------------
-
-def to_rgb(color):
- return ((color >> 16 & 0xFF), (color >> 8 & 0xFF), (color & 0xFF))
-
-def test_color_brightness(color):
- """
- Test the brightness of a color.
- """
- if color.lightness() > 255.0/2:
- return "Light"
- else:
- return "Dark"
-
-def compute_color_on_gradiant(percent, color1, color2):
- """
- Compute the color specified by a percent between two colors.
-
- TODO/PERF: This is silly, heavy, and can be refactored.
- """
-
- # dump the rgb values from QColor objects
- 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)
-
- # return the new color
- return QtGui.QColor(r,g,b)
diff --git a/plugin/lighthouse/ui/resources/themes/classic.json b/plugin/lighthouse/ui/resources/themes/classic.json
new file mode 100644
index 00000000..b0b20251
--- /dev/null
+++ b/plugin/lighthouse/ui/resources/themes/classic.json
@@ -0,0 +1,59 @@
+{
+ "name": "Classic",
+
+ "colors":
+ {
+ "black": [0, 0, 0],
+ "white": [255, 255, 255],
+
+ "darkGray": [20, 20, 20],
+ "darkGray2": [30, 30, 30],
+
+ "gray": [100, 100, 100],
+ "lightGray": [160, 160, 160],
+
+ "red": [221, 0, 0],
+ "green": [64, 255, 64],
+ "blue": [51, 153, 255],
+ "lightBlue": [128, 200, 255],
+ "darkBlue": [0, 0, 153],
+ "purple": [100, 0, 130]
+
+ },
+
+ "fields":
+ {
+ "coverage_paint": "darkBlue",
+
+ "table_coverage_none": "darkGray2",
+ "table_coverage_bad": "red",
+ "table_coverage_good": "blue",
+ "table_background": "darkGray",
+ "table_selection": "purple",
+
+ "shell_text": "white",
+ "shell_text_valid": "lightBlue",
+ "shell_text_invalid": "red",
+ "shell_highlight_invalid": "red",
+
+ "shell_border": "gray",
+ "shell_border_focus": "lightGray",
+ "shell_background": "darkGray2",
+
+ "shell_hint_text": "white",
+ "shell_hint_background": "darkGray2",
+
+ "logic_token": "red",
+ "comma_token": "green",
+ "paren_token": "green",
+ "coverage_token": "lightBlue",
+
+ "combobox_text": "white",
+ "combobox_selection_text": "white",
+ "combobox_selection_background": "blue",
+
+ "combobox_border": "gray",
+ "combobox_border_focus": "lightGray",
+ "combobox_background": "darkGray2"
+ }
+}
From 72ec6cccf44839a2d987bec418ecc9b63b15d2dd Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 31 Mar 2020 02:30:24 -0400
Subject: [PATCH 062/154] updates, more tweakable fields
---
plugin/lighthouse/composer/shell.py | 4 ++--
plugin/lighthouse/director.py | 21 ++-----------------
plugin/lighthouse/ui/coverage_table.py | 8 ++-----
.../ui/resources/themes/classic.json | 2 ++
4 files changed, 8 insertions(+), 27 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 2afad64b..739da5b4 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -858,7 +858,7 @@ def _color_invalid(self):
invalid_color = self._palette.shell_highlight_invalid
highlight = QtGui.QTextCharFormat()
highlight.setFontWeight(QtGui.QFont.Bold)
- highlight.setBackground(QtGui.QBrush(QtGui.QColor(invalid_color)))
+ highlight.setBackground(QtGui.QBrush(invalid_color))
self._line.blockSignals(True)
################# UPDATES DISABLED #################
@@ -907,7 +907,7 @@ def _color_text(self, color=None, start=0, end=0):
# setup a simple font coloring (or clearing) text format
simple = QtGui.QTextCharFormat()
if color:
- simple.setForeground(QtGui.QBrush(QtGui.QColor(color)))
+ simple.setForeground(QtGui.QBrush(color))
self._line.blockSignals(True)
################# UPDATES DISABLED #################
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index e0e57f98..18442d7a 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -8,7 +8,7 @@
from lighthouse.util.qt import flush_qt_events
from lighthouse.util.misc import *
from lighthouse.util.python import *
-from lighthouse.util.qt import await_future, await_lock, color_text
+from lighthouse.util.qt import await_future, await_lock
from lighthouse.util.disassembler import disassembler
from lighthouse.reader import CoverageReader
@@ -876,7 +876,7 @@ def get_coverage(self, name):
# could not locate coverage
return None
- def get_coverage_string(self, coverage_name, color=False):
+ def get_coverage_string(self, coverage_name):
"""
Retrieve a detailed coverage string for the given coverage_name.
"""
@@ -897,23 +897,6 @@ def get_coverage_string(self, coverage_name, color=False):
# eg: 'A - 73.45% - drcov.boombox.exe.03820.0000.proc.log'
#
- if color:
-
- # color the symbol token like the shell
- symbol = color_text(symbol, self._palette.coverage_token)
-
- # low coverage color
- if percent < 30.0:
- percent_str = color_text(percent_str, self._palette.coverage_bad)
-
- # okay coverage color
- elif percent < 60.0:
- percent_str = color_text(percent_str, self._palette.coverage_okay)
-
- # good coverage color
- else:
- percent_str = color_text(percent_str, self._palette.coverage_good)
-
return "%s - %s%% - %s" % (symbol, percent_str, coverage_name)
#----------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 68077ffe..bab170c8 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -91,9 +91,9 @@ def _ui_init_table(self):
# widget style
self.setStyleSheet(
"QTableView {"
- " gridline-color: black;"
+ " gridline-color: %s;" % palette.table_grid.name() +
" background-color: %s;" % palette.table_background.name() +
- #" color: %s;" % palette.combobox_fg.name() +
+ " color: %s;" % palette.table_text.name() +
" outline: none; "
"} " +
"QTableView::item:selected {"
@@ -863,10 +863,6 @@ def data(self, index, role=QtCore.Qt.DisplayRole):
)
return function_coverage.coverage_color
- # cell text color request
- elif role == QtCore.Qt.ForegroundRole:
- return QtGui.QColor(QtCore.Qt.white)
-
# cell font style format request
elif role == QtCore.Qt.FontRole:
return self._entry_font
diff --git a/plugin/lighthouse/ui/resources/themes/classic.json b/plugin/lighthouse/ui/resources/themes/classic.json
index b0b20251..2b5462d5 100644
--- a/plugin/lighthouse/ui/resources/themes/classic.json
+++ b/plugin/lighthouse/ui/resources/themes/classic.json
@@ -25,6 +25,8 @@
{
"coverage_paint": "darkBlue",
+ "table_text": "white",
+ "table_grid": "black",
"table_coverage_none": "darkGray2",
"table_coverage_bad": "red",
"table_coverage_good": "blue",
From 2d7d0d598b1e3e7eb6d7d8a1c76293d9751d9e01 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 31 Mar 2020 02:31:16 -0400
Subject: [PATCH 063/154] adds light theme
---
.../ui/resources/themes/dullien.json | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 plugin/lighthouse/ui/resources/themes/dullien.json
diff --git a/plugin/lighthouse/ui/resources/themes/dullien.json b/plugin/lighthouse/ui/resources/themes/dullien.json
new file mode 100644
index 00000000..ae010383
--- /dev/null
+++ b/plugin/lighthouse/ui/resources/themes/dullien.json
@@ -0,0 +1,54 @@
+{
+ "name": "Dullien",
+
+ "colors":
+ {
+ "black": [0, 0, 0],
+ "white": [255, 255, 255],
+ "gray": [100, 100, 100],
+ "red": [255, 0, 0],
+ "blue": [0, 0, 255],
+ "lightRed": [240, 150, 150],
+ "lightGreen": [150, 240, 150],
+ "lightBlue": [140, 170, 220]
+ },
+
+ "fields":
+ {
+ "coverage_paint": "lightGreen",
+
+ "table_text": "black",
+ "table_grid": "gray",
+ "table_coverage_none": "lightRed",
+ "table_coverage_bad": "lightRed",
+ "table_coverage_good": "lightGreen",
+ "table_background": "white",
+ "table_selection": "lightBlue",
+
+ "shell_text": "black",
+ "shell_text_valid": "blue",
+ "shell_text_invalid": "red",
+ "shell_highlight_invalid": "lightRed",
+
+ "shell_border": "gray",
+ "shell_border_focus": "lightBlue",
+ "shell_background": "white",
+
+ "shell_hint_text": "black",
+ "shell_hint_background": "white",
+
+ "logic_token": "red",
+ "comma_token": "black",
+ "paren_token": "black",
+ "coverage_token": "blue",
+
+ "combobox_text": "black",
+ "combobox_selection_text": "white",
+ "combobox_selection_background": "lightBlue",
+
+ "combobox_border": "gray",
+ "combobox_border_focus": "lightBlue",
+ "combobox_background": "white"
+ }
+}
+
From 9946863e4188de0994b0b42d57bd057f66c40476 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 31 Mar 2020 19:18:25 -0400
Subject: [PATCH 064/154] populate user theme dir, auto-select best theme,
save/load theme preference from disk,
---
plugin/lighthouse/core.py | 6 +-
plugin/lighthouse/ui/palette.py | 287 ++++++++++++++++++++++++++------
2 files changed, 235 insertions(+), 58 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 1954701c..ae91dc85 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -208,7 +208,7 @@ def open_coverage_overview(self):
"""
Open the dockable 'Coverage Overview' dialog.
"""
- self.palette.refresh_colors()
+ self.palette.refresh_theme()
# the coverage overview is already open & visible, simply refresh it
if self._ui_coverage_overview and self._ui_coverage_overview.isVisible():
@@ -256,7 +256,7 @@ def interactive_load_batch(self):
"""
Perform the user-interactive loading of a coverage batch.
"""
- self.palette.refresh_colors()
+ self.palette.refresh_theme()
#
# kick off an asynchronous metadata refresh. this will run in the
@@ -339,7 +339,7 @@ def interactive_load_file(self):
"""
Perform the user-interactive loading of individual coverage files.
"""
- self.palette.refresh_colors()
+ self.palette.refresh_theme()
#
# kick off an asynchronous metadata refresh. this will run in the
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 08feedf9..51348c37 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -1,9 +1,12 @@
import os
import json
import struct
+import shutil
import logging
+import traceback
from lighthouse.util.qt import *
+from lighthouse.util.log import lmsg
from lighthouse.util.misc import plugin_resource
from lighthouse.util.disassembler import disassembler
@@ -24,9 +27,9 @@ def test_color_brightness(color):
Test the brightness of a color.
"""
if color.lightness() > 255.0/2:
- return "Light"
+ return "light"
else:
- return "Dark"
+ return "dark"
def compute_color_on_gradiant(percent, color1, color2):
"""
@@ -47,16 +50,22 @@ def compute_color_on_gradiant(percent, color1, color2):
# return the new color
return QtGui.QColor(r,g,b)
-def get_theme_dir():
+def get_plugin_theme_dir():
"""
- Return the Lighthouse theme directory.
+ Return the Lighthouse plugin theme directory.
"""
- #theme_directory = os.path.join(
- # disassembler.get_disassembler_user_directory(),
- # "lighthouse_themes"
- #)
return plugin_resource("themes")
+def get_user_theme_dir():
+ """
+ Return the Lighthouse user theme directory.
+ """
+ theme_directory = os.path.join(
+ disassembler.get_disassembler_user_directory(),
+ "lighthouse_themes"
+ )
+ return theme_directory
+
#------------------------------------------------------------------------------
# Plugin Color Palette
#------------------------------------------------------------------------------
@@ -70,29 +79,188 @@ def __init__(self):
"""
Initialize default palette colors for Lighthouse.
"""
+ self._last_directory = None
- # one-time initialization flag, used for selecting initial palette
- self._initialized = False
+ # hints about the user theme (light/dark)
+ self._user_qt_hint = "dark"
+ self._user_disassembly_hint = "dark"
- # the active theme name
- self._qt_theme = "Dark"
- self._disassembly_theme = "Dark"
-
- # the list of available themes
- self._themes = \
+ self.theme = None
+ self._default_themes = \
{
- "Dark": 0,
- "Light": 1,
+ "dark": "classic.json",
+ "light": "dullien.json"
}
- theme_path = os.path.join(get_theme_dir(), "classic.json")
- theme = self.read_theme(theme_path)
- self.apply_theme(theme)
+ # TODO
+ self._populate_user_theme_dir()
+
+ def _populate_user_theme_dir(self):
+ """
+ Create the Lighthouse user theme directory and install default themes.
+ """
+
+ # create the user theme directory if it does not exist
+ user_theme_dir = get_user_theme_dir()
+ if not os.path.exists(user_theme_dir):
+ os.makedirs(user_theme_dir)
+
+ # copy the default themes into the user directory if they don't exist
+ for theme_name in self._default_themes.values():
+
+ #
+ # check if lighthouse has copied the default themes into the user
+ # theme directory before. when 'default' themes exists, skip them
+ # rather than overwriting... as the user may have modified it
+ #
+
+ user_theme_file = os.path.join(user_theme_dir, theme_name)
+ if os.path.exists(user_theme_file):
+ continue
+
+ # copy the in-box themes to the user theme directory
+ plugin_theme_file = os.path.join(get_plugin_theme_dir(), theme_name)
+ shutil.copy(plugin_theme_file, user_theme_file)
+
+ #
+ # if the user tries to switch themes, ensure the file dialog will start
+ # in their user theme directory
+ #
+
+ self._last_directory = user_theme_dir
+
+ def _select_preferred_theme(self):
+ """
+ Return the name of the preferred theme to try loading.
+ """
+ user_theme_dir = get_user_theme_dir()
+
+ # attempt te read the name of the user's active / preferred theme
+ active_filepath = os.path.join(user_theme_dir, ".active_theme")
+ try:
+ theme_name = open(active_filepath).read().strip()
+ except OSError:
+ theme_name = None
+
+ #
+ # there is no preferred theme set, let's try to peek at the user's
+ # disassembler theme & active Qt context and figure out what theme
+ # might work best for them (a light theme or dark one, basically)
+ #
+
+ if not theme_name:
+ self._user_qt_hint = self._qt_theme_hint()
+ self._user_disassembly_hint = self._disassembly_theme_hint()
+
+ # if both hints agree with each other, let's shoot for that theme
+ if self._user_qt_hint == self._user_disassembly_hint:
+ theme_name = self._default_themes[self._user_qt_hint]
+
+ #
+ # the UI hints don't match, so the user is using some ... weird
+ # colors. let's just default to the 'dark' lighthouse theme as
+ # it is more robust and can look okay in both light and dark envs
+ #
+
+ else:
+ theme_name = self._default_themes["dark"]
+
+ # at this point, a theme_name to load should be known
+ return theme_name
+
+ def interactive_change_theme(self):
+ """
+ Open a file dialog and let the user select a new Lighthoue theme.
+ """
+
+ # create & configure a Qt File Dialog for immediate use
+ file_dialog = QtWidgets.QFileDialog(
+ None,
+ "Open Lighthouse theme file",
+ self._last_directory,
+ "JSON Files (*.json)"
+ )
+ file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
+
+ # prompt the user with the file dialog, and await filename(s)
+ filename, _ = file_dialog.getOpenFileName()
+
+ #
+ # ensure the user is only trying to load themes from the user theme
+ # directory as it helps ensure some of our intenal loading logic
+ #
+
+ file_dir = os.path.abspath(os.path.dirname(filename))
+ user_dir = os.path.abspath(get_user_theme_dir())
+ if file_dir != user_dir:
+ text = "Please install your Lighthouse theme into the user theme directory:\n\n" + user_dir
+ disassembler.warning(text)
+ lmsg(text)
+ return
+
+ #
+ # remember the last directory we were in (parsed from a selected file)
+ # for the next time the user comes to load coverage files
+ #
+
+ if filename:
+ self._last_directory = os.path.dirname(filename) + os.sep
+
+ # log the captured (selected) filenames from the dialog
+ logger.debug("Captured filename from theme file dialog: '%s'" % filename)
+
+ # load & apply theme from disk
+ if self.load_theme(filename):
+ return
+
+ # if the selected theme failed to load, throw a visible warning
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
#--------------------------------------------------------------------------
# Theme Loading
#--------------------------------------------------------------------------
+ def load_theme(self, filepath):
+ """
+ TODO
+ """
+
+ # attempt to read json theme from disk
+ try:
+ theme = self.read_theme(filepath)
+
+ # reading file from dsik failed
+ except OSError:
+ lmsg("Could not open theme file at '%s'" % filepath)
+ return False
+
+ # JSON decoding failed
+ except json.decoder.JSONDecodeError as e:
+ lmsg("Failed to decode theme '%s' to json" % filepath)
+ lmsg(" - " + str(e))
+ return False
+
+ # if the theme appears identical to the applied theme. nothing to do!
+ if theme == self.theme:
+ return True
+
+ # try applying the loaded theme to Lighthouse
+ try:
+ self.apply_theme(theme)
+ except Exception as e:
+ lmsg("Failed to load Lighthouse user theme\n%s" % e)
+ return False
+
+ # since everthing looks like it loaded okay, save this as the preferred theme
+ with open(os.path.join(get_user_theme_dir(), ".active_theme"), "w") as f:
+ f.write(filepath)
+
+ # return success
+ return True
+
def read_theme(self, filepath):
"""
Load a Lighthouse theme file from the given filepath
@@ -100,19 +268,12 @@ def read_theme(self, filepath):
logging.debug("Opening theme '%s'..." % filepath)
# attempt to load the theme file contents from disk
- try:
- raw_theme = open(filepath, "r").read()
- except Exception as e:
- logger.debug("Could not open theme from '%s'" % filepath)
- return None
+ raw_theme = open(filepath, "r").read()
# convert the theme file contents to a json object/dict
- try:
- theme = json.loads(raw_theme)
- except Exception as e:
- logger.exception("Could not convert thme '%s' to json" % filepath)
- return None
+ theme = json.loads(raw_theme)
+ # all good
return theme
def apply_theme(self, theme):
@@ -131,29 +292,29 @@ def apply_theme(self, theme):
# set theme self.[field_name] = color
setattr(self, field_name, color)
- # patchup the theme...
+ # HACK: a little dirty, but patchup the theme...
rgb = int(self.coverage_paint.name()[1:], 16)
self.coverage_paint = swap_rgb(rgb)
+ # all done, save the theme in case we need it later
+ self.theme = theme
+
#--------------------------------------------------------------------------
# Theme Management
#--------------------------------------------------------------------------
- @property
- def disassembly_theme(self):
- """
- Return the active IDA theme number.
- """
- return self._themes[self._disassembly_theme]
-
- @property
- def qt_theme(self):
+ def _load_preferred_theme(self, fallback=False):
"""
- Return the active Qt theme number.
+ TODO
"""
- return self._themes[self._qt_theme]
-
- def refresh_colors(self):
+ theme_name = self._select_preferred_theme()
+ if fallback:
+ theme_path = os.path.join(get_plugin_theme_dir(), theme_name)
+ else:
+ theme_path = os.path.join(get_user_theme_dir(), theme_name)
+ return self.load_theme(theme_path)
+
+ def refresh_theme(self):
"""
Dynamically compute palette color based on IDA theme.
@@ -161,23 +322,39 @@ def refresh_colors(self):
to select colors that will hopefully keep things most readable.
"""
- # TODO/FUTURE: temporary until I have a cleaner way to do one-time init
- if self._initialized:
+ #
+ # attempt to load the user's preferred (or hinted) theme. if we are
+ # successful, then there's nothing else to do!
+ #
+
+ if self._load_preferred_theme():
return
#
- # TODO/THEME:
+ # failed to load the preferred theme... so delete the 'active'
+ # file (if there is one) and warn the user before falling back
+ #
+
+ os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
+
+ # if there is already a theme loaded, continue to use it...
+ if self.theme:
+ return
+
#
- # the dark table (Qt) theme is way better than the light theme
- # right now, so we're just going to force that on for everyone
- # for the time being.
+ # if no theme is loaded, we will attempt to detect & load the in-box
+ # themes based on the user's disassembler theme
#
- self._qt_theme = "Dark" # self._qt_theme_hint()
- self._disassembly_theme = self._disassembly_theme_hint()
+ loaded = self._load_preferred_theme(fallback=True)
+ if loaded:
+ return
- # mark the palette as initialized
- self._initialized = True
+ lmsg("Could not load Lighthouse fallback theme!")
def _disassembly_theme_hint(self):
"""
From 2a2b646f29fa1aa3c5efc7fe2f3acaf6aa93cf3e Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 31 Mar 2020 22:43:27 -0400
Subject: [PATCH 065/154] minor cleanup / refactoring of palette
---
plugin/lighthouse/composer/shell.py | 2 +-
plugin/lighthouse/director.py | 12 +-
plugin/lighthouse/ui/coverage_combobox.py | 4 +-
plugin/lighthouse/ui/coverage_table.py | 6 +-
plugin/lighthouse/ui/palette.py | 275 +++++++++++-----------
5 files changed, 149 insertions(+), 150 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 739da5b4..d23620d5 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -27,7 +27,7 @@ def __init__(self, director, table_model, table_view=None):
# external entities
self._director = director
- self._palette = director._palette
+ self._palette = director.palette
self._table_model = table_model
self._table_view = table_view
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 18442d7a..6b95f817 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -56,7 +56,7 @@ def __init__(self, metadata, palette):
self.metadata = metadata
# the plugin color palette
- self._palette = palette
+ self.palette = palette
#----------------------------------------------------------------------
# Coverage
@@ -741,7 +741,7 @@ def update_coverage(self, coverage_name, coverage_data, coverage_filepath=None):
# create a new database coverage mapping from the given coverage data
new_coverage = DatabaseCoverage(
- self._palette,
+ self.palette,
coverage_name,
coverage_filepath,
coverage_data
@@ -856,7 +856,7 @@ def _delete_aggregate_coverage(self):
# TODO/FUTURE: check if there's any references to the coverage aggregate?
# assign a new, blank aggregate set
- self._special_coverage[AGGREGATE] = DatabaseCoverage(self._palette, AGGREGATE)
+ self._special_coverage[AGGREGATE] = DatabaseCoverage(self.palette, AGGREGATE)
self._refresh_aggregate() # probably not needed
def get_coverage(self, name):
@@ -1092,7 +1092,7 @@ def _evaluate_composition(self, ast):
# if the AST is effectively 'null', return a blank coverage set
if isinstance(ast, TokenNull):
- return DatabaseCoverage(self._palette)
+ return DatabaseCoverage(self.palette)
#
# the director's composition evaluation code (this function) is most
@@ -1230,7 +1230,7 @@ def _evaluate_composition_recursive(self, node):
# we use the mask to generate a new DatabaseCoverage mapping.
#
- new_composition = DatabaseCoverage(self._palette, data=coverage_mask)
+ new_composition = DatabaseCoverage(self.palette, data=coverage_mask)
# cache & return the newly computed composition
self._composition_cache[composition_hash] = new_composition
@@ -1277,7 +1277,7 @@ def _evaluate_coverage_range(self, range_token):
assert isinstance(range_token, TokenCoverageRange)
# initialize output to a null coverage set
- output = DatabaseCoverage(self._palette)
+ output = DatabaseCoverage(self.palette)
# expand 'A,Z' to ['A', 'B', 'C', ... , 'Z']
symbols = [chr(x) for x in range(ord(range_token.symbol_start), ord(range_token.symbol_end) + 1)]
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 008bacfe..8fcef02e 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -147,7 +147,7 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- palette = self._director._palette
+ palette = self._director.palette
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
@@ -389,7 +389,7 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- palette = self.model()._director._palette
+ palette = self.model()._director.palette
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index bab170c8..a24a9233 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -84,7 +84,7 @@ def _ui_init_table(self):
"""
Initialize the coverage table.
"""
- palette = self._model._director._palette
+ palette = self._model._director.palette
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
@@ -689,7 +689,7 @@ def __init__(self, director, parent=None):
# a fallback coverage object for functions with no coverage
self._blank_coverage = FunctionCoverage(BADADDR)
- self._blank_coverage.coverage_color = director._palette.table_coverage_none
+ self._blank_coverage.coverage_color = director.palette.table_coverage_none
# set the default column text alignment for each column (centered)
self._default_alignment = QtCore.Qt.AlignCenter
@@ -1066,7 +1066,7 @@ def _generate_html_table(self):
"""
Generate the HTML coverage table.
"""
- palette = self._director._palette
+ palette = self._director.palette
table_rows = []
# generate the table's column title row
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 51348c37..69d86e2f 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -95,6 +95,129 @@ def __init__(self):
# TODO
self._populate_user_theme_dir()
+ @property
+ def TOKEN_COLORS(self):
+ """
+ Return the palette of token colors.
+ """
+
+ return \
+ {
+
+ # logic operators
+ "OR": self.logic_token,
+ "XOR": self.logic_token,
+ "AND": self.logic_token,
+ "MINUS": self.logic_token,
+
+ # misc
+ "COMMA": self.comma_token,
+ "LPAREN": self.paren_token,
+ "RPAREN": self.paren_token,
+ #"WS": self.whitepsace_token,
+ #"UNKNOWN": self.unknown_token,
+
+ # coverage
+ "COVERAGE_TOKEN": self.coverage_token,
+ }
+
+ def interactive_change_theme(self):
+ """
+ Open a file dialog and let the user select a new Lighthoue theme.
+ """
+
+ # create & configure a Qt File Dialog for immediate use
+ file_dialog = QtWidgets.QFileDialog(
+ None,
+ "Open Lighthouse theme file",
+ self._last_directory,
+ "JSON Files (*.json)"
+ )
+ file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
+
+ # prompt the user with the file dialog, and await filename(s)
+ filename, _ = file_dialog.getOpenFileName()
+
+ #
+ # ensure the user is only trying to load themes from the user theme
+ # directory as it helps ensure some of our intenal loading logic
+ #
+
+ file_dir = os.path.abspath(os.path.dirname(filename))
+ user_dir = os.path.abspath(get_user_theme_dir())
+ if file_dir != user_dir:
+ text = "Please install your Lighthouse theme into the user theme directory:\n\n" + user_dir
+ disassembler.warning(text)
+ lmsg(text)
+ return
+
+ #
+ # remember the last directory we were in (parsed from a selected file)
+ # for the next time the user comes to load coverage files
+ #
+
+ if filename:
+ self._last_directory = os.path.dirname(filename) + os.sep
+
+ # log the captured (selected) filenames from the dialog
+ logger.debug("Captured filename from theme file dialog: '%s'" % filename)
+
+ # load & apply theme from disk
+ if self._load_theme(filename):
+ return
+
+ # if the selected theme failed to load, throw a visible warning
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
+
+ def refresh_theme(self):
+ """
+ Dynamically compute palette color based on IDA theme.
+
+ Depending on if IDA is using a dark or light theme, we *try*
+ to select colors that will hopefully keep things most readable.
+ """
+
+ #
+ # attempt to load the user's preferred (or hinted) theme. if we are
+ # successful, then there's nothing else to do!
+ #
+
+ if self._load_preferred_theme():
+ return
+
+ #
+ # failed to load the preferred theme... so delete the 'active'
+ # file (if there is one) and warn the user before falling back
+ #
+
+ os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
+
+ # if there is already a theme loaded, continue to use it...
+ if self.theme:
+ return
+
+ #
+ # if no theme is loaded, we will attempt to detect & load the in-box
+ # themes based on the user's disassembler theme
+ #
+
+ loaded = self._load_preferred_theme(fallback=True)
+ if loaded:
+ return
+
+ lmsg("Could not load Lighthouse fallback theme!")
+
+ #--------------------------------------------------------------------------
+ # Theme Internals
+ #--------------------------------------------------------------------------
+
def _populate_user_theme_dir(self):
"""
Create the Lighthouse user theme directory and install default themes.
@@ -168,69 +291,25 @@ def _select_preferred_theme(self):
# at this point, a theme_name to load should be known
return theme_name
- def interactive_change_theme(self):
+ def _load_preferred_theme(self, fallback=False):
"""
- Open a file dialog and let the user select a new Lighthoue theme.
+ TODO
"""
+ theme_name = self._select_preferred_theme()
+ if fallback:
+ theme_path = os.path.join(get_plugin_theme_dir(), theme_name)
+ else:
+ theme_path = os.path.join(get_user_theme_dir(), theme_name)
+ return self._load_theme(theme_path)
- # create & configure a Qt File Dialog for immediate use
- file_dialog = QtWidgets.QFileDialog(
- None,
- "Open Lighthouse theme file",
- self._last_directory,
- "JSON Files (*.json)"
- )
- file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile)
-
- # prompt the user with the file dialog, and await filename(s)
- filename, _ = file_dialog.getOpenFileName()
-
- #
- # ensure the user is only trying to load themes from the user theme
- # directory as it helps ensure some of our intenal loading logic
- #
-
- file_dir = os.path.abspath(os.path.dirname(filename))
- user_dir = os.path.abspath(get_user_theme_dir())
- if file_dir != user_dir:
- text = "Please install your Lighthouse theme into the user theme directory:\n\n" + user_dir
- disassembler.warning(text)
- lmsg(text)
- return
-
- #
- # remember the last directory we were in (parsed from a selected file)
- # for the next time the user comes to load coverage files
- #
-
- if filename:
- self._last_directory = os.path.dirname(filename) + os.sep
-
- # log the captured (selected) filenames from the dialog
- logger.debug("Captured filename from theme file dialog: '%s'" % filename)
-
- # load & apply theme from disk
- if self.load_theme(filename):
- return
-
- # if the selected theme failed to load, throw a visible warning
- disassembler.warning(
- "Failed to load Lighthouse user theme!\n\n"
- "Please check the console for more information..."
- )
-
- #--------------------------------------------------------------------------
- # Theme Loading
- #--------------------------------------------------------------------------
-
- def load_theme(self, filepath):
+ def _load_theme(self, filepath):
"""
TODO
"""
# attempt to read json theme from disk
try:
- theme = self.read_theme(filepath)
+ theme = self._read_theme(filepath)
# reading file from dsik failed
except OSError:
@@ -249,7 +328,7 @@ def load_theme(self, filepath):
# try applying the loaded theme to Lighthouse
try:
- self.apply_theme(theme)
+ self._apply_theme(theme)
except Exception as e:
lmsg("Failed to load Lighthouse user theme\n%s" % e)
return False
@@ -261,7 +340,7 @@ def load_theme(self, filepath):
# return success
return True
- def read_theme(self, filepath):
+ def _read_theme(self, filepath):
"""
Load a Lighthouse theme file from the given filepath
"""
@@ -276,7 +355,7 @@ def read_theme(self, filepath):
# all good
return theme
- def apply_theme(self, theme):
+ def _apply_theme(self, theme):
"""
Apply a given theme to Lighthouse.
"""
@@ -300,62 +379,9 @@ def apply_theme(self, theme):
self.theme = theme
#--------------------------------------------------------------------------
- # Theme Management
+ # Theme Inference
#--------------------------------------------------------------------------
- def _load_preferred_theme(self, fallback=False):
- """
- TODO
- """
- theme_name = self._select_preferred_theme()
- if fallback:
- theme_path = os.path.join(get_plugin_theme_dir(), theme_name)
- else:
- theme_path = os.path.join(get_user_theme_dir(), theme_name)
- return self.load_theme(theme_path)
-
- def refresh_theme(self):
- """
- Dynamically compute palette color based on IDA theme.
-
- Depending on if IDA is using a dark or light theme, we *try*
- to select colors that will hopefully keep things most readable.
- """
-
- #
- # attempt to load the user's preferred (or hinted) theme. if we are
- # successful, then there's nothing else to do!
- #
-
- if self._load_preferred_theme():
- return
-
- #
- # failed to load the preferred theme... so delete the 'active'
- # file (if there is one) and warn the user before falling back
- #
-
- os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
- disassembler.warning(
- "Failed to load Lighthouse user theme!\n\n"
- "Please check the console for more information..."
- )
-
- # if there is already a theme loaded, continue to use it...
- if self.theme:
- return
-
- #
- # if no theme is loaded, we will attempt to detect & load the in-box
- # themes based on the user's disassembler theme
- #
-
- loaded = self._load_preferred_theme(fallback=True)
- if loaded:
- return
-
- lmsg("Could not load Lighthouse fallback theme!")
-
def _disassembly_theme_hint(self):
"""
Binary hint of the IDA color theme.
@@ -423,30 +449,3 @@ def _qt_theme_hint(self):
# return 'Dark' or 'Light'
return test_color_brightness(bg_color)
-
- @property
- def TOKEN_COLORS(self):
- """
- Return the palette of token colors.
- """
-
- return \
- {
-
- # logic operators
- "OR": self.logic_token,
- "XOR": self.logic_token,
- "AND": self.logic_token,
- "MINUS": self.logic_token,
-
- # misc
- "COMMA": self.comma_token,
- "LPAREN": self.paren_token,
- "RPAREN": self.paren_token,
- #"WS": self.whitepsace_token,
- #"UNKNOWN": self.unknown_token,
-
- # coverage
- "COVERAGE_TOKEN": self.coverage_token,
- }
-
From 5ae17c85c8673c600e199d900feb59385f0b3239 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 07:32:18 -0400
Subject: [PATCH 066/154] add theme change callback to the palette
---
plugin/lighthouse/ui/palette.py | 30 +++++++++++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 69d86e2f..1ca9545d 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -7,8 +7,8 @@
from lighthouse.util.qt import *
from lighthouse.util.log import lmsg
-from lighthouse.util.misc import plugin_resource
from lighthouse.util.disassembler import disassembler
+from lighthouse.util.misc import plugin_resource, register_callback, notify_callback
logger = logging.getLogger("Lighthouse.UI.Palette")
@@ -92,9 +92,16 @@ def __init__(self):
"light": "dullien.json"
}
+ # list of objects requesting a callback after a theme change
+ self._theme_changed_callbacks = []
+
# TODO
self._populate_user_theme_dir()
+ #----------------------------------------------------------------------
+ # Properties
+ #----------------------------------------------------------------------
+
@property
def TOKEN_COLORS(self):
"""
@@ -121,6 +128,26 @@ def TOKEN_COLORS(self):
"COVERAGE_TOKEN": self.coverage_token,
}
+ #----------------------------------------------------------------------
+ # Callbacks
+ #----------------------------------------------------------------------
+
+ def theme_changed(self, callback):
+ """
+ Subscribe a callback for theme change events.
+ """
+ register_callback(self._theme_changed_callbacks, callback)
+
+ def _notify_theme_changed(self):
+ """
+ Notify listeners of a theme change event.
+ """
+ notify_callback(self._theme_changed_callbacks)
+
+ #----------------------------------------------------------------------
+ # Public
+ #----------------------------------------------------------------------
+
def interactive_change_theme(self):
"""
Open a file dialog and let the user select a new Lighthoue theme.
@@ -338,6 +365,7 @@ def _load_theme(self, filepath):
f.write(filepath)
# return success
+ self._notify_theme_changed()
return True
def _read_theme(self, filepath):
From a52b8d5fda47ebf98c32478b37bb3c4bd574fba8 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 08:47:45 -0400
Subject: [PATCH 067/154] split out all theme-dependent code into refreshable
functions
---
plugin/lighthouse/composer/shell.py | 52 ++++++++------
plugin/lighthouse/core.py | 11 +++
plugin/lighthouse/coverage.py | 13 ++++
plugin/lighthouse/director.py | 11 +++
plugin/lighthouse/ui/coverage_combobox.py | 85 +++++++++++++----------
plugin/lighthouse/ui/coverage_overview.py | 10 +++
plugin/lighthouse/ui/coverage_table.py | 44 ++++++++----
7 files changed, 155 insertions(+), 71 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index d23620d5..d8253615 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -46,6 +46,7 @@ def __init__(self, director, table_model, table_view=None):
# configure the widget for use
self._ui_init()
+ self.refresh_theme()
#--------------------------------------------------------------------------
# Properties
@@ -93,23 +94,6 @@ def _ui_init_shell(self):
# the text box / shell / ComposingLine
self._line = ComposingLine()
- # configure the shell background & default text color
- qpal = self._line.palette()
- #qpal.setColor(QtGui.QPalette.Base, self._palette.shell_background)
- qpal.setColor(QtGui.QPalette.Text, self._palette.shell_text)
- qpal.setColor(QtGui.QPalette.WindowText, self._palette.shell_text)
- self._line.setPalette(qpal)
-
- self._line.setStyleSheet(
- "QPlainTextEdit {"
- " background-color: %s;" % self._palette.shell_background.name() +
- " border: 1px solid %s;" % self._palette.shell_border.name() +
- "} "
- "QPlainTextEdit:hover, QPlainTextEdit:focus {"
- " border: 1px solid %s;" % self._palette.shell_border_focus.name() +
- "}"
- )
-
def _ui_init_completer(self):
"""
Initialize the coverage hint UI elements.
@@ -123,10 +107,6 @@ def _ui_init_completer(self):
self._completer.setModel(self._completer_model)
self._completer.setWrapAround(False)
self._completer.popup().setFont(self._font)
- self._completer.popup().setStyleSheet(
- "background: %s;" % self._palette.shell_hint_background.name() +
- "color: %s;" % self._palette.shell_hint_text.name()
- )
self._completer.setWidget(self._line)
def _ui_init_signals(self):
@@ -181,6 +161,36 @@ def refresh(self):
"""
self._internal_refresh()
+ @disassembler.execute_ui
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+ """
+ assert (self._line and self._completer), "UI not yet initialized..."
+
+ # configure the shell background & default text color
+ qpal = self._line.palette()
+ qpal.setColor(QtGui.QPalette.Text, self._palette.shell_text)
+ qpal.setColor(QtGui.QPalette.WindowText, self._palette.shell_text)
+ self._line.setPalette(qpal)
+
+ # set other hard to access shell theme elements
+ self._line.setStyleSheet(
+ "QPlainTextEdit {"
+ " background-color: %s;" % self._palette.shell_background.name() +
+ " border: 1px solid %s;" % self._palette.shell_border.name() +
+ "} "
+ "QPlainTextEdit:hover, QPlainTextEdit:focus {"
+ " border: 1px solid %s;" % self._palette.shell_border_focus.name() +
+ "}"
+ )
+
+ # refresh completer popup style...
+ self._completer.popup().setStyleSheet(
+ "background: %s;" % self._palette.shell_hint_background.name() +
+ "color: %s;" % self._palette.shell_hint_text.name()
+ )
+
@disassembler.execute_ui
def _internal_refresh(self):
"""
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index ae91dc85..329f3acb 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -57,6 +57,7 @@ def _init(self):
# the plugin color palette
self.palette = LighthousePalette()
+ self.palette.theme_changed(self.refresh_theme)
# the coverage engine
self.director = CoverageDirector(self.metadata, self.palette)
@@ -78,6 +79,7 @@ def _init(self):
# expose the live CoverageDirector object instance for external scripts
lighthouse.coverage_director = self.director
+
def print_banner(self):
"""
Print the plugin banner.
@@ -204,6 +206,15 @@ def _uninstall_open_coverage_overview(self):
# UI Actions (Public)
#--------------------------------------------------------------------------
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+ """
+ self.director.refresh_theme()
+ if self._ui_coverage_overview:
+ self._ui_coverage_overview.refresh_theme()
+ self.painter.repaint()
+
def open_coverage_overview(self):
"""
Open the dockable 'Coverage Overview' dialog.
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index be6ac2d4..28648da7 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -302,6 +302,19 @@ def refresh(self):
# dump the unmappable coverage data
#self.dump_unmapped()
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+
+ 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.instruction_percent,
+ self.palette.table_coverage_bad,
+ self.palette.table_coverage_good
+ )
+
def _finalize(self, dirty_nodes, dirty_functions):
"""
Finalize the DatabaseCoverage statistics / data for use.
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 6b95f817..3804b34b 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1319,6 +1319,17 @@ def refresh(self):
# all done ...
disassembler.hide_wait_box()
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+
+ Does not require @disassembler.execute_ui decorator as no Qt is touched.
+ """
+ for coverage in self._database_coverage.values():
+ coverage.refresh_theme()
+ for coverage in self._special_coverage.values():
+ coverage.refresh_theme()
+
def _refresh_database_coverage(self):
"""
Refresh all the database coverage mappings managed by the director.
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 8fcef02e..576d6880 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -41,6 +41,7 @@ def __init__(self, director, parent=None):
# configure the widget for use
self._ui_init()
+ self.refresh_theme()
#--------------------------------------------------------------------------
# QComboBox Overloads
@@ -147,7 +148,6 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- palette = self._director.palette
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
@@ -173,16 +173,6 @@ def _ui_init(self):
self.lineEdit().setReadOnly(True) # text can't be edited
self.lineEdit().setEnabled(False) # text can't be selected
- # configure the combobox style
- self.lineEdit().setStyleSheet(
- "QLineEdit { "
- " border: none;"
- " padding: 0 0 0 2ex;"
- " margin: 0;"
- " background-color: %s;" % palette.combobox_background.name() +
- "}"
- )
-
#
# the combobox will pick a size based on its contents when it is first
# made visible, but we also make it is arbitrarily resizable for the
@@ -195,16 +185,6 @@ def _ui_init(self):
# draw the QComboBox with a 'Windows'-esque style
self.setStyle(QtWidgets.QStyleFactory.create("Windows"))
- self.setStyleSheet(
- "QComboBox {"
- " color: %s;" % palette.combobox_text.name() +
- " border: 1px solid %s;" % palette.combobox_border.name() +
- " padding: 0;"
- "} "
- "QComboBox:hover, QComboBox:focus {"
- " border: 1px solid %s;" % palette.combobox_border_focus.name() +
- "}"
- )
# connect relevant signals
self._ui_init_signals()
@@ -312,6 +292,36 @@ def refresh(self):
"""
self._internal_refresh()
+ @disassembler.execute_ui
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+ """
+ palette = self._director.palette
+ self.view().refresh_theme()
+
+ # configure the combobox's top row / visible dropdown
+ self.lineEdit().setStyleSheet(
+ "QLineEdit { "
+ " border: none;"
+ " padding: 0 0 0 2ex;"
+ " margin: 0;"
+ " background-color: %s;" % palette.combobox_background.name() +
+ "}"
+ )
+
+ # style the combobox dropdown
+ self.setStyleSheet(
+ "QComboBox {"
+ " color: %s;" % palette.combobox_text.name() +
+ " border: 1px solid %s;" % palette.combobox_border.name() +
+ " padding: 0;"
+ "} "
+ "QComboBox:hover, QComboBox:focus {"
+ " border: 1px solid %s;" % palette.combobox_border_focus.name() +
+ "}"
+ )
+
@disassembler.execute_ui
def _internal_refresh(self):
"""
@@ -358,6 +368,7 @@ def __init__(self, model, parent=None):
# initialize UI elements
self._ui_init()
+ self.refresh_theme()
#--------------------------------------------------------------------------
# QTableView Overloads
@@ -389,7 +400,6 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- palette = self.model()._director.palette
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
@@ -397,19 +407,6 @@ def _ui_init(self):
self._font_metrics = QtGui.QFontMetricsF(self._font)
self.setFont(self._font)
- # widget style
- self.setStyleSheet(
- "QTableView {"
- " background-color: %s;" % palette.combobox_background.name() +
- " color: %s;" % palette.combobox_text.name() +
- " selection-background-color: %s;" % palette.combobox_selection_background.name() +
- " selection-color: %s;" % palette.combobox_selection_text.name() +
- " margin: 0; outline: none;"
- "} "
- "QTableView::item{ padding: 0.5ex; } "
- "QTableView::item:focus { padding: 0; }"
- )
-
# hide dropdown table headers, and default grid
self.horizontalHeader().setVisible(False)
self.verticalHeader().setVisible(False)
@@ -477,6 +474,24 @@ def refresh(self):
else:
self.setSpan(row, 0, 0, model.columnCount())
+ @disassembler.execute_ui
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+ """
+ palette = self.model()._director.palette
+ self.setStyleSheet(
+ "QTableView {"
+ " background-color: %s;" % palette.combobox_background.name() +
+ " color: %s;" % palette.combobox_text.name() +
+ " selection-background-color: %s;" % palette.combobox_selection_background.name() +
+ " selection-color: %s;" % palette.combobox_selection_text.name() +
+ " margin: 0; outline: none;"
+ "} "
+ "QTableView::item{ padding: 0.5ex; } "
+ "QTableView::item:focus { padding: 0; }"
+ )
+
#------------------------------------------------------------------------------
# Coverage ComboBox - TableModel
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 23dccd4b..5fe1fa51 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -244,6 +244,16 @@ def refresh(self):
self._shell.refresh()
self._combobox.refresh()
+ @disassembler.execute_ui
+ def refresh_theme(self):
+ """
+ Update visual elements based on theme change.
+ """
+ self._table_view.refresh_theme()
+ self._table_model.refresh_theme()
+ self._shell.refresh_theme()
+ self._combobox.refresh_theme()
+
#------------------------------------------------------------------------------
# Qt Event Filter
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index a24a9233..11ad7008 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -35,6 +35,26 @@ def __init__(self, controller, model, parent=None):
# configure the widget for use
self._ui_init()
+ self.refresh_theme()
+
+ @disassembler.execute_ui
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+ """
+ palette = self._model._director.palette
+ self.setStyleSheet(
+ "QTableView {"
+ " gridline-color: %s;" % palette.table_grid.name() +
+ " background-color: %s;" % palette.table_background.name() +
+ " color: %s;" % palette.table_text.name() +
+ " outline: none; "
+ "} " +
+ "QTableView::item:selected {"
+ " color: white; "
+ " background-color: %s;" % palette.table_selection.name() +
+ "}"
+ )
#--------------------------------------------------------------------------
# QTableView Overloads
@@ -84,24 +104,9 @@ def _ui_init_table(self):
"""
Initialize the coverage table.
"""
- palette = self._model._director.palette
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
- # widget style
- self.setStyleSheet(
- "QTableView {"
- " gridline-color: %s;" % palette.table_grid.name() +
- " background-color: %s;" % palette.table_background.name() +
- " color: %s;" % palette.table_text.name() +
- " outline: none; "
- "} " +
- "QTableView::item:selected {"
- " color: white; "
- " background-color: %s;" % palette.table_selection.name() +
- "}"
- )
-
# these properties will allow the user shrink the table to any size
self.setMinimumHeight(0)
self.setSizePolicy(
@@ -736,6 +741,15 @@ def __init__(self, director, parent=None):
self._director.coverage_modified(self._internal_refresh)
self._director.metadata.function_renamed(self._data_changed)
+ def refresh_theme(self):
+ """
+ Refresh UI facing elements to reflect the current theme.
+
+ Does not require @disassembler.execute_ui decorator, data_changed() has its own.
+ """
+ self._blank_coverage.coverage_color = self._director.palette.table_coverage_none
+ self._data_changed()
+
#--------------------------------------------------------------------------
# QAbstractTableModel Overloads
#--------------------------------------------------------------------------
From be7815ff0614150b2e1cc52041610ed3f6ae53b6 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 23:08:08 -0400
Subject: [PATCH 068/154] remove hardcoded colors from parts of HTML report
---
plugin/lighthouse/ui/coverage_table.py | 33 +++++++++++++------
.../ui/resources/themes/classic.json | 9 +++--
.../ui/resources/themes/dullien.json | 5 +++
3 files changed, 35 insertions(+), 12 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 11ad7008..a44ff629 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -1009,6 +1009,7 @@ def to_html(self):
"""
Generate an HTML representation of the coverage table.
"""
+ palette = self._director.palette
# table summary
summary_html, summary_css = self._generate_html_summary()
@@ -1021,13 +1022,16 @@ def to_html(self):
body_html = "%s" % '\n'.join(body_elements)
body_css = \
"""
- body {
+ body {{
font-family: Arial, Helvetica, sans-serif;
- color: white;
- background-color: #363636;
- }
- """
+ color: {page_fg};
+ background-color: {page_bg};
+ }}
+ """.format(
+ page_fg=palette.table_text.name(),
+ page_bg=palette.html_page_background.name()
+ )
# HTML tag
css_elements = [body_css, summary_css, table_css]
@@ -1045,6 +1049,7 @@ def _generate_html_summary(self):
"""
Generate the HTML table summary.
"""
+ palette = self._director.palette
metadata = self._director.metadata
coverage = self._director.coverage
@@ -1067,9 +1072,17 @@ def _generate_html_summary(self):
list_html = "" % '\n'.join(details)
list_css = \
"""
- .detail { font-weight: bold; color: white; }
- li { color: #c0c0c0; }
- """
+ .detail {{
+ font-weight: bold;
+ color: {page_fg};
+ }}
+ li {{
+ color: {detail_fg};
+ }}
+ """.format(
+ page_fg=palette.table_text.name(),
+ detail_fg=palette.html_summary_text.name()
+ )
# title + summary
summary_html = title_html + list_html
@@ -1089,7 +1102,7 @@ def _generate_html_table(self):
header_cells.append(
"%s | " % self.headerData(i, QtCore.Qt.Horizontal)
)
- table_rows.append(("#505050", header_cells))
+ table_rows.append((palette.html_table_header.name(), header_cells))
# generate the table's coverage rows
for row in xrange(self.rowCount()):
@@ -1138,7 +1151,7 @@ def _generate_html_table(self):
}}
""".format(
table_bg=palette.table_background.name(),
- table_fg="white"
+ table_fg=palette.table_text.name()
)
return (table_html, table_css)
diff --git a/plugin/lighthouse/ui/resources/themes/classic.json b/plugin/lighthouse/ui/resources/themes/classic.json
index 2b5462d5..4f53bca1 100644
--- a/plugin/lighthouse/ui/resources/themes/classic.json
+++ b/plugin/lighthouse/ui/resources/themes/classic.json
@@ -8,9 +8,10 @@
"darkGray": [20, 20, 20],
"darkGray2": [30, 30, 30],
+ "darkGray3": [54, 54, 54],
"gray": [100, 100, 100],
- "lightGray": [160, 160, 160],
+ "lightGray": [180, 180, 180],
"red": [221, 0, 0],
"green": [64, 255, 64],
@@ -33,6 +34,10 @@
"table_background": "darkGray",
"table_selection": "purple",
+ "html_summary_text": "lightGray",
+ "html_table_header": "gray",
+ "html_page_background": "darkGray3",
+
"shell_text": "white",
"shell_text_valid": "lightBlue",
"shell_text_invalid": "red",
@@ -43,7 +48,7 @@
"shell_background": "darkGray2",
"shell_hint_text": "white",
- "shell_hint_background": "darkGray2",
+ "shell_hint_background": "darkGray3",
"logic_token": "red",
"comma_token": "green",
diff --git a/plugin/lighthouse/ui/resources/themes/dullien.json b/plugin/lighthouse/ui/resources/themes/dullien.json
index ae010383..bd04e5bd 100644
--- a/plugin/lighthouse/ui/resources/themes/dullien.json
+++ b/plugin/lighthouse/ui/resources/themes/dullien.json
@@ -6,6 +6,7 @@
"black": [0, 0, 0],
"white": [255, 255, 255],
"gray": [100, 100, 100],
+ "lightGray": [220, 220, 220],
"red": [255, 0, 0],
"blue": [0, 0, 255],
"lightRed": [240, 150, 150],
@@ -25,6 +26,10 @@
"table_background": "white",
"table_selection": "lightBlue",
+ "html_summary_text": "gray",
+ "html_table_header": "lightGray",
+ "html_page_background": "white",
+
"shell_text": "black",
"shell_text_valid": "blue",
"shell_text_invalid": "red",
From f7ade4eaa31746ff9f21d88ce505f4bcd25d4ba1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 23:18:59 -0400
Subject: [PATCH 069/154] enable theme change button
---
plugin/lighthouse/ui/coverage_settings.py | 15 ++++++++-------
1 file changed, 8 insertions(+), 7 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 5f0291ee..5352fe8f 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -47,26 +47,26 @@ def _ui_init_actions(self):
"""
# lighthouse colors
- self._action_colors = QtWidgets.QAction("Colors", None)
- self._action_colors.setToolTip("Lighthouse color & theme customization")
- #self.addAction(self._action_colors)
- #self.addSeparator()
+ self._action_change_theme = QtWidgets.QAction("Change theme", None)
+ self._action_change_theme.setToolTip("Lighthouse color & theme customization")
+ self.addAction(self._action_change_theme)
+ self.addSeparator()
# painting
self._action_pause_paint = QtWidgets.QAction("Pause painting", None)
self._action_pause_paint.setCheckable(True)
- self._action_pause_paint.setToolTip("Disable coverage painting")
+ self._action_pause_paint.setToolTip("Disable the coverage painting subsystem")
self.addAction(self._action_pause_paint)
# misc
self._action_clear_paint = QtWidgets.QAction("Clear paint", None)
- self._action_clear_paint.setToolTip("Forcefully clear all paint")
+ self._action_clear_paint.setToolTip("Forcefully clear all paint from the database")
self.addAction(self._action_clear_paint)
self.addSeparator()
# table actions
self._action_refresh_metadata = QtWidgets.QAction("Full table refresh", None)
- self._action_refresh_metadata.setToolTip("Refresh metadata & coverage for db")
+ self._action_refresh_metadata.setToolTip("Refresh the database metadata and coverage mapping")
self.addAction(self._action_refresh_metadata)
self._action_export_html = QtWidgets.QAction("Export to HTML", None)
@@ -82,6 +82,7 @@ def connect_signals(self, controller, core):
"""
Connect UI signals.
"""
+ self._action_change_theme.triggered.connect(core.palette.interactive_change_theme)
self._action_refresh_metadata.triggered.connect(core.director.refresh)
self._action_hide_zero.triggered[bool].connect(controller._model.filter_zero_coverage)
self._action_pause_paint.triggered[bool].connect(lambda x: core.painter.set_enabled(not x))
From 9b85603828afc56c9c2762d8556dd8d513b77cf2 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 23:19:54 -0400
Subject: [PATCH 070/154] rename 'classic' theme to 'synthwave'
---
plugin/lighthouse/ui/palette.py | 2 +-
.../ui/resources/themes/{classic.json => synthwave.json} | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
rename plugin/lighthouse/ui/resources/themes/{classic.json => synthwave.json} (98%)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 1ca9545d..01174a56 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -88,7 +88,7 @@ def __init__(self):
self.theme = None
self._default_themes = \
{
- "dark": "classic.json",
+ "dark": "synthwave.json",
"light": "dullien.json"
}
diff --git a/plugin/lighthouse/ui/resources/themes/classic.json b/plugin/lighthouse/ui/resources/themes/synthwave.json
similarity index 98%
rename from plugin/lighthouse/ui/resources/themes/classic.json
rename to plugin/lighthouse/ui/resources/themes/synthwave.json
index 4f53bca1..66677c83 100644
--- a/plugin/lighthouse/ui/resources/themes/classic.json
+++ b/plugin/lighthouse/ui/resources/themes/synthwave.json
@@ -1,5 +1,5 @@
{
- "name": "Classic",
+ "name": "Synthwave",
"colors":
{
From 2af8854673756a7e4a4a84d33c142c5ea548b549 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 23:54:57 -0400
Subject: [PATCH 071/154] add a bit of theme validation ...
---
plugin/lighthouse/ui/palette.py | 43 ++++++++++++++++++++++++++++++++-
1 file changed, 42 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 01174a56..b8903d19 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -80,6 +80,7 @@ def __init__(self):
Initialize default palette colors for Lighthouse.
"""
self._last_directory = None
+ self._required_fields = []
# hints about the user theme (light/dark)
self._user_qt_hint = "dark"
@@ -95,7 +96,10 @@ def __init__(self):
# list of objects requesting a callback after a theme change
self._theme_changed_callbacks = []
- # TODO
+ # get a list of required theme fields, for user theme validation
+ self._load_required_fields()
+
+ # initialize the user theme directory
self._populate_user_theme_dir()
#----------------------------------------------------------------------
@@ -279,6 +283,22 @@ def _populate_user_theme_dir(self):
self._last_directory = user_theme_dir
+ def _load_required_fields(self):
+ """
+ Load the required theme fields from a donor in-box theme.
+ """
+
+ # load a known-good theme from the plugin's in-box themes
+ filepath = os.path.join(get_plugin_theme_dir(), self._default_themes["dark"])
+ theme = self._read_theme(filepath)
+
+ #
+ # save all the defined fields in this 'good' theme as a ground truth
+ # to validate user themes against...
+ #
+
+ self._required_fields = theme["fields"].keys()
+
def _select_preferred_theme(self):
"""
Return the name of the preferred theme to try loading.
@@ -329,6 +349,23 @@ def _load_preferred_theme(self, fallback=False):
theme_path = os.path.join(get_user_theme_dir(), theme_name)
return self._load_theme(theme_path)
+ def _validate_theme(self, theme):
+ """
+ Pefrom rudimentary theme validation.
+ """
+ user_fields = theme.get("fields", None)
+ if not user_fields:
+ lmsg("Could not find theme 'fields' definition")
+ return False
+
+ # check that all the 'required' fields exist in the given theme
+ for field in self._required_fields:
+ if field not in user_fields:
+ lmsg("Could not find required theme field '%s'" % field)
+ return False
+
+ # theme looks good enough for now...
+ return True
def _load_theme(self, filepath):
"""
TODO
@@ -353,6 +390,10 @@ def _load_theme(self, filepath):
if theme == self.theme:
return True
+ # do some basic sanity checking on the given theme file
+ if not self._validate_theme(theme):
+ return False
+
# try applying the loaded theme to Lighthouse
try:
self._apply_theme(theme)
From fb65c06b1c39081f71a70129c311d8c9df94e2d5 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 1 Apr 2020 23:55:09 -0400
Subject: [PATCH 072/154] some cleanup
---
plugin/lighthouse/composer/shell.py | 1 +
plugin/lighthouse/director.py | 2 +-
plugin/lighthouse/ui/palette.py | 30 ++++++++++++++++++-----------
3 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index d8253615..cadabf1a 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -177,6 +177,7 @@ def refresh_theme(self):
# set other hard to access shell theme elements
self._line.setStyleSheet(
"QPlainTextEdit {"
+ " color: %s;" % self._palette.shell_text.name() + # this line ensures the text cursor changes color, with the theme
" background-color: %s;" % self._palette.shell_background.name() +
" border: 1px solid %s;" % self._palette.shell_border.name() +
"} "
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 3804b34b..b3d8a729 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1310,7 +1310,7 @@ def refresh(self):
# (re) build our metadata cache of the underlying database
self.metadata.refresh()
- # (re)map each set of loaded coverage data to the database
+ # (re) map each set of loaded coverage data to the database
self._refresh_database_coverage()
# notify of full-refresh
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index b8903d19..24838c34 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -17,9 +17,15 @@
#------------------------------------------------------------------------------
def swap_rgb(i):
+ """
+ Swap RRGGBB (integer) to BBGGRR.
+ """
return struct.unpack("I", i))[0] >> 8
def to_rgb(color):
+ """
+ Split RRGGBB (integer) to (RR, GG, BB) tuple.
+ """
return ((color >> 16 & 0xFF), (color >> 8 & 0xFF), (color & 0xFF))
def test_color_brightness(color):
@@ -340,7 +346,7 @@ def _select_preferred_theme(self):
def _load_preferred_theme(self, fallback=False):
"""
- TODO
+ Load the user's preferred theme, or the one hinted at by the theme subsystem.
"""
theme_name = self._select_preferred_theme()
if fallback:
@@ -366,9 +372,10 @@ def _validate_theme(self, theme):
# theme looks good enough for now...
return True
+
def _load_theme(self, filepath):
"""
- TODO
+ Load and apply the Lighthouse theme at the given filepath.
"""
# attempt to read json theme from disk
@@ -411,7 +418,7 @@ def _load_theme(self, filepath):
def _read_theme(self, filepath):
"""
- Load a Lighthouse theme file from the given filepath
+ Parse the Lighthouse theme file from the given filepath.
"""
logging.debug("Opening theme '%s'..." % filepath)
@@ -426,7 +433,7 @@ def _read_theme(self, filepath):
def _apply_theme(self, theme):
"""
- Apply a given theme to Lighthouse.
+ Apply the given theme definition to Lighthouse.
"""
logging.debug("Applying theme '%s'..." % theme["name"])
colors = theme["colors"]
@@ -440,9 +447,10 @@ def _apply_theme(self, theme):
# set theme self.[field_name] = color
setattr(self, field_name, color)
- # HACK: a little dirty, but patchup the theme...
- rgb = int(self.coverage_paint.name()[1:], 16)
- self.coverage_paint = swap_rgb(rgb)
+ # HACK: IDA uses BBGGRR for its databasse highlighting
+ if disassembler.NAME == "IDA":
+ rgb = int(self.coverage_paint.name()[1:], 16)
+ self.coverage_paint = swap_rgb(rgb)
# all done, save the theme in case we need it later
self.theme = theme
@@ -458,7 +466,7 @@ def _disassembly_theme_hint(self):
This routine returns a best effort hint as to what kind of theme is
in use for the IDA Views (Disas, Hex, HexRays, etc).
- Returns 'Dark' or 'Light' indicating the user's theme
+ Returns 'dark' or 'light' indicating the user's theme
"""
#
@@ -468,7 +476,7 @@ def _disassembly_theme_hint(self):
bg_color = disassembler.get_disassembly_background_color()
- # return 'Dark' or 'Light'
+ # return 'dark' or 'light'
return test_color_brightness(bg_color)
def _qt_theme_hint(self):
@@ -480,7 +488,7 @@ def _qt_theme_hint(self):
who may be using Zyantific's IDASkins plugins (or others) to further
customize IDA's appearance.
- Returns 'Dark' or 'Light' indicating the user's theme
+ Returns 'dark' or 'light' indicating the user's theme
"""
#
@@ -516,5 +524,5 @@ def _qt_theme_hint(self):
test_widget.hide()
test_widget.deleteLater()
- # return 'Dark' or 'Light'
+ # return 'dark' or 'light'
return test_color_brightness(bg_color)
From 4b63a0f857fb722719e35c946b571b587f38ef4c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 00:02:25 -0400
Subject: [PATCH 073/154] Rename dark theme, again. crosses off themes from
readme
---
README.md | 2 +-
plugin/lighthouse/ui/palette.py | 2 +-
.../ui/resources/themes/{synthwave.json => synth.json} | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
rename plugin/lighthouse/ui/resources/themes/{synthwave.json => synth.json} (98%)
diff --git a/README.md b/README.md
index 6f2381fe..94691d59 100644
--- a/README.md
+++ b/README.md
@@ -226,7 +226,7 @@ Time and motivation permitting, future work may include:
* ~~Additional coverage sources, trace formats, etc~~
* Improved pseudocode painting
* ~~Lighthouse console access~~, headless usage
-* Custom themes
+* ~~Custom themes~~
* ~~Python 3 support~~
I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repo.
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 24838c34..61a08567 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -95,7 +95,7 @@ def __init__(self):
self.theme = None
self._default_themes = \
{
- "dark": "synthwave.json",
+ "dark": "synth.json",
"light": "dullien.json"
}
diff --git a/plugin/lighthouse/ui/resources/themes/synthwave.json b/plugin/lighthouse/ui/resources/themes/synth.json
similarity index 98%
rename from plugin/lighthouse/ui/resources/themes/synthwave.json
rename to plugin/lighthouse/ui/resources/themes/synth.json
index 66677c83..83c817a4 100644
--- a/plugin/lighthouse/ui/resources/themes/synthwave.json
+++ b/plugin/lighthouse/ui/resources/themes/synth.json
@@ -1,5 +1,5 @@
{
- "name": "Synthwave",
+ "name": "Synth",
"colors":
{
From 79c90db5b069e86a87cda73730f1a0cfd28b1348 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 00:39:30 -0400
Subject: [PATCH 074/154] fixes bug where a leftover / mostly deleted coverage
overview could get left hanging around
---
plugin/lighthouse/ui/coverage_overview.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 5fe1fa51..37ea4631 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -264,7 +264,7 @@ class EventProxy(QtCore.QObject):
def __init__(self, target):
super(EventProxy, self).__init__()
- self._target = target
+ self._target = weakref.proxy(target)
def eventFilter(self, source, event):
@@ -274,6 +274,7 @@ def eventFilter(self, source, event):
#
if int(event.type()) == 16: # NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7?
+ source.removeEventFilter(self)
self._target.terminate()
#
From f6902baf387ef2dac5fac4c26fe494f1414c3f36 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 04:42:57 -0400
Subject: [PATCH 075/154] update drcov parser to account for multi-segment
modules
---
plugin/lighthouse/reader/coverage_reader.py | 3 +-
plugin/lighthouse/reader/parsers/drcov.py | 55 ++++++++++++---------
2 files changed, 33 insertions(+), 25 deletions(-)
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 68888391..8bce11d4 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -42,7 +42,8 @@ def open(self, filepath):
# log the exceptions for each parse failure
except Exception as e:
parse_failures[name] = traceback.format_exc()
- logger.debug("| Parse FAILED")
+ logger.debug("| Parse FAILED - " + str(e))
+ #logger.exception("| Parse FAILED")
#
# if *all* the coverage file parsers failed, raise an exception with
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 0be49298..14b148e5 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -4,6 +4,7 @@
import sys
import mmap
import struct
+import collections
from ctypes import *
#
@@ -59,16 +60,26 @@ def get_offsets(self, module_name):
"""
Return coverage data as basic block offsets for the named module.
"""
- try:
- module = self.modules[module_name]
- except KeyError:
+ modules = self.modules.get(module_name, [])
+ if not modules:
return []
- # extract module id for speed
- mod_id = module.id
+ #
+ # I don't know if this should ever actually trigger, but if it does,
+ # it is a strange testcase to collect coverage against. It means that
+ # maybe the target library/module was loaded, unloaded, and reloaded?
+ #
+ # if someone ever actally triggers this, we can look into it :S
+ #
+
+ if self.version > 2:
+ assert all(module.containing_id == modules[0].id for module in modules)
- # loop through the coverage data and filter out data for only this module
- coverage_blocks = [bb.start for bb in self.bbs if bb.mod_id == mod_id]
+ # extract the unique module ids that we need to collect blocks for
+ 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 for bb in self.bbs if bb.mod_id in mod_ids]
# return the filtered coverage blocks
return coverage_blocks
@@ -77,16 +88,19 @@ def get_offset_blocks(self, module_name):
"""
Return coverage data as basic blocks (offset, size) for the named module.
"""
- try:
- module = self.modules[module_name]
- except KeyError:
+ modules = self.modules.get(module_name, [])
+ if not modules:
return []
- # extract module id for speed
- mod_id = module.id
+ # NOTE: see comment in get_offsets() for more info...
+ if self.version > 2:
+ assert all(module.containing_id == modules[0].id for module in modules)
+
+ # extract the unique module ids that we need to collect blocks for
+ mod_ids = [module.id for module in modules]
- # loop through the coverage data and filter out data for only this module
- coverage_blocks = [(bb.start, bb.size) for bb in self.bbs if bb.mod_id == mod_id]
+ # 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]
# return the filtered coverage blocks
return coverage_blocks
@@ -234,21 +248,14 @@ def _parse_module_table_modules(self, f):
"""
Parse drcov log modules in the module table from filestream.
"""
+ modules = collections.defaultdict(list)
# loop through each *expected* line in the module table and parse it
for i in range(self.module_table_count):
module = DrcovModule(f.readline().decode('utf-8').strip(), self.module_table_version)
+ modules[module.filename].append(module)
- # try to handle module name collisions...
- if module.filename in self.modules:
- public_name = module.filename + "_" + str(i)
- else:
- public_name = module.filename
-
- assert not (public_name in self.modules), "Stop doing weird stuff."
-
- # save the parsed module
- self.modules[public_name] = module
+ self.modules = modules
def _parse_bb_table(self, f):
"""
From e89a36b9d67dde36dbfbbfa11462cdda186b03e6 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 06:00:34 -0400
Subject: [PATCH 076/154] minor binja tweaks, to keep dev working with it for
now...
---
plugin/lighthouse/painting/binja_painter.py | 10 +++++++---
plugin/lighthouse/ui/palette.py | 12 +++++-------
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index 24d2577d..65bf243d 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -4,7 +4,6 @@
from binaryninja import HighlightStandardColor
from binaryninja.highlight import HighlightColor
-from lighthouse.palette import to_rgb
from lighthouse.painting import DatabasePainter
from lighthouse.util.disassembler import disassembler
@@ -43,7 +42,7 @@ def _clear_instructions(self, instructions):
def _paint_nodes(self, nodes_coverage):
bv = disassembler.bv
- b, g, r = to_rgb(self.palette.coverage_paint)
+ r, g, b, _ = self.palette.coverage_paint.getRgb()
color = HighlightColor(red=r, green=g, blue=b)
for node_coverage in nodes_coverage:
node_metadata = node_coverage.database._metadata.nodes[node_coverage.address]
@@ -71,9 +70,14 @@ def _cancel_action(self, job):
#--------------------------------------------------------------------------
def _priority_paint(self):
+ db_metadata = self._director.metadata
+
current_address = disassembler.get_current_address()
current_function = disassembler.bv.get_function_at(current_address)
- if current_function:
+ function_metadata = db_metadata.get_closest_function(current_address)
+
+ if current_function and function_metadata:
self._paint_function(current_function.start)
+
return True
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 61a08567..99740f6a 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -22,12 +22,6 @@ def swap_rgb(i):
"""
return struct.unpack("I", i))[0] >> 8
-def to_rgb(color):
- """
- Split RRGGBB (integer) to (RR, GG, BB) tuple.
- """
- return ((color >> 16 & 0xFF), (color >> 8 & 0xFF), (color & 0xFF))
-
def test_color_brightness(color):
"""
Test the brightness of a color.
@@ -512,7 +506,11 @@ def _qt_theme_hint(self):
# lmao, don't ask me why they forgot about this attribute from 5.0 - 5.6
#
- test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
+ if disassembler.NAME == "BINJA":
+ test_widget.setAttribute(QtCore.Qt.WA_DontShowOnScreen)
+ else:
+ test_widget.setAttribute(103) # taken from http://doc.qt.io/qt-5/qt.html
+
# render the (invisible) widget
test_widget.show()
From 02ea88e3d77b51adda66aedf77bdc4f8b97aa92a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 08:03:35 -0400
Subject: [PATCH 077/154] adds fallback selector dialog if the database's
loaded module (binary) cannot be found in a coverage file. closes #63
---
plugin/lighthouse/director.py | 37 ++++--
plugin/lighthouse/ui/__init__.py | 1 +
plugin/lighthouse/ui/module_selector.py | 149 ++++++++++++++++++++++++
3 files changed, 180 insertions(+), 7 deletions(-)
create mode 100644 plugin/lighthouse/ui/module_selector.py
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index b3d8a729..d291b372 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -11,6 +11,7 @@
from lighthouse.util.qt import await_future, await_lock
from lighthouse.util.disassembler import disassembler
+from lighthouse.ui import ModuleSelector
from lighthouse.reader import CoverageReader
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.coverage import DatabaseCoverage
@@ -64,6 +65,7 @@ def __init__(self, metadata, palette):
# the coverage file parser
self.reader = CoverageReader()
+ self._target_whitelist = []
# the name of the active coverage
self.coverage_name = NEW_COMPOSITION
@@ -491,27 +493,48 @@ def _extract_coverage_data(self, coverage_file):
"""
Internal routine to extract relevant coverage data from a CoverageFile.
"""
- imagebase = self.metadata.imagebase
+ database_target = self.metadata.filename
+ target_names = [database_target] + self._target_whitelist
#
- # inspect the coverage file and extract the module name that seems
- # to match the executable loaded by the disassembler (fuzzy lookup)
+ # inspect the coverage file and extract the module name that seems to
+ # match the executable loaded by the disassembler (fuzzy lookup) or
+ # otherwise aliased by the user through the fallback dialog
#
- module_name = self._find_fuzzy_name(coverage_file, self.metadata.filename)
+ for name in target_names:
+ module_name = self._find_fuzzy_name(coverage_file, name)
+ if module_name:
+ break
#
- # TODO/BAILOUT
+ # if the fuzzy name lookup failed and there are named modules in the
+ # coverage file, then we will show them to the user and see if they
+ # can pick out a matching module to load coverage from
#
if not module_name and coverage_file.modules:
- logger.debug("TODO/BAILOUT DIALOG")
- return []
+
+ #
+ # if the user closes the dialog without selecting a name, there's
+ # nothing we can do for them ...
+ #
+
+ dialog = ModuleSelector(database_target, coverage_file.modules, coverage_file.filepath)
+ if not dialog.exec_():
+ return [] # no coverage data extracted ...
+
+ # the user selected a module name! use that to extract coverage
+ module_name = dialog.selected_name
+ if dialog.remember_alias:
+ self._target_whitelist.append(module_name)
#
# (module, offset, size) style logs (eg, drcov)
#
+ imagebase = self.metadata.imagebase
+
try:
coverage_blocks = coverage_file.get_offset_blocks(module_name)
coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
diff --git a/plugin/lighthouse/ui/__init__.py b/plugin/lighthouse/ui/__init__.py
index 0963495b..f8b013f2 100644
--- a/plugin/lighthouse/ui/__init__.py
+++ b/plugin/lighthouse/ui/__init__.py
@@ -1,3 +1,4 @@
from .palette import LighthousePalette
from .coverage_xref import CoverageXref
+from .module_selector import ModuleSelector
from .coverage_overview import CoverageOverview
diff --git a/plugin/lighthouse/ui/module_selector.py b/plugin/lighthouse/ui/module_selector.py
new file mode 100644
index 00000000..3ed7cad5
--- /dev/null
+++ b/plugin/lighthouse/ui/module_selector.py
@@ -0,0 +1,149 @@
+import os
+import logging
+
+from lighthouse.util import lmsg
+from lighthouse.util.qt import *
+from lighthouse.util.misc import human_timestamp
+from lighthouse.util.python import *
+
+logger = logging.getLogger("Lighthouse.UI.ModuleSelector")
+
+#------------------------------------------------------------------------------
+# Coverage Xref Dialog
+#------------------------------------------------------------------------------
+
+class ModuleSelector(QtWidgets.QDialog):
+ """
+ A Qt Dialog to list all the coverage modules in a coverage file.
+
+ This class makes up a rudimentary selector dialog. It does not follow Qt
+ 'best practices' because it does not need to be super flashy, nor does
+ it demand much facetime.
+ """
+
+ def __init__(self, target_name, module_names, coverage_file):
+ super(ModuleSelector, self).__init__()
+
+ self._target_name = target_name
+ self._module_names = module_names
+ self._coverage_file = os.path.basename(coverage_file)
+
+ # dialog attributes
+ self.selected_name = None
+ self.remember_alias = False
+
+ # configure the widget for use
+ self._ui_init()
+
+ #--------------------------------------------------------------------------
+ # Initialization - UI
+ #--------------------------------------------------------------------------
+
+ def _ui_init(self):
+ """
+ Initialize UI elements.
+ """
+ self.setWindowTitle("Select module matching this session")
+ self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
+ self.setModal(True)
+
+ # initialize module selector table
+ self._ui_init_header()
+ self._ui_init_table()
+ self._populate_table()
+
+ # layout the populated UI just before showing it
+ self._ui_layout()
+
+ def _ui_init_header(self):
+ """
+ Initialize the module selector header UI elements.
+ """
+
+ description_text = \
+ "Lighthouse could not automatically identify the target module in the given coverage file:
" \
+ "
" \
+ "-- Target: %s
" \
+ "-- Coverage File: %s
" \
+ "
" \
+ "Please double click the name of the module that matches this database, or close this dialog
" \
+ "if you do not see your binary listed in the table belowp..." % (self._target_name, self._coverage_file)
+
+ self._label_description = QtWidgets.QLabel(description_text)
+ self._label_description.setTextFormat(QtCore.Qt.RichText)
+ #self._label_description.setWordWrap(True)
+
+ # a checkbox to save the user selected alias to the database
+ self._checkbox_remember = QtWidgets.QCheckBox("Save target module alias to database")
+
+ def _ui_init_table(self):
+ """
+ Initialize the module selector table UI elements.
+ """
+ self._table = QtWidgets.QTableWidget()
+ self._table.verticalHeader().setVisible(False)
+ self._table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
+
+ # Create a simple table / list
+ self._table.setColumnCount(1)
+ self._table.setHorizontalHeaderLabels(["Module Name"])
+
+ # left align text in column headers
+ self._table.horizontalHeaderItem(0).setTextAlignment(QtCore.Qt.AlignLeft)
+
+ # disable bolding of column headers when selected
+ self._table.horizontalHeader().setHighlightSections(False)
+
+ # stretch the last column of the table (aesthetics)
+ self._table.horizontalHeader().setStretchLastSection(True)
+
+ # make table read only, select a full row by default
+ self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self._table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+
+ # catch double click events on table rows
+ self._table.cellDoubleClicked.connect(self._ui_cell_double_click)
+
+ def _populate_table(self):
+ """
+ Populate the module table with the module names provided to this dialog.
+ """
+ self._table.setSortingEnabled(False)
+ self._table.setRowCount(len(self._module_names))
+ for i, module_name in enumerate(self._module_names, 0):
+ self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(module_name))
+ self._table.resizeRowsToContents()
+ self._table.setSortingEnabled(True)
+
+ def _ui_layout(self):
+ """
+ Layout the major UI elements of the widget.
+ """
+ layout = QtWidgets.QVBoxLayout()
+ #layout.setContentsMargins(0,0,0,0)
+
+ # layout child widgets
+ layout.addWidget(self._label_description)
+ layout.addWidget(self._table)
+ layout.addWidget(self._checkbox_remember)
+
+ # scale widget dimensions based on DPI
+ height = get_dpi_scale() * 250
+ width = get_dpi_scale() * 400
+ self.setMinimumHeight(height)
+ self.setMinimumWidth(width)
+
+ # apply the widget layout
+ self.setLayout(layout)
+
+ #--------------------------------------------------------------------------
+ # Signal Handlers
+ #--------------------------------------------------------------------------
+
+ def _ui_cell_double_click(self, row, column):
+ """
+ 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()
From 410adc45a5738c3ad49caaa0a917ece4b2e4c4c7 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 08:03:59 -0400
Subject: [PATCH 078/154] improve coverage xref styling
---
plugin/lighthouse/ui/coverage_xref.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 49592210..127db2d4 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -124,6 +124,7 @@ def _populate_table(self):
self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(filepath))
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(timestamp))
+ self._table.resizeRowsToContents()
self._table.setSortingEnabled(True)
def _ui_layout(self):
@@ -131,7 +132,6 @@ def _ui_layout(self):
Layout the major UI elements of the widget.
"""
layout = QtWidgets.QVBoxLayout()
- layout.setContentsMargins(0,0,0,0)
# layout child widgets
layout.addWidget(self._table)
From cad8679170d0f93d4c48eea1b325eda1a0b365fc Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 08:13:04 -0400
Subject: [PATCH 079/154] tweaks & typos, i'm tired
---
plugin/lighthouse/ui/module_selector.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/ui/module_selector.py b/plugin/lighthouse/ui/module_selector.py
index 3ed7cad5..2c90f451 100644
--- a/plugin/lighthouse/ui/module_selector.py
+++ b/plugin/lighthouse/ui/module_selector.py
@@ -43,7 +43,7 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- self.setWindowTitle("Select module matching this session")
+ self.setWindowTitle("Select module matching this database")
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.setModal(True)
@@ -67,14 +67,14 @@ def _ui_init_header(self):
"-- Coverage File: %s
" \
"
" \
"Please double click the name of the module that matches this database, or close this dialog
" \
- "if you do not see your binary listed in the table belowp..." % (self._target_name, self._coverage_file)
+ "if you do not see your binary listed in the table below..." % (self._target_name, self._coverage_file)
self._label_description = QtWidgets.QLabel(description_text)
self._label_description.setTextFormat(QtCore.Qt.RichText)
#self._label_description.setWordWrap(True)
# a checkbox to save the user selected alias to the database
- self._checkbox_remember = QtWidgets.QCheckBox("Save target module alias to database")
+ self._checkbox_remember = QtWidgets.QCheckBox("Remember target module alias for this session")
def _ui_init_table(self):
"""
From 11d5f9e62fa4eaac8c4ad4ec5de3c71bb85be9ce Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 19:19:52 -0400
Subject: [PATCH 080/154] enforce stricter logic around fuzzy name matching #63
---
plugin/lighthouse/director.py | 48 +++++++++++++++++++++++++++--------
1 file changed, 38 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index d291b372..e85b8ad3 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1,3 +1,4 @@
+import os
import time
import string
import logging
@@ -674,26 +675,53 @@ def _find_fuzzy_name(self, coverage_file, target_name):
"""
Return the closest matching module name in the given coverage file.
"""
+ target_name = target_name.lower()
+
+ #
+ # 1. exact, case-insensitive filename matching
+ #
- # attempt lookup using case-insensitive filename
for module_name in coverage_file.modules:
- if module_name.lower() in target_name.lower():
+ if target_name == module_name.lower():
return module_name
#
- # no hits yet... let's cleave the extension from the given module
- # name (if present) and try again
+ # 2. cleave the extension from the target module name (the source)
+ # and try again to see if matches anything in the coverage file
#
- if "." in target_name:
- target_name = target_name.split(".")[0]
-
- # attempt lookup using case-insensitive filename without extension
+ target_name, extension = os.path.splitext(target_name)
for module_name in coverage_file.modules:
- if module_name.lower() in target_name.lower():
+ if target_name == module_name.lower():
return module_name
- return None
+ # too risky to do fuzzy matching on short names...
+ if len(target_name) < 6:
+ return None
+
+ #
+ # 3. try to match *{target_name}*{extension} in module_name, assuming
+ # target_name is more than 6 characters and there is no othe ambiguity
+ #
+
+ possible_names = []
+ for module_name in coverage_file.modules:
+ if target_name in module_name.lower() and extension in module_name.lower():
+ possible_names.append(module_name)
+
+ # there were no matches on the wildcarding, so we're done
+ if not possible_names:
+ return None
+
+ #
+ # if there is multiple potential matches it is too risky to pick one,
+ # so we are not going to return anything as a viable match
+ #
+
+ if len(possible_names) > 1:
+ return None
+
+ return possible_names[0]
#----------------------------------------------------------------------
# Coverage Management
From 6dcb3c769c58e591c5af3147fc970d1117e5e455 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 20:10:03 -0400
Subject: [PATCH 081/154] Update README.md
at least some basic updates to the readme so as to not confuse anyone trying to use the dev branch
---
README.md | 45 +++++++--------------------------------------
1 file changed, 7 insertions(+), 38 deletions(-)
diff --git a/README.md b/README.md
index 94691d59..d0261bea 100644
--- a/README.md
+++ b/README.md
@@ -22,48 +22,17 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
* v0.2 -- Multifile support, performance improvements, bugfixes.
* v0.1 -- Initial release
-# IDA Pro Installation
+# Installation
-Lighthouse is a cross-platform (Windows, macOS, Linux) python plugin, supporting IDA Pro 6.8 and newer.
+Lighthouse is a cross-platform (Windows, macOS, Linux) python plugin. It takes zero third party dependencies, making the code both portable and easy to install.
-- Copy the contents of the `plugin` folder to the IDA plugins folder
- - On Windows, the folder is at `C:\Program Files (x86)\IDA 6.8\plugins`
- - On macOS, the folder is at `/Applications/IDA\ Pro\ 6.8/idaq.app/Contents/MacOS/plugins`
- - On Linux, the folder may be at `/opt/IDA/plugins/`
+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()`
-(If you need to locate the plugin directory for your setup, just type `idaapi.idadir(idaapi.PLG_SUBDIR)` in IDAPython console)
+2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
-It has been primarily developed and tested on Windows, so that is where we expect the best experience.
-
-# Binary Ninja Installation (Experimental)
-
-At this time, support for Binary Ninja is considered experimental. Please feel free to report any bugs that you encounter.
-
-You can install Lighthouse & PyQt5 for Binary Ninja by following the instructions below.
-
-## Windows Installation
-
-1. Install PyQt5 from a Windows command prompt with the following command:
-
-```
-pip install --target="%appdata%\Binary Ninja\plugins\Lib\site-packages" python-qt5
-```
-
-2. Copy the contents of the `/plugin/` folder in this repo to your Binary Ninja [plugins folder](https://docs.binary.ninja/guide/plugins/index.html#using-plugins).
-
-## Linux Installation
-
-1. Install PyQt5 from a Linux shell with the following command:
-
-```
-sudo apt install python-pyqt5
-```
-
-2. Copy the contents of the `/plugin/` folder in this repo to your Binary Ninja [plugins folder](https://docs.binary.ninja/guide/plugins/index.html#using-plugins).
-
-## macOS Installation
-
-¯\\\_(ツ)\_/¯
+This project is primarily developed and tested with IDA for Windows, so that is where we expect the best experience. Support for Binary Ninja and other disassemblers is still considered exprimental at this time.
# Usage
From 98745a09c46b865ff9e4e5d4777b32d7669d308c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 20:34:27 -0400
Subject: [PATCH 082/154] a few more readme updates
---
README.md | 44 +++-----------------------------------------
coverage/README.md | 38 ++++++++++++++++++++++++++++++++++++++
2 files changed, 41 insertions(+), 41 deletions(-)
create mode 100644 coverage/README.md
diff --git a/README.md b/README.md
index d0261bea..04b2a902 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,8 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
Lighthouse is a cross-platform (Windows, macOS, Linux) python 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()`
+ - **IDA Pro**: `os.path.join(idaapi.get_user_idadir(), "plugins")`
+ - **Binary Ninja**: `binaryninja.user_plugin_path()`
2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
@@ -42,7 +42,7 @@ Lighthouse loads automatically when a database is opened, installing a handful o
-These are the entry points for a user to load and view coverage data.
+These are the entry points for a user to load and view coverage data. To generate coverage data that can be loaded into Lighthouse, please look at the [README](https://github.com/gaasedelen/lighthouse/tree/develop/coverage) in the coverage directory of this repository.
## Coverage Painting
@@ -146,44 +146,6 @@ A sample report can be seen [here](https://rawgit.com/gaasedelen/lighthouse/mast
-# Collecting Coverage
-
-Before using Lighthouse, one will need to collect code coverage data for their target binary / application.
-
-The examples below demonstrate how one can use [DynamoRIO](http://www.dynamorio.org), [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) or [Frida](https://www.frida.re) to collect Lighthouse compatible coverage against a target. The `.log` files produced by these instrumentation tools can be loaded directly into Lighthouse.
-
-## DynamoRIO
-
-Code coverage data can be collected via DynamoRIO's [drcov](http://dynamorio.org/docs/page_drcov.html) code coverage module.
-
-Example usage:
-
-```
-..\DynamoRIO-Windows-7.0.0-RC1\bin64\drrun.exe -t drcov -- boombox.exe
-```
-
-## 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.
-
-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.
-
-## Frida (Experimental)
-
-Lighthouse offers limited support for Frida based code coverage via a custom [instrumentation script](coverage/frida) contributed by [yrp](https://twitter.com/yrp604).
-
-Example usage:
-
-```
-sudo python frida-drcov.py bb-bench
-```
-
# Future Work
Time and motivation permitting, future work may include:
diff --git a/coverage/README.md b/coverage/README.md
new file mode 100644
index 00000000..e27b2af7
--- /dev/null
+++ b/coverage/README.md
@@ -0,0 +1,38 @@
+# Collecting Coverage
+
+Before using Lighthouse, one will need to collect code coverage data for their target binary / application.
+
+The examples below demonstrate how one can use [DynamoRIO](http://www.dynamorio.org), [Intel Pin](https://software.intel.com/en-us/articles/pin-a-dynamic-binary-instrumentation-tool) or [Frida](https://www.frida.re) to collect Lighthouse compatible coverage against a target. The `.log` files produced by these instrumentation tools can be loaded directly into Lighthouse.
+
+## DynamoRIO
+
+Code coverage data can be collected via DynamoRIO's [drcov](http://dynamorio.org/docs/page_drcov.html) code coverage module.
+
+Example usage:
+
+```
+..\DynamoRIO-Windows-7.0.0-RC1\bin64\drrun.exe -t drcov -- boombox.exe
+```
+
+## 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.
+
+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.
+
+## Frida (Experimental)
+
+Lighthouse offers limited support for Frida based code coverage via a custom [instrumentation script](coverage/frida) contributed by [yrp](https://twitter.com/yrp604).
+
+Example usage:
+
+```
+sudo python frida-drcov.py bb-bench
+```
+
From 4d36be57c87431460c0650ee31c8e7a1929b168d Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 21:06:51 -0400
Subject: [PATCH 083/154] add a menu option to dump unmappable coverage data to
the console...
---
plugin/lighthouse/coverage.py | 6 +++++-
plugin/lighthouse/director.py | 6 ++++++
plugin/lighthouse/ui/coverage_settings.py | 9 +++++++--
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index 28648da7..f950441f 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -656,7 +656,11 @@ def dump_unmapped(self):
"""
Dump the unmapped coverage data.
"""
- lmsg("Unmapped Coverage:")
+ 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)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index e85b8ad3..911aad64 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -950,6 +950,12 @@ def get_coverage_string(self, coverage_name):
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
#----------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 5352fe8f..6f34e17d 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -59,7 +59,7 @@ def _ui_init_actions(self):
self.addAction(self._action_pause_paint)
# misc
- self._action_clear_paint = QtWidgets.QAction("Clear paint", None)
+ self._action_clear_paint = QtWidgets.QAction("Clear database paint", None)
self._action_clear_paint.setToolTip("Forcefully clear all paint from the database")
self.addAction(self._action_clear_paint)
self.addSeparator()
@@ -69,10 +69,14 @@ def _ui_init_actions(self):
self._action_refresh_metadata.setToolTip("Refresh the database metadata and coverage mapping")
self.addAction(self._action_refresh_metadata)
- self._action_export_html = QtWidgets.QAction("Export to HTML", None)
+ 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)
+ 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_hide_zero = QtWidgets.QAction("Hide 0% coverage", None)
self._action_hide_zero.setToolTip("Hide table entries with no coverage data")
self._action_hide_zero.setCheckable(True)
@@ -88,6 +92,7 @@ def connect_signals(self, controller, core):
self._action_pause_paint.triggered[bool].connect(lambda x: core.painter.set_enabled(not x))
self._action_clear_paint.triggered.connect(core.painter.clear_paint)
self._action_export_html.triggered.connect(controller.export_to_html)
+ self._action_dump_unmapped.triggered.connect(core.director.dump_unmapped)
core.painter.status_changed(self._ui_painter_changed_status)
#--------------------------------------------------------------------------
From 7d7ee5b9f0ee57f6c92a41571d9da5b995b3d37c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 21:38:12 -0400
Subject: [PATCH 084/154] improve click + drag text selection in composing
shell
---
plugin/lighthouse/composer/shell.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index cadabf1a..b707b162 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -669,6 +669,18 @@ def _ui_hint_coverage_refresh(self):
self._ui_hint_coverage_hide()
return
+ #
+ # if the text cursor is moving and the user has their left mouse
+ # button held, then they are probably doing a click + drag text
+ # selection so we shouldn't be naggin them with hints and stuff
+ #
+ # without this condition, click+drag selection gets really choppy
+ #
+
+ if QtWidgets.QApplication.mouseButtons() & QtCore.Qt.LeftButton:
+ self._ui_hint_coverage_hide()
+ return
+
# scrape info from the current shell text state
cursor_index = self._line.textCursor().position()
text_token = self._get_cursor_coverage_token(cursor_index)
From c44f35e5f4f26d2993b99b5fa42a3dd8ce83dce8 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 2 Apr 2020 23:47:58 -0400
Subject: [PATCH 085/154] add a dark / light coverage paint variant to the
themes
---
plugin/lighthouse/ui/palette.py | 37 +++++++++++++++++--
.../ui/resources/themes/dullien.json | 3 +-
.../lighthouse/ui/resources/themes/synth.json | 2 +-
3 files changed, 37 insertions(+), 5 deletions(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 99740f6a..b8dfbb19 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -224,7 +224,11 @@ def refresh_theme(self):
# file (if there is one) and warn the user before falling back
#
- os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ try:
+ os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ except:
+ pass
+
disassembler.warning(
"Failed to load Lighthouse user theme!\n\n"
"Please check the console for more information..."
@@ -309,7 +313,7 @@ def _select_preferred_theme(self):
active_filepath = os.path.join(user_theme_dir, ".active_theme")
try:
theme_name = open(active_filepath).read().strip()
- except OSError:
+ except (OSError, IOError):
theme_name = None
#
@@ -432,7 +436,15 @@ def _apply_theme(self, theme):
logging.debug("Applying theme '%s'..." % theme["name"])
colors = theme["colors"]
- for field_name, color_name in theme["fields"].items():
+ for field_name, color_entry in theme["fields"].items():
+
+ # color has 'light' and 'dark' variants
+ if isinstance(color_entry, list):
+ color_name = self._pick_best_color(field_name, color_entry)
+
+ # there is only one color defined
+ else:
+ color_name = color_entry
# load the color
color_value = colors[color_name]
@@ -449,6 +461,25 @@ def _apply_theme(self, theme):
# all done, save the theme in case we need it later
self.theme = theme
+ def _pick_best_color(self, field_name, color_entry):
+ """
+ Given a variable color_entry, select the best color based on the theme hints.
+ """
+ assert len(color_entry) == 2, "Malformed color entry, must be (dark, light)"
+ dark, light = color_entry
+
+ # coverage_paint is actually the only field that applies to disas...
+ if field_name == "coverage_paint":
+ if self._user_disassembly_hint == "dark":
+ return dark
+ else:
+ return light
+
+ # the rest of the fields should be considered 'qt' fields
+ if self._user_qt_hint == "dark":
+ return dark
+ return light
+
#--------------------------------------------------------------------------
# Theme Inference
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/resources/themes/dullien.json b/plugin/lighthouse/ui/resources/themes/dullien.json
index bd04e5bd..0d0cf1d6 100644
--- a/plugin/lighthouse/ui/resources/themes/dullien.json
+++ b/plugin/lighthouse/ui/resources/themes/dullien.json
@@ -11,12 +11,13 @@
"blue": [0, 0, 255],
"lightRed": [240, 150, 150],
"lightGreen": [150, 240, 150],
+ "darkGreen": [0, 60, 0],
"lightBlue": [140, 170, 220]
},
"fields":
{
- "coverage_paint": "lightGreen",
+ "coverage_paint": ["darkGreen", "lightGreen"],
"table_text": "black",
"table_grid": "gray",
diff --git a/plugin/lighthouse/ui/resources/themes/synth.json b/plugin/lighthouse/ui/resources/themes/synth.json
index 83c817a4..223c9dc9 100644
--- a/plugin/lighthouse/ui/resources/themes/synth.json
+++ b/plugin/lighthouse/ui/resources/themes/synth.json
@@ -24,7 +24,7 @@
"fields":
{
- "coverage_paint": "darkBlue",
+ "coverage_paint": ["darkBlue", "lightBlue"],
"table_text": "white",
"table_grid": "black",
From db1f4ebc68beee5fd2f8a591d57d42d6700e6f4f Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 3 Apr 2020 00:19:05 -0400
Subject: [PATCH 086/154] make coverage xref menu action only appear if there
is coverage loaded
---
plugin/lighthouse/ida_integration.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/ida_integration.py
index f52233be..8634320e 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/ida_integration.py
@@ -335,6 +335,7 @@ def finish_populating_widget_popup(self, widget, popup):
"""
A right click menu is about to be shown. (IDA 7.0+)
"""
- self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
+ if self.integration.director.aggregate.instruction_percent:
+ self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
return 0
From 416a46b8ab8af4ec775e3fd3c7a2dac74011e4d6 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 3 Apr 2020 07:16:00 -0400
Subject: [PATCH 087/154] small QoL tweaks
---
plugin/lighthouse/util/debug.py | 43 ++++++++++++++++++++++++++++
plugin/lighthouse/util/qt/waitbox.py | 5 ++++
2 files changed, 48 insertions(+)
diff --git a/plugin/lighthouse/util/debug.py b/plugin/lighthouse/util/debug.py
index deaadf07..a2555cc7 100644
--- a/plugin/lighthouse/util/debug.py
+++ b/plugin/lighthouse/util/debug.py
@@ -1,4 +1,9 @@
+import sys
import cProfile
+import traceback
+
+from .log import lmsg
+from .disassembler import disassembler
#------------------------------------------------------------------------------
# Debug
@@ -52,6 +57,44 @@ def nothing(*args, **kwargs):
return func(*args, **kwargs)
return nothing
+#------------------------------------------------------------------------------
+# Error Logging
+#------------------------------------------------------------------------------
+
+def catch_errors(func):
+ """
+ A simple catch-all decorator to try and log Lighthouse crashes.
+
+ This will be used to wrap high-risk or new code, in an effort to catch
+ and fix bugs without leaving the user in a stuck state.
+ """
+
+ def wrap(*args, **kwargs):
+ try:
+ return func(*args, **kwargs)
+ except Exception:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+
+ st = traceback.format_stack()[:-1]
+ ex = traceback.format_exception(exc_type, exc_value, exc_traceback)[2:]
+
+ # log full crashing callstack to console
+ full_error = st + ex
+ full_error = ''.join(full_error).splitlines()
+
+ lmsg("Lighthouse experienced an error... please file an issue on GitHub with this traceback:")
+ lmsg("")
+ for line in full_error:
+ lmsg(line)
+
+ # notify the user that a bug occurred
+ disassembler.warning(
+ "Something bad happend to Lighthouse :-(\n\n" \
+ "Please file an issue on GitHub with the traceback from your disassembler console."
+ )
+
+ return wrap
+
#------------------------------------------------------------------------------
# Module Line Profiling
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/qt/waitbox.py b/plugin/lighthouse/util/qt/waitbox.py
index c2af7ff0..e32b804d 100644
--- a/plugin/lighthouse/util/qt/waitbox.py
+++ b/plugin/lighthouse/util/qt/waitbox.py
@@ -35,6 +35,11 @@ def set_text(self, text):
qta = QtCore.QCoreApplication.instance()
qta.processEvents()
+ def show(self):
+ result = super(WaitBox, self).show()
+ qta = QtCore.QCoreApplication.instance()
+ qta.processEvents()
+
#--------------------------------------------------------------------------
# Initialization - UI
#--------------------------------------------------------------------------
From 5433cdc8e4bdf473cc3595544126ca22a3814aae Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 3 Apr 2020 07:18:54 -0400
Subject: [PATCH 088/154] streamline metadata collection, allow transition from
async to synchronous
---
plugin/lighthouse/core.py | 2 +
plugin/lighthouse/director.py | 23 +++---
plugin/lighthouse/metadata.py | 148 ++++++++++++++++++++++------------
3 files changed, 109 insertions(+), 64 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 329f3acb..515329a1 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -313,6 +313,7 @@ def interactive_load_batch(self):
#
disassembler.show_wait_box("Building database metadata...")
+ self.metadata.go_synchronous()
await_future(future)
#
@@ -378,6 +379,7 @@ def interactive_load_file(self):
#
disassembler.show_wait_box("Building database metadata...")
+ self.metadata.go_synchronous()
await_future(future)
#
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 911aad64..c528ce93 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -6,10 +6,10 @@
import traceback
import collections
-from lighthouse.util.qt import flush_qt_events
from lighthouse.util.misc import *
+from lighthouse.util.debug import catch_errors
from lighthouse.util.python import *
-from lighthouse.util.qt import await_future, await_lock
+from lighthouse.util.qt import await_future, await_lock, flush_qt_events
from lighthouse.util.disassembler import disassembler
from lighthouse.ui import ModuleSelector
@@ -1354,15 +1354,15 @@ def refresh(self):
"""
Complete refresh of the director and mapped coverage.
"""
- lmsg("Refreshing Lighthouse...")
-
- #
- # refreshing might take awhile, so pop a waitbox that should update
- # with status messages as the refresh runs...
- #
-
disassembler.show_wait_box("Refreshing Lighthouse...")
- flush_qt_events()
+ self._refresh()
+ disassembler.hide_wait_box()
+
+ @catch_errors
+ def _refresh(self):
+ """
+ Internal refresh routine, wrapped to help catch bugs for now.
+ """
# (re) build our metadata cache of the underlying database
self.metadata.refresh()
@@ -1373,9 +1373,6 @@ def refresh(self):
# notify of full-refresh
self._notify_refreshed()
- # all done ...
- disassembler.hide_wait_box()
-
def refresh_theme(self):
"""
Refresh UI facing elements to reflect the current theme.
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 238fd646..4cad354e 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -39,11 +39,11 @@
#
# 2. Building the metadata comes with an upfront cost, but this cost has
# been reduced as much as possible. For example, generating metadata for
-# a database with ~17k functions, ~95k nodes (basic blocks), and ~563k
-# instructions takes only ~6 seconds.
+# a larger database with ~25k functions, ~725k nodes (basic blocks), and
+# ~3.4m instructions took ~27 seconds.
#
-# This will be negligible for small-medium sized databases, but may still
-# be jarring for larger databases.
+# This will be negligible for small-medium sized databases, but will be
+# measurable for larger databases.
#
# Ultimately, this model provides us a more responsive user experience at
# the expense of the occasional inaccuracies that can be corrected by
@@ -89,6 +89,7 @@ def __init__(self):
# asynchronous metadata collection thread
self._refresh_worker = None
self._stop_threads = False
+ self._go_synchronous = False
#----------------------------------------------------------------------
# Callbacks
@@ -255,13 +256,21 @@ def is_big(self):
# Refresh
#--------------------------------------------------------------------------
- def refresh(self, function_addresses=None):
+ def refresh(self):
"""
Refresh the database metadata cache.
"""
- self._core_refresh(function_addresses)
+ self._refresh()
- def refresh_async(self, function_addresses=None, progress_callback=None, force=False):
+ def go_synchronous(self):
+ """
+ Switch an ongoing async refresh into a synchronous one.
+
+ This will make it go ... significantly faster ... but cannot be interrupted.
+ """
+ self._go_synchronous = True
+
+ def refresh_async(self, progress_callback=None, force=False):
"""
Refresh the database metadata cache asynchronously.
@@ -280,19 +289,20 @@ def refresh_async(self, function_addresses=None, progress_callback=None, force=F
return result_queue
#
- # reset the async abort/stop flag so that it can be used to cancel our
- # new refresh task if needed
+ # reset the async abort and go_synchronous flags so that we can use them
+ # for this new refresh if needed
#
self._stop_threads = False
+ self._go_synchronous = False
#
# kick off an asynchronous metadata collection task
#
self._refresh_worker = threading.Thread(
- target=self._async_refresh,
- args=(function_addresses, progress_callback, result_queue,)
+ target=self._refresh_async,
+ args=(result_queue, progress_callback,)
)
self._refresh_worker.start()
@@ -378,24 +388,23 @@ def _refresh_lookup(self):
#--------------------------------------------------------------------------
@not_mainthread
- def _async_refresh(self, function_addresses=None, progress_callback=None, result_queue=None):
+ def _refresh_async(self, result_queue, progress_callback=None):
"""
Internal thread worker routine to refresh the database metadata asynchronously.
"""
# start an interruptable refresh
- completed = self._core_refresh(function_addresses, progress_callback, True)
+ completed = self._refresh(progress_callback, True)
# clean up our thread's reference as it is basically done/dead
self._refresh_worker = None
# send the refresh result (good/bad) incase anyone is still listening
- if result_queue:
- result_queue.put(completed)
+ result_queue.put(completed)
# exit thread...
- def _core_refresh(self, function_addresses=None, progress_callback=None, is_async=False):
+ def _refresh(self, progress_callback=None, is_async=False):
"""
Internal routine that will update the database metadata cache.
"""
@@ -412,8 +421,8 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
self._sync_refresh_properties()
#
- # if a rebase occured, trash all present metadata as its easier to
- # rebuild the cache from scratch...
+ # if a rebase occured, trash all present metadata as code-wise, it is
+ # easier much easier to just rebuild the cache from scratch...
#
if prev_imagebase != self.imagebase:
@@ -422,25 +431,28 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
self.instructions = []
#
- # if the caller provided no function addresses to target for refresh,
# we will perform a complete metadata refresh of all database defined
# functions. let's retrieve that list from the disassembler now...
#
- if not function_addresses:
- function_addresses = disassembler.execute_read(
- disassembler.get_function_addresses
- )()
- function_addresses = list(set(function_addresses+list(self.functions)))
+ function_addresses = disassembler.execute_read(disassembler.get_function_addresses)()
+ #function_addresses = list(set(function_addresses+list(self.functions))) # TODO remove??
+ total = len(function_addresses)
+
+ start = time.time()
+ #----------------------------------------------------------------------
# refresh the core database metadata asynchronously
- if is_async:
- completed = self._async_collect_metadata(function_addresses, progress_callback)
+ if is_async and self._async_collect_metadata(function_addresses, progress_callback):
+ return False
# refresh the core database metadata synchronously
- else:
- self._sync_collect_metadata(function_addresses)
- completed = True
+ completed = total - len(function_addresses)
+ self._sync_collect_metadata(function_addresses, progress_callback, completed)
+
+ #----------------------------------------------------------------------
+ end = time.time()
+ logger.debug("Metadata collection took %s seconds" % (end - start))
# regenerate the instruction list from collected metadata
self._refresh_instructions()
@@ -476,15 +488,14 @@ def _core_refresh(self, function_addresses=None, progress_callback=None, is_asyn
self._rename_hooks.hook()
# the metadata refresh is effectively done, and the data is now 'cached'
- if completed:
- self.cached = True
+ self.cached = True
# detect & notify of a rebase event
if was_cached and (prev_imagebase != self.imagebase):
self._notify_rebased(prev_imagebase, self.imagebase)
# return true/false to indicates completion
- return completed
+ return True
@disassembler.execute_read
def _sync_refresh_properties(self):
@@ -495,16 +506,36 @@ def _sync_refresh_properties(self):
self.imagebase = disassembler.get_imagebase()
@disassembler.execute_read
- def _sync_collect_metadata(self, function_addresses):
+ def _sync_collect_metadata(self, function_addresses, progress_callback, progress_base=0):
"""
Collect metadata from the underlying database.
"""
- start = time.time()
- #----------------------------------------------------------------------
- self._update_functions({ ea: FunctionMetadata(ea) for ea in function_addresses })
- #----------------------------------------------------------------------
- end = time.time()
- logger.debug("Synchronous metadata collection took %s seconds" % (end - start))
+ CHUNK_SIZE = 500
+ completed = progress_base
+ total = progress_base + len(function_addresses)
+ logger.debug("Refreshing synchronously from %u/%u" % (completed, total))
+
+ while function_addresses:
+
+ # split off a chunk of functions to process metadata for
+ try:
+ addresses_chunk = function_addresses[:CHUNK_SIZE]
+ del function_addresses[:CHUNK_SIZE]
+ except IndexError:
+ addresses_chunk = function_addresses[:]
+ function_addresses.clear()
+ CHUNK_SIZE = len(addresses_chunk)
+
+ # collect metadata from the database
+ self._update_functions({ ea: FunctionMetadata(ea) for ea in addresses_chunk })
+
+ # report incremental progress to an optional progress_callback
+ if progress_callback:
+ completed += CHUNK_SIZE
+ progress_callback(completed, total)
+
+ # sleep some so we don't choke the mainthread
+ time.sleep(.001)
@not_mainthread
def _async_collect_metadata(self, function_addresses, progress_callback):
@@ -513,11 +544,25 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
"""
CHUNK_SIZE = 150
completed = 0
+ total = len(function_addresses)
+ logger.debug("Refreshing asynchronously from %u/%u" % (completed, total))
- start = time.time()
- #----------------------------------------------------------------------
+ while function_addresses:
+
+ #
+ # here we will split off CHUNK_SIZE elements from the function
+ # addresses list, in-place. this allows the list to keep track of
+ # what has not been processed, such that the caller can continue
+ # to operate on it if needed
+ #
- for addresses_chunk in chunks(function_addresses, CHUNK_SIZE):
+ try:
+ addresses_chunk = function_addresses[:CHUNK_SIZE]
+ del function_addresses[:CHUNK_SIZE]
+ except IndexError:
+ addresses_chunk = function_addresses[:]
+ function_addresses.clear()
+ CHUNK_SIZE = len(addresses_chunk)
#
# collect function metadata from the open database in groups of
@@ -532,22 +577,23 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
# report incremental progress to an optional progress_callback
if progress_callback:
- completed += len(addresses_chunk)
- progress_callback(completed, len(function_addresses))
+ completed += CHUNK_SIZE
+ progress_callback(completed, total)
# if the refresh was canceled, stop collecting metadata and bail
if self._stop_threads:
- return False
+ logger.debug("Async metadata collection is bailing!")
+ return True
+
+ # ALL SYSTEMS GO!!
+ if self._go_synchronous:
+ break
# sleep some so we don't choke the mainthread
time.sleep(.0015)
- #----------------------------------------------------------------------
- end = time.time()
- logger.debug("Metadata collection took %s seconds" % (end - start))
-
- # refresh completed normally / was not interrupted
- return True
+ # the refresh either completed, or it is going synchronous!
+ return False
def _update_functions(self, fresh_metadata):
"""
From 181b13d0f74933a69a7b41d8cae09c3f454c6393 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 3 Apr 2020 21:26:06 -0400
Subject: [PATCH 089/154] more metadata cache cleanup, robustness, QUALITY
SOFTWARE
---
plugin/lighthouse/director.py | 2 +-
plugin/lighthouse/metadata.py | 148 +++++++++++++---------------------
2 files changed, 58 insertions(+), 92 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index c528ce93..5b7562bc 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1365,7 +1365,7 @@ def _refresh(self):
"""
# (re) build our metadata cache of the underlying database
- self.metadata.refresh()
+ self.metadata.refresh(metadata_progress)
# (re) map each set of loaded coverage data to the database
self._refresh_database_coverage()
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 4cad354e..eba7ab54 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -4,6 +4,7 @@
import weakref
import threading
import collections
+
from lighthouse.util.misc import *
from lighthouse.util.python import *
from lighthouse.util.disassembler import disassembler
@@ -74,7 +75,6 @@ def __init__(self):
self.instructions = []
# internal members to help index & navigate the cached metadata
- self._stale_lookup = False
self._name2func = {}
self._node_addresses = []
self._function_addresses = []
@@ -150,7 +150,6 @@ def get_node(self, address):
"""
Get the node (basic block) metadata for a given address.
"""
- assert not self._stale_lookup, "Stale metadata is unsafe to use..."
# fast path, effectively a LRU cache of 1 ;P
if address in self._last_node.instructions:
@@ -256,19 +255,11 @@ def is_big(self):
# Refresh
#--------------------------------------------------------------------------
- def refresh(self):
+ def refresh(self, progress_callback=None):
"""
Refresh the database metadata cache.
"""
- self._refresh()
-
- def go_synchronous(self):
- """
- Switch an ongoing async refresh into a synchronous one.
-
- This will make it go ... significantly faster ... but cannot be interrupted.
- """
- self._go_synchronous = True
+ self._refresh(progress_callback)
def refresh_async(self, progress_callback=None, force=False):
"""
@@ -381,7 +372,14 @@ def _refresh_lookup(self):
self._name2func = { f.name: f.address for f in itervalues(self.functions) }
self._node_addresses = sorted(self.nodes.keys())
self._function_addresses = sorted(self.functions.keys())
- self._stale_lookup = False
+
+ def go_synchronous(self):
+ """
+ Switch an ongoing async refresh into a synchronous one.
+
+ This will make it go ... significantly faster ... but cannot be interrupted.
+ """
+ self._go_synchronous = True
#--------------------------------------------------------------------------
# Metadata Collection
@@ -404,10 +402,22 @@ def _refresh_async(self, result_queue, progress_callback=None):
# exit thread...
+ def _clear_cache(self):
+ """
+ Cleare the metadata cache of all collected info.
+ """
+ self.nodes = {}
+ self.functions = {}
+ self.instructions = []
+ self._refresh_lookup()
+ self.cached = False
+ # TODO
+
def _refresh(self, progress_callback=None, is_async=False):
"""
Internal routine that will update the database metadata cache.
"""
+ self._clear_cache()
# pause our rename listening hooks (more performant collection)
if self._rename_hooks:
@@ -415,28 +425,16 @@ def _refresh(self, progress_callback=None, is_async=False):
# grab the cached imagebase as it might have changed
prev_imagebase = self.imagebase
- was_cached = self.cached
# refresh high level database properties that we wish to cache
self._sync_refresh_properties()
- #
- # if a rebase occured, trash all present metadata as code-wise, it is
- # easier much easier to just rebuild the cache from scratch...
- #
-
- if prev_imagebase != self.imagebase:
- self.nodes = {}
- self.functions = {}
- self.instructions = []
-
#
# we will perform a complete metadata refresh of all database defined
# functions. let's retrieve that list from the disassembler now...
#
function_addresses = disassembler.execute_read(disassembler.get_function_addresses)()
- #function_addresses = list(set(function_addresses+list(self.functions))) # TODO remove??
total = len(function_addresses)
start = time.time()
@@ -444,6 +442,7 @@ def _refresh(self, progress_callback=None, is_async=False):
# refresh the core database metadata asynchronously
if is_async and self._async_collect_metadata(function_addresses, progress_callback):
+ self._clear_cache()
return False
# refresh the core database metadata synchronously
@@ -491,7 +490,7 @@ def _refresh(self, progress_callback=None, is_async=False):
self.cached = True
# detect & notify of a rebase event
- if was_cached and (prev_imagebase != self.imagebase):
+ if prev_imagebase != BADADDR and prev_imagebase != self.imagebase:
self._notify_rebased(prev_imagebase, self.imagebase)
# return true/false to indicates completion
@@ -521,13 +520,15 @@ def _sync_collect_metadata(self, function_addresses, progress_callback, progress
try:
addresses_chunk = function_addresses[:CHUNK_SIZE]
del function_addresses[:CHUNK_SIZE]
+
+ # reached the end of the function_addresses list... take whatever is left
except IndexError:
addresses_chunk = function_addresses[:]
function_addresses.clear()
CHUNK_SIZE = len(addresses_chunk)
# collect metadata from the database
- self._update_functions({ ea: FunctionMetadata(ea) for ea in addresses_chunk })
+ self._cache_functions(addresses_chunk)
# report incremental progress to an optional progress_callback
if progress_callback:
@@ -559,21 +560,15 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
try:
addresses_chunk = function_addresses[:CHUNK_SIZE]
del function_addresses[:CHUNK_SIZE]
+
+ # reached the end of the function_addresses list... take whatever is left
except IndexError:
addresses_chunk = function_addresses[:]
function_addresses.clear()
CHUNK_SIZE = len(addresses_chunk)
- #
- # collect function metadata from the open database in groups of
- # CHUNK_SIZE. collect_function_metadata() takes a list of function
- # addresses and collects their metadata in a thread-safe manner
- #
-
- fresh_metadata = collect_function_metadata(addresses_chunk)
-
- # update our database metadata cache with the new function metadata
- self._update_functions(fresh_metadata)
+ # collect metadata from the database
+ self._async_cache_functions(addresses_chunk)
# report incremental progress to an optional progress_callback
if progress_callback:
@@ -595,63 +590,41 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
# the refresh either completed, or it is going synchronous!
return False
- def _update_functions(self, fresh_metadata):
+ @disassembler.execute_read
+ def _async_cache_functions(self, addresses_chunk):
"""
- Update stored function metadata with the given fresh metadata.
-
- Returns a map of {address: function metadata} that has been updated.
+ Wrapped version of self._cache_functions, safe for use from an async worker thread.
"""
- blank_function = FunctionMetadata(-1)
+ self._cache_functions(addresses_chunk)
- #
- # the first step is to loop through the 'fresh' function metadata that
- # has been given to us, and identify what is truly new or different
- # from any existing metadata we hold.
- #
-
- for function_address, new_metadata in iteritems(fresh_metadata):
+ def _cache_functions(self, addresses_chunk):
+ """
+ Lift and cache function metadata for the given list of function addresses.
+ """
+ for address in addresses_chunk:
- # extract the 'old' metadata from the database metadata cache
- old_metadata = self.functions.get(function_address, blank_function)
+ # attempt to 'lift' the function from the database
+ try:
+ function_metadata = FunctionMetadata(address)
#
- # if the fresh metadata for this function is identical to the
- # existing metadata we have collected for it, there's nothing
- # else for us to do -- just ignore it.
+ # this is not exactly a good thing but it indicates that the
+ # disassembler didn't see the a function that we thought should
+ # have been there based on what it told us previously...
#
-
- if old_metadata == new_metadata:
- continue
-
- # delete nodes that explicitly no longer exist
- old = viewkeys(old_metadata.nodes) - viewkeys(new_metadata.nodes)
- for node_address in old:
- del self.nodes[node_address]
-
- #
- # the newly collected metadata for a given function is empty, this
- # indicates that the function has been deleted. we go ahead and
- # remove its old function metadata from the db metadata entirely
+ # this means the database might have changed, while the refresh
+ # was running. it's not the end of the world, but it might mean
+ # the cache will not be fully accurate...
#
- if new_metadata.empty:
- del self.functions[function_address]
+ except Exception:
+ lmsg(" - Caching function at 0x%08X failed..." % address)
+ logger.exception("FunctionMetadata Error:")
continue
- # add or overwrite the new/updated basic blocks
- self.nodes.update(new_metadata.nodes)
-
- # save the new/updated function
- self.functions[function_address] = new_metadata
-
- #
- # since the node / function metadata cache has probably changed, we
- # will need to refresh the internal fast lookup lists. this flag is
- # only really used for debugging, and will probably be removed
- # in the TODO/FUTURE collection refactor (v0.9?)
- #
-
- self._stale_lookup = True
+ # add the updated info
+ self.nodes.update(function_metadata.nodes)
+ self.functions[address] = function_metadata
#--------------------------------------------------------------------------
# Signal Handlers
@@ -791,7 +764,7 @@ def empty(self):
"""
Return a bool indicating whether the object is populated.
"""
- return len(self.nodes) == 0
+ return self.size == 0
#--------------------------------------------------------------------------
# Public
@@ -1126,13 +1099,6 @@ def __eq__(self, other):
# Async Metadata Helpers
#------------------------------------------------------------------------------
-@disassembler.execute_read
-def collect_function_metadata(function_addresses):
- """
- Collect function metadata for a list of addresses.
- """
- return { ea: FunctionMetadata(ea) for ea in function_addresses }
-
@disassembler.execute_ui
def metadata_progress(completed, total):
"""
From 6ad0af71cf1fd184a8bbb18a2391f7bd843ee0f1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 02:11:59 -0400
Subject: [PATCH 090/154] fix bug when unable to find mappable intructions in
cov data
---
plugin/lighthouse/director.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 5b7562bc..3b2bbecf 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -598,7 +598,7 @@ def _optimize_coverage_data(self, coverage_addresses):
if not instructions:
logger.debug("No mappable instruction addresses in coverage data")
- return None
+ return []
#
# TODO/COMMENT
From 8a2c011636daf06fdf9606b5b5f9fce5291dcaec Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 02:37:14 -0400
Subject: [PATCH 091/154] combobox style tweaks
---
plugin/lighthouse/ui/coverage_combobox.py | 54 ++++++++++++-----------
1 file changed, 29 insertions(+), 25 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 576d6880..923a6b87 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -444,7 +444,7 @@ def _ui_init(self):
hh.resizeSection(COLUMN_DELETE, icon_column_width)
# install a delegate to do some custom painting against the combobox
- self.setItemDelegate(ComboBoxDelegate())
+ self.setItemDelegate(ComboBoxDelegate(self))
#--------------------------------------------------------------------------
# Refresh
@@ -484,12 +484,16 @@ def refresh_theme(self):
"QTableView {"
" background-color: %s;" % palette.combobox_background.name() +
" color: %s;" % palette.combobox_text.name() +
- " selection-background-color: %s;" % palette.combobox_selection_background.name() +
- " selection-color: %s;" % palette.combobox_selection_text.name() +
" margin: 0; outline: none;"
+ " border: 1px solid %s; " % palette.shell_border.name() +
+ "} "
+ "QTableView::item { " +
+ " padding: 0.5ex; border: 0; "
+ "} "
+ "QTableView::item:focus { " +
+ " background-color: %s; " % palette.combobox_selection_background.name() +
+ " color: %s; " % palette.combobox_selection_text.name() +
"} "
- "QTableView::item{ padding: 0.5ex; } "
- "QTableView::item:focus { padding: 0; }"
)
#------------------------------------------------------------------------------
@@ -746,12 +750,11 @@ class ComboBoxDelegate(QtWidgets.QStyledItemDelegate):
dropdown table in the Coverage ComboBox a bit more to our liking.
"""
- def __init__(self, parent=None):
+ def __init__(self, parent):
super(ComboBoxDelegate, self).__init__(parent)
# painting property definitions
- self._grid_color = QtGui.QColor(0x909090)
- self._separator_color = QtGui.QColor(0x505050)
+ self._grid_color = parent.model()._director.palette.shell_border
def sizeHint(self, option, index):
"""
@@ -770,13 +773,15 @@ def paint(self, painter, option, index):
if index.data(QtCore.Qt.AccessibleDescriptionRole) == ENTRY_USER:
painter.save()
painter.setPen(self._grid_color)
+ final_entry = (index.sibling(index.row()+1, 0).row() == -1)
# draw the grid line beneath the current row (a coverage entry)
tweak = QtCore.QPoint(0, 1) # 1px tweak provides better spacing
- painter.drawLine(
- option.rect.bottomLeft() + tweak,
- option.rect.bottomRight() + tweak
- )
+ if not final_entry:
+ painter.drawLine(
+ option.rect.bottomLeft() + tweak,
+ option.rect.bottomRight() + tweak
+ )
#
# now we will re-draw the grid line *above* the current entry,
@@ -792,18 +797,6 @@ def paint(self, painter, option, index):
painter.restore()
- # custom paint the separator entry between special & normal coverage
- if index.data(QtCore.Qt.AccessibleDescriptionRole) == SEPARATOR:
- painter.save()
- painter.setPen(self._separator_color)
- painter.drawRect(
- option.rect
- )
- painter.restore()
-
- # nothing else to paint for the separator entry
- return
-
# custom paint the 'X' icon where applicable
if index.data(QtCore.Qt.DecorationRole):
@@ -825,9 +818,20 @@ def paint(self, painter, option, index):
# draw the icon to the column
painter.drawPixmap(destination_rect, pixmap)
+ return
+
+ # custom paint the separator entry between special & normal coverage
+ if index.data(QtCore.Qt.AccessibleDescriptionRole) == SEPARATOR:
+ painter.save()
+ painter.setPen(self._grid_color)
+ painter.drawRect(
+ option.rect
+ )
+ painter.restore()
- # nothing else to paint for the icon column entry
+ # nothing else to paint for the separator entry
return
# pass through to the standard painting
super(ComboBoxDelegate, self).paint(painter, option, index)
+
From 5670e3116c9663e071653826bc02747f018ff2fb Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 03:19:49 -0400
Subject: [PATCH 092/154] improve xref dialog
---
plugin/lighthouse/ui/coverage_xref.py | 15 ++++++++++-----
1 file changed, 10 insertions(+), 5 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 127db2d4..5bf4c5f4 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -59,12 +59,13 @@ def _ui_init_table(self):
self._table = QtWidgets.QTableWidget()
self._table.verticalHeader().setVisible(False)
self._table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
+ self._table.setWordWrap(False)
# symbol, cov %, name, time
self._table.setColumnCount(4)
self._table.setHorizontalHeaderLabels(["Sym", "Cov %", "Coverage Name", "Timestamp"])
- self._table.setColumnWidth(0, 40)
- self._table.setColumnWidth(1, 50)
+ self._table.setColumnWidth(0, 45)
+ self._table.setColumnWidth(1, 55)
self._table.setColumnWidth(2, 300)
self._table.setColumnWidth(3, 200)
@@ -105,7 +106,9 @@ def _populate_table(self):
for i, coverage in enumerate(cov_xrefs, 0):
self._table.setItem(i, 0, QtWidgets.QTableWidgetItem(self._director.get_shorthand(coverage.name)))
self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("%5.2f" % (coverage.instruction_percent*100)))
- self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(coverage.name))
+ name_entry = QtWidgets.QTableWidgetItem(coverage.name)
+ name_entry.setToolTip(coverage.filepath)
+ self._table.setItem(i, 2, name_entry)
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, human_timestamp(coverage.timestamp))))
# filepaths
@@ -121,7 +124,9 @@ def _populate_table(self):
# populate table entry
self._table.setItem(i, 0, QtWidgets.QTableWidgetItem("-"))
self._table.setItem(i, 1, QtWidgets.QTableWidgetItem("-"))
- self._table.setItem(i, 2, QtWidgets.QTableWidgetItem(filepath))
+ name_entry = QtWidgets.QTableWidgetItem(os.path.basename(filepath))
+ name_entry.setToolTip(filepath)
+ self._table.setItem(i, 2, name_entry)
self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(timestamp))
self._table.resizeRowsToContents()
@@ -154,7 +159,7 @@ def _ui_cell_double_click(self, row, column):
A cell/row has been double clicked in the xref table.
"""
if self._table.item(row, 0).text() == "-":
- self.selected_filepath = self._table.item(row, 2).text()
+ self.selected_filepath = self._table.item(row, 2).toolTip()
else:
self.selected_coverage = self._table.item(row, 2).text()
self.accept()
From 3054246a8d7c6a099b764652b6fcb4451c5706fa Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 08:30:01 -0400
Subject: [PATCH 093/154] bugfix for theme auto paint color selection
---
plugin/lighthouse/ui/palette.py | 51 ++++++++++++++++++---------------
1 file changed, 28 insertions(+), 23 deletions(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index b8dfbb19..d8594d0b 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -309,12 +309,12 @@ def _select_preferred_theme(self):
"""
user_theme_dir = get_user_theme_dir()
- # attempt te read the name of the user's active / preferred theme
+ # attempt te read the name of the user's active / preferred theme name
active_filepath = os.path.join(user_theme_dir, ".active_theme")
try:
- theme_name = open(active_filepath).read().strip()
+ return open(active_filepath).read().strip()
except (OSError, IOError):
- theme_name = None
+ pass
#
# there is no preferred theme set, let's try to peek at the user's
@@ -322,25 +322,19 @@ def _select_preferred_theme(self):
# might work best for them (a light theme or dark one, basically)
#
- if not theme_name:
- self._user_qt_hint = self._qt_theme_hint()
- self._user_disassembly_hint = self._disassembly_theme_hint()
-
- # if both hints agree with each other, let's shoot for that theme
- if self._user_qt_hint == self._user_disassembly_hint:
- theme_name = self._default_themes[self._user_qt_hint]
+ self._refresh_theme_hints()
- #
- # the UI hints don't match, so the user is using some ... weird
- # colors. let's just default to the 'dark' lighthouse theme as
- # it is more robust and can look okay in both light and dark envs
- #
+ # if both hints agree with each other, let's shoot for that theme
+ if self._user_qt_hint == self._user_disassembly_hint:
+ return self._default_themes[self._user_qt_hint]
- else:
- theme_name = self._default_themes["dark"]
+ #
+ # the UI hints don't match, so the user is using some ... weird
+ # colors. let's just default to the 'dark' lighthouse theme as
+ # it is more robust and can look okay in both light and dark envs
+ #
- # at this point, a theme_name to load should be known
- return theme_name
+ return self._default_themes[s]
def _load_preferred_theme(self, fallback=False):
"""
@@ -391,14 +385,18 @@ def _load_theme(self, filepath):
lmsg(" - " + str(e))
return False
- # if the theme appears identical to the applied theme. nothing to do!
- if theme == self.theme:
- return True
-
# do some basic sanity checking on the given theme file
if not self._validate_theme(theme):
return False
+ #
+ # before applying the selected lighthouse theme, we should ensure that
+ # we know if the user is using a light or dark disassembler theme as
+ # it may change which colors get used by the lighthouse theme
+ #
+
+ self._refresh_theme_hints()
+
# try applying the loaded theme to Lighthouse
try:
self._apply_theme(theme)
@@ -484,6 +482,13 @@ def _pick_best_color(self, field_name, color_entry):
# Theme Inference
#--------------------------------------------------------------------------
+ def _refresh_theme_hints(self):
+ """
+ Peek at the UI context to infer what kind of theme the user might be using.
+ """
+ self._user_qt_hint = self._qt_theme_hint()
+ self._user_disassembly_hint = self._disassembly_theme_hint()
+
def _disassembly_theme_hint(self):
"""
Binary hint of the IDA color theme.
From 25ff8ed245d74506187dfd5a049cdceed0cdc09c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 16:24:27 -0400
Subject: [PATCH 094/154] fix theme auto color selection bugginess
---
plugin/lighthouse/core.py | 7 +-
plugin/lighthouse/ui/palette.py | 158 ++++++++++++++++++--------------
2 files changed, 93 insertions(+), 72 deletions(-)
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/core.py
index 515329a1..b3e994e6 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/core.py
@@ -219,7 +219,7 @@ def open_coverage_overview(self):
"""
Open the dockable 'Coverage Overview' dialog.
"""
- self.palette.refresh_theme()
+ self.palette.warmup()
# the coverage overview is already open & visible, simply refresh it
if self._ui_coverage_overview and self._ui_coverage_overview.isVisible():
@@ -252,7 +252,6 @@ def open_coverage_xref(self, address):
disassembler.replace_wait_box
)
- # TODO rough...
if not created_coverage:
lmsg("No coverage files could be loaded...")
disassembler.hide_wait_box()
@@ -267,7 +266,7 @@ def interactive_load_batch(self):
"""
Perform the user-interactive loading of a coverage batch.
"""
- self.palette.refresh_theme()
+ self.palette.warmup()
#
# kick off an asynchronous metadata refresh. this will run in the
@@ -351,7 +350,7 @@ def interactive_load_file(self):
"""
Perform the user-interactive loading of individual coverage files.
"""
- self.palette.refresh_theme()
+ self.palette.warmup()
#
# kick off an asynchronous metadata refresh. this will run in the
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index d8594d0b..98794342 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -79,6 +79,7 @@ def __init__(self):
"""
Initialize default palette colors for Lighthouse.
"""
+ self._initialized = False
self._last_directory = None
self._required_fields = []
@@ -152,6 +153,50 @@ def _notify_theme_changed(self):
# Public
#----------------------------------------------------------------------
+ def warmup(self):
+ """
+ Warms up the theming system prior to initial use.
+ """
+ if self._initialized:
+ return
+
+ #
+ # attempt to load the user's preferred (or hinted) theme. if we are
+ # successful, then there's nothing else to do!
+ #
+
+ self._refresh_theme_hints()
+ if self._load_preferred_theme():
+ self._initialized = True
+ return
+
+ #
+ # failed to load the preferred theme... so delete the 'active'
+ # file (if there is one) and warn the user before falling back
+ #
+
+ try:
+ os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ except:
+ pass
+
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
+
+ #
+ # if no theme is loaded, we will attempt to detect & load the in-box
+ # themes based on the user's disassembler theme
+ #
+
+ loaded = self._load_preferred_theme(fallback=True)
+ if not loaded:
+ lmsg("Could not load Lighthouse fallback theme!") # this is a bad place to be...
+ return
+
+ self._initialized = True
+
def interactive_change_theme(self):
"""
Open a file dialog and let the user select a new Lighthoue theme.
@@ -168,6 +213,8 @@ def interactive_change_theme(self):
# prompt the user with the file dialog, and await filename(s)
filename, _ = file_dialog.getOpenFileName()
+ if not filename:
+ return
#
# ensure the user is only trying to load themes from the user theme
@@ -179,7 +226,6 @@ def interactive_change_theme(self):
if file_dir != user_dir:
text = "Please install your Lighthouse theme into the user theme directory:\n\n" + user_dir
disassembler.warning(text)
- lmsg(text)
return
#
@@ -193,6 +239,14 @@ def interactive_change_theme(self):
# log the captured (selected) filenames from the dialog
logger.debug("Captured filename from theme file dialog: '%s'" % filename)
+ #
+ # before applying the selected lighthouse theme, we should ensure that
+ # we know if the user is using a light or dark disassembler theme as
+ # it may change which colors get used by the lighthouse theme
+ #
+
+ self._refresh_theme_hints()
+
# load & apply theme from disk
if self._load_theme(filename):
return
@@ -210,44 +264,8 @@ def refresh_theme(self):
Depending on if IDA is using a dark or light theme, we *try*
to select colors that will hopefully keep things most readable.
"""
-
- #
- # attempt to load the user's preferred (or hinted) theme. if we are
- # successful, then there's nothing else to do!
- #
-
- if self._load_preferred_theme():
- return
-
- #
- # failed to load the preferred theme... so delete the 'active'
- # file (if there is one) and warn the user before falling back
- #
-
- try:
- os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
- except:
- pass
-
- disassembler.warning(
- "Failed to load Lighthouse user theme!\n\n"
- "Please check the console for more information..."
- )
-
- # if there is already a theme loaded, continue to use it...
- if self.theme:
- return
-
- #
- # if no theme is loaded, we will attempt to detect & load the in-box
- # themes based on the user's disassembler theme
- #
-
- loaded = self._load_preferred_theme(fallback=True)
- if loaded:
- return
-
- lmsg("Could not load Lighthouse fallback theme!")
+ self._refresh_theme_hints()
+ self._load_preferred_theme()
#--------------------------------------------------------------------------
# Theme Internals
@@ -303,48 +321,60 @@ def _load_required_fields(self):
self._required_fields = theme["fields"].keys()
- def _select_preferred_theme(self):
+ def _load_preferred_theme(self, fallback=False):
"""
- Return the name of the preferred theme to try loading.
+ Load the user's preferred theme, or the one hinted at by the theme subsystem.
"""
user_theme_dir = get_user_theme_dir()
# attempt te read the name of the user's active / preferred theme name
active_filepath = os.path.join(user_theme_dir, ".active_theme")
try:
- return open(active_filepath).read().strip()
+ theme_name = open(active_filepath).read().strip()
+ logger.debug("Got '%s' from .active_theme" % theme_name)
except (OSError, IOError):
- pass
+ theme_name = None
#
- # there is no preferred theme set, let's try to peek at the user's
- # disassembler theme & active Qt context and figure out what theme
- # might work best for them (a light theme or dark one, basically)
+ # if the user does not have a preferred theme set yet, we will try to
+ # pick one for them based on their disassembler UI.
#
- self._refresh_theme_hints()
+ if not theme_name:
+
+ #
+ # we have two themes hints which roughly correspond to the tone of
+ # their disassembly background, and then their general Qt widgets.
+ #
+ # if both themes seem to align on style (eg the user is using a
+ # 'dark' UI), then we will select the appropriate in-box theme
+ #
+
+ if self._user_qt_hint == self._user_disassembly_hint:
+ theme_name = self._default_themes[self._user_qt_hint]
+ logger.debug("No preferred theme, hints suggest theme '%s'" % theme_name)
- # if both hints agree with each other, let's shoot for that theme
- if self._user_qt_hint == self._user_disassembly_hint:
- return self._default_themes[self._user_qt_hint]
+ #
+ # the UI hints don't match, so the user is using some ... weird
+ # mismatched theming in their disassembler. let's just default to
+ # the 'dark' lighthouse theme as it is more robust
+ #
+
+ else:
+ theme_name = self._default_themes["dark"]
#
- # the UI hints don't match, so the user is using some ... weird
- # colors. let's just default to the 'dark' lighthouse theme as
- # it is more robust and can look okay in both light and dark envs
+ # should the user themes be in a bad state, we can fallback to the
+ # in-box themes. this should only happen if users malform the default
+ # themes that have been copied into the user theme directory
#
- return self._default_themes[s]
-
- def _load_preferred_theme(self, fallback=False):
- """
- Load the user's preferred theme, or the one hinted at by the theme subsystem.
- """
- theme_name = self._select_preferred_theme()
if fallback:
theme_path = os.path.join(get_plugin_theme_dir(), theme_name)
else:
theme_path = os.path.join(get_user_theme_dir(), theme_name)
+
+ # finally, attempt to load & apply the theme -- return True/False
return self._load_theme(theme_path)
def _validate_theme(self, theme):
@@ -389,14 +419,6 @@ def _load_theme(self, filepath):
if not self._validate_theme(theme):
return False
- #
- # before applying the selected lighthouse theme, we should ensure that
- # we know if the user is using a light or dark disassembler theme as
- # it may change which colors get used by the lighthouse theme
- #
-
- self._refresh_theme_hints()
-
# try applying the loaded theme to Lighthouse
try:
self._apply_theme(theme)
From 2ed77f305daa9a974b54b327f9f449e8940f887f Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 17:44:48 -0400
Subject: [PATCH 095/154] improve xref dialog for high dpi
---
plugin/lighthouse/ui/coverage_xref.py | 23 ++++++++++++++---------
1 file changed, 14 insertions(+), 9 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 5bf4c5f4..26e621e6 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -41,7 +41,7 @@ def _ui_init(self):
"""
Initialize UI elements.
"""
- self.setWindowTitle("Coverage xrefs to 0x%X" % self.address)
+ self.setWindowTitle("Coverage Xrefs to 0x%X" % self.address)
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.setModal(True)
@@ -66,8 +66,8 @@ def _ui_init_table(self):
self._table.setHorizontalHeaderLabels(["Sym", "Cov %", "Coverage Name", "Timestamp"])
self._table.setColumnWidth(0, 45)
self._table.setColumnWidth(1, 55)
- self._table.setColumnWidth(2, 300)
- self._table.setColumnWidth(3, 200)
+ self._table.setColumnWidth(2, 400)
+ self._table.setColumnWidth(3, 100)
# left align text in column headers
for i in range(4):
@@ -76,8 +76,8 @@ def _ui_init_table(self):
# disable bolding of column headers when selected
self._table.horizontalHeader().setHighlightSections(False)
- # stretch the last column of the table (aesthetics)
- self._table.horizontalHeader().setStretchLastSection(True)
+ # stretch the filename field, as it is the most important
+ self._table.horizontalHeader().setSectionResizeMode(2, QtWidgets.QHeaderView.Stretch)
# make table read only, select a full row by default
self._table.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
@@ -109,7 +109,9 @@ def _populate_table(self):
name_entry = QtWidgets.QTableWidgetItem(coverage.name)
name_entry.setToolTip(coverage.filepath)
self._table.setItem(i, 2, name_entry)
- self._table.setItem(i, 3, QtWidgets.QTableWidgetItem("%u (%s)" % (coverage.timestamp, human_timestamp(coverage.timestamp))))
+ date_entry = QtWidgets.QTableWidgetItem()
+ date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(coverage.timestamp*1000))
+ self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(date_entry))
# filepaths
for i, filepath in enumerate(file_xrefs, len(cov_xrefs)):
@@ -117,9 +119,8 @@ def _populate_table(self):
# try to read timestamp of the file on disk (if it exists)
try:
timestamp = os.path.getmtime(filepath)
- timestamp = "%u (%s)" % (timestamp, human_timestamp(timestamp))
except (OSError, TypeError):
- timestamp = "(unknown)"
+ timestamp = 0
# populate table entry
self._table.setItem(i, 0, QtWidgets.QTableWidgetItem("-"))
@@ -127,9 +128,13 @@ def _populate_table(self):
name_entry = QtWidgets.QTableWidgetItem(os.path.basename(filepath))
name_entry.setToolTip(filepath)
self._table.setItem(i, 2, name_entry)
- self._table.setItem(i, 3, QtWidgets.QTableWidgetItem(timestamp))
+ date_entry = QtWidgets.QTableWidgetItem()
+ date_entry.setData(QtCore.Qt.DisplayRole, QtCore.QDateTime.fromMSecsSinceEpoch(timestamp*1000))
+ self._table.setItem(i, 3, date_entry)
+ self._table.resizeColumnsToContents()
self._table.resizeRowsToContents()
+
self._table.setSortingEnabled(True)
def _ui_layout(self):
From 83e3b423b9c477d75a36110f0643b15295e5b99c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 19:10:58 -0400
Subject: [PATCH 096/154] removes range syntax from grammar, buggy & nobody
uses it...
---
README.md | 1 -
plugin/lighthouse/composer/parser.py | 79 ++++++----------------------
plugin/lighthouse/director.py | 30 -----------
3 files changed, 15 insertions(+), 95 deletions(-)
diff --git a/README.md b/README.md
index 04b2a902..8dc847f2 100644
--- a/README.md
+++ b/README.md
@@ -91,7 +91,6 @@ Coverage composition, or _Composing_ as demonstrated above is achieved through a
### Grammar Tokens
* Logical Operators: `|, &, ^, -`
* Coverage Symbol: `A, B, C, ..., Z`
-* Coverage Range: `A,C`, `Q,Z`, ...
* Parenthesis: `(...)`
### Example Compositions
diff --git a/plugin/lighthouse/composer/parser.py b/plugin/lighthouse/composer/parser.py
index 5298e3b8..70d081f6 100644
--- a/plugin/lighthouse/composer/parser.py
+++ b/plugin/lighthouse/composer/parser.py
@@ -117,21 +117,6 @@ def str2op(op_char):
return operator.sub
raise ValueError("Unknown Operator")
-class TokenCoverageRange(AstToken):
- """
- AST Token for a coverage range reference.
-
- eg: 'A,Z'
- """
-
- def __init__(self, start, comma, end):
- super(TokenCoverageRange, self).__init__()
- self.text_tokens = [start, comma, end]
-
- # referenced coverage sets
- self.symbol_start = start.value.upper()
- self.symbol_end = end.value.upper()
-
class TokenCoverageSingle(AstToken):
"""
AST Token for a single coverage reference.
@@ -182,28 +167,25 @@ def _ast_equal_recursive(first, second):
if type(first) != type(second):
return False
+ #
+ # if both tokens are terminating / None, they are a match
+ #
+
+ if first == second == None:
+ return True
+
#
# if the current node is a logic operator, we need to evaluate the
# expressions that make up its input.
#
- if isinstance(first, TokenLogicOperator):
+ elif isinstance(first, TokenLogicOperator):
if not _ast_equal_recursive(first.op1, second.op1):
return False
if not _ast_equal_recursive(first.op2, second.op2):
return False
return first.operator == second.operator
- #
- # if the current node is a coverage range, we need to evaluate the
- # range expression. this will produce an aggregate coverage set
- # described by the start/end of the range (Eg, 'A,D')
- #
-
- elif isinstance(first, TokenCoverageRange):
- return first.symbol_start == second.symbol_start and \
- first.symbol_end == second.symbol_end
-
#
# if the current node is a coverage token, we need simply need
# to compare its symbol.
@@ -216,7 +198,7 @@ def _ast_equal_recursive(first, second):
# unknown token? (this should never happen)
#
- raise False
+ raise ValueError("Unknown token types, cannot compare them...")
#------------------------------------------------------------------------------
# Parsing
@@ -273,17 +255,11 @@ class CompositionParser(object):
EXPRESSION:
'(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE COMPOSITION_TAIL
- COVERAGE:
- COVERAGE_TOKEN COVERAGE_RANGE
-
- COVERAGE_RANGE:
- ',' COVERAGE_TOKEN | None
-
COVERAGE_TOKEN:
'A' | 'B' | 'C' | ... | 'Z'
LOGIC_TOKEN:
- '&' | '|' | '^' | '-'
+ '&' | '|' | '^' | '-' | None
"""
@@ -420,7 +396,7 @@ def _COMPOSITION_TAIL(self, head):
def _EXPRESSION(self):
"""
EXPRESSION:
- '(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE COMPOSITION_TAIL
+ '(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE_TOKEN COMPOSITION_TAIL
"""
#
@@ -449,49 +425,24 @@ def _EXPRESSION(self):
#
else:
- expression = self._COVERAGE()
+ expression = self._COVERAGE_TOKEN()
# ... [COMPOSITION_TAIL]
return self._COMPOSITION_TAIL(expression)
- def _COVERAGE(self):
- """
- COVERAGE:
- COVERAGE_TOKEN COVERAGE_RANGE
- """
- coverage_start = self._COVERAGE_TOKEN()
- coverage_range = self._COVERAGE_RANGE()
-
- # if a there was a trailing ',A-Za-z' parsed, it's a coverage range
- if coverage_range:
- comma, coverage_end = coverage_range
- return TokenCoverageRange(coverage_start, comma, coverage_end)
-
- # return a single coverage set
- return TokenCoverageSingle(coverage_start)
-
- def _COVERAGE_RANGE(self):
- """
- COVERAGE_RANGE:
- ',' COVERAGE_TOKEN | None
- """
- if self._accept("COMMA"):
- return (self.current_token, self._COVERAGE_TOKEN())
- return None
-
def _COVERAGE_TOKEN(self):
"""
COVERAGE_TOKEN:
'A' | 'B' | 'C' | ... | 'Z'
"""
if self._accept("COVERAGE_TOKEN"):
- return self.current_token
- self._parse_error("Expected COVERAGE_TOKEN", TokenCoverageSingle)
+ return TokenCoverageSingle(self.current_token)
+ return None
def _LOGIC_TOKEN(self):
"""
LOGIC_TOKEN:
- '&' | '|' | '^' | '-'
+ '&' | '|' | '^' | '-' | None
"""
if self._accept("OR") or \
self._accept("XOR") or \
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 3b2bbecf..1e242494 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1293,15 +1293,6 @@ def _evaluate_composition_recursive(self, node):
self._composition_cache[composition_hash] = new_composition
return new_composition
- #
- # if the current AST node is a coverage range, we need to evaluate the
- # range expression. this will produce an aggregate coverage set
- # described by the start/end of the range (eg, 'A,D')
- #
-
- elif isinstance(node, TokenCoverageRange):
- return self._evaluate_coverage_range(node)
-
#
# if the current AST node is a coverage token, we need simply need to
# return its associated DatabaseCoverage.
@@ -1325,27 +1316,6 @@ def _evaluate_coverage(self, coverage_token):
assert isinstance(coverage_token, TokenCoverageSingle)
return self.get_coverage(self._alias2name[coverage_token.symbol])
- def _evaluate_coverage_range(self, range_token):
- """
- Evaluate a TokenCoverageRange AST token.
-
- Returns a new aggregate database coverage mapping.
- """
- assert isinstance(range_token, TokenCoverageRange)
-
- # initialize output to a null coverage set
- output = DatabaseCoverage(self.palette)
-
- # expand 'A,Z' to ['A', 'B', 'C', ... , 'Z']
- symbols = [chr(x) for x in range(ord(range_token.symbol_start), ord(range_token.symbol_end) + 1)]
-
- # build a coverage aggregate described by the range of shorthand symbols
- for symbol in symbols:
- output.add_data(self.get_coverage(self._alias2name[symbol]).data)
-
- # return the computed coverage
- return output
-
#----------------------------------------------------------------------
# Refresh
#----------------------------------------------------------------------
From 6571b0735ff368ee7aecbcbcb146603aa6e5ec7d Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 19:13:34 -0400
Subject: [PATCH 097/154] aggregate symbol was unusable in compositions
---
plugin/lighthouse/composer/parser.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/composer/parser.py b/plugin/lighthouse/composer/parser.py
index 70d081f6..db90c85b 100644
--- a/plugin/lighthouse/composer/parser.py
+++ b/plugin/lighthouse/composer/parser.py
@@ -34,6 +34,7 @@ def index(self):
# NOTE: this is now dynamically computed in parse(...)
#COVERAGE_TOKEN = r'(?P[A-Za-z])'
+AGGREGATE_TOKEN = '*'
#
# LOGIC_TOKEN:
@@ -256,7 +257,7 @@ class CompositionParser(object):
'(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE COMPOSITION_TAIL
COVERAGE_TOKEN:
- 'A' | 'B' | 'C' | ... | 'Z'
+ 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN | None
LOGIC_TOKEN:
'&' | '|' | '^' | '-' | None
@@ -286,7 +287,7 @@ def parse(self, text, coverage_tokens):
# reflect the state of loaded coverage
#
- COVERAGE_TOKEN = r'(?P[%s])' % ''.join(coverage_tokens)
+ COVERAGE_TOKEN = r'(?P[%s])' % ''.join(coverage_tokens + [AGGREGATE_TOKEN])
#
# if there were any coverage tokens defined, then we definitely need
@@ -433,7 +434,7 @@ def _EXPRESSION(self):
def _COVERAGE_TOKEN(self):
"""
COVERAGE_TOKEN:
- 'A' | 'B' | 'C' | ... | 'Z'
+ 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN | None
"""
if self._accept("COVERAGE_TOKEN"):
return TokenCoverageSingle(self.current_token)
From 914b7316767982c6e3f8a312afb9041f0fd416f3 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 4 Apr 2020 23:15:07 -0400
Subject: [PATCH 098/154] fixes bug where combobox could get stuck closed
---
plugin/lighthouse/ui/coverage_combobox.py | 140 ++++++++++++----------
1 file changed, 75 insertions(+), 65 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 923a6b87..dd106261 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -1,4 +1,6 @@
import logging
+import weakref
+
from lighthouse.util import *
from lighthouse.util.qt import *
from lighthouse.util.disassembler import disassembler
@@ -47,71 +49,6 @@ def __init__(self, director, parent=None):
# QComboBox Overloads
#--------------------------------------------------------------------------
- def showPopup(self):
- """
- Show the QComboBox dropdown/popup.
- """
- super(CoverageComboBox, self).showPopup()
-
- #
- # the next line of code will prevent the combobox 'head' from getting
- # any mouse actions now that the popup/dropdown is visible.
- #
- # this is pretty aggressive, but it will allow the user to 'collapse'
- # the combobox dropdown while it is in an expanded state by simply
- # clicking the combobox head as one can do to expand it.
- #
- # the reason this dirty trick is able to simulate a 'collapsing click'
- # is because the user clicks 'outside' the popup/dropdown which
- # automatically closes it. if the click was on the combobox head, it
- # is simply ignored because we set this attribute!
- #
- # when the popup is closing, we undo this action in hidePopup()
- #
- # we have to use this workaround because we are using an 'editable' Qt
- # combobox which behaves differently to clicks than a normal combobox.
- #
-
- self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
-
- def hidePopup(self):
- """
- Hide the QComboBox dropdown/popup.
- """
- super(CoverageComboBox, self).hidePopup()
-
- #
- # the combobox popup is now hidden / collapsed. the combobox head needs
- # to be re-enlightened to direct mouse clicks (eg, to expand it). this
- # undos the setAttribute action in showPopup() above.
- #
- # if the coverage combobox is *not* visible, the coverage window is
- # probably being closed / deleted. but just in case, we should attempt
- # to restore the combobox's ability to accept clicks before bailing.
- #
- # this fixes a bug / Qt warning first printed in IDA 7.4 where 'self'
- # (the comobobox) would be deleted by the time the 100ms timer in the
- # 'normal' case fires below
- #
-
- if not self.isVisible():
- self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
- return
-
- #
- # in the more normal case, the comobobox is simply being collapsed
- # by the user clicking it, or clicking away from it.
- #
- # we use a short timer of 100ms to ensure the 'hiding' of the dropdown
- # and its associated click are processed first. aftwards, it is safe to
- # begin accepting clicks again.
- #
-
- QtCore.QTimer.singleShot(100, self.__hidePopup_setattr)
-
- def __hidePopup_setattr(self):
- self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
-
def mousePressEvent(self, e):
"""
Capture mouse click events to the QComboBox.
@@ -362,6 +299,8 @@ class CoverageComboBoxView(QtWidgets.QTableView):
def __init__(self, model, parent=None):
super(CoverageComboBoxView, self).__init__(parent)
self.setObjectName(self.__class__.__name__)
+ self._combobox = weakref.proxy(parent)
+ self._timer = None
# install the given data model into the table view
self.setModel(model)
@@ -374,6 +313,77 @@ def __init__(self, model, parent=None):
# QTableView Overloads
#--------------------------------------------------------------------------
+ def showEvent(self, e):
+ """
+ Show the QComboBox dropdown/popup.
+ """
+
+ #
+ # the next line of code will prevent the combobox 'head' from getting
+ # any mouse actions now that the popup/dropdown is visible.
+ #
+ # this is pretty aggressive, but it will allow the user to 'collapse'
+ # the combobox dropdown while it is in an expanded state by simply
+ # clicking the combobox head as one can do to expand it.
+ #
+ # the reason this dirty trick is able to simulate a 'collapsing click'
+ # is because the user clicks 'outside' the popup/dropdown which
+ # automatically closes it. if the click was on the combobox head, it
+ # is simply ignored because we set this attribute!
+ #
+ # when the popup is closing, we undo this action in hideEvent().
+ #
+ # we have to use this workaround because we are using an 'editable' Qt
+ # combobox which behaves differently to clicks than a normal combobox.
+ #
+ # NOTE: we have to do this here in the tableview because the combobox's
+ # showPopup() and hidePopup() do not always trigger symmetrically.
+ #
+ # for example, hidePopup() was not being triggered when focus was lost
+ # via virutal desktop switch, and other external focus changes. this
+ # is really bad, because the combobox would get stuck *closed* as it
+ # was never re-enabled for mouse events
+ #
+
+ self._combobox.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
+
+ def hideEvent(self, e):
+ """
+ Hide the QComboBox dropdown/popup.
+ """
+
+ #
+ # the combobox popup is now hidden / collapsed. the combobox head needs
+ # to be re-enlightened to direct mouse clicks (eg, to expand it). this
+ # undos the setAttribute action in showPopup() above.
+ #
+ # if the coverage combobox is *not* visible, the coverage window is
+ # probably being closed / deleted. but just in case, we should attempt
+ # to restore the combobox's ability to accept clicks before bailing.
+ #
+ # this fixes a bug / Qt warning first printed in IDA 7.4 where 'self'
+ # (the comobobox) would be deleted by the time the 100ms timer in the
+ # 'normal' case fires below
+ #
+
+ if not self._combobox.isVisible():
+ self._combobox.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
+ return
+
+ #
+ # in the more normal case, the comobobox is simply being collapsed
+ # by the user clicking it, or clicking away from it.
+ #
+ # we use a short timer of 100ms to ensure the 'hiding' of the dropdown
+ # and its associated click are processed first. aftwards, it is safe to
+ # begin accepting clicks again.
+ #
+
+ self._timer = QtCore.QTimer.singleShot(100, self.__hidePopup_setattr)
+
+ def __hidePopup_setattr(self):
+ self._combobox.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents, False)
+
def leaveEvent(self, e):
"""
Overload the mouse leave event.
From 553eeb7d23a91a91f4ce9356d38b0277a55d362c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 8 Apr 2020 00:43:06 -0400
Subject: [PATCH 099/154] fixes grammar regression...
---
plugin/lighthouse/composer/parser.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/composer/parser.py b/plugin/lighthouse/composer/parser.py
index db90c85b..20a16e10 100644
--- a/plugin/lighthouse/composer/parser.py
+++ b/plugin/lighthouse/composer/parser.py
@@ -257,7 +257,7 @@ class CompositionParser(object):
'(' EXPRESSION ')' COMPOSITION_TAIL | COVERAGE COMPOSITION_TAIL
COVERAGE_TOKEN:
- 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN | None
+ 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN
LOGIC_TOKEN:
'&' | '|' | '^' | '-' | None
@@ -434,11 +434,11 @@ def _EXPRESSION(self):
def _COVERAGE_TOKEN(self):
"""
COVERAGE_TOKEN:
- 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN | None
+ 'A' | 'B' | 'C' | ... | 'Z' | AGGREGATE_TOKEN
"""
if self._accept("COVERAGE_TOKEN"):
return TokenCoverageSingle(self.current_token)
- return None
+ self._parse_error("Expected COVERAGE_TOKEN", TokenCoverageSingle)
def _LOGIC_TOKEN(self):
"""
From 9eca228925d0e754ab9652a2b7ba6cee71c7b057 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 8 Apr 2020 01:06:22 -0400
Subject: [PATCH 100/154] fixes bug that could cause one to be prompted
multiple times for a composition name
---
plugin/lighthouse/util/qt/util.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index d19525d3..1225e0b0 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -109,6 +109,9 @@ def prompt_string(label, title, default=""):
dpi_scale*400,
dpi_scale*50
)
+ dlg.setModal(True)
+ dlg.show()
+ dlg.setFocus(QtCore.Qt.PopupFocusReason)
ok = dlg.exec_()
text = str(dlg.textValue())
return (ok, text)
From 8c4e29fe50d889a8a58d2962f7ed082b9c321cf1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 8 Apr 2020 01:43:13 -0400
Subject: [PATCH 101/154] bugfix: automatically evaluate the shell expression
when switching to the hot shell
---
plugin/lighthouse/composer/shell.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index b707b162..00be6aa0 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -127,6 +127,7 @@ def _ui_init_signals(self):
self._director.coverage_created(self._internal_refresh)
self._director.coverage_deleted(self._internal_refresh)
self._director.coverage_modified(self._internal_refresh)
+ self._director.coverage_switched(self._coverage_switched)
# register for cues from the model
self._table_model.layoutChanged.connect(self._ui_shell_text_changed)
@@ -198,6 +199,7 @@ def _internal_refresh(self):
Internal refresh of the shell.
"""
self._refresh_hint_list()
+ self._ui_shell_text_changed()
def _refresh_hint_list(self):
"""
@@ -219,6 +221,22 @@ def _refresh_hint_list(self):
# queue a UI coverage hint if necessary
self._ui_hint_coverage_refresh()
+ def _coverage_switched(self):
+ """
+ Handle a coverage switched event.
+
+ specifically, we want cover the specical case where the hot shell is
+ being switched to. In these cases, we should forcefully clear the
+ 'last' AST so that the full shell expression is re-evaluated and
+ sent forward to the director.
+
+ this will ensure that the director will evaluate and display the
+ results of the present expression as the 'Hot Shell' is now active.
+ """
+ if self._director.coverage_name == "Hot Shell":
+ self._last_ast = None
+ self._internal_refresh()
+
#--------------------------------------------------------------------------
# Signal Handlers
#--------------------------------------------------------------------------
From a6eeafe1803015e2b9a08719ca3df0f1ddbf81b8 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 8 Apr 2020 11:42:32 -0400
Subject: [PATCH 102/154] overhaul for proper binja integration
---
plugin/lighthouse/binja_integration.py | 65 ----
plugin/lighthouse/context.py | 97 +++++
plugin/lighthouse/integration/__init__.py | 0
.../integration/binja_integration.py | 153 ++++++++
.../{ => integration}/binja_loader.py | 2 +-
plugin/lighthouse/{ => integration}/core.py | 204 ++++-------
.../{ => integration}/ida_integration.py | 2 +-
.../{ => integration}/ida_loader.py | 0
plugin/lighthouse/metadata.py | 98 ++---
plugin/lighthouse/painting/binja_painter.py | 14 +-
plugin/lighthouse/painting/painter.py | 27 +-
plugin/lighthouse/ui/coverage_overview.py | 93 ++---
plugin/lighthouse/ui/coverage_settings.py | 14 +-
plugin/lighthouse/ui/coverage_table.py | 17 +-
plugin/lighthouse/ui/palette.py | 2 +
.../lighthouse/util/disassembler/__init__.py | 5 +-
plugin/lighthouse/util/disassembler/api.py | 257 ++++++++++---
.../lighthouse/util/disassembler/binja_api.py | 338 ++++++++----------
plugin/lighthouse_plugin.py | 4 +-
19 files changed, 819 insertions(+), 573 deletions(-)
delete mode 100644 plugin/lighthouse/binja_integration.py
create mode 100644 plugin/lighthouse/context.py
create mode 100644 plugin/lighthouse/integration/__init__.py
create mode 100644 plugin/lighthouse/integration/binja_integration.py
rename plugin/lighthouse/{ => integration}/binja_loader.py (94%)
rename plugin/lighthouse/{ => integration}/core.py (69%)
rename plugin/lighthouse/{ => integration}/ida_integration.py (99%)
rename plugin/lighthouse/{ => integration}/ida_loader.py (100%)
diff --git a/plugin/lighthouse/binja_integration.py b/plugin/lighthouse/binja_integration.py
deleted file mode 100644
index d2fe4d8c..00000000
--- a/plugin/lighthouse/binja_integration.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import logging
-
-from binaryninja import PluginCommand
-from lighthouse.core import Lighthouse
-from lighthouse.util.disassembler import disassembler
-
-logger = logging.getLogger("Lighthouse.Binja.Integration")
-
-#------------------------------------------------------------------------------
-# Lighthouse Binja Integration
-#------------------------------------------------------------------------------
-
-class LighthouseBinja(Lighthouse):
- """
- Lighthouse UI Integration for Binary Ninja.
- """
-
- def __init__(self):
- super(LighthouseBinja, self).__init__()
-
- def interactive_load_file(self, bv):
- disassembler.bv = bv
- super(LighthouseBinja, self).interactive_load_file()
-
- def interactive_load_batch(self, bv):
- disassembler.bv = bv
- super(LighthouseBinja, self).interactive_load_batch()
-
- def interactive_load_batch(self, bv):
- disassembler.bv = bv
- super(LighthouseBinja, self).open_coverage_overview()
-
- def _install_load_file(self):
- PluginCommand.register(
- r"Lighthouse\Load code coverage file...",
- "Load individual code coverage file(s)",
- self.interactive_load_file
- )
- logger.info("Installed the 'Code coverage file' menu entry")
-
- def _install_load_batch(self):
- PluginCommand.register(
- r"Lighthouse\Load code coverage batch...",
- "Load and aggregate code coverage files",
- self.interactive_load_batch
- )
- logger.info("Installed the 'Code coverage batch' menu entry")
-
- def _install_open_coverage_overview(self):
- PluginCommand.register(
- r"Lighthouse\Coverage Overview",
- "Open the database code coverage overview",
- self.interactive_load_batch
- )
- logger.info("Installed the 'Coverage Overview' menu entry")
-
- # TODO/V35: No good signals to unload (core) plugin on
- def _uninstall_load_file(self):
- pass
-
- def _uninstall_load_batch(self):
- pass
-
- def _uninstall_open_coverage_overview(self):
- pass
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
new file mode 100644
index 00000000..1752a0e5
--- /dev/null
+++ b/plugin/lighthouse/context.py
@@ -0,0 +1,97 @@
+import os
+import logging
+
+from lighthouse.util.qt import *
+from lighthouse.painting import CoveragePainter
+from lighthouse.director import CoverageDirector
+from lighthouse.coverage import DatabaseCoverage
+from lighthouse.metadata import DatabaseMetadata
+
+from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI
+
+logger = logging.getLogger("Lighthouse.Context")
+
+#------------------------------------------------------------------------------
+# Lighthouse Session Context
+#------------------------------------------------------------------------------
+
+class LighthouseContext(object):
+ """
+ TODO
+ """
+
+ def __init__(self, core, dctx):
+ disassembler[self] = DisassemblerContextAPI(dctx)
+ self.dctx = dctx
+ self.core = core
+
+ # the database metadata cache
+ self.metadata = DatabaseMetadata(self)
+
+ # the coverage engine
+ self.director = CoverageDirector(self.metadata, self.core.palette)
+
+ # the coverage painter
+ self.painter = CoveragePainter(self, self.director, self.core.palette)
+
+ # the coverage overview widget
+ self.coverage_overview = None
+
+ # the directory to start the coverage file dialog in
+ self._last_directory = None
+
+ # TODO: re-enable
+ # expose the live CoverageDirector object instance for external scripts
+ #lighthouse.coverage_director = self.director
+
+ def terminate(self):
+ """
+ Spin down any session subsystems before the session is deleted.
+ """
+
+ # TODO
+ # remove access to the exposed CoverageDirector
+ #lighthouse.coverage_director = None
+
+ # spin down the rest of the session subsystems
+ self.painter.terminate()
+ self.director.terminate()
+ self.metadata.terminate()
+
+ def select_coverage_files(self):
+ """
+ Prompt a file selection dialog, returning file selections.
+
+ NOTE: This saves & reuses the last known directory for subsequent uses.
+ """
+ if not self._last_directory:
+ self._last_directory = disassembler[self].get_database_directory()
+
+ # create & configure a Qt File Dialog for immediate use
+ file_dialog = QtWidgets.QFileDialog(
+ None,
+ 'Open code coverage file',
+ self._last_directory,
+ 'All Files (*.*)'
+ )
+ file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
+
+ # prompt the user with the file dialog, and await filename(s)
+ filenames, _ = file_dialog.getOpenFileNames()
+
+ #
+ # remember the last directory we were in (parsed from a selected file)
+ # for the next time the user comes to load coverage files
+ #
+
+ if filenames:
+ self._last_directory = os.path.dirname(filenames[0]) + os.sep
+
+ # log the captured (selected) filenames from the dialog
+ logger.debug("Captured filenames from file dialog:")
+ for name in filenames:
+ logger.debug(" - %s" % name)
+
+ # return the captured filenames
+ return filenames
+
diff --git a/plugin/lighthouse/integration/__init__.py b/plugin/lighthouse/integration/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
new file mode 100644
index 00000000..e1c8f9c9
--- /dev/null
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -0,0 +1,153 @@
+import ctypes
+import logging
+
+from binaryninja import PluginCommand
+from binaryninjaui import UIAction, UIActionHandler, Menu
+
+from lighthouse.context import LighthouseContext
+from lighthouse.integration.core import LighthouseCore
+from lighthouse.util.disassembler import disassembler
+
+logger = logging.getLogger("Lighthouse.Binja.Integration")
+
+#------------------------------------------------------------------------------
+# Lighthouse Binja Integration
+#------------------------------------------------------------------------------
+
+class LighthouseBinja(LighthouseCore):
+ """
+ Lighthouse UI Integration for Binary Ninja.
+ """
+
+ def __init__(self):
+ super(LighthouseBinja, self).__init__()
+
+ def get_context(self, dctx):
+ """
+ Get the LighthouseContext object for a given disassembler context.
+ """
+ dctx_id = ctypes.addressof(dctx.handle.contents)
+
+ # create a new LighthouseContext if this is a new disassembler ctx / bv
+ if dctx_id not in self.lighthouse_contexts:
+ print("Creating new Lctx!", dctx)
+ self.lighthouse_contexts[dctx_id] = LighthouseContext(self, dctx)
+ else:
+ print("Using ctx...", dctx)
+
+ # return the lighthouse context object for this disassembler ctx / bv
+ return self.lighthouse_contexts[dctx_id]
+
+ #--------------------------------------------------------------------------
+ # UI Integration (Internal)
+ #--------------------------------------------------------------------------
+
+ #
+ # NOTE / HACK / XXX: 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
+ # then tries to execute our UIActions (like 'Load Coverage File'), the
+ # given 'contxet.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.
+ #
+ # TODO/V35: There is no good way to enable/disable UIActions on the fly...
+ #
+
+ def _interactive_load_file(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Lighthouse requires an open BNDB to load coverage.")
+ return
+ super(LighthouseBinja, self).interactive_load_file(dctx)
+
+ def _interactive_load_batch(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Lighthouse requires an open BNDB to load coverage.")
+ return
+ super(LighthouseBinja, self).interactive_load_batch(dctx)
+
+ def _open_coverage_xref(self, bv, addr):
+ super(LighthouseBinja, self).open_coverage_xref(bv, addr)
+
+ #--------------------------------------------------------------------------
+ # Binja Actions
+ #--------------------------------------------------------------------------
+
+ ACTION_LOAD_FILE = "Lighthouse\\Load code coverage file..."
+ ACTION_LOAD_BATCH = "Lighthouse\\Load code coverage batch..."
+ ACTION_COVERAGE_XREF = "Lighthouse\\Coverage Xref"
+ ACTION_COVERAGE_OVERVIEW = "Lighthouse\\Open Coverage Overview"
+
+ def _install_load_file(self):
+ action = self.ACTION_LOAD_FILE
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._interactive_load_file))
+ Menu.mainMenu("Tools").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)
+ logger.info("Installed the 'Code coverage batch' menu entry")
+
+ def _install_open_coverage_xref(self):
+ PluginCommand.register_for_address(
+ self.ACTION_COVERAGE_XREF,
+ "Open the coverage xref window",
+ self._open_coverage_xref,
+ lambda bv, addr: bool(self.get_context(bv).director.aggregate.instruction_percent)
+ )
+
+ # TODO: enable as a UI action once we can disable/disable them on the fly
+ #action = self.ACTION_COVERAGE_XREF
+ #UIAction.registerAction(action)
+ #UIActionHandler.globalActions().bindAction(action, UIAction(self._open_coverage_xref))
+ #Menu.mainMenu("Tools").addAction(action, "Coverage Xref")
+ #logger.info("Installed the 'Coverage Xref' menu entry")
+
+ 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("Lighthouse", action, "Coverage Overview")
+ logger.info("Installed the 'Coverage Overview' menu entry")
+
+ #
+ # TODO/V35: No good signals to unload (core) UI entries on
+ #
+
+ def _uninstall_load_file(self):
+ action = self.ACTTION_LOAD_FILE
+ UIActionHandler.globalActions().unbindAction(action)
+ Menu.mainMenu("Tools").removeAction(action)
+ UIAction.unregisterAction(action)
+ logger.info("Uninstalled the 'Code coverage file' menu entry")
+
+ def _uninstall_load_batch(self):
+ action = self.ACTTION_LOAD_BATCH
+ UIActionHandler.globalActions().unbindAction(action)
+ Menu.mainMenu("Tools").removeAction(action)
+ UIAction.unregisterAction(action)
+ logger.info("Uninstalled the 'Code coverage batch' menu entry")
+
+ def _uninstall_open_coverage_xref(self):
+ pass
+ #action = self.ACTTION_COVERAGE_XREF
+ #UIActionHandler.globalActions().unbindAction(action)
+ ##Menu.mainMenu("Tools").removeAction(action)
+ #UIAction.unregisterAction(action)
+ #logger.info("Uninstalled the 'Coverage Xref' menu entry")
+
+ def _uninstall_open_coverage_overview(self):
+ #action = self.ACTTION_COVERAGE_OVERVIEW
+ #UIActionHandler.globalActions().unbindAction(action)
+ #Menu.mainMenu("Tools").removeAction(action)
+ #UIAction.unregisterAction(action)
+ logger.info("Uninstalled the 'Coverage Overview' menu entry")
+
diff --git a/plugin/lighthouse/binja_loader.py b/plugin/lighthouse/integration/binja_loader.py
similarity index 94%
rename from plugin/lighthouse/binja_loader.py
rename to plugin/lighthouse/integration/binja_loader.py
index 1896a136..3c02aca4 100644
--- a/plugin/lighthouse/binja_loader.py
+++ b/plugin/lighthouse/integration/binja_loader.py
@@ -1,7 +1,7 @@
import logging
from lighthouse.util.log import lmsg
-from lighthouse.binja_integration import LighthouseBinja
+from lighthouse.integration.binja_integration import LighthouseBinja
logger = logging.getLogger("Lighthouse.Binja.Loader")
diff --git a/plugin/lighthouse/core.py b/plugin/lighthouse/integration/core.py
similarity index 69%
rename from plugin/lighthouse/core.py
rename to plugin/lighthouse/integration/core.py
index b3e994e6..86bc6770 100644
--- a/plugin/lighthouse/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -2,16 +2,11 @@
import abc
import logging
-import lighthouse
-
from lighthouse.ui import *
from lighthouse.util import lmsg
from lighthouse.util.qt import *
-from lighthouse.util.disassembler import disassembler
+from lighthouse.util.disassembler import disassembler, DisassemblerContextAPI
-from lighthouse.painting import CoveragePainter
-from lighthouse.director import CoverageDirector
-from lighthouse.coverage import DatabaseCoverage
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.exceptions import *
@@ -29,7 +24,7 @@
# Lighthouse Plugin Core
#------------------------------------------------------------------------------
-class Lighthouse(object):
+class LighthouseCore(object):
__metaclass__ = abc.ABCMeta
#--------------------------------------------------------------------------
@@ -40,45 +35,54 @@ def load(self):
"""
Load the plugin, and integrate its UI into the disassembler.
"""
- self._init()
+ self.lighthouse_contexts = {}
+
+ # the plugin color palette
+ self.palette = LighthousePalette()
+ self.palette.theme_changed(self.refresh_theme)
+
+ def create_overview_instance(name, parent, data = None):
+ print("Creating CoverageOverview instance ...") # TODO remove try/catch
+ try:
+ return CoverageOverview(self, parent, name, data)
+ except Exception as e:
+ logger.exception("Wid failed")
+
+ # the coverage overview widget
+ disassembler.create_dockable_widget("Coverage Overview", create_overview_instance)
+
+ # install disassembler UI
self._install_ui()
# plugin loaded successfully, print the plugin banner
self.print_banner()
logger.info("Successfully loaded plugin")
- def _init(self):
+ def unload(self):
"""
- Initialize the core components of the plugin.
+ Unload the plugin, and remove any UI integrations.
"""
+ self._uninstall_ui()
- # the database metadata cache
- self.metadata = DatabaseMetadata()
-
- # the plugin color palette
- self.palette = LighthousePalette()
- self.palette.theme_changed(self.refresh_theme)
-
- # the coverage engine
- self.director = CoverageDirector(self.metadata, self.palette)
-
- # the coverage painter
- self.painter = CoveragePainter(self.director, self.palette)
-
- # the coverage overview widget
- self._ui_coverage_overview = None
+ # spin donw any active contexts (stop threads, cleanup qt state, etc)
+ for lctx in self.lighthouse_contexts:
+ lctx.terminate()
- # the directory to start the coverage file dialog in
- self._last_directory = None
+ logger.info("-"*75)
+ logger.info("Plugin terminated")
- # a timed callback for lighthouse to check for certain state changes
- self._scheduled = QtCore.QTimer()
- self._scheduled.timeout.connect(self.scheduled)
- #self._scheduled.start(1000) # TODO: re-enable once more testing is done...
+ @abc.abstractmethod
+ def get_context(self, dctx):
+ """
+ Get the LighthouseContext object for a given disassembler context.
+ """
- # expose the live CoverageDirector object instance for external scripts
- lighthouse.coverage_director = self.director
+ # create a new LighthouseContext if this is a new disassembler ctx / bv
+ if id(dctx) not in self.lighthouse_contexts:
+ self.lighthouse_contexts[id(dctx)] = LighthouseContext(self, dctx)
+ # return the lighthouse context object for this disassembler ctx / bv
+ return self.lighthouse_contexts[id(dctx)]
def print_banner(self):
"""
@@ -96,34 +100,6 @@ def print_banner(self):
lmsg("-"*75)
lmsg("")
- #--------------------------------------------------------------------------
- # Termination
- #--------------------------------------------------------------------------
-
- def unload(self):
- """
- Unload the plugin, and remove any UI integrations.
- """
- self._uninstall_ui()
- self._cleanup()
-
- logger.info("-"*75)
- logger.info("Plugin terminated")
-
- def _cleanup(self):
- """
- Spin down any lingering core components before plugin unload.
- """
-
- # remove access to the exposed CoverageDirector
- lighthouse.coverage_director = None
-
- # spin down the rest of the core subsystems
- self._scheduled.stop()
- self.painter.terminate()
- self.director.terminate()
- self.metadata.terminate()
-
#--------------------------------------------------------------------------
# UI Integration (Internal)
#--------------------------------------------------------------------------
@@ -210,44 +186,44 @@ def refresh_theme(self):
"""
Refresh UI facing elements to reflect the current theme.
"""
- self.director.refresh_theme()
- if self._ui_coverage_overview:
- self._ui_coverage_overview.refresh_theme()
- self.painter.repaint()
+ for lctx in self.lighthouse_contexts.values():
+ lctx.director.refresh_theme()
+ lctx.coverage_overview.refresh_theme()
+ lctx.painter.repaint()
- def open_coverage_overview(self):
+ def open_coverage_overview(self, dctx):
"""
Open the dockable 'Coverage Overview' dialog.
"""
self.palette.warmup()
+ lctx = self.get_context(dctx)
# the coverage overview is already open & visible, simply refresh it
- if self._ui_coverage_overview and self._ui_coverage_overview.isVisible():
- self._ui_coverage_overview.refresh()
+ if lctx.coverage_overview.visible:
+ lctx.coverage_overview.refresh()
return
- # create a new coverage overview if there is not one visible
- self._ui_coverage_overview = CoverageOverview(self)
- self._ui_coverage_overview.show()
+ disassembler.show_dockable_widget(lctx.coverage_overview.m_name)
- def open_coverage_xref(self, address):
+ def open_coverage_xref(self, dctx, address):
"""
Open the 'Coverage Xref' dialog for a given address.
"""
+ lctx = self.get_context(dctx)
# show the coverage xref dialog
- dialog = CoverageXref(self.director, address)
+ dialog = CoverageXref(lctx.director, address)
if not dialog.exec_():
return
# activate the user selected xref (if one was double clicked)
if dialog.selected_coverage:
- self.director.select_coverage(dialog.selected_coverage)
+ lctx.director.select_coverage(dialog.selected_coverage)
return
# load a coverage file from disk
disassembler.show_wait_box("Loading coverage from disk...")
- created_coverage, errors = self.director.load_coverage_files(
+ created_coverage, errors = lctx.director.load_coverage_files(
[dialog.selected_filepath],
disassembler.replace_wait_box
)
@@ -259,34 +235,35 @@ def open_coverage_xref(self, address):
return
disassembler.replace_wait_box("Selecting coverage...")
- self.director.select_coverage(created_coverage[0].name)
+ lctx.director.select_coverage(created_coverage[0].name)
disassembler.hide_wait_box()
- def interactive_load_batch(self):
+ def interactive_load_batch(self, ctx):
"""
Perform the user-interactive loading of a coverage batch.
"""
self.palette.warmup()
+ lctx = self.get_context(dctx)
#
# kick off an asynchronous metadata refresh. this will run in the
# background while the user is selecting which coverage files to load
#
- future = self.metadata.refresh_async(progress_callback=metadata_progress)
+ future = lctx.metadata.refresh_async(progress_callback=metadata_progress)
#
# we will now prompt the user with an interactive file dialog so they
# can select the coverage files they would like to load from disk
#
- filepaths = self._select_coverage_files()
+ filepaths = lctx.select_coverage_files()
if not filepaths:
- self.director.metadata.abort_refresh()
+ lctx.director.metadata.abort_refresh()
return
# prompt the user to name the new coverage aggregate
- default_name = "BATCH_%s" % self.director.peek_shorthand()
+ default_name = "BATCH_%s" % lctx.director.peek_shorthand()
ok, batch_name = prompt_string(
"Batch Name:",
"Please enter a name for this coverage",
@@ -300,7 +277,7 @@ def interactive_load_batch(self):
if not (ok and batch_name):
lmsg("User failed to enter a name for the batch coverage...")
- self.director.metadata.abort_refresh()
+ lctx.director.metadata.abort_refresh()
return
#
@@ -312,7 +289,7 @@ def interactive_load_batch(self):
#
disassembler.show_wait_box("Building database metadata...")
- self.metadata.go_synchronous()
+ lctx.metadata.go_synchronous()
await_future(future)
#
@@ -321,7 +298,7 @@ def interactive_load_batch(self):
#
disassembler.replace_wait_box("Loading coverage from disk...")
- batch_coverage, errors = self.director.load_coverage_batch(
+ batch_coverage, errors = lctx.director.load_coverage_batch(
filepaths,
batch_name,
disassembler.replace_wait_box
@@ -336,37 +313,38 @@ def interactive_load_batch(self):
# select the newly created batch coverage
disassembler.replace_wait_box("Selecting coverage...")
- self.director.select_coverage(batch_name)
+ lctx.director.select_coverage(batch_name)
# all done! pop the coverage overview to show the user their results
disassembler.hide_wait_box()
lmsg("Successfully loaded batch %s..." % batch_name)
- self.open_coverage_overview()
+ self.open_coverage_overview(lctx.dctx)
# finally, emit any notable issues that occurred during load
warn_errors(errors)
- def interactive_load_file(self):
+ def interactive_load_file(self, dctx):
"""
Perform the user-interactive loading of individual coverage files.
"""
self.palette.warmup()
+ lctx = self.get_context(dctx)
#
# kick off an asynchronous metadata refresh. this will run in the
# background while the user is selecting which coverage files to load
#
- future = self.metadata.refresh_async(progress_callback=metadata_progress)
+ future = lctx.metadata.refresh_async(progress_callback=metadata_progress)
#
# we will now prompt the user with an interactive file dialog so they
# can select the coverage files they would like to load from disk
#
- filenames = self._select_coverage_files()
+ filenames = lctx.select_coverage_files()
if not filenames:
- self.director.metadata.abort_refresh()
+ lctx.metadata.abort_refresh()
return
#
@@ -378,7 +356,7 @@ def interactive_load_file(self):
#
disassembler.show_wait_box("Building database metadata...")
- self.metadata.go_synchronous()
+ lctx.metadata.go_synchronous()
await_future(future)
#
@@ -387,7 +365,7 @@ def interactive_load_file(self):
#
disassembler.replace_wait_box("Loading coverage from disk...")
- created_coverage, errors = self.director.load_coverage_files(filenames, disassembler.replace_wait_box)
+ created_coverage, errors = lctx.director.load_coverage_files(filenames, disassembler.replace_wait_box)
#
# if the director failed to map any coverage, the user probably
@@ -406,57 +384,21 @@ def interactive_load_file(self):
#
disassembler.replace_wait_box("Selecting coverage...")
- self.director.select_coverage(created_coverage[0].name)
+ lctx.director.select_coverage(created_coverage[0].name)
# all done! pop the coverage overview to show the user their results
disassembler.hide_wait_box()
lmsg("Successfully loaded %u coverage file(s)..." % len(created_coverage))
- self.open_coverage_overview()
+ self.open_coverage_overview(lctx.dctx)
# finally, emit any notable issues that occurred during load
warn_errors(errors)
- def _select_coverage_files(self):
- """
- Prompt a file selection dialog, returning file selections.
-
- NOTE: This saves & reuses the last known directory for subsequent uses.
- """
- if not self._last_directory:
- self._last_directory = disassembler.get_database_directory()
-
- # create & configure a Qt File Dialog for immediate use
- file_dialog = QtWidgets.QFileDialog(
- None,
- 'Open code coverage file',
- self._last_directory,
- 'All Files (*.*)'
- )
- file_dialog.setFileMode(QtWidgets.QFileDialog.ExistingFiles)
-
- # prompt the user with the file dialog, and await filename(s)
- filenames, _ = file_dialog.getOpenFileNames()
-
- #
- # remember the last directory we were in (parsed from a selected file)
- # for the next time the user comes to load coverage files
- #
-
- if filenames:
- self._last_directory = os.path.dirname(filenames[0]) + os.sep
-
- # log the captured (selected) filenames from the dialog
- logger.debug("Captured filenames from file dialog:")
- for name in filenames:
- logger.debug(" - %s" % name)
-
- # return the captured filenames
- return filenames
-
#--------------------------------------------------------------------------
# Scheduled
#--------------------------------------------------------------------------
+ # TODO
@disassembler.execute_read
def scheduled(self):
metadata = self.director.metadata
diff --git a/plugin/lighthouse/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
similarity index 99%
rename from plugin/lighthouse/ida_integration.py
rename to plugin/lighthouse/integration/ida_integration.py
index 8634320e..f5587ae7 100644
--- a/plugin/lighthouse/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -137,7 +137,7 @@ def _install_open_coverage_xref(self):
RuntimeError("Failed to register coverage_xref action with IDA")
self._ui_hooks.hook()
- logger.info("Installed the 'Code coverage batch' menu entry")
+ logger.info("Installed the 'Coverage Xref' menu entry")
def _install_open_coverage_overview(self):
"""
diff --git a/plugin/lighthouse/ida_loader.py b/plugin/lighthouse/integration/ida_loader.py
similarity index 100%
rename from plugin/lighthouse/ida_loader.py
rename to plugin/lighthouse/integration/ida_loader.py
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index eba7ab54..99b93b68 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -60,7 +60,8 @@ class DatabaseMetadata(object):
Database level metadata cache.
"""
- def __init__(self):
+ def __init__(self, lctx=None):
+ self.lctx = lctx
# name & imagebase of the executable this metadata is based on
self.filename = ""
@@ -83,8 +84,13 @@ def __init__(self):
self._last_node = lambda: None
self._last_node.instructions = []
- # placeholder attribute for disassembler event hooks
- self._rename_hooks = None
+ # create the disassembler hooks to listen for rename events
+ if lctx:
+ self._rename_hooks = disassembler[lctx].create_rename_hooks()
+ self._rename_hooks.renamed = self._name_changed
+ self._rename_hooks.metadata = weakref.proxy(self)
+ else:
+ self._rename_hooks = None
# asynchronous metadata collection thread
self._refresh_worker = None
@@ -434,7 +440,8 @@ def _refresh(self, progress_callback=None, is_async=False):
# functions. let's retrieve that list from the disassembler now...
#
- function_addresses = disassembler.execute_read(disassembler.get_function_addresses)()
+ disassembler_ctx = disassembler[self.lctx]
+ function_addresses = disassembler.execute_read(disassembler_ctx.get_function_addresses)()
total = len(function_addresses)
start = time.time()
@@ -459,28 +466,6 @@ def _refresh(self, progress_callback=None, is_async=False):
# refresh the internal function/node fast lookup lists
self._refresh_lookup()
- #
- # NOTE:
- #
- # creating the hooks inline like this is less than ideal, but they
- # they have been moved here (from the metadata constructor) to
- # accomodate shortcomings of the Binary Ninja API.
- #
- # TODO/FUTURE/V35:
- #
- # it would be nice to move these back to the constructor once the
- # Binary Ninja API allows us to detect BV / sessions as they are
- # created, and able to load plugins on such events.
- #
-
- #----------------------------------------------------------------------
-
- # create the disassembler hooks to listen for rename events
- if not self._rename_hooks:
- self._rename_hooks = disassembler.create_rename_hooks()
- self._rename_hooks.renamed = self._name_changed
- self._rename_hooks.metadata = weakref.proxy(self)
-
#----------------------------------------------------------------------
# reinstall the rename listener hooks now that the refresh is done
@@ -501,8 +486,9 @@ def _sync_refresh_properties(self):
"""
Refresh a selection of interesting database properties.
"""
- self.filename = disassembler.get_root_filename()
- self.imagebase = disassembler.get_imagebase()
+ disassembler_ctx = disassembler[self.lctx]
+ self.filename = disassembler_ctx.get_root_filename()
+ self.imagebase = disassembler_ctx.get_imagebase()
@disassembler.execute_read
def _sync_collect_metadata(self, function_addresses, progress_callback, progress_base=0):
@@ -601,11 +587,13 @@ def _cache_functions(self, addresses_chunk):
"""
Lift and cache function metadata for the given list of function addresses.
"""
+ disassembler_ctx = disassembler[self.lctx]
+
for address in addresses_chunk:
# attempt to 'lift' the function from the database
try:
- function_metadata = FunctionMetadata(address)
+ function_metadata = FunctionMetadata(address, disassembler_ctx)
#
# this is not exactly a good thing but it indicates that the
@@ -630,7 +618,7 @@ def _cache_functions(self, addresses_chunk):
# Signal Handlers
#--------------------------------------------------------------------------
- @mainthread
+ #@mainthread # TODO update fore IDA
def _name_changed(self, address, new_name, local_name=None):
"""
Handler for rename event in IDA.
@@ -665,12 +653,11 @@ def _name_changed(self, address, new_name, local_name=None):
return 0
logger.debug("Name changing @ 0x%X" % address)
- logger.debug(" Old name: %s" % function.name)
- logger.debug(" New name: %s" % new_name)
+ logger.debug(" Old name: %s" % function.name.encode("utf-8"))
+ logger.debug(" New name: %s" % new_name.encode("utf-8"))
# rename the function, and notify metadata listeners
- #function.name = new_name
- function.refresh_name()
+ function.name = new_name
self._notify_function_renamed()
# necessary for IDP/IDB_Hooks
@@ -727,7 +714,7 @@ class FunctionMetadata(object):
Function level metadata cache.
"""
- def __init__(self, address):
+ def __init__(self, address, disassembler_ctx=None):
# function metadata
self.address = address
@@ -745,8 +732,8 @@ def __init__(self, address):
self.cyclomatic_complexity = 0
# collect metdata from the underlying database
- if address != -1:
- self._build_metadata()
+ if disassembler_ctx:
+ self._cache(disassembler_ctx)
#--------------------------------------------------------------------------
# Properties
@@ -766,30 +753,19 @@ def empty(self):
"""
return self.size == 0
- #--------------------------------------------------------------------------
- # Public
- #--------------------------------------------------------------------------
-
- @disassembler.execute_read
- def refresh_name(self):
- """
- Refresh the function name against the open database.
- """
- self.name = disassembler.get_function_name_at(self.address)
-
#--------------------------------------------------------------------------
# Metadata Population
#--------------------------------------------------------------------------
- def _build_metadata(self):
+ def _cache(self, disassembler_ctx):
"""
Collect function metadata from the underlying database.
"""
- self.name = disassembler.get_function_name_at(self.address)
- self._refresh_nodes()
+ self.name = disassembler_ctx.get_function_name_at(self.address)
+ self._refresh_nodes(disassembler_ctx)
self._finalize()
- def _refresh_nodes(self):
+ def _refresh_nodes(self, disassembler_ctx):
"""
This will be replaced with a disassembler-specific function at runtime.
@@ -844,7 +820,7 @@ def _ida_refresh_nodes(self):
if edge_dst in function_metadata.nodes:
function_metadata.edges[edge_src].append(edge_dst)
- def _binja_refresh_nodes(self):
+ def _binja_refresh_nodes(self, disassembler_ctx):
"""
Refresh function node metadata against an open Binary Ninja database.
"""
@@ -852,7 +828,7 @@ def _binja_refresh_nodes(self):
function_metadata.nodes = {}
# get the function from the Binja database
- function = disassembler.bv.get_function_at(self.address)
+ function = disassembler_ctx.bv.get_function_at(self.address)
#
# now we will walk the flowchart for this function, collecting
@@ -863,7 +839,7 @@ def _binja_refresh_nodes(self):
for node in function.basic_blocks:
# create a new metadata object for this node
- node_metadata = NodeMetadata(node.start, node.end, node.index)
+ node_metadata = NodeMetadata(node.start, node.end, node.index, disassembler_ctx)
#
# establish a relationship between this node (basic block) and
@@ -971,7 +947,7 @@ class NodeMetadata(object):
Node (basic block) level metadata cache.
"""
- def __init__(self, start_ea, end_ea, node_id=None):
+ def __init__(self, start_ea, end_ea, node_id=None, disassembler_ctx=None):
# node metadata
self.size = end_ea - start_ea
@@ -991,13 +967,13 @@ def __init__(self, start_ea, end_ea, node_id=None):
#----------------------------------------------------------------------
# collect metadata from the underlying database
- self._build_metadata()
+ self._cache(disassembler_ctx)
#--------------------------------------------------------------------------
# Metadata Population
#--------------------------------------------------------------------------
- def _build_metadata(self):
+ def _cache(self, disassembler_ctx):
"""
This will be replaced with a disassembler-specific function at runtime.
@@ -1029,11 +1005,11 @@ def _ida_build_metadata(self):
# save the number of instructions in this block
self.instruction_count = len(self.instructions)
- def _binja_build_metadata(self):
+ def _binja_cache(self, disassembler_ctx):
"""
Collect node metadata from the underlying database.
"""
- bv = disassembler.bv
+ bv = disassembler_ctx.bv
current_address = self.address
node_end = self.address + self.size
@@ -1131,7 +1107,7 @@ def metadata_progress(completed, total):
elif disassembler.NAME == "BINJA":
import binaryninja
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
- NodeMetadata._build_metadata = NodeMetadata._binja_build_metadata
+ NodeMetadata._cache = NodeMetadata._binja_cache
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index 65bf243d..76766011 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -19,8 +19,8 @@ class BinjaPainter(DatabasePainter):
"""
PAINTER_SLEEP = 0.01
- def __init__(self, director, palette):
- super(BinjaPainter, self).__init__(director, palette)
+ def __init__(self, lctx, director, palette):
+ super(BinjaPainter, self).__init__(lctx, director, palette)
#--------------------------------------------------------------------------
# Paint Primitives
@@ -41,7 +41,7 @@ def _clear_instructions(self, instructions):
self._action_complete.set()
def _paint_nodes(self, nodes_coverage):
- bv = disassembler.bv
+ bv = disassembler[self.lctx].bv
r, g, b, _ = self.palette.coverage_paint.getRgb()
color = HighlightColor(red=r, green=g, blue=b)
for node_coverage in nodes_coverage:
@@ -52,7 +52,7 @@ def _paint_nodes(self, nodes_coverage):
self._action_complete.set()
def _clear_nodes(self, nodes_metadata):
- bv = disassembler.bv
+ bv = disassembler[self.lctx].bv
for node_metadata in nodes_metadata:
for node in bv.get_basic_blocks_starting_at(node_metadata.address):
node.highlight = HighlightStandardColor.NoHighlightColor
@@ -70,9 +70,11 @@ def _cancel_action(self, job):
#--------------------------------------------------------------------------
def _priority_paint(self):
- db_metadata = self._director.metadata
+ disassembler_ctx = disassembler[self.lctx]
+ db_metadata = self.director.metadata
+ return True # TODO
- current_address = disassembler.get_current_address()
+ current_address = disassembler_ctx.get_current_address()
current_function = disassembler.bv.get_function_at(current_address)
function_metadata = db_metadata.get_closest_function(current_address)
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index a27e9a8f..09529ff4 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -20,14 +20,15 @@ class DatabasePainter(object):
MSG_CLEAR = 2
MSG_REBASE = 3
- def __init__(self, director, palette):
+ def __init__(self, lctx, director, palette):
#----------------------------------------------------------------------
# Misc
#----------------------------------------------------------------------
+ self.lctx = lctx
self.palette = palette
- self._director = director
+ self.director = director
self._enabled = True
#----------------------------------------------------------------------
@@ -75,9 +76,9 @@ def __init__(self, director, palette):
self._status_changed_callbacks = []
# register for cues from the director
- self._director.coverage_switched(self.repaint)
- self._director.coverage_modified(self.repaint)
- self._director.refreshed(self.check_rebase)
+ self.director.coverage_switched(self.repaint)
+ self.director.coverage_modified(self.repaint)
+ self.director.refreshed(self.check_rebase)
#--------------------------------------------------------------------------
# Status
@@ -137,7 +138,7 @@ def clear_paint(self):
# to *preemptively* disable painting if no other coverage is loaded.
#
- if self.enabled and len(self._director.coverage_names):
+ if self.enabled and len(self.director.coverage_names):
self.set_enabled(False)
# trigger the database clear
@@ -220,8 +221,8 @@ def _paint_function(self, address):
"""
Paint function instructions & nodes with the current database mappings.
"""
- function_metadata = self._director.metadata.functions[address]
- function_coverage = self._director.coverage.functions.get(address, None)
+ function_metadata = self.director.metadata.functions[address]
+ function_coverage = self.director.coverage.functions.get(address, None)
if not function_coverage:
return False
@@ -274,7 +275,7 @@ def _clear_function(self, address):
"""
Clear paint from the given function.
"""
- function_metadata = self._director.metadata.functions[address]
+ function_metadata = self.director.metadata.functions[address]
instructions = function_metadata.instructions
nodes = itervalues(function_metadata.nodes)
@@ -295,8 +296,8 @@ def _paint_database(self):
"""
# more code-friendly, readable aliases (db_XX == database_XX)
- db_coverage = self._director.coverage
- db_metadata = self._director.metadata
+ db_coverage = self.director.coverage
+ db_metadata = self.director.metadata
start = time.time()
#------------------------------------------------------------------
@@ -352,7 +353,7 @@ def _clear_database(self):
"""
Clear all paint from the current database.
"""
- db_metadata = self._director.metadata
+ db_metadata = self.director.metadata
instructions = db_metadata.instructions
nodes = viewvalues(db_metadata.nodes)
@@ -374,7 +375,7 @@ def _rebase_database(self):
TODO/XXX: there may be some edgecases where painting can be wrong if
a rebase occurs while the painter is running.
"""
- db_metadata = self._director.metadata
+ db_metadata = self.director.metadata
instructions = db_metadata.instructions
nodes = viewvalues(db_metadata.nodes)
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 37ea4631..51dd9732 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -4,7 +4,7 @@
from lighthouse.util.qt import *
from lighthouse.util.misc import plugin_resource
-from lighthouse.util.disassembler import disassembler, DockableWindow
+from lighthouse.util.disassembler import disassembler, DockableChild
from lighthouse.composer import ComposingShell
from lighthouse.ui.coverage_table import CoverageTableView, CoverageTableModel, CoverageTableController
from lighthouse.ui.coverage_combobox import CoverageComboBox
@@ -16,22 +16,23 @@
# Coverage Overview
#------------------------------------------------------------------------------
-class CoverageOverview(DockableWindow):
+class CoverageOverview(DockableChild):
"""
The Coverage Overview Widget.
"""
- def __init__(self, core):
- super(CoverageOverview, self).__init__(
- "Coverage Overview",
- plugin_resource(os.path.join("icons", "overview.png"))
- )
+ def __init__(self, core, parent, name, dctx=None):
+ super(CoverageOverview, self).__init__(parent, name, dctx)
+ # plugin_resource(os.path.join("icons", "overview.png"))
self._core = core
- self._visible = False
+
+ self.lctx = self._core.get_context(self.dctx)
+ self.lctx.coverage_overview = self
+ self.director = self.lctx.director
# see the EventProxy class below for more details
- self._events = EventProxy(self)
- self._widget.installEventFilter(self._events)
+ #self._events = EventProxy(self)
+ #self._widget.installEventFilter(self._events)
# initialize the plugin UI
self._ui_init()
@@ -40,43 +41,52 @@ def __init__(self, core):
self.refresh()
# register for cues from the director
- self._core.director.refreshed(self.refresh)
+ self.director.refreshed(self.refresh)
#--------------------------------------------------------------------------
# Pseudo Widget Functions
#--------------------------------------------------------------------------
- def show(self):
- """
- Show the CoverageOverview UI / widget.
- """
- self.refresh()
- super(CoverageOverview, self).show()
- self._visible = True
-
- #
- # if no metadata had been collected prior to showing the coverage
- # overview (eg, through loading coverage), we should do that now
- # before the user can interact with the view...
- #
-
- if not self._core.director.metadata.cached:
- self._core.director.refresh()
+ #def showEvent(self, e):
+ # """
+ # Test
+ # """
+ # super(CoverageOverview, self).showEvent(e)
+ # print("Becoming visible says qt!!")
+ # print(self._visible)
+ # if not self._visible:
+ # return
+ # self.refresh()
+ # if not self.director.metadata.cached:
+ # self.director.refresh()
+ # return super(CoverageOverview, self).showEvent(e)
+
+ #def show(self):
+ # """
+ # Show the CoverageOverview UI / widget.
+ # """
+ # self.refresh()
+ # super(CoverageOverview, self).show()
+ # self._visible = True
+
+ # #
+ # # if no metadata had been collected prior to showing the coverage
+ # # overview (eg, through loading coverage), we should do that now
+ # # before the user can interact with the view...
+ # #
+
+ # if not self._core.director.metadata.cached:
+ # self._core.director.refresh()
def terminate(self):
"""
The CoverageOverview is being hidden / deleted.
"""
- self._visible = False
self._combobox = None
self._shell = None
self._table_view = None
self._table_controller = None
self._table_model = None
- self._widget = None
-
- def isVisible(self):
- return self._visible
#--------------------------------------------------------------------------
# Initialization - UI
@@ -99,13 +109,9 @@ def _ui_init_table(self):
"""
Initialize the coverage table.
"""
- self._table_model = CoverageTableModel(self._core.director, self._widget)
+ self._table_model = CoverageTableModel(self.director, self)
self._table_controller = CoverageTableController(self._table_model)
- self._table_view = CoverageTableView(
- self._table_controller,
- self._table_model,
- self._widget
- )
+ self._table_view = CoverageTableView(self._table_controller, self._table_model, self)
def _ui_init_toolbar(self):
"""
@@ -133,16 +139,15 @@ def _ui_init_toolbar_elements(self):
"""
Initialize the coverage toolbar UI elements.
"""
-
# the composing shell
self._shell = ComposingShell(
- self._core.director,
+ self.director,
weakref.proxy(self._table_model),
weakref.proxy(self._table_view)
)
# the coverage combobox
- self._combobox = CoverageComboBox(self._core.director)
+ self._combobox = CoverageComboBox(self.director)
# the splitter to make the shell / combobox resizable
self._shell_elements = QtWidgets.QSplitter(QtCore.Qt.Horizontal)
@@ -189,13 +194,13 @@ def _ui_init_settings(self):
self._settings_button.setStyleSheet("QToolButton::menu-indicator{image: none;}")
# settings menu
- self._settings_menu = TableSettingsMenu(self._widget)
+ self._settings_menu = TableSettingsMenu(self)
def _ui_init_signals(self):
"""
Connect UI signals.
"""
- self._settings_menu.connect_signals(self._table_controller, self._core)
+ self._settings_menu.connect_signals(self._table_controller, self.lctx)
self._settings_button.clicked.connect(self._ui_show_settings)
def _ui_layout(self):
@@ -210,7 +215,7 @@ def _ui_layout(self):
layout.addWidget(self._toolbar)
# apply the layout to the containing form
- self._widget.setLayout(layout)
+ self.setLayout(layout)
#--------------------------------------------------------------------------
# Signal Handlers
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index 6f34e17d..ada6098a 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -82,18 +82,18 @@ def _ui_init_actions(self):
self._action_hide_zero.setCheckable(True)
self.addAction(self._action_hide_zero)
- def connect_signals(self, controller, core):
+ def connect_signals(self, controller, lctx):
"""
Connect UI signals.
"""
- self._action_change_theme.triggered.connect(core.palette.interactive_change_theme)
- self._action_refresh_metadata.triggered.connect(core.director.refresh)
+ self._action_change_theme.triggered.connect(lctx.core.palette.interactive_change_theme)
+ self._action_refresh_metadata.triggered.connect(lctx.director.refresh)
self._action_hide_zero.triggered[bool].connect(controller._model.filter_zero_coverage)
- self._action_pause_paint.triggered[bool].connect(lambda x: core.painter.set_enabled(not x))
- self._action_clear_paint.triggered.connect(core.painter.clear_paint)
+ self._action_pause_paint.triggered[bool].connect(lambda x: lctx.painter.set_enabled(not x))
+ self._action_clear_paint.triggered.connect(lctx.painter.clear_paint)
self._action_export_html.triggered.connect(controller.export_to_html)
- self._action_dump_unmapped.triggered.connect(core.director.dump_unmapped)
- core.painter.status_changed(self._ui_painter_changed_status)
+ self._action_dump_unmapped.triggered.connect(lctx.director.dump_unmapped)
+ lctx.painter.status_changed(self._ui_painter_changed_status)
#--------------------------------------------------------------------------
# Signal Handlers
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index a44ff629..062568ba 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -424,10 +424,11 @@ def rename_table_function(self, row):
"""
Interactive rename of a database function via the coverage table.
"""
+ lctx = self._model._director.metadata.lctx # TODO dirty
# retrieve details about the function targeted for rename
function_address = self._model.row2func[row]
- original_name = disassembler.get_function_raw_name_at(function_address)
+ original_name = disassembler[lctx].get_function_raw_name_at(function_address)
# prompt the user for a new function name
ok, new_name = prompt_string(
@@ -445,13 +446,14 @@ def rename_table_function(self, row):
return
# rename the function
- disassembler.set_function_name_at(function_address, new_name)
+ disassembler[lctx].set_function_name_at(function_address, new_name)
@mainthread
def prefix_table_functions(self, rows):
"""
Interactive prefixing of database functions via the coverage table.
"""
+ lctx = self._model._director.metadata.lctx # TODO dirty
# prompt the user for a new function name
ok, prefix = prompt_string(
@@ -466,15 +468,16 @@ def prefix_table_functions(self, rows):
# apply the user prefix to the functions depicted in the given rows
function_addresses = self._get_function_addresses(rows)
- disassembler.prefix_functions(function_addresses, prefix)
+ disassembler[lctx].prefix_functions(function_addresses, prefix)
@mainthread
def clear_function_prefixes(self, rows):
"""
Clear prefixes of database functions via the coverage table.
"""
+ lctx = self._model._director.metadata.lctx # TODO dirty
function_addresses = self._get_function_addresses(rows)
- disassembler.clear_prefixes(function_addresses)
+ disassembler[lctx].clear_prefixes(function_addresses)
#---------------------------------------------------------------------------
# Copy-to-Clipboard
@@ -533,7 +536,8 @@ def navigate_to_function(self, row):
"""
Navigate to the function depicted by the given row.
"""
- disassembler.navigate(self._model.row2func[row])
+ lctx = self._model._director.metadata.lctx # TODO dirty
+ disassembler[lctx].navigate(self._model.row2func[row])
def toggle_column_alignment(self, column):
"""
@@ -571,8 +575,9 @@ def export_to_html(self):
"""
Export the coverage table to an HTML report.
"""
+ lctx = self._model._director.metadata.lctx # TODO dirty
if not self._last_directory:
- self._last_directory = disassembler.get_database_directory()
+ self._last_directory = disassembler[lctx].get_database_directory()
# build filename for the coverage report based off the coverage name
name, _ = os.path.splitext(self._model._director.coverage_name)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 98794342..06440bb1 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -103,6 +103,8 @@ def __init__(self):
# initialize the user theme directory
self._populate_user_theme_dir()
+ self.warmup()
+
#----------------------------------------------------------------------
# Properties
#----------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/__init__.py b/plugin/lighthouse/util/disassembler/__init__.py
index 0d6eba94..7149023f 100644
--- a/plugin/lighthouse/util/disassembler/__init__.py
+++ b/plugin/lighthouse/util/disassembler/__init__.py
@@ -27,8 +27,9 @@
if disassembler == None:
try:
- from .binja_api import BinjaAPI, DockableWindow
- disassembler = BinjaAPI()
+ from .binja_api import BinjaCoreAPI, BinjaContextAPI, DockableChild
+ disassembler = BinjaCoreAPI()
+ DisassemblerContextAPI = BinjaContextAPI
except ImportError:
pass
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index c3a25db7..cd75d976 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -17,7 +17,7 @@
# to any given interactive disassembler.
#
-class DisassemblerAPI(object):
+class DisassemblerCoreAPI(object):
"""
An abstract implementation of the required disassembler API.
"""
@@ -28,7 +28,7 @@ class DisassemblerAPI(object):
@abc.abstractmethod
def __init__(self):
- self._waitbox = None
+ self._ctxs = {}
# required version fields
self._version_major = NotImplemented
@@ -38,6 +38,17 @@ def __init__(self):
if not self.headless and QT_AVAILABLE:
from ..qt import WaitBox
self._waitbox = WaitBox("Please wait...")
+ else:
+ self._waitbox = None
+
+ def __delitem__(self, key):
+ del self._ctxs[key]
+
+ def __getitem__(self, key):
+ return self._ctxs[key]
+
+ def __setitem__(self, key, value):
+ self._ctxs[key] = value
#--------------------------------------------------------------------------
# Properties
@@ -109,20 +120,95 @@ def execute_ui(function):
raise NotImplementedError("execute_ui() has not been implemented")
#--------------------------------------------------------------------------
- # API Shims
+ # Disassembler Universal APIs
#--------------------------------------------------------------------------
@abc.abstractmethod
- def get_database_directory(self):
+ def get_disassembler_user_directory(self):
"""
- Return the directory for the open database.
+ Return the 'user' directory for the disassembler.
"""
pass
@abc.abstractmethod
- def get_disassembler_user_directory(self):
+ def get_disassembly_background_color(self):
"""
- Return the 'user' directory for the disassembler.
+ Return the background color of the disassembly text view.
+ """
+ pass
+
+ @abc.abstractmethod
+ def is_msg_inited(self):
+ """
+ Return a bool if the disassembler output window is initialized.
+ """
+ pass
+
+ @abc.abstractmethod
+ def warning(self, text):
+ """
+ Display a warning dialog box with the given text.
+ """
+ pass
+
+ @abc.abstractmethod
+ def message(self, function_address, new_name):
+ """
+ Print a message to the disassembler console.
+ """
+ pass
+
+ #------------------------------------------------------------------------------
+ # WaitBox API
+ #------------------------------------------------------------------------------
+
+ def show_wait_box(self, text):
+ """
+ Show the disassembler universal WaitBox.
+ """
+ assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
+ self._waitbox.set_text(text)
+ self._waitbox.show()
+
+ def hide_wait_box(self):
+ """
+ Hide the disassembler universal WaitBox.
+ """
+ assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
+ self._waitbox.hide()
+
+ def replace_wait_box(self, text):
+ """
+ Replace the text in the disassembler universal WaitBox.
+ """
+ assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
+ self._waitbox.set_text(text)
+
+#------------------------------------------------------------------------------
+# Disassembler Contextual API
+#------------------------------------------------------------------------------
+#
+# TODO
+#
+
+class DisassemblerContextAPI(object):
+ """
+ An abstract implementation of the required binary-specific disassembler API.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def __init__(self, dctx):
+ self.dctx = dctx
+
+ #--------------------------------------------------------------------------
+ # API Shims
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def get_database_directory(self):
+ """
+ Return the directory for the open database.
"""
pass
@@ -181,38 +267,149 @@ def set_function_name_at(self, function_address, new_name):
"""
pass
+ #--------------------------------------------------------------------------
+ # Hooks API
+ #--------------------------------------------------------------------------
+
@abc.abstractmethod
- def message(self, function_address, new_name):
+ def create_rename_hooks(self, function_address, new_name):
"""
- Print a message to the disassembler console.
+ Returns a hooking object that can capture rename events for this context.
"""
pass
#--------------------------------------------------------------------------
- # UI API Shims
+ # Function Prefix API
#--------------------------------------------------------------------------
+ #
+ # the following APIs are used to apply or clear prefixes to multiple
+ # functions in the disassembly database. the only thing you're expected
+ # to do here is select an appropriate PREFIX_SEPARATOR.
+ #
+ # your prefix separator is expected to be something unique, that a user
+ # would probably *never* put into their function name themselves but
+ # looks somewhat normal.
+ #
+ # in IDA, putting '%' in a function name appears as '_' in the function
+ # list, so we use that as a prefix separator. in Binary Ninja, we use a
+ # unicode character that looks like an underscore character.
+ #
+ # it is probably safe to steal the unicode char we use with binja for
+ # your own implementation.
+ #
+
+ PREFIX_SEPARATOR = NotImplemented
+
+ def prefix_function(self, function_address, prefix):
+ """
+ Prefix a function name with the given string.
+ """
+ original_name = self.get_function_raw_name_at(function_address)
+ new_name = str(prefix) + self.PREFIX_SEPARATOR + str(original_name)
+
+ # rename the function with the newly prefixed name
+ self.set_function_name_at(function_address, new_name)
+
+ def prefix_functions(self, function_addresses, prefix):
+ """
+ Prefix a list of functions with the given string.
+ """
+ for function_address in function_addresses:
+ self.prefix_function(function_address, prefix)
+
+ def clear_prefix(self, function_address):
+ """
+ Clear the prefix from a given function.
+ """
+ prefixed_name = self.get_function_raw_name_at(function_address)
+
+ #
+ # split the function name on the last prefix separator, saving
+ # everything that comes after (eg, the original func name)
+ #
+
+ new_name = prefixed_name.rsplit(self.PREFIX_SEPARATOR)[-1]
+
+ # the name doesn't appear to have had a prefix, nothing to do...
+ if new_name == prefixed_name:
+ return
+
+ # rename the function with the prefix(s) now stripped
+ self.set_function_name_at(function_address, new_name)
+
+ def clear_prefixes(self, function_addresses):
+ """
+ Clear the prefix from a list of given functions.
+ """
+ for function_address in function_addresses:
+ self.clear_prefix(function_address)
+
+#------------------------------------------------------------------------------
+# Hooking
+#------------------------------------------------------------------------------
+
+class RenameHooks(object):
+ """
+ An abstract implementation of disassembler hooks to capture rename events.
+ """
+ __metaclass__ = abc.ABCMeta
+
@abc.abstractmethod
- def get_disassembly_background_color(self):
+ def hook(self):
"""
- Return the background color of the disassembly text view.
+ Install hooks into the disassembler that capture rename events.
"""
pass
@abc.abstractmethod
- def is_msg_inited(self):
+ def unhook(self):
"""
- Return a bool if the disassembler output window is initialized.
+ Remove hooks used to capture rename events.
"""
pass
- @abc.abstractmethod
- def warning(self, text):
+ def renamed(self, address, new_name):
"""
- Display a warning dialog box with the given text.
+ This will be hooked by Lighthouse at runtime to capture rename events.
"""
pass
+#------------------------------------------------------------------------------
+# Dockable Window
+#------------------------------------------------------------------------------
+
+class DockableShim(object):
+ """
+ A minimal template of the DockableWindow.
+
+ this class is only to demonstrate the minimal set of attributes and
+ functions that a disassembler's DockableWindow class should contain.
+
+ show/hide can be overridden entirely depending on your needs, but the
+ self._widget field should contain a reference to a blank widget that has
+ been installed into a QDockWidget in the disassembler interface.
+ """
+ __metaclass__ = abc.ABCMeta
+
+ def __init__(self, window_title, icon_path):
+ self._window_title = window_title
+ self._window_icon = QtGui.QIcon(icon_path)
+ self._widget = None
+
+ def show(self):
+ """
+ Show the dockable widget.
+ """
+ self._widget.show()
+
+ def hide(self):
+ """
+ Show the dockable widget.
+ """
+ self._widget.hide()
+
+
#--------------------------------------------------------------------------
# Function Prefix API
#--------------------------------------------------------------------------
@@ -280,32 +477,6 @@ def clear_prefixes(self, function_addresses):
for function_address in function_addresses:
self.clear_prefix(function_address)
- #------------------------------------------------------------------------------
- # WaitBox API
- #------------------------------------------------------------------------------
-
- def show_wait_box(self, text):
- """
- Show the disassembler universal WaitBox.
- """
- assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
- self._waitbox.set_text(text)
- self._waitbox.show()
-
- def hide_wait_box(self):
- """
- Hide the disassembler universal WaitBox.
- """
- assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
- self._waitbox.hide()
-
- def replace_wait_box(self, text):
- """
- Replace the text in the disassembler universal WaitBox.
- """
- assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
- self._waitbox.set_text(text)
-
#------------------------------------------------------------------------------
# Hooking
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index ffa5000d..87d2386b 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -7,10 +7,10 @@
import binaryninja
from binaryninja import PythonScriptingInstance, binaryview
-from binaryninjaui import DockHandler, DockContextHandler, UIContext
+from binaryninjaui import DockHandler, DockContextHandler, UIContext, UIActionHandler
from binaryninja.plugin import BackgroundTaskThread
-from .api import DisassemblerAPI, DockableShim
+from .api import DisassemblerCoreAPI, DisassemblerContextAPI
from ..qt import *
from ..misc import is_mainthread, not_mainthread
@@ -75,27 +75,18 @@ def run(self):
# Disassembler API
#------------------------------------------------------------------------------
-class BinjaAPI(DisassemblerAPI):
+class BinjaCoreAPI(DisassemblerCoreAPI):
"""
The Binary Ninja implementation of the disassembler API abstraction.
"""
NAME = "BINJA"
- def __init__(self, bv=None):
- super(BinjaAPI, self).__init__()
+ def __init__(self):
+ super(BinjaCoreAPI, self).__init__()
self._init_version()
- # binja specific amenities
- self._bv = bv
- self._python = _binja_get_scripting_instance()
-
def _init_version(self):
- version_string = None
- # Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
- try:
- version_string = binaryninja.core_version()
- except TypeError:
- version_string = binaryninja.core_version
+ version_string = binaryninja.core_version()
# retrieve Binja's version #
if "-" in version_string: # dev
@@ -114,18 +105,6 @@ def _init_version(self):
# Properties
#--------------------------------------------------------------------------
- @property
- def bv(self):
- return self._bv
-
- @bv.setter
- def bv(self, bv):
- if self._bv == bv:
- return
- if self._bv:
- raise ValueError("BinaryView cannot be changed once set...")
- self._bv = bv
-
@property
def headless(self):
ret = None
@@ -136,10 +115,6 @@ def headless(self):
ret = binaryninja.core_ui_enabled
return not ret
- @property
- def busy(self):
- return False # TODO
-
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
@@ -165,10 +140,7 @@ def wrapper(*args, **kwargs):
return
# schedule the task to run in the main thread
- try:
- binaryninja.execute_on_main_thread(ff)
- except AttributeError: # TODO/V35: binja bug, fixed on dev
- pass
+ binaryninja.execute_on_main_thread(ff)
return wrapper
@@ -176,6 +148,62 @@ def wrapper(*args, **kwargs):
# API Shims
#--------------------------------------------------------------------------
+ def get_disassembler_user_directory(self):
+ return os.path.split(binaryninja.user_plugin_path())[0]
+
+ def get_disassembly_background_color(self):
+ palette = QtGui.QPalette()
+ return palette.color(QtGui.QPalette.Button)
+
+ def is_msg_inited(self):
+ return True
+
+ def warning(self, text):
+ binaryninja.interaction.show_message_box("Warning", text)
+
+ def message(self, message):
+ print(message)
+
+ #--------------------------------------------------------------------------
+ # UI API Shims
+ #--------------------------------------------------------------------------
+
+ def create_dockable_widget(self, dockable_name, create_widget_callback):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.addDockWidget(dockable_name, create_widget_callback, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, False)
+
+ def show_dockable_widget(self, dockable_name):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(dockable_name, True)
+
+ #--------------------------------------------------------------------------
+ # XXX Binja Specfic Helpers
+ #--------------------------------------------------------------------------
+
+ def binja_get_bv_from_dock(self):
+ dh = DockHandler.getActiveDockHandler()
+ if not dh:
+ return None
+ vf = dh.getViewFrame()
+ if not vf:
+ return None
+ vi = vf.getCurrentViewInterface()
+ bv = vi.getData()
+ return bv
+
+class BinjaContextAPI(DisassemblerContextAPI):
+ """
+ TODO
+ """
+
+ def __init__(self, dctx):
+ super(BinjaContextAPI, self).__init__(dctx)
+ self.bv = dctx
+
+ @property
+ def busy(self):
+ return self.bv.analysis_info.state != binaryninja.enums.AnalysisState.IdleState
+
#
# NOTE/TODO/V35:
#
@@ -188,23 +216,18 @@ def wrapper(*args, **kwargs):
# which *needs* database accesses to be made from the mainthread
#
- def create_rename_hooks(self):
- return RenameHooks(self.bv)
+ #--------------------------------------------------------------------------
+ # API Shims
+ #--------------------------------------------------------------------------
def get_current_address(self):
- if not self._python:
- self._python = _binja_get_scripting_instance()
- if not self._python:
- return -1
- return self._python.current_addr
+ raise NotImplementedError("TODO!")
+ return 0
- @execute_read.__func__
+ @BinjaCoreAPI.execute_read
def get_database_directory(self):
return os.path.dirname(self.bv.file.filename)
- def get_disassembler_user_directory(self):
- return os.path.split(binaryninja.user_plugin_path())[0]
-
@not_mainthread
def get_function_addresses(self):
return [x.start for x in self.bv.functions]
@@ -216,7 +239,7 @@ def get_function_name_at(self, address):
return None
return func.symbol.short_name
- @execute_read.__func__
+ @BinjaCoreAPI.execute_read
def get_function_raw_name_at(self, address):
func = self.bv.get_function_at(address)
if not func:
@@ -234,7 +257,7 @@ def get_root_filename(self):
def navigate(self, address):
return self.bv.navigate(self.bv.view, address)
- @execute_write.__func__
+ @BinjaCoreAPI.execute_write
def set_function_name_at(self, function_address, new_name):
func = self.bv.get_function_at(function_address)
if not func:
@@ -243,29 +266,12 @@ def set_function_name_at(self, function_address, new_name):
new_name = None
func.name = new_name
- #
- # TODO/V35: As a workaround for no symbol events, we trigger a data
- # notification for this function instead.
- #
-
- self.bv.write(function_address, self.bv.read(function_address, 1))
-
- def message(self, message):
- print(message)
-
#--------------------------------------------------------------------------
- # UI API Shims
+ # Hooks API
#--------------------------------------------------------------------------
- def get_disassembly_background_color(self):
- palette = QtGui.QPalette()
- return palette.color(QtGui.QPalette.Button)
-
- def is_msg_inited(self):
- return True
-
- def warning(self, text):
- binaryninja.interaction.show_message_box("Warning", text)
+ def create_rename_hooks(self):
+ return RenameHooks(self.bv)
#------------------------------------------------------------------------------
# Function Prefix API
@@ -277,147 +283,97 @@ def warning(self, text):
# Hooking
#------------------------------------------------------------------------------
-class RenameHooks(object):
+class RenameHooks(binaryview.BinaryDataNotification):
"""
- A Hooking class to catch function renames in Binary Ninja.
+ A hooking class to catch symbol changes in Binary Ninja.
"""
def __init__(self, bv):
self._bv = bv
-
- # hook certain Binary Ninja notifications
- self._hooks = binaryview.BinaryDataNotification()
- self._hooks.function_updated = self._workaround
-
- # TODO/V35: turns out there are no adequate symbol event hooks...
- #self._hooks.function_update_requested = self._before
- #self._hooks.function_updated = self._after
- #self._names = {}
+ 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._hooks)
+ self._bv.register_notification(self)
def unhook(self):
- self._bv.unregister_notification(self._hooks)
-
- @BinjaAPI.execute_ui
- def _renamed(self, address, new_name):
- """
- Pass off the (internal) rename event to the mainthread.
- """
- self.renamed(address, new_name)
-
- def _before(self, _, function):
- """
- Capture function name prior to modification.
- """
- self._names[function.start] = function.name
-
- def _after(self, _, function):
- """
- Capture function name post modification
- """
+ self._bv.unregister_notification(self)
- #
- # if we don't have an old name for a given function logged, that
- # means we must have missed the function_update_requested event for it.
- #
- # hopefully this should never happen during real *rename* events...
- #
-
- old_name = self._names.get(function.start, None)
- if not old_name:
- return
-
- # if the function name hasn't changed, then there is nothing to do!
- if old_name == function.name:
- return
-
- # fire our custom 'function renamed' event
- self._renamed(function.start, function.name)
-
- #--------------------------------------------------------------------------
- # Temporary Workaound
- #--------------------------------------------------------------------------
-
- def _workaround(self, _, function):
- """
- TODO/V35: workaround to detect name changes pending better API's
- """
- function_metadata = self.metadata.get_function(function.start)
- if not function_metadata:
- return
-
- # if the function name hasn't changed, then there is nothing to do!
- if function_metadata.name == function.symbol.short_name:
+ def __symbol_handler(self, view, symbol):
+ func = self._bv.get_function_at(symbol.address)
+ if not func.start == symbol.address:
return
-
- # fire our custom 'function renamed' event
- self._renamed(function.start, function.symbol.short_name)
+ self.renamed(symbol.address, symbol.name)
#------------------------------------------------------------------------------
# UI
#------------------------------------------------------------------------------
-class DockableWindow(DockableShim):
+class DockableChild(QtWidgets.QWidget, DockContextHandler):
"""
A dockable Qt widget for Binary Ninja.
"""
- def __init__(self, window_title, icon_path):
- super(DockableWindow, self).__init__(window_title, icon_path)
-
- # configure dockable widget container
- self._active_context = UIContext.allContexts()[0]
- self._main_window = self._active_context.mainWindow()
- self._dock_handler = self._main_window.findChild(DockHandler, '__DockHandler')
- self._widget = QtWidgets.QWidget(self._dock_handler.parent())
- self._dock_contxet = DockContextHandler(self._widget, self._window_title)
-
- self._widget.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding,
- QtWidgets.QSizePolicy.Expanding
- )
-
- # dock the widget on the right side of Binja
- self._dock_handler.addDockWidget(self._widget, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, True, False)
- self._dockable = self._dock_handler.getDockWidget(self._window_title)
-
- self._dockable = QtWidgets.QDockWidget(window_title, self._main_window)
- self._dockable.setWindowIcon(self._window_icon)
- self._dockable.setAttribute(QtCore.Qt.WA_DeleteOnClose)
- self._dockable.setSizePolicy(
- QtWidgets.QSizePolicy.Expanding,
- QtWidgets.QSizePolicy.Expanding
- )
-
-#------------------------------------------------------------------------------
-# Binary Ninja Hacks XXX / TODO / V35
-#------------------------------------------------------------------------------
-
-def _binja_get_scripting_instance():
- """
- Get the python scripting console in Binary Ninja.
- """
- for t in threading.enumerate():
- if type(t) == PythonScriptingInstance.InterpreterThread:
- return t
- return None
-
-def binja_get_bv():
- """
- Get the current BinaryView in Binary Ninja.
- """
- python = _binja_get_scripting_instance()
- if not python:
- return None
- return python.current_view
+ def __init__(self, parent, name, dctx=None):
+
+ QtWidgets.QWidget.__init__(self, parent)
+ DockContextHandler.__init__(self, self, name)
+
+ self.name = name
+ self.dctx = dctx
+
+ self.actionHandler = UIActionHandler()
+ self.actionHandler.setupActionHandler(self)
+
+ self.visible = False
+
+ #self._widget.setSizePolicy(
+ # QtWidgets.QSizePolicy.Expanding,
+ # QtWidgets.QSizePolicy.Expanding
+ #)
+
+ ## dock the widget on the right side of Binja
+ #self._dock_handler.addDockWidget(self._widget, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, True, False)
+ #self._dockable = self._dock_handler.getDockWidget(self._window_title)
+
+ #self._dockable = QtWidgets.QDockWidget(window_title, self._main_window)
+ #self._dockable.setWindowIcon(self._window_icon)
+ #self._dockable.setSizePolicy(
+ # QtWidgets.QSizePolicy.Expanding,
+ # QtWidgets.QSizePolicy.Expanding
+ #)
+
+ def notifyOffsetChanged(self, offset):
+ #print("Offset changed..")
+ #self.offset.setText(hex(offset))
+ pass
+
+ def shouldBeVisible(self, view_frame):
+ print("Should be visible called...")
+ if view_frame is None:
+ print(" - No, there's no BV")
+ return False
+ print("%r" % self.visible)
+ return self.visible
+
+ def notifyVisibilityChanged(self, is_visible):
+ print("Vis changed...")
+ self.visible = is_visible
+
+ def notifyViewChanged(self, view_frame):
+ print("Notify view changed", view_frame)
+ return
+ if view_frame is None:
+ self.datatype.setText("None")
+ self.data = None
+ else:
+ self.datatype.setText(view_frame.getCurrentView())
+ view = view_frame.getCurrentViewInterface()
+ self.data = view.getData()
+
+ def contextMenuEvent(self, event):
+ print("CTX Menu event")
+ return
+ #self.m_contextMenuManager.show(self.m_menu, self.actionHandler)
-def binja_get_function_at(address):
- """
- Get the function object at the given address.
- """
- bv = binja_get_bv()
- if not bv:
- return None
- return bv.get_function_at(address)
diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py
index 973a10be..9f220d72 100644
--- a/plugin/lighthouse_plugin.py
+++ b/plugin/lighthouse_plugin.py
@@ -21,8 +21,8 @@
elif disassembler.NAME == "BINJA":
logger.info("Selecting Binary Ninja loader...")
- from lighthouse.binja_loader import *
- from lighthouse import coverage_director
+ from lighthouse.integration.binja_loader import *
+ #from lighthouse import coverage_director
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
From 67e5caf62d14870744877728bdc7380a02077311 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 03:03:53 -0400
Subject: [PATCH 103/154] mutual perf wins for metadata caching
---
plugin/lighthouse/metadata.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 99b93b68..7cb950a4 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -2,6 +2,7 @@
import bisect
import logging
import weakref
+import itertools
import threading
import collections
@@ -352,8 +353,8 @@ def _refresh_instructions(self):
"""
instructions = []
for function_metadata in itervalues(self.functions):
- instructions.extend(function_metadata.instructions)
- instructions = list(set(instructions))
+ instructions.append(function_metadata.instructions)
+ instructions = list(set(itertools.chain.from_iterable(instructions)))
instructions.sort()
# commit the updated instruction list
@@ -744,7 +745,7 @@ def instructions(self):
"""
Return the instruction addresses in this function.
"""
- return set([ea for node in itervalues(self.nodes) for ea in node.instructions])
+ return set(itertools.chain.from_iterable([node.instructions for node in itervalues(self.nodes)]))
@property
def empty(self):
From ebea88465f435d6700824668dbbd562af9116dab Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 03:05:37 -0400
Subject: [PATCH 104/154] binja-specific perf wins
---
plugin/lighthouse/metadata.py | 27 ++++++++++++++-----
.../lighthouse/util/disassembler/binja_api.py | 1 -
2 files changed, 21 insertions(+), 7 deletions(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 7cb950a4..a9eb8244 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -827,9 +827,10 @@ def _binja_refresh_nodes(self, disassembler_ctx):
"""
function_metadata = self
function_metadata.nodes = {}
+ bv = disassembler_ctx.bv
# get the function from the Binja database
- function = disassembler_ctx.bv.get_function_at(self.address)
+ function = bv.get_function_at(self.address)
#
# now we will walk the flowchart for this function, collecting
@@ -856,8 +857,18 @@ def _binja_refresh_nodes(self, disassembler_ctx):
#
edge_src = node_metadata.edge_out
- for edge in node.outgoing_edges:
- function_metadata.edges[edge_src].append(edge.target.start)
+
+ count = ctypes.c_ulonglong(0)
+ edges = core.BNGetBasicBlockOutgoingEdges(node.handle, count)
+
+ for i in range(0, count.value):
+ if edges[i].target:
+ function_metadata.edges[edge_src].append(node._create_instance(core.BNNewBasicBlockReference(edges[i].target), bv).start)
+ core.BNFreeBasicBlockEdgeList(edges, count.value)
+
+ # NOTE/PERF ~28% of metadata collection time alone...
+ #for edge in node.outgoing_edges:
+ # function_metadata.edges[edge_src].append(edge.target.start)
def _compute_complexity(self):
"""
@@ -1010,10 +1021,13 @@ def _binja_cache(self, disassembler_ctx):
"""
Collect node metadata from the underlying database.
"""
- bv = disassembler_ctx.bv
current_address = self.address
node_end = self.address + self.size
+ # NOTE/PERF: gotta go fast :D
+ bh = disassembler_ctx.bv.handle
+ ah = disassembler_ctx.bv.arch.handle
+
#
# Note that we 'iterate over' the instructions using their byte length
# because it is far more performant than Binary Ninja's instruction
@@ -1021,8 +1035,7 @@ def _binja_cache(self, disassembler_ctx):
#
while current_address < node_end:
- instruction_size = bv.get_instruction_length(current_address)
- instruction_size = instruction_size if instruction_size else 1 # TODO/HACK: binja can return 0 for undef/bad inst
+ instruction_size = core.BNGetInstructionLength(bh, ah, current_address) or 1
self.instructions[current_address] = instruction_size
current_address += instruction_size
@@ -1106,7 +1119,9 @@ def metadata_progress(completed, total):
NodeMetadata._build_metadata = NodeMetadata._ida_build_metadata
elif disassembler.NAME == "BINJA":
+ import ctypes
import binaryninja
+ from binaryninja import core
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
NodeMetadata._cache = NodeMetadata._binja_cache
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 87d2386b..d3c55e33 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -232,7 +232,6 @@ def get_database_directory(self):
def get_function_addresses(self):
return [x.start for x in self.bv.functions]
- @not_mainthread
def get_function_name_at(self, address):
func = self.bv.get_function_at(address)
if not func:
From ff2c0d96192ca72d5f355eca338113de2478bb41 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 05:27:08 -0400
Subject: [PATCH 105/154] switch to pprofile for line profiling
---
plugin/lighthouse/util/debug.py | 31 +++++++------------------------
1 file changed, 7 insertions(+), 24 deletions(-)
diff --git a/plugin/lighthouse/util/debug.py b/plugin/lighthouse/util/debug.py
index a2555cc7..5e9f4b49 100644
--- a/plugin/lighthouse/util/debug.py
+++ b/plugin/lighthouse/util/debug.py
@@ -1,4 +1,5 @@
import sys
+import inspect
import cProfile
import traceback
@@ -37,18 +38,17 @@ def wrap(*args, **kwargs):
# Function Line Profiling
#------------------------------------------------------------------------------
-# from: https://gist.github.com/sibelius/3920b3eb5adab482b105
try:
- from line_profiler import LineProfiler
+ import pprofile
def line_profile(func):
def profiled_func(*args, **kwargs):
try:
- profiler = LineProfiler()
- profiler.add_function(func)
- profiler.enable_by_count()
- return func(*args, **kwargs)
+ profiler = pprofile.ThreadProfile()
+ with profiler():
+ return func(*args, **kwargs)
finally:
- profiler.print_stats()
+ caller_file = inspect.getfile(func)
+ profiler.annotate(pprofile.EncodeOrReplaceWriter(sys.stdout), [caller_file])
return profiled_func
except ImportError:
@@ -95,20 +95,3 @@ def wrap(*args, **kwargs):
return wrap
-#------------------------------------------------------------------------------
-# Module Line Profiling
-#------------------------------------------------------------------------------
-
-if False:
- from line_profiler import LineProfiler
- lpr = LineProfiler()
-
- # change this to the target file / module to profile
- import lighthouse.metadata as metadata
- lpr.add_module(metadata)
-
- # put this code somewhere to dump results:
- #global lpr
- #lpr.enable_by_count()
- #lpr.disable_by_count()
- #lpr.print_stats(stripzeros=True)
From 52fb3e70d0246843a1d963edfea706d684f5ab47 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 07:46:58 -0400
Subject: [PATCH 106/154] emancipate nodes from functions, this allows coverage
to properly be computed for functions that share nodes
---
plugin/lighthouse/composer/shell.py | 6 +--
plugin/lighthouse/coverage.py | 31 ++++++++-------
plugin/lighthouse/metadata.py | 48 ++++++++++++++++-------
plugin/lighthouse/painting/ida_painter.py | 11 +++++-
4 files changed, 63 insertions(+), 33 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 00be6aa0..3d911f2e 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -474,9 +474,9 @@ def _compute_jump(self, text):
except ValueError:
pass
else:
- function_metadata = self._director.metadata.get_function(address)
- if function_metadata:
- return function_metadata.address
+ functions = self._director.metadata.get_functions_containing(address)
+ if functions:
+ return functions[0].address
#
# the user string did not translate to a parsable hex number (address)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index f950441f..e68dd511 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -607,28 +607,31 @@ def _map_functions(self, dirty_nodes):
# (parent) metadata.
#
- function_metadata = self._metadata.nodes[node_coverage.address].function
+ functions = self._metadata.get_functions_by_node(node_coverage.address)
#
- # now we will attempt to retrieve the the FunctionCoverage object
+ # now we will attempt to retrieve the FunctionCoverage objects
# that we need to parent the given NodeCoverage object to
#
- function_coverage = self.functions.get(function_metadata.address, None)
+ for function_metadata in functions:
+ function_coverage = self.functions.get(function_metadata.address, None)
- #
- # if we failed to locate a FunctionCoverage for this node, it means
- # that this is the first time we have seen coverage for this
- # function. create a new coverage function object and use it now.
- #
+ #
+ # if we failed to locate the FunctionCoverage for a function
+ # that references this node, then it is the first time we have
+ # seen coverage for it.
+ #
+ # create a new coverage function object and use it now.
+ #
- if not function_coverage:
- function_coverage = FunctionCoverage(function_metadata.address, self._weak_self)
- self.functions[function_metadata.address] = function_coverage
+ if not function_coverage:
+ function_coverage = FunctionCoverage(function_metadata.address, self._weak_self)
+ self.functions[function_metadata.address] = function_coverage
- # add the NodeCoverage object to its parent FunctionCoverage
- function_coverage.mark_node(node_coverage)
- dirty_functions[function_metadata.address] = function_coverage
+ # add the NodeCoverage object to its parent FunctionCoverage
+ function_coverage.mark_node(node_coverage)
+ dirty_functions[function_metadata.address] = function_coverage
# done, return a map of FunctionCoverage objects that were modified
return dirty_functions
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index a9eb8244..b22cdde4 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -78,6 +78,7 @@ def __init__(self, lctx=None):
# internal members to help index & navigate the cached metadata
self._name2func = {}
+ self._node2func = collections.defaultdict(list)
self._node_addresses = []
self._function_addresses = []
@@ -171,12 +172,22 @@ def get_node(self, address):
node_metadata = self.nodes.get(self._node_addresses[index], None)
#
- # if the given address does not fall within the selected node (or the
- # node simply does not exist), then we have no match/metadata to return
+ # this should hit 99.9% of the time on the first index...
+ #
+ # but we added a fallback in the rare case when binja creates an edge
+ # to an unknown/undefined instruction, whose address happens to fall
+ # within a real one, thus throwing off the basic block lookup...
+ #
+ # technically, we could also fail going back only one block, but at
+ # that point, idc, the user is looking at some weird binaries... :\
#
if not (node_metadata and address in node_metadata.instructions):
- return None
+ node_metadata = self.nodes.get(self._node_addresses[index-1], None)
+
+ # double fault, let's just dip...
+ if not (node_metadata and address in node_metadata.instructions):
+ return None
#
# if the selected node metadata contains the given target address, it
@@ -189,14 +200,20 @@ def get_node(self, address):
# return the located node_metadata
return node_metadata
- def get_function(self, address):
+ def get_function(self, function_address):
+ """
+ Get the function metadata that starts at the given address.
+ """
+ return self.functions.get(function_address, None)
+
+ def get_functions_containing(self, address):
"""
- Get the function metadata for a given address.
+ Get the list of function metadata objects that contain the given address.
"""
node_metadata = self.get_node(address)
if not node_metadata:
- return None
- return node_metadata.function
+ return []
+ return self.get_functions_by_node(node_metadata.address)
def get_function_by_name(self, function_name):
"""
@@ -222,6 +239,12 @@ def get_function_index(self, address):
"""
return self._function_addresses.index(address)
+ def get_functions_by_node(self, node_address):
+ """
+ Get the functions containing the given node.
+ """
+ return self._node2func.get(node_address, [])
+
def get_closest_function(self, address):
"""
Get the function metadata for the function closest to the give address.
@@ -379,6 +402,9 @@ def _refresh_lookup(self):
self._name2func = { f.name: f.address for f in itervalues(self.functions) }
self._node_addresses = sorted(self.nodes.keys())
self._function_addresses = sorted(self.functions.keys())
+ for function_metadata in itervalues(self.functions):
+ for node_address in function_metadata.nodes:
+ self._node2func[node_address].append(function_metadata)
def go_synchronous(self):
"""
@@ -416,6 +442,7 @@ def _clear_cache(self):
self.nodes = {}
self.functions = {}
self.instructions = []
+ self._node2func = collections.defaultdict(list)
self._refresh_lookup()
self.cached = False
# TODO
@@ -811,7 +838,6 @@ def _ida_refresh_nodes(self):
# this function metadata (its parent)
#
- node_metadata.function = function_metadata
function_metadata.nodes[node.start_ea] = node_metadata
# compute all of the edges between nodes in the current function
@@ -848,7 +874,6 @@ def _binja_refresh_nodes(self, disassembler_ctx):
# this function metadata (its parent)
#
- node_metadata.function = function_metadata
function_metadata.nodes[node.start] = node_metadata
#
@@ -970,9 +995,6 @@ def __init__(self, start_ea, end_ea, node_id=None, disassembler_ctx=None):
# flowchart node_id
self.id = node_id
- # parent function_metadata
- self.function = None
-
# instruction addresses
self.instructions = {}
@@ -1059,7 +1081,6 @@ def __str__(self):
output += " Size: %u\n" % self.size
output += " Instruction Count: %u\n" % self.instruction_count
output += " Id: %u\n" % self.id
- output += " Function: %s\n" % self.function
output += " Instructions: %s" % self.instructions
return output
@@ -1081,7 +1102,6 @@ def __eq__(self, other):
result &= self.size == other.size
result &= self.address == other.address
result &= self.instruction_count == other.instruction_count
- result &= self.function == other.function
result &= self.id == other.id
return result
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index cfbd500e..71c2a829 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -235,12 +235,15 @@ def paint_nodes(self, nodes_coverage):
if node_coverage.instructions_executed != node_metadata.instruction_count:
continue
+ # get the function address for this node (there should only be one...)
+ function_address = db_metadata.get_functions_by_node(node_coverage.address)[0]
+
# assign the background color we would like to paint to this node
node_info.bg_color = self.palette.coverage_paint
# do the *actual* painting of a single node instance
idaapi.set_node_info(
- node_metadata.function.address,
+ function_address,
node_metadata.id,
node_info,
idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
@@ -252,6 +255,7 @@ def clear_nodes(self, nodes_metadata):
"""
Clear paint from the given graph nodes.
"""
+ db_metadata = self._director.metadata
# create a node info object as our vehicle for resetting the node color
node_info = idaapi.node_info_t()
@@ -264,9 +268,12 @@ def clear_nodes(self, nodes_metadata):
for node_metadata in nodes_metadata:
+ # get the function address for this node (there should only be one...)
+ function_address = db_metadata.get_functions_by_node(node_metadata.address)[0]
+
# do the *actual* painting of a single node instance
idaapi.set_node_info(
- node_metadata.function.address,
+ function_address,
node_metadata.id,
node_info,
idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
From 1a13b233456ecbee97ca697de52b1e1cf136743a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 08:11:52 -0400
Subject: [PATCH 107/154] fixes bug where one could not 'jump' to a renamed
function
---
plugin/lighthouse/metadata.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index b22cdde4..bcec8d72 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -684,8 +684,11 @@ def _name_changed(self, address, new_name, local_name=None):
logger.debug(" Old name: %s" % function.name.encode("utf-8"))
logger.debug(" New name: %s" % new_name.encode("utf-8"))
- # rename the function, and notify metadata listeners
+ # update the function name in the cached lookup & rename it for real
+ self._name2func[new_name] = self._name2func.pop(function.name)
function.name = new_name
+
+ # notify metadata listeners of the rename event
self._notify_function_renamed()
# necessary for IDP/IDB_Hooks
From 1f7e525aeb463dd5d258587f0c0f72ef0e1c4085 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 08:12:49 -0400
Subject: [PATCH 108/154] made 'jump' case insensitive for 'sub_...' funcs in
binja
---
plugin/lighthouse/composer/shell.py | 19 ++++++++++++++++---
1 file changed, 16 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 3d911f2e..6fe13773 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -488,16 +488,29 @@ def _compute_jump(self, text):
# special case to make 'sub_*' prefixed user inputs case insensitive
if text.lower().startswith("sub_"):
- text = "sub_" + text[4:].upper()
- # look up the text function name within the director's metadata
+ # attempt uppercase hex (IDA...)
+ function_metadata = self._director.metadata.get_function_by_name("sub_" + text[4:].upper())
+ if function_metadata:
+ return function_metadata.address
+
+ # attempt lowercase hex (Binja...)
+ function_metadata = self._director.metadata.get_function_by_name("sub_" + text[4:].lower())
+ if function_metadata:
+ return function_metadata.address
+
+ #
+ # no luck yet, let's just throw the user's raw text at the lookup. this
+ # would probably be a function they renamed, such as 'foobar'
+ #
+
function_metadata = self._director.metadata.get_function_by_name(text)
if function_metadata:
return function_metadata.address
#
# the user string did not translate to a function name that could
- # be found in the director.
+ # be found in the director. so I guess they're not trying to jump...
#
# failure, the user input (text) isn't a jump ...
From 00f82a2181097ff017592b3df993e2bee5f5047d Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 16:54:20 -0400
Subject: [PATCH 109/154] only paint instructions in partially executed binja
nodes
---
plugin/lighthouse/painting/binja_painter.py | 26 ++++++++++++++++++---
1 file changed, 23 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index 76766011..b66a3b38 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -29,25 +29,45 @@ def __init__(self, lctx, director, palette):
#
# NOTE:
# due to the manner in which Binary Ninja implements basic block
- # (node) highlighting, I am not sure it is worth it to paint individual
- # instructions. for now we, will simply make the instruction
- # painting functions no-op's
+ # (node) highlighting, there is almost no need to paint individual
+ # instructions. for now we, will simply make the main instruction
+ # painting function a no-op's
#
def _paint_instructions(self, instructions):
self._action_complete.set()
def _clear_instructions(self, instructions):
+ bv = disassembler[self.lctx].bv
+ for address in instructions:
+ for func in bv.get_functions_containing(address):
+ func.set_auto_instr_highlight(address, HighlightStandardColor.NoHighlightColor)
+ self._painted_instructions -= set(instructions)
self._action_complete.set()
+ def _partial_paint(self, bv, instructions, color):
+ for address in instructions:
+ for func in bv.get_functions_containing(address):
+ func.set_auto_instr_highlight(address, color)
+ self._painted_instructions |= set(instructions)
+
def _paint_nodes(self, nodes_coverage):
bv = disassembler[self.lctx].bv
+
r, g, b, _ = self.palette.coverage_paint.getRgb()
color = HighlightColor(red=r, green=g, blue=b)
+
for node_coverage in nodes_coverage:
node_metadata = node_coverage.database._metadata.nodes[node_coverage.address]
+
+ # special case for nodes that are only partially executed...
+ if node_coverage.instructions_executed != node_metadata.instruction_count:
+ self._partial_paint(bv, node_coverage.executed_instructions.keys(), color)
+ continue
+
for node in bv.get_basic_blocks_starting_at(node_metadata.address):
node.highlight = color
+
self._painted_nodes.add(node_metadata.address)
self._action_complete.set()
From aa4936e269ac7f022ae57db9c71b806f9127c65b Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 17:42:40 -0400
Subject: [PATCH 110/154] enable priority painting for binja
---
plugin/lighthouse/painting/binja_painter.py | 3 +--
plugin/lighthouse/util/disassembler/api.py | 7 +++++++
.../lighthouse/util/disassembler/binja_api.py | 19 +++++++++++++++++--
3 files changed, 25 insertions(+), 4 deletions(-)
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index b66a3b38..46173535 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -92,10 +92,9 @@ def _cancel_action(self, job):
def _priority_paint(self):
disassembler_ctx = disassembler[self.lctx]
db_metadata = self.director.metadata
- return True # TODO
current_address = disassembler_ctx.get_current_address()
- current_function = disassembler.bv.get_function_at(current_address)
+ current_function = disassembler_ctx.bv.get_function_at(current_address)
function_metadata = db_metadata.get_closest_function(current_address)
if current_function and function_metadata:
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index cd75d976..f7e13dc8 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -205,6 +205,13 @@ def __init__(self, dctx):
# API Shims
#--------------------------------------------------------------------------
+ @abc.abstractmethod
+ def get_current_address(self):
+ """
+ Return the current cursor address in the open database.
+ """
+ pass
+
@abc.abstractmethod
def get_database_directory(self):
"""
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index d3c55e33..66a831ad 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -221,8 +221,23 @@ def busy(self):
#--------------------------------------------------------------------------
def get_current_address(self):
- raise NotImplementedError("TODO!")
- return 0
+
+ # TODO/V35: this doen't work because of the loss of context bug...
+ #ctx = UIContext.activeContext()
+ #ah = ctx.contentActionHandler()
+ #ac = ah.actionContext()
+ #return ac.address
+
+ dh = DockHandler.getActiveDockHandler()
+ if not dh:
+ return 0
+ vf = dh.getViewFrame()
+ if not vf:
+ return 0
+ ac = vf.actionContext()
+ if not ac:
+ return 0
+ return ac.address
@BinjaCoreAPI.execute_read
def get_database_directory(self):
From 65fd677758961ec05d9091f06ee942a453d125a3 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 21:57:52 -0400
Subject: [PATCH 111/154] prioritize navigating to a function start, if a
function start block is shared
---
plugin/lighthouse/ui/coverage_table.py | 3 ++-
plugin/lighthouse/util/disassembler/api.py | 9 ++++++++-
.../lighthouse/util/disassembler/binja_api.py | 18 ++++++++++++++++++
3 files changed, 28 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 062568ba..245c4810 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -537,7 +537,8 @@ def navigate_to_function(self, row):
Navigate to the function depicted by the given row.
"""
lctx = self._model._director.metadata.lctx # TODO dirty
- disassembler[lctx].navigate(self._model.row2func[row])
+ function_address = self._model.row2func[row]
+ disassembler[lctx].navigate_to_function(function_address, function_address)
def toggle_column_alignment(self, column):
"""
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index f7e13dc8..c00669b6 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -261,12 +261,19 @@ def get_root_filename(self):
pass
@abc.abstractmethod
- def navigate(self, address):
+ def navigate(self, address, function_address=None):
"""
Jump the disassembler UI to the given address.
"""
pass
+ @abc.abstractmethod
+ def navigate_to_function(self, function_address, address):
+ """
+ Jump the disassembler UI to the given address, within a function.
+ """
+ pass
+
@abc.abstractmethod
def set_function_name_at(self, function_address, new_name):
"""
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 66a831ad..37ef7a30 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -271,6 +271,24 @@ def get_root_filename(self):
def navigate(self, address):
return self.bv.navigate(self.bv.view, address)
+ def navigate_to_function(self, function_address, address):
+
+ #
+ # attempt a more 'precise' jump, that guarantees to place us within
+ # the given function. this is necessary when trying to jump to an
+ # an address/node that is shared between two functions
+ #
+
+ func = self.bv.get_function_at(address)
+ if not func:
+ return False
+
+ dh = DockHandler.getActiveDockHandler()
+ vf = dh.getViewFrame()
+ vi = vf.getCurrentViewInterface()
+
+ 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)
From 36a37935dc87f3ea1fe3686cdf7210103f95f63a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 10 Apr 2020 23:25:55 -0400
Subject: [PATCH 112/154] reduce debug prints in binja, manage widget view
state per tab
---
.../integration/binja_integration.py | 3 --
plugin/lighthouse/ui/coverage_overview.py | 3 +-
.../lighthouse/util/disassembler/binja_api.py | 52 +++++++++----------
3 files changed, 27 insertions(+), 31 deletions(-)
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
index e1c8f9c9..902cbf4e 100644
--- a/plugin/lighthouse/integration/binja_integration.py
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -30,10 +30,7 @@ def get_context(self, dctx):
# create a new LighthouseContext if this is a new disassembler ctx / bv
if dctx_id not in self.lighthouse_contexts:
- print("Creating new Lctx!", dctx)
self.lighthouse_contexts[dctx_id] = LighthouseContext(self, dctx)
- else:
- print("Using ctx...", dctx)
# return the lighthouse context object for this disassembler ctx / bv
return self.lighthouse_contexts[dctx_id]
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 51dd9732..04c1cda4 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -22,9 +22,10 @@ class CoverageOverview(DockableChild):
"""
def __init__(self, core, parent, name, dctx=None):
- super(CoverageOverview, self).__init__(parent, name, dctx)
+ super(CoverageOverview, self).__init__(parent, name)
# plugin_resource(os.path.join("icons", "overview.png"))
self._core = core
+ self.dctx = dctx
self.lctx = self._core.get_context(self.dctx)
self.lctx.coverage_overview = self
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 37ef7a30..ff0e526e 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -4,6 +4,8 @@
import logging
import functools
import threading
+import collections
+
import binaryninja
from binaryninja import PythonScriptingInstance, binaryview
@@ -347,18 +349,18 @@ class DockableChild(QtWidgets.QWidget, DockContextHandler):
A dockable Qt widget for Binary Ninja.
"""
- def __init__(self, parent, name, dctx=None):
+ def __init__(self, parent, name):
QtWidgets.QWidget.__init__(self, parent)
DockContextHandler.__init__(self, self, name)
self.name = name
- self.dctx = dctx
self.actionHandler = UIActionHandler()
self.actionHandler.setupActionHandler(self)
- self.visible = False
+ self._active_view = None
+ self._visible_for_view = collections.defaultdict(lambda: False)
#self._widget.setSizePolicy(
# QtWidgets.QSizePolicy.Expanding,
@@ -376,36 +378,32 @@ def __init__(self, parent, name, dctx=None):
# QtWidgets.QSizePolicy.Expanding
#)
- def notifyOffsetChanged(self, offset):
- #print("Offset changed..")
- #self.offset.setText(hex(offset))
- pass
+ @property
+ def visible(self):
+ return self._visible_for_view[self._active_view]
+
+ @visible.setter
+ def visible(self, is_visible):
+ self._visible_for_view[self._active_view] = is_visible
def shouldBeVisible(self, view_frame):
- print("Should be visible called...")
- if view_frame is None:
- print(" - No, there's no BV")
+ if not view_frame:
return False
- print("%r" % self.visible)
- return self.visible
+
+ import shiboken2 as shiboken
+ vf_ptr = shiboken.getCppPointer(view_frame)[0]
+ return self._visible_for_view[vf_ptr]
def notifyVisibilityChanged(self, is_visible):
- print("Vis changed...")
self.visible = is_visible
def notifyViewChanged(self, view_frame):
- print("Notify view changed", view_frame)
- return
- if view_frame is None:
- self.datatype.setText("None")
- self.data = None
- else:
- self.datatype.setText(view_frame.getCurrentView())
- view = view_frame.getCurrentViewInterface()
- self.data = view.getData()
-
- def contextMenuEvent(self, event):
- print("CTX Menu event")
- return
- #self.m_contextMenuManager.show(self.m_menu, self.actionHandler)
+ if not view_frame:
+ self._active_view = None
+ return
+ 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)
From e5b29f97b712e09134797ae349ce62103cb7045c Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 11 Apr 2020 00:00:17 -0400
Subject: [PATCH 113/154] misc cleanup of binja refactor branch & TODO's
---
dev_scripts/reload_BINJA_IDA.bat | 2 +-
dev_scripts/reload_BINJA_None.bat | 18 +++++++
plugin/lighthouse/context.py | 6 +--
plugin/lighthouse/coverage.py | 2 +-
plugin/lighthouse/director.py | 10 ++--
.../integration/binja_integration.py | 53 ++++---------------
plugin/lighthouse/integration/binja_loader.py | 2 -
plugin/lighthouse/integration/core.py | 2 +-
plugin/lighthouse/metadata.py | 1 -
plugin/lighthouse/reader/coverage_reader.py | 2 +-
plugin/lighthouse/ui/coverage_table.py | 16 ------
plugin/lighthouse/util/disassembler/api.py | 3 --
.../lighthouse/util/disassembler/binja_api.py | 14 +----
13 files changed, 40 insertions(+), 91 deletions(-)
create mode 100644 dev_scripts/reload_BINJA_None.bat
diff --git a/dev_scripts/reload_BINJA_IDA.bat b/dev_scripts/reload_BINJA_IDA.bat
index 3fdb8637..e2745d31 100644
--- a/dev_scripts/reload_BINJA_IDA.bat
+++ b/dev_scripts/reload_BINJA_IDA.bat
@@ -14,5 +14,5 @@ xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\"
del /F /Q "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\.#lighthouse_plugin.py"
REM - Launch a new IDA session
-start "" "C:\tools\disassemblers\BinaryNinja\binaryninja.exe" "..\..\testcase\idaq.bndb"
+start "" "C:\tools\disassemblers\BinaryNinja\binaryninja.exe" "..\..\testcase\ida74\ida64.bndb"
diff --git a/dev_scripts/reload_BINJA_None.bat b/dev_scripts/reload_BINJA_None.bat
new file mode 100644
index 00000000..430492e1
--- /dev/null
+++ b/dev_scripts/reload_BINJA_None.bat
@@ -0,0 +1,18 @@
+set LIGHTHOUSE_LOGGING=1
+REM - Close any running instances of IDA
+call close_BINJA.bat
+
+REM - Purge old lighthouse log files
+del /F /Q "C:\Users\user\AppData\Roaming\Binary Ninja\lighthouse_logs\*"
+
+REM - Delete the old plugin bits
+del /F /Q "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\*lighthouse_plugin.py"
+rmdir "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\lighthouse" /s /q
+
+REM - Copy over the new plugin bits
+xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\"
+del /F /Q "C:\Users\user\AppData\Roaming\Binary Ninja\plugins\.#lighthouse_plugin.py"
+
+REM - Launch a new IDA session
+start "" "C:\tools\disassemblers\BinaryNinja\binaryninja.exe"
+
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index 1752a0e5..5d117bb7 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -17,7 +17,7 @@
class LighthouseContext(object):
"""
- TODO
+ TODO/COMMENT
"""
def __init__(self, core, dctx):
@@ -40,7 +40,7 @@ def __init__(self, core, dctx):
# the directory to start the coverage file dialog in
self._last_directory = None
- # TODO: re-enable
+ # TODO/HEADLESS: re-enable
# expose the live CoverageDirector object instance for external scripts
#lighthouse.coverage_director = self.director
@@ -49,7 +49,7 @@ def terminate(self):
Spin down any session subsystems before the session is deleted.
"""
- # TODO
+ # TODO/HEADLESS: re-enable
# remove access to the exposed CoverageDirector
#lighthouse.coverage_director = None
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index e68dd511..f5c0e229 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -557,7 +557,7 @@ def _map_nodes(self):
## address range, but doesn't line up with the known
## instructions, log it as 'misaligned' / suspicious
##
- ## TODO / XXX: This will need to be moved as instruction to
+ ## TODO/COV: This will need to be moved as instruction to
## node mapping is now guaranteed
##
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 1e242494..28c54a46 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -74,7 +74,7 @@ def __init__(self, metadata, palette):
# a map of loaded or composed database coverages
self._database_coverage = collections.OrderedDict()
- # TODO
+ # TODO/COMMENT
self.owners = collections.defaultdict(set)
#
@@ -538,7 +538,7 @@ def _extract_coverage_data(self, coverage_file):
try:
coverage_blocks = coverage_file.get_offset_blocks(module_name)
- coverage_addresses = [imagebase+offset for s, n in coverage_blocks for offset in xrange(s, s+n)]
+ coverage_addresses = [imagebase+offset for bb_start, bb_len in coverage_blocks for offset in xrange(bb_start, bb_start+bb_len)]
return coverage_addresses
except NotImplementedError:
pass
@@ -549,7 +549,7 @@ def _extract_coverage_data(self, coverage_file):
try:
coverage_offsets = coverage_file.get_offsets(module_name)
- coverage_addresses = [imagebase+x for x in coverage_offsets]
+ coverage_addresses = [imagebase+offset for offset in coverage_offsets]
return coverage_addresses
except NotImplementedError:
pass
@@ -592,8 +592,8 @@ def _optimize_coverage_data(self, coverage_addresses):
misaligned.append(address)
#
- # TODO: what if there are no defined instructions?
- # TODO: display undefined/misaligned data somehow
+ # TODO/LOADING: what if there are no defined instructions?
+ # TODO/LOADING: display undefined/misaligned data somehow
#
if not instructions:
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
index 902cbf4e..867a011e 100644
--- a/plugin/lighthouse/integration/binja_integration.py
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -40,8 +40,8 @@ def get_context(self, dctx):
#--------------------------------------------------------------------------
#
- # NOTE / HACK / XXX: Some of Binja's UI elements (such as the terminal) do
- # not get assigned a BV, even if there is only one open.
+ # TODO / HACK / XXX / V35: 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
# then tries to execute our UIActions (like 'Load Coverage File'), the
@@ -50,8 +50,6 @@ def get_context(self, dctx):
# 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.
#
- # TODO/V35: There is no good way to enable/disable UIActions on the fly...
- #
def _interactive_load_file(self, context):
dctx = disassembler.binja_get_bv_from_dock()
@@ -93,6 +91,7 @@ def _install_load_batch(self):
Menu.mainMenu("Tools").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,
@@ -101,50 +100,16 @@ def _install_open_coverage_xref(self):
lambda bv, addr: bool(self.get_context(bv).director.aggregate.instruction_percent)
)
- # TODO: enable as a UI action once we can disable/disable them on the fly
- #action = self.ACTION_COVERAGE_XREF
- #UIAction.registerAction(action)
- #UIActionHandler.globalActions().bindAction(action, UIAction(self._open_coverage_xref))
- #Menu.mainMenu("Tools").addAction(action, "Coverage Xref")
- #logger.info("Installed the 'Coverage Xref' menu entry")
-
+ # 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("Lighthouse", action, "Coverage Overview")
- logger.info("Installed the 'Coverage Overview' menu entry")
-
- #
- # TODO/V35: No good signals to unload (core) UI entries on
- #
+ pass
+ # NOTE/V35: Binja doesn't really 'unload' plugins, so whatever...
def _uninstall_load_file(self):
- action = self.ACTTION_LOAD_FILE
- UIActionHandler.globalActions().unbindAction(action)
- Menu.mainMenu("Tools").removeAction(action)
- UIAction.unregisterAction(action)
- logger.info("Uninstalled the 'Code coverage file' menu entry")
-
+ pass
def _uninstall_load_batch(self):
- action = self.ACTTION_LOAD_BATCH
- UIActionHandler.globalActions().unbindAction(action)
- Menu.mainMenu("Tools").removeAction(action)
- UIAction.unregisterAction(action)
- logger.info("Uninstalled the 'Code coverage batch' menu entry")
-
+ pass
def _uninstall_open_coverage_xref(self):
pass
- #action = self.ACTTION_COVERAGE_XREF
- #UIActionHandler.globalActions().unbindAction(action)
- ##Menu.mainMenu("Tools").removeAction(action)
- #UIAction.unregisterAction(action)
- #logger.info("Uninstalled the 'Coverage Xref' menu entry")
-
def _uninstall_open_coverage_overview(self):
- #action = self.ACTTION_COVERAGE_OVERVIEW
- #UIActionHandler.globalActions().unbindAction(action)
- #Menu.mainMenu("Tools").removeAction(action)
- #UIAction.unregisterAction(action)
- logger.info("Uninstalled the 'Coverage Overview' menu entry")
-
+ pass
diff --git a/plugin/lighthouse/integration/binja_loader.py b/plugin/lighthouse/integration/binja_loader.py
index 3c02aca4..3491c17c 100644
--- a/plugin/lighthouse/integration/binja_loader.py
+++ b/plugin/lighthouse/integration/binja_loader.py
@@ -23,8 +23,6 @@
# when Binary Ninja is starting up. As such, this is our only opportunity
# to load & integrate Lighthouse.
#
-# TODO/V35: it would be nice load/unload plugins with BNDB's like IDA
-#
try:
lighthouse = LighthouseBinja()
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 86bc6770..71ede74a 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -398,7 +398,7 @@ def interactive_load_file(self, dctx):
# Scheduled
#--------------------------------------------------------------------------
- # TODO
+ # TODO/REBASING
@disassembler.execute_read
def scheduled(self):
metadata = self.director.metadata
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index bcec8d72..88295574 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -445,7 +445,6 @@ def _clear_cache(self):
self._node2func = collections.defaultdict(list)
self._refresh_lookup()
self.cached = False
- # TODO
def _refresh(self, progress_callback=None, is_async=False):
"""
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 8bce11d4..2c95d287 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -14,7 +14,7 @@
class CoverageReader(object):
"""
- TODO
+ TODO/COMMENT
"""
def __init__(self):
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 245c4810..afa97634 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -556,22 +556,6 @@ def toggle_column_alignment(self, column):
# send the new alignment to the model
self._model.set_column_alignment(column, new_alignment)
- def refresh_metadata(self):
- """
- Hard refresh of the director and table metadata layers.
-
- TODO: remove
- """
- disassembler.show_wait_box("Building database metadata...")
- self._model._director.refresh()
-
- # ensure the table's model gets refreshed
- disassembler.replace_wait_box("Refreshing Coverage Overview...")
- self._model.refresh()
-
- # all done
- disassembler.hide_wait_box()
-
def export_to_html(self):
"""
Export the coverage table to an HTML report.
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index c00669b6..a7684547 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -187,9 +187,6 @@ def replace_wait_box(self, text):
#------------------------------------------------------------------------------
# Disassembler Contextual API
#------------------------------------------------------------------------------
-#
-# TODO
-#
class DisassemblerContextAPI(object):
"""
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index ff0e526e..9ff4ebf9 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -195,7 +195,7 @@ def binja_get_bv_from_dock(self):
class BinjaContextAPI(DisassemblerContextAPI):
"""
- TODO
+ TODO/COMMENT
"""
def __init__(self, dctx):
@@ -206,18 +206,6 @@ def __init__(self, dctx):
def busy(self):
return self.bv.analysis_info.state != binaryninja.enums.AnalysisState.IdleState
- #
- # NOTE/TODO/V35:
- #
- # The use of @not_mainthread or @execute_read on some of these API's
- # is to ensure the function is called from a background thread/task.
- # This is because calling database functions from the mainthread can
- # cause deadlocks (not threadsafe?) in Binary Ninja...
- #
- # this is pretty annoying because it conflicts directly with IDA
- # which *needs* database accesses to be made from the mainthread
- #
-
#--------------------------------------------------------------------------
# API Shims
#--------------------------------------------------------------------------
From c4cf78c1ddf426d52426b4c9a99216847eb8361e Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 12 Apr 2020 04:19:04 -0400
Subject: [PATCH 114/154] reconcile the binja refactor with IDA
---
dev_scripts/reload_IDA_74.bat | 18 ++
dev_scripts/reload_IDA_74_ida.bat | 19 ++
plugin/lighthouse/director.py | 2 +-
.../integration/binja_integration.py | 4 +-
plugin/lighthouse/integration/core.py | 35 ++-
.../lighthouse/integration/ida_integration.py | 19 +-
plugin/lighthouse/integration/ida_loader.py | 2 +-
plugin/lighthouse/metadata.py | 63 ++----
plugin/lighthouse/painting/ida_painter.py | 28 +--
plugin/lighthouse/painting/painter.py | 6 +
plugin/lighthouse/ui/coverage_overview.py | 63 ++----
.../lighthouse/util/disassembler/__init__.py | 7 +-
plugin/lighthouse/util/disassembler/api.py | 210 +++---------------
.../lighthouse/util/disassembler/binja_api.py | 44 ++--
.../lighthouse/util/disassembler/ida_api.py | 204 +++++++++++------
plugin/lighthouse_plugin.py | 4 +-
16 files changed, 331 insertions(+), 397 deletions(-)
create mode 100644 dev_scripts/reload_IDA_74.bat
create mode 100644 dev_scripts/reload_IDA_74_ida.bat
diff --git a/dev_scripts/reload_IDA_74.bat b/dev_scripts/reload_IDA_74.bat
new file mode 100644
index 00000000..d6ad8dcc
--- /dev/null
+++ b/dev_scripts/reload_IDA_74.bat
@@ -0,0 +1,18 @@
+set LIGHTHOUSE_LOGGING=1
+REM - Close any running instances of IDA
+call close_IDA.bat
+
+REM - Purge old lighthouse log files
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
+
+REM - Delete the old plugin bits
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py"
+rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q
+
+REM - Copy over the new plugin bits
+xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\"
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py"
+
+REM - Launch a new IDA session
+start "" "C:\tools\disassemblers\IDA 7.4\ida64.exe" "..\..\testcase\boombox74.i64"
+
diff --git a/dev_scripts/reload_IDA_74_ida.bat b/dev_scripts/reload_IDA_74_ida.bat
new file mode 100644
index 00000000..6ab3c013
--- /dev/null
+++ b/dev_scripts/reload_IDA_74_ida.bat
@@ -0,0 +1,19 @@
+set LIGHTHOUSE_LOGGING=1
+REM - Close any running instances of IDA
+call close_IDA.bat
+
+REM - Purge old lighthouse log files
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
+
+REM - Delete the old plugin bits
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py"
+rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q
+
+REM - Copy over the new plugin bits
+xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\"
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py"
+
+REM - Launch a new IDA session
+start "" "C:\tools\disassemblers\IDA 7.4\ida64.exe" "..\..\testcase\ida74\ida64.exe.i64"
+REM start "" "C:\tools\disassemblers\IDA 7.4\ida64.exe" "C:\Users\user\Desktop\JavaScriptCore_13.4.i64"
+
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 28c54a46..903ef838 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -7,7 +7,7 @@
import collections
from lighthouse.util.misc import *
-from lighthouse.util.debug import catch_errors
+from lighthouse.util.debug import *
from lighthouse.util.python import *
from lighthouse.util.qt import await_future, await_lock, flush_qt_events
from lighthouse.util.disassembler import disassembler
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
index 867a011e..fe75abae 100644
--- a/plugin/lighthouse/integration/binja_integration.py
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -65,8 +65,8 @@ def _interactive_load_batch(self, context):
return
super(LighthouseBinja, self).interactive_load_batch(dctx)
- def _open_coverage_xref(self, bv, addr):
- super(LighthouseBinja, self).open_coverage_xref(bv, addr)
+ def _open_coverage_xref(self, dctx, addr):
+ super(LighthouseBinja, self).open_coverage_xref(addr, dctx)
#--------------------------------------------------------------------------
# Binja Actions
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 71ede74a..878b6bff 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -41,15 +41,17 @@ def load(self):
self.palette = LighthousePalette()
self.palette.theme_changed(self.refresh_theme)
- def create_overview_instance(name, parent, data = None):
+ def create_coverage_overview(name, parent, dctx):
print("Creating CoverageOverview instance ...") # TODO remove try/catch
try:
- return CoverageOverview(self, parent, name, data)
+ widget = disassembler.create_dockable_widget(parent, name)
+ overview = CoverageOverview(self, dctx, widget)
+ return widget
except Exception as e:
logger.exception("Wid failed")
# the coverage overview widget
- disassembler.create_dockable_widget("Coverage Overview", create_overview_instance)
+ disassembler.register_dockable("Coverage Overview", create_coverage_overview)
# install disassembler UI
self._install_ui()
@@ -65,7 +67,7 @@ def unload(self):
self._uninstall_ui()
# spin donw any active contexts (stop threads, cleanup qt state, etc)
- for lctx in self.lighthouse_contexts:
+ for lctx in self.lighthouse_contexts.values():
lctx.terminate()
logger.info("-"*75)
@@ -76,13 +78,7 @@ def get_context(self, dctx):
"""
Get the LighthouseContext object for a given disassembler context.
"""
-
- # create a new LighthouseContext if this is a new disassembler ctx / bv
- if id(dctx) not in self.lighthouse_contexts:
- self.lighthouse_contexts[id(dctx)] = LighthouseContext(self, dctx)
-
- # return the lighthouse context object for this disassembler ctx / bv
- return self.lighthouse_contexts[id(dctx)]
+ pass
def print_banner(self):
"""
@@ -191,21 +187,22 @@ def refresh_theme(self):
lctx.coverage_overview.refresh_theme()
lctx.painter.repaint()
- def open_coverage_overview(self, dctx):
+ def open_coverage_overview(self, dctx=None):
"""
Open the dockable 'Coverage Overview' dialog.
"""
self.palette.warmup()
lctx = self.get_context(dctx)
- # the coverage overview is already open & visible, simply refresh it
- if lctx.coverage_overview.visible:
- lctx.coverage_overview.refresh()
+ # the coverage overview is already open & visible, nothing to do
+ if lctx.coverage_overview and lctx.coverage_overview.visible:
+ print(lctx.coverage_overview.widget.sizeHint())
return
- disassembler.show_dockable_widget(lctx.coverage_overview.m_name)
+ # show the coverage overview
+ disassembler.show_dockable("Coverage Overview")
- def open_coverage_xref(self, dctx, address):
+ def open_coverage_xref(self, address, dctx=None):
"""
Open the 'Coverage Xref' dialog for a given address.
"""
@@ -238,7 +235,7 @@ def open_coverage_xref(self, dctx, address):
lctx.director.select_coverage(created_coverage[0].name)
disassembler.hide_wait_box()
- def interactive_load_batch(self, ctx):
+ def interactive_load_batch(self, dctx=None):
"""
Perform the user-interactive loading of a coverage batch.
"""
@@ -323,7 +320,7 @@ def interactive_load_batch(self, ctx):
# finally, emit any notable issues that occurred during load
warn_errors(errors)
- def interactive_load_file(self, dctx):
+ def interactive_load_file(self, dctx=None):
"""
Perform the user-interactive loading of individual coverage files.
"""
diff --git a/plugin/lighthouse/integration/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
index f5587ae7..125b69d4 100644
--- a/plugin/lighthouse/integration/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -2,8 +2,10 @@
import logging
import idaapi
-from lighthouse.core import Lighthouse
+
+from lighthouse.context import LighthouseContext
from lighthouse.util.misc import plugin_resource
+from lighthouse.integration.core import LighthouseCore
logger = logging.getLogger("Lighthouse.IDA.Integration")
@@ -11,7 +13,7 @@
# Lighthouse IDA Integration
#------------------------------------------------------------------------------
-class LighthouseIDA(Lighthouse):
+class LighthouseIDA(LighthouseCore):
"""
Lighthouse UI Integration for IDA Pro.
"""
@@ -30,6 +32,18 @@ def __init__(self):
# run initialization
super(LighthouseIDA, self).__init__()
+ def get_context(self, dctx):
+ """
+ TODO
+ """
+
+ # create a new LighthouseContext if this is a new disassembler ctx / bv
+ if dctx not in self.lighthouse_contexts:
+ self.lighthouse_contexts[dctx] = LighthouseContext(self, dctx)
+
+ # return the lighthouse context object for this disassembler ctx / bv
+ return self.lighthouse_contexts[dctx]
+
#--------------------------------------------------------------------------
# IDA Actions
#--------------------------------------------------------------------------
@@ -335,6 +349,7 @@ def finish_populating_widget_popup(self, widget, popup):
"""
A right click menu is about to be shown. (IDA 7.0+)
"""
+ # TODO/IDA
if self.integration.director.aggregate.instruction_percent:
self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
return 0
diff --git a/plugin/lighthouse/integration/ida_loader.py b/plugin/lighthouse/integration/ida_loader.py
index 746cfd30..2ddf669c 100644
--- a/plugin/lighthouse/integration/ida_loader.py
+++ b/plugin/lighthouse/integration/ida_loader.py
@@ -2,7 +2,7 @@
import idaapi
from lighthouse.util.log import lmsg
-from lighthouse.ida_integration import LighthouseIDA
+from lighthouse.integration.ida_integration import LighthouseIDA
logger = logging.getLogger("Lighthouse.IDA.Loader")
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 88295574..cfee63ba 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -10,6 +10,8 @@
from lighthouse.util.python import *
from lighthouse.util.disassembler import disassembler
+from lighthouse.util.debug import catch_errors
+
logger = logging.getLogger("Lighthouse.Metadata")
#------------------------------------------------------------------------------
@@ -89,8 +91,7 @@ def __init__(self, lctx=None):
# create the disassembler hooks to listen for rename events
if lctx:
self._rename_hooks = disassembler[lctx].create_rename_hooks()
- self._rename_hooks.renamed = self._name_changed
- self._rename_hooks.metadata = weakref.proxy(self)
+ self._rename_hooks.name_changed = self._name_changed
else:
self._rename_hooks = None
@@ -580,14 +581,17 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
function_addresses.clear()
CHUNK_SIZE = len(addresses_chunk)
+
# collect metadata from the database
self._async_cache_functions(addresses_chunk)
+
# report incremental progress to an optional progress_callback
if progress_callback:
completed += CHUNK_SIZE
progress_callback(completed, total)
+
# if the refresh was canceled, stop collecting metadata and bail
if self._stop_threads:
logger.debug("Async metadata collection is bailing!")
@@ -610,6 +614,7 @@ def _async_cache_functions(self, addresses_chunk):
"""
self._cache_functions(addresses_chunk)
+ @catch_errors
def _cache_functions(self, addresses_chunk):
"""
Lift and cache function metadata for the given list of function addresses.
@@ -645,39 +650,17 @@ def _cache_functions(self, addresses_chunk):
# Signal Handlers
#--------------------------------------------------------------------------
- #@mainthread # TODO update fore IDA
- def _name_changed(self, address, new_name, local_name=None):
+ def _name_changed(self, address, new_name):
"""
- Handler for rename event in IDA.
-
- TODO/FUTURE: refactor this to not be so IDA-specific
+ Handle function rename event.
"""
-
- # we should never care about local renames (eg, loc_40804b), ignore
- if local_name or new_name.startswith("loc_"):
- return 0
-
- # get the function that this address falls within
function = self.get_function(address)
-
- # if the address does not fall within a function (might happen?), ignore
- if not function:
- return 0
-
- #
- # ensure the renamed address matches the function start before
- # renaming the function in our metadata cache.
- #
- # I am not sure when this would not be the case (globals? maybe)
- # but I'd rather not find out.
- #
-
- if address != function.address:
- return 0
+ if not (function and function.address == address):
+ return
# if the name isn't actually changing (misfire?) nothing to do
if new_name == function.name:
- return 0
+ return
logger.debug("Name changing @ 0x%X" % address)
logger.debug(" Old name: %s" % function.name.encode("utf-8"))
@@ -690,9 +673,6 @@ def _name_changed(self, address, new_name, local_name=None):
# notify metadata listeners of the rename event
self._notify_function_renamed()
- # necessary for IDP/IDB_Hooks
- return 0
-
#--------------------------------------------------------------------------
# Callbacks
#--------------------------------------------------------------------------
@@ -762,8 +742,7 @@ def __init__(self, address, disassembler_ctx=None):
self.cyclomatic_complexity = 0
# collect metdata from the underlying database
- if disassembler_ctx:
- self._cache(disassembler_ctx)
+ self._cache_function(disassembler_ctx)
#--------------------------------------------------------------------------
# Properties
@@ -787,7 +766,7 @@ def empty(self):
# Metadata Population
#--------------------------------------------------------------------------
- def _cache(self, disassembler_ctx):
+ def _cache_function(self, disassembler_ctx):
"""
Collect function metadata from the underlying database.
"""
@@ -803,7 +782,7 @@ def _refresh_nodes(self, disassembler_ctx):
"""
raise RuntimeError("This function should have been monkey patched...")
- def _ida_refresh_nodes(self):
+ def _ida_refresh_nodes(self, _):
"""
Refresh function node metadata against an open IDA database.
"""
@@ -1003,13 +982,13 @@ def __init__(self, start_ea, end_ea, node_id=None, disassembler_ctx=None):
#----------------------------------------------------------------------
# collect metadata from the underlying database
- self._cache(disassembler_ctx)
+ self._cache_node(disassembler_ctx)
#--------------------------------------------------------------------------
# Metadata Population
#--------------------------------------------------------------------------
- def _cache(self, disassembler_ctx):
+ def _cache_node(self, disassembler_ctx):
"""
This will be replaced with a disassembler-specific function at runtime.
@@ -1017,7 +996,7 @@ def _cache(self, disassembler_ctx):
"""
raise RuntimeError("This function should have been monkey patched...")
- def _ida_build_metadata(self):
+ def _ida_cache_node(self, _):
"""
Collect node metadata from the underlying database.
"""
@@ -1041,7 +1020,7 @@ def _ida_build_metadata(self):
# save the number of instructions in this block
self.instruction_count = len(self.instructions)
- def _binja_cache(self, disassembler_ctx):
+ def _binja_cache_node(self, disassembler_ctx):
"""
Collect node metadata from the underlying database.
"""
@@ -1138,14 +1117,14 @@ def metadata_progress(completed, total):
import idaapi
import idautils
FunctionMetadata._refresh_nodes = FunctionMetadata._ida_refresh_nodes
- NodeMetadata._build_metadata = NodeMetadata._ida_build_metadata
+ NodeMetadata._cache_node = NodeMetadata._ida_cache_node
elif disassembler.NAME == "BINJA":
import ctypes
import binaryninja
from binaryninja import core
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
- NodeMetadata._cache = NodeMetadata._binja_cache
+ NodeMetadata._cache_node = NodeMetadata._binja_cache_node
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 71c2a829..6763951a 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -117,7 +117,7 @@ class IDAPainter(DatabasePainter):
Asynchronous IDA database painter.
"""
- def __init__(self, director, palette):
+ def __init__(self, lctx, director, palette):
#----------------------------------------------------------------------
# HexRays Hooking
@@ -135,7 +135,7 @@ def __init__(self, director, palette):
self._signal = ToMainthread()
# continue normal painter initialization
- super(IDAPainter, self).__init__(director, palette)
+ super(IDAPainter, self).__init__(lctx, director, palette)
def repaint(self):
"""
@@ -218,7 +218,7 @@ def paint_nodes(self, nodes_coverage):
"""
Paint node level coverage defined by the current database mappings.
"""
- db_metadata = self._director.metadata
+ db_metadata = self.director.metadata
# create a node info object as our vehicle for setting the node color
node_info = idaapi.node_info_t()
@@ -236,14 +236,14 @@ def paint_nodes(self, nodes_coverage):
continue
# get the function address for this node (there should only be one...)
- function_address = db_metadata.get_functions_by_node(node_coverage.address)[0]
+ function_metadata = db_metadata.get_functions_by_node(node_coverage.address)[0]
# assign the background color we would like to paint to this node
node_info.bg_color = self.palette.coverage_paint
# do the *actual* painting of a single node instance
idaapi.set_node_info(
- function_address,
+ function_metadata.address,
node_metadata.id,
node_info,
idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
@@ -255,7 +255,7 @@ def clear_nodes(self, nodes_metadata):
"""
Clear paint from the given graph nodes.
"""
- db_metadata = self._director.metadata
+ db_metadata = self.director.metadata
# create a node info object as our vehicle for resetting the node color
node_info = idaapi.node_info_t()
@@ -269,11 +269,11 @@ def clear_nodes(self, nodes_metadata):
for node_metadata in nodes_metadata:
# get the function address for this node (there should only be one...)
- function_address = db_metadata.get_functions_by_node(node_metadata.address)[0]
+ function_metadata = db_metadata.get_functions_by_node(node_metadata.address)[0]
# do the *actual* painting of a single node instance
idaapi.set_node_info(
- function_address,
+ function_metadata.address,
node_metadata.id,
node_info,
idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
@@ -393,11 +393,11 @@ def _hxe_callback(self, event, *args):
cfunc = vdui.cfunc
# if there's no coverage data for this function, there's nothing to do
- if not cfunc.entry_ea in self._director.coverage.functions:
+ if not cfunc.entry_ea in self.director.coverage.functions:
return 0
# paint the decompilation text for this function
- self.paint_hexrays(cfunc, self._director.coverage)
+ self.paint_hexrays(cfunc, self.director.coverage)
return 0
@@ -431,8 +431,8 @@ def _priority_paint_functions(self, target_address):
This will paint both the instructions & graph nodes of defined functions.
"""
- db_metadata = self._director.metadata
- db_coverage = self._director.coverage
+ db_metadata = self.director.metadata
+ db_coverage = self.director.coverage
# the number of functions before and after the cursor to paint
FUNCTION_BUFFER = 1
@@ -476,8 +476,8 @@ def _priority_paint_instructions(self, target_address):
"""
Paint instructions in the immediate vicinity of the given address.
"""
- db_metadata = self._director.metadata
- db_coverage = self._director.coverage
+ db_metadata = self.director.metadata
+ db_coverage = self.director.coverage
# the number of instruction bytes before and after the cursor to paint
INSTRUCTION_BUFFER = 200
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 09529ff4..f65aaec1 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -427,6 +427,12 @@ def _priority_paint_instructions(self, target_address, ignore=set()):
#--------------------------------------------------------------------------
def _async_database_painter(self):
+ try:
+ self._async_database_painter2()
+ except:
+ logger.exception("Painter crashed...")
+
+ def _async_database_painter2(self):
"""
Asynchronous database painting worker loop.
"""
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 04c1cda4..05aebe92 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -4,7 +4,7 @@
from lighthouse.util.qt import *
from lighthouse.util.misc import plugin_resource
-from lighthouse.util.disassembler import disassembler, DockableChild
+from lighthouse.util.disassembler import disassembler
from lighthouse.composer import ComposingShell
from lighthouse.ui.coverage_table import CoverageTableView, CoverageTableModel, CoverageTableController
from lighthouse.ui.coverage_combobox import CoverageComboBox
@@ -16,24 +16,25 @@
# Coverage Overview
#------------------------------------------------------------------------------
-class CoverageOverview(DockableChild):
+class CoverageOverview(object):
"""
The Coverage Overview Widget.
"""
- def __init__(self, core, parent, name, dctx=None):
- super(CoverageOverview, self).__init__(parent, name)
+ def __init__(self, core, dctx, widget):
+ #super(CoverageOverview, self).__init__(parent, name)
# plugin_resource(os.path.join("icons", "overview.png"))
self._core = core
self.dctx = dctx
+ self.widget = widget
self.lctx = self._core.get_context(self.dctx)
self.lctx.coverage_overview = self
self.director = self.lctx.director
# see the EventProxy class below for more details
- #self._events = EventProxy(self)
- #self._widget.installEventFilter(self._events)
+ self._events = EventProxy(self)
+ self.widget.installEventFilter(self._events)
# initialize the plugin UI
self._ui_init()
@@ -48,36 +49,17 @@ def __init__(self, core, parent, name, dctx=None):
# Pseudo Widget Functions
#--------------------------------------------------------------------------
- #def showEvent(self, e):
- # """
- # Test
- # """
- # super(CoverageOverview, self).showEvent(e)
- # print("Becoming visible says qt!!")
- # print(self._visible)
- # if not self._visible:
- # return
- # self.refresh()
- # if not self.director.metadata.cached:
- # self.director.refresh()
- # return super(CoverageOverview, self).showEvent(e)
-
- #def show(self):
- # """
- # Show the CoverageOverview UI / widget.
- # """
- # self.refresh()
- # super(CoverageOverview, self).show()
- # self._visible = True
-
- # #
- # # if no metadata had been collected prior to showing the coverage
- # # overview (eg, through loading coverage), we should do that now
- # # before the user can interact with the view...
- # #
-
- # if not self._core.director.metadata.cached:
- # self._core.director.refresh()
+ @property
+ def name(self):
+ if not self.widget:
+ return "Coverage Overview"
+ return self.widget.name
+
+ @property
+ def visible(self):
+ if not self.widget:
+ return False
+ return self.widget.visible
def terminate(self):
"""
@@ -88,6 +70,7 @@ def terminate(self):
self._table_view = None
self._table_controller = None
self._table_model = None
+ self.widget = None
#--------------------------------------------------------------------------
# Initialization - UI
@@ -110,9 +93,9 @@ def _ui_init_table(self):
"""
Initialize the coverage table.
"""
- self._table_model = CoverageTableModel(self.director, self)
+ self._table_model = CoverageTableModel(self.director, self.widget)
self._table_controller = CoverageTableController(self._table_model)
- self._table_view = CoverageTableView(self._table_controller, self._table_model, self)
+ self._table_view = CoverageTableView(self._table_controller, self._table_model, self.widget)
def _ui_init_toolbar(self):
"""
@@ -195,7 +178,7 @@ def _ui_init_settings(self):
self._settings_button.setStyleSheet("QToolButton::menu-indicator{image: none;}")
# settings menu
- self._settings_menu = TableSettingsMenu(self)
+ self._settings_menu = TableSettingsMenu(self.widget)
def _ui_init_signals(self):
"""
@@ -216,7 +199,7 @@ def _ui_layout(self):
layout.addWidget(self._toolbar)
# apply the layout to the containing form
- self.setLayout(layout)
+ self.widget.setLayout(layout)
#--------------------------------------------------------------------------
# Signal Handlers
diff --git a/plugin/lighthouse/util/disassembler/__init__.py b/plugin/lighthouse/util/disassembler/__init__.py
index 7149023f..e6f65169 100644
--- a/plugin/lighthouse/util/disassembler/__init__.py
+++ b/plugin/lighthouse/util/disassembler/__init__.py
@@ -16,8 +16,9 @@
if disassembler == None:
try:
- from .ida_api import IDAAPI, DockableWindow
- disassembler = IDAAPI()
+ from .ida_api import IDACoreAPI, IDAContextAPI
+ disassembler = IDACoreAPI()
+ DisassemblerContextAPI = IDAContextAPI
except ImportError:
pass
@@ -27,7 +28,7 @@
if disassembler == None:
try:
- from .binja_api import BinjaCoreAPI, BinjaContextAPI, DockableChild
+ from .binja_api import BinjaCoreAPI, BinjaContextAPI
disassembler = BinjaCoreAPI()
DisassemblerContextAPI = BinjaContextAPI
except ImportError:
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index a7684547..d1322562 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -82,13 +82,6 @@ def headless(self):
"""
pass
- @abc.abstractproperty
- def busy(self):
- """
- Return a bool indicating if the disassembler is busy / processing.
- """
- pass
-
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
@@ -158,6 +151,31 @@ def message(self, function_address, new_name):
"""
pass
+ #--------------------------------------------------------------------------
+ # UI APIs
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def register_dockable(self, dockable_name, create_widget_callback):
+ """
+ TODO/COMMENT
+ """
+ pass
+
+ @abc.abstractmethod
+ def create_dockable_widget(self, parent, dockable_name):
+ """
+ TODO/COMMENT
+ """
+ pass
+
+ @abc.abstractmethod
+ def show_dockable(self, dockable_name):
+ """
+ TODO/COMMENT
+ """
+ pass
+
#------------------------------------------------------------------------------
# WaitBox API
#------------------------------------------------------------------------------
@@ -198,6 +216,17 @@ class DisassemblerContextAPI(object):
def __init__(self, dctx):
self.dctx = dctx
+ #--------------------------------------------------------------------------
+ # Properties
+ #--------------------------------------------------------------------------
+
+ @abc.abstractproperty
+ def busy(self):
+ """
+ Return a bool indicating if the disassembler is busy / processing.
+ """
+ pass
+
#--------------------------------------------------------------------------
# API Shims
#--------------------------------------------------------------------------
@@ -385,170 +414,3 @@ def renamed(self, address, new_name):
This will be hooked by Lighthouse at runtime to capture rename events.
"""
pass
-
-#------------------------------------------------------------------------------
-# Dockable Window
-#------------------------------------------------------------------------------
-
-class DockableShim(object):
- """
- A minimal template of the DockableWindow.
-
- this class is only to demonstrate the minimal set of attributes and
- functions that a disassembler's DockableWindow class should contain.
-
- show/hide can be overridden entirely depending on your needs, but the
- self._widget field should contain a reference to a blank widget that has
- been installed into a QDockWidget in the disassembler interface.
- """
- __metaclass__ = abc.ABCMeta
-
- def __init__(self, window_title, icon_path):
- self._window_title = window_title
- self._window_icon = QtGui.QIcon(icon_path)
- self._widget = None
-
- def show(self):
- """
- Show the dockable widget.
- """
- self._widget.show()
-
- def hide(self):
- """
- Show the dockable widget.
- """
- self._widget.hide()
-
-
- #--------------------------------------------------------------------------
- # Function Prefix API
- #--------------------------------------------------------------------------
-
- #
- # the following APIs are used to apply or clear prefixes to multiple
- # functions in the disassembly database. the only thing you're expected
- # to do here is select an appropriate PREFIX_SEPARATOR.
- #
- # your prefix separator is expected to be something unique, that a user
- # would probably *never* put into their function name themselves but
- # looks somewhat normal.
- #
- # in IDA, putting '%' in a function name appears as '_' in the function
- # list, so we use that as a prefix separator. in Binary Ninja, we use a
- # unicode character that looks like an underscore character.
- #
- # it is probably safe to steal the unicode char we use with binja for
- # your own implementation.
- #
-
- PREFIX_SEPARATOR = NotImplemented
-
- def prefix_function(self, function_address, prefix):
- """
- Prefix a function name with the given string.
- """
- original_name = self.get_function_raw_name_at(function_address)
- new_name = str(prefix) + self.PREFIX_SEPARATOR + str(original_name)
-
- # rename the function with the newly prefixed name
- self.set_function_name_at(function_address, new_name)
-
- def prefix_functions(self, function_addresses, prefix):
- """
- Prefix a list of functions with the given string.
- """
- for function_address in function_addresses:
- self.prefix_function(function_address, prefix)
-
- def clear_prefix(self, function_address):
- """
- Clear the prefix from a given function.
- """
- prefixed_name = self.get_function_raw_name_at(function_address)
-
- #
- # split the function name on the last prefix separator, saving
- # everything that comes after (eg, the original func name)
- #
-
- new_name = prefixed_name.rsplit(self.PREFIX_SEPARATOR)[-1]
-
- # the name doesn't appear to have had a prefix, nothing to do...
- if new_name == prefixed_name:
- return
-
- # rename the function with the prefix(s) now stripped
- self.set_function_name_at(function_address, new_name)
-
- def clear_prefixes(self, function_addresses):
- """
- Clear the prefix from a list of given functions.
- """
- for function_address in function_addresses:
- self.clear_prefix(function_address)
-
-#------------------------------------------------------------------------------
-# Hooking
-#------------------------------------------------------------------------------
-
-class RenameHooks(object):
- """
- An abstract implementation of disassembler hooks to capture rename events.
- """
- __metaclass__ = abc.ABCMeta
-
- @abc.abstractmethod
- def hook(self):
- """
- Install hooks into the disassembler that capture rename events.
- """
- pass
-
- @abc.abstractmethod
- def unhook(self):
- """
- Remove hooks used to capture rename events.
- """
- pass
-
- def renamed(self, address, new_name):
- """
- This will be hooked by Lighthouse at runtime to capture rename events.
- """
- pass
-
-#------------------------------------------------------------------------------
-# Dockable Window
-#------------------------------------------------------------------------------
-
-class DockableShim(object):
- """
- A minimal template of the DockableWindow.
-
- this class is only to demonstrate the minimal set of attributes and
- functions that a disassembler's DockableWindow class should contain.
-
- show/hide can be overridden entirely depending on your needs, but the
- self._widget field should contain a reference to a blank widget that has
- been installed into a QDockWidget in the disassembler interface.
- """
- __metaclass__ = abc.ABCMeta
-
- def __init__(self, window_title, icon_path):
- self._window_title = window_title
- self._window_icon = QtGui.QIcon(icon_path)
- self._widget = None
-
- def show(self):
- """
- Show the dockable widget.
- """
- self._widget.show()
-
- def hide(self):
- """
- Show the dockable widget.
- """
- self._widget.hide()
-
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 9ff4ebf9..a375b0c1 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -109,13 +109,7 @@ def _init_version(self):
@property
def headless(self):
- ret = None
- # Compatibility for Binary Ninja Stable & Dev channels (Jan 2019)
- try:
- ret = binaryninja.core_ui_enabled()
- except TypeError:
- ret = binaryninja.core_ui_enabled
- return not ret
+ return not(binaryninja.core_ui_enabled())
#--------------------------------------------------------------------------
# Synchronization Decorators
@@ -153,7 +147,7 @@ def wrapper(*args, **kwargs):
def get_disassembler_user_directory(self):
return os.path.split(binaryninja.user_plugin_path())[0]
- def get_disassembly_background_color(self):
+ def get_disassembly_background_color(self): # TODO/BINJA - use theme apis...
palette = QtGui.QPalette()
return palette.color(QtGui.QPalette.Button)
@@ -170,11 +164,14 @@ def message(self, message):
# UI API Shims
#--------------------------------------------------------------------------
- def create_dockable_widget(self, dockable_name, create_widget_callback):
+ def register_dockable(self, dockable_name, create_widget_callback):
dock_handler = DockHandler.getActiveDockHandler()
dock_handler.addDockWidget(dockable_name, create_widget_callback, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, False)
- def show_dockable_widget(self, dockable_name):
+ def create_dockable_widget(self, parent, dockable_name):
+ return DockableWidget(parent, dockable_name)
+
+ def show_dockable(self, dockable_name):
dock_handler = DockHandler.getActiveDockHandler()
dock_handler.setVisible(dockable_name, True)
@@ -202,6 +199,10 @@ def __init__(self, dctx):
super(BinjaContextAPI, self).__init__(dctx)
self.bv = dctx
+ #--------------------------------------------------------------------------
+ # Properties
+ #--------------------------------------------------------------------------
+
@property
def busy(self):
return self.bv.analysis_info.state != binaryninja.enums.AnalysisState.IdleState
@@ -332,40 +333,21 @@ def __symbol_handler(self, view, symbol):
# UI
#------------------------------------------------------------------------------
-class DockableChild(QtWidgets.QWidget, DockContextHandler):
+class DockableWidget(QtWidgets.QWidget, DockContextHandler):
"""
A dockable Qt widget for Binary Ninja.
"""
def __init__(self, parent, name):
-
QtWidgets.QWidget.__init__(self, parent)
DockContextHandler.__init__(self, self, name)
- self.name = name
-
self.actionHandler = UIActionHandler()
self.actionHandler.setupActionHandler(self)
self._active_view = None
self._visible_for_view = collections.defaultdict(lambda: False)
- #self._widget.setSizePolicy(
- # QtWidgets.QSizePolicy.Expanding,
- # QtWidgets.QSizePolicy.Expanding
- #)
-
- ## dock the widget on the right side of Binja
- #self._dock_handler.addDockWidget(self._widget, QtCore.Qt.RightDockWidgetArea, QtCore.Qt.Horizontal, True, False)
- #self._dockable = self._dock_handler.getDockWidget(self._window_title)
-
- #self._dockable = QtWidgets.QDockWidget(window_title, self._main_window)
- #self._dockable.setWindowIcon(self._window_icon)
- #self._dockable.setSizePolicy(
- # QtWidgets.QSizePolicy.Expanding,
- # QtWidgets.QSizePolicy.Expanding
- #)
-
@property
def visible(self):
return self._visible_for_view[self._active_view]
@@ -394,4 +376,4 @@ def notifyViewChanged(self, view_frame):
self._active_view = shiboken.getCppPointer(view_frame)[0]
if self.visible:
dock_handler = DockHandler.getActiveDockHandler()
- dock_handler.setVisible(self.m_name, True)
+ dock_handler.setVisible(self.name, True)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index b1e11495..eeb43be3 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -11,7 +11,7 @@
idaapi.warning("Lighthouse has deprecated support for IDA 6, please upgrade.")
raise ImportError
-from .api import DisassemblerAPI, DockableShim
+from .api import DisassemblerCoreAPI, DisassemblerContextAPI
from ..qt import *
from ..misc import is_mainthread
@@ -52,17 +52,19 @@ def thunk():
return wrapper
#------------------------------------------------------------------------------
-# Disassembler API
+# Disassembler Core API (universal)
#------------------------------------------------------------------------------
-class IDAAPI(DisassemblerAPI):
+class IDACoreAPI(DisassemblerCoreAPI):
"""
The IDA implementation of the disassembler API abstraction.
"""
NAME = "IDA"
def __init__(self):
- super(IDAAPI, self).__init__()
+ super(IDACoreAPI, self).__init__()
+ self._dockable_factory = {}
+ self._dockable_widgets = {}
self._init_version()
def _init_version(self):
@@ -84,10 +86,6 @@ def _init_version(self):
def headless(self):
return False
- @property
- def busy(self):
- return not(idaapi.auto_is_ok())
-
#--------------------------------------------------------------------------
# Synchronization Decorators
#--------------------------------------------------------------------------
@@ -108,46 +106,9 @@ def execute_ui(function):
# API Shims
#--------------------------------------------------------------------------
- def create_rename_hooks(self):
- class RenameHooks(idaapi.IDB_Hooks):
- def renamed(self, a, b, c): # temporary, required for IDA 7.3/py3?
- return 0
- return RenameHooks()
-
- def get_database_directory(self):
- return idautils.GetIdbDir()
-
def get_disassembler_user_directory(self):
return idaapi.get_user_idadir()
- def get_function_addresses(self):
- return list(idautils.Functions())
-
- def get_function_name_at(self, address):
- return idaapi.get_short_name(address)
-
- def get_function_raw_name_at(self, function_address):
- return idaapi.get_name(function_address)
-
- def get_imagebase(self):
- return idaapi.get_imagebase()
-
- def get_root_filename(self):
- return idaapi.get_root_filename()
-
- def navigate(self, address):
- return idaapi.jumpto(address)
-
- def set_function_name_at(self, function_address, new_name):
- idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN)
-
- def message(self, message):
- print(message)
-
- #--------------------------------------------------------------------------
- # UI API Shims
- #--------------------------------------------------------------------------
-
def get_disassembly_background_color(self):
"""
Get the background color of the IDA disassembly view.
@@ -165,11 +126,61 @@ def is_msg_inited(self):
def warning(self, text):
idaapi.warning(text)
- #------------------------------------------------------------------------------
- # Function Prefix API
- #------------------------------------------------------------------------------
+ def message(self, message):
+ print(message)
- PREFIX_SEPARATOR = "%"
+ #--------------------------------------------------------------------------
+ # UI API Shims
+ #--------------------------------------------------------------------------
+
+ def register_dockable(self, dockable_name, create_widget_callback):
+ self._dockable_factory[dockable_name] = create_widget_callback
+
+ def create_dockable_widget(self, parent, dockable_name):
+ import sip
+
+ # 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.name = dockable_name
+
+ # return the dockable QtWidget / container
+ return widget
+
+ def show_dockable(self, dockable_name):
+ try:
+ make_dockable = self._dockable_factory[dockable_name]
+ except KeyError:
+ return False
+
+ # TODO/IDA remove try/cacth after cleanup
+ try:
+ parent, dctx = None, None # not used for IDA's integration
+ widget = make_dockable(dockable_name, parent, dctx)
+ except Exception:
+ logger.exception("Error showing dockable...")
+ return False
+
+ # get the original twidget, so we can use it with the IDA API's
+ #twidget = idaapi.TWidget__from_ptrval__(widget) NOTE: IDA 7.2+ only...
+ twidget = self._dockable_widgets.pop(dockable_name)
+ if not twidget:
+ self.warning("Could not open dockable window, because its reference is gone?!?")
+ return
+
+ # show the dockable widget
+ flags = idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WOPN_RESTORE | idaapi.PluginForm.WOPN_PERSIST
+ idaapi.display_widget(twidget, flags)
+
+ # attempt to 'dock' the widget in a reasonable location
+ for target in ["IDA View-A", "Pseudocode-A"]:
+ dwidget = idaapi.find_widget(target)
+ if dwidget:
+ idaapi.set_dock_pos(dockable_name, 'IDA View-A', idaapi.DP_RIGHT)
+ break
#--------------------------------------------------------------------------
# Theme Prediction Helpers (Internal)
@@ -179,6 +190,7 @@ def _get_ida_bg_color_ida7(self):
"""
Get the background color of the IDA disassembly view. (IDA 7+)
"""
+ return QtGui.QColor(0,0,0) # TODO/IDA
names = ["Enums", "Structures"]
names += ["Hex View-%u" % i for i in range(5)]
names += ["IDA View-%c" % chr(ord('A') + i) for i in range(5)]
@@ -236,34 +248,94 @@ def _touch_ida_window(self, target):
flush_qt_events()
#------------------------------------------------------------------------------
-# Dockable Window
+# Disassembler Context API (database-specific)
#------------------------------------------------------------------------------
-class DockableWindow(DockableShim):
+class IDAContextAPI(DisassemblerContextAPI):
"""
- A Dockable Qt widget for IDA 7.0 and above.
+ TODO/COMMENT
"""
- def __init__(self, window_title, icon_path):
- super(DockableWindow, self).__init__(window_title, icon_path)
+ def __init__(self, dctx):
+ super(IDAContextAPI, self).__init__(dctx)
- import sip
- self._form = idaapi.create_empty_widget(self._window_title)
- self._widget = sip.wrapinstance(int(self._form), QtWidgets.QWidget)
+ @property
+ def busy(self):
+ return not(idaapi.auto_is_ok())
+
+ #--------------------------------------------------------------------------
+ # API Shims
+ #--------------------------------------------------------------------------
+
+ def get_current_address(self):
+ return idaapi.get_screen_ea()
+
+ def get_database_directory(self):
+ return idautils.GetIdbDir()
+
+ def get_function_addresses(self):
+ return list(idautils.Functions())
+
+ def get_function_name_at(self, address):
+ return idaapi.get_short_name(address)
+
+ def get_function_raw_name_at(self, function_address):
+ return idaapi.get_name(function_address)
+
+ def get_imagebase(self):
+ return idaapi.get_imagebase()
+
+ def get_root_filename(self):
+ return idaapi.get_root_filename()
+
+ def navigate(self, address):
+ return idaapi.jumpto(address)
+
+ def navigate_to_function(self, function_address, address):
+ return self.navigate(function_address)
+
+ def set_function_name_at(self, function_address, new_name):
+ idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN)
+
+ #--------------------------------------------------------------------------
+ # Hooks API
+ #--------------------------------------------------------------------------
+
+ def create_rename_hooks(self):
+ return RenameHooks()
+
+ #------------------------------------------------------------------------------
+ # Function Prefix API
+ #------------------------------------------------------------------------------
+
+ PREFIX_SEPARATOR = "%"
+
+#------------------------------------------------------------------------------
+# Hooking
+#------------------------------------------------------------------------------
+
+class RenameHooks(idaapi.IDB_Hooks):
+
+ def renamed(self, address, new_name, local_name):
+ """
+ Capture all IDA rename events.
+ """
+
+ # we should never care about local renames (eg, loc_40804b), ignore
+ if local_name or new_name.startswith("loc_"):
+ return 0
+ # call the 'renamed' callback, that will get hooked by a listener
+ self.name_changed(address, new_name)
- # set the window icon
- self._widget.setWindowIcon(self._window_icon)
+ # must return 0 to keep IDA happy...
+ return 0
- def show(self):
+ def name_changed(self, address, new_name):
"""
- Show the dockable widget.
+ A placeholder callback, which will get hooked / replaced once live.
"""
- flags = idaapi.PluginForm.WOPN_TAB | \
- idaapi.PluginForm.WOPN_MENU | \
- idaapi.PluginForm.WOPN_RESTORE | \
- idaapi.PluginForm.WOPN_PERSIST
- idaapi.display_widget(self._form, flags)
+ pass
#------------------------------------------------------------------------------
# HexRays Util
diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py
index 9f220d72..16611590 100644
--- a/plugin/lighthouse_plugin.py
+++ b/plugin/lighthouse_plugin.py
@@ -16,8 +16,8 @@
elif disassembler.NAME == "IDA":
logger.info("Selecting IDA loader...")
- from lighthouse.ida_loader import *
- from lighthouse import coverage_director
+ from lighthouse.integration.ida_loader import *
+ #from lighthouse import coverage_director
elif disassembler.NAME == "BINJA":
logger.info("Selecting Binary Ninja loader...")
From 28b0ecd49cdc2bac1a4465627528281f3cc69e9f Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 12 Apr 2020 08:48:06 -0400
Subject: [PATCH 115/154] automatically build the metadata cache when the
coverage overview is first shown (if the cache is not already built)
---
plugin/lighthouse/director.py | 25 ++++++++++-
plugin/lighthouse/ui/coverage_overview.py | 51 +++++++++++++++++++----
2 files changed, 68 insertions(+), 8 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 903ef838..04a572f1 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1334,8 +1334,31 @@ def _refresh(self):
Internal refresh routine, wrapped to help catch bugs for now.
"""
+ #
# (re) build our metadata cache of the underlying database
- self.metadata.refresh(metadata_progress)
+ #
+
+ if not is_mainthread():
+ self.metadata.refresh(metadata_progress)
+
+ #
+ # NOTE: optionally, we call the async vesrion here so that we do not pin
+ # the mainthread for disassemblers that will primarily read from the
+ # database in a background thread (eg, Binja)
+ #
+ # for example, this refresh action may be called from a UI event or
+ # clicking 'Open Coverage Overview' (eg, the mainthread). if we pin
+ # the mainthread while doing database reads from a background thread,
+ # we cannot post UI updates such as progress updates to the user
+ #
+ # using an async refresh allows us to 'softly' spin the main (UI)
+ # thread and get UI updates while the refresh runs
+ #
+
+ else:
+ future = self.metadata.refresh_async(metadata_progress)
+ self.metadata.go_synchronous()
+ await_future(future)
# (re) map each set of loaded coverage data to the database
self._refresh_database_coverage()
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 05aebe92..84fbcd0d 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -22,8 +22,6 @@ class CoverageOverview(object):
"""
def __init__(self, core, dctx, widget):
- #super(CoverageOverview, self).__init__(parent, name)
- # plugin_resource(os.path.join("icons", "overview.png"))
self._core = core
self.dctx = dctx
self.widget = widget
@@ -31,10 +29,12 @@ def __init__(self, core, dctx, widget):
self.lctx = self._core.get_context(self.dctx)
self.lctx.coverage_overview = self
self.director = self.lctx.director
+ self.initialized = False
# see the EventProxy class below for more details
self._events = EventProxy(self)
self.widget.installEventFilter(self._events)
+ # plugin_resource(os.path.join("icons", "overview.png"))
# initialize the plugin UI
self._ui_init()
@@ -251,6 +251,15 @@ def refresh_theme(self):
class EventProxy(QtCore.QObject):
+ #
+ # NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7? Just gonna ship our own...
+ # - https://doc.qt.io/qt-5/qevent.html#Type-enum
+ #
+
+ EventShow = 17
+ EventDestroy = 16
+ EventLayoutRequest = 76
+
def __init__(self, target):
super(EventProxy, self).__init__()
self._target = weakref.proxy(target)
@@ -262,20 +271,48 @@ def eventFilter(self, source, event):
# cleanup after ourselves in the interest of stability
#
- if int(event.type()) == 16: # NOTE/COMPAT: QtCore.QEvent.Destroy not in IDA7?
+ if int(event.type()) == self.EventDestroy:
source.removeEventFilter(self)
self._target.terminate()
+ #
+ # this seems to be 'roughly' the last event triggered after the widget
+ # is done initializing in both IDA and Binja, but prior to the first
+ # user-triggered 'show' events.
+ #
+ # this is mostly to account for the fact that binja 'shows' the widget
+ # when it is initially created (outside of our control). this was
+ # causing lighthouse to automatically cache database metadata when
+ # every database was opened ...
+ #
+
+ elif int(event.type()) == self.EventLayoutRequest and not self._target.initialized:
+ self._target.initialized = True
+
+ #
+ # this is used to hook the 'show' event of the coverage overview. in
+ # particular, we use this event to ensure the metadata cache is built
+ # and available for use.
+ #
+ # this should only ever kick off a run if the user attempts to open the
+ # coverage overview before loading a coverage file. this is useful,
+ # because the overview does have some utility even without coverage...
+ #
+
+ elif int(event.type()) == self.EventShow and self._target.initialized:
+ if not self._target.director.metadata.cached:
+ self._target.director.refresh()
+
#
# this is an unknown event, but it seems to fire when the widget is
- # being saved/restored by a QMainWidget. we use this to try and ensure
- # the Coverage Overview stays docked when flipping between Reversing
- # and Debugging states in IDA.
+ # being saved/restored by a QMainWidget (in IDA). we use this to try
+ # and ensure the Coverage Overview stays docked when flipping between
+ # Reversing and Debugging states in IDA.
#
# See issue #16 on github for more information.
#
- if int(event.type()) == 2002 and disassembler.NAME == "IDA":
+ elif int(event.type()) == 2002 and disassembler.NAME == "IDA":
import idaapi
#
From 4ab65c985b4ba41de132cedbce12a9eb3e753e22 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 12 Apr 2020 09:03:43 -0400
Subject: [PATCH 116/154] cleanup, fixes right click regression with IDA
---
plugin/lighthouse/integration/core.py | 11 +++--------
.../lighthouse/integration/ida_integration.py | 17 +++++++++++++++--
plugin/lighthouse/util/disassembler/ida_api.py | 2 ++
3 files changed, 20 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 878b6bff..b853d9a7 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -42,13 +42,9 @@ def load(self):
self.palette.theme_changed(self.refresh_theme)
def create_coverage_overview(name, parent, dctx):
- print("Creating CoverageOverview instance ...") # TODO remove try/catch
- try:
- widget = disassembler.create_dockable_widget(parent, name)
- overview = CoverageOverview(self, dctx, widget)
- return widget
- except Exception as e:
- logger.exception("Wid failed")
+ widget = disassembler.create_dockable_widget(parent, name)
+ overview = CoverageOverview(self, dctx, widget)
+ return widget
# the coverage overview widget
disassembler.register_dockable("Coverage Overview", create_coverage_overview)
@@ -196,7 +192,6 @@ def open_coverage_overview(self, dctx=None):
# the coverage overview is already open & visible, nothing to do
if lctx.coverage_overview and lctx.coverage_overview.visible:
- print(lctx.coverage_overview.widget.sizeHint())
return
# show the coverage overview
diff --git a/plugin/lighthouse/integration/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
index 125b69d4..bf94a206 100644
--- a/plugin/lighthouse/integration/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -349,8 +349,21 @@ def finish_populating_widget_popup(self, widget, popup):
"""
A right click menu is about to be shown. (IDA 7.0+)
"""
- # TODO/IDA
- if self.integration.director.aggregate.instruction_percent:
+
+ #
+ # if lighthouse hasn't been used yet, there's nothing to do. we also
+ # don't want this event to trigger the creation of a lighthouse
+ # context! so we should bail early in this case...
+ #
+
+ if not self.integration.lighthouse_contexts:
+ return 0
+
+ # inject any of lighthouse's right click context menu's into IDA
+ lctx = self.integration.get_context(None)
+ if lctx.director.aggregate.instruction_percent:
self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
+
+ # must return 0 for ida...
return 0
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index eeb43be3..32e6639e 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -146,6 +146,7 @@ def create_dockable_widget(self, parent, dockable_name):
# cast the IDA 'twidget' as a Qt widget for use
widget = sip.wrapinstance(int(twidget), QtWidgets.QWidget)
widget.name = dockable_name
+ widget.visible = False
# return the dockable QtWidget / container
return widget
@@ -174,6 +175,7 @@ def show_dockable(self, dockable_name):
# show the dockable widget
flags = idaapi.PluginForm.WOPN_TAB | idaapi.PluginForm.WOPN_RESTORE | idaapi.PluginForm.WOPN_PERSIST
idaapi.display_widget(twidget, flags)
+ widget.visible = True
# attempt to 'dock' the widget in a reasonable location
for target in ["IDA View-A", "Pseudocode-A"]:
From 7fd1e46e53cec607156ea979fafeed2071b438ba Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 12 Apr 2020 10:35:53 -0400
Subject: [PATCH 117/154] fixes instructions in partially painted nodes not
getting unpainted in binja...
---
plugin/lighthouse/coverage.py | 9 +++++++++
plugin/lighthouse/painting/painter.py | 2 ++
2 files changed, 11 insertions(+)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index f5c0e229..5e6f9fcd 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -3,6 +3,7 @@
import logging
import weakref
import datetime
+import itertools
import collections
from lighthouse.util import *
@@ -166,6 +167,7 @@ def __init__(self, palette, name="", filepath=None, data=None):
self.functions = {}
self.instruction_percent = 0.0
self.partial_nodes = set()
+ self.partial_instructions = set()
#
# we instantiate a single weakref of ourself (the DatbaseCoverage
@@ -337,6 +339,12 @@ def _finalize_nodes(self, dirty_nodes):
else:
self.partial_nodes.discard(address)
+ # finalize the set of instructions executed in partially executed nodes
+ instructions = []
+ for node_address in self.partial_nodes:
+ instructions.append(self.nodes[node_address].executed_instructions)
+ self.partial_instructions = set(itertools.chain.from_iterable(instructions))
+
def _finalize_functions(self, dirty_functions):
"""
Finalize the FunctionCoverage objects statistics / data for use.
@@ -645,6 +653,7 @@ def unmap_all(self):
self.nodes = {}
self.functions = {}
self.partial_nodes = set()
+ self.partial_instructions = set()
self._misaligned_data = set()
# dump the source coverage data back into an 'unmapped' state
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index f65aaec1..1888e184 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -315,7 +315,9 @@ def _paint_database(self):
return False # a repaint was requested
# compute the painted instructions that will not get painted over
+ stale_partial_inst = self._painted_instructions - db_coverage.partial_instructions
stale_inst = self._painted_instructions - db_coverage.coverage
+ stale_inst |= stale_partial_inst
# compute the painted nodes that will not get painted over
stale_nodes_ea = self._painted_nodes - viewkeys(db_coverage.nodes)
From a3183f21f29862d5582c4ed398fca9f904b78ae6 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 12 Apr 2020 19:53:58 -0400
Subject: [PATCH 118/154] improve theme loading / hinting for IDA
---
plugin/lighthouse/ui/palette.py | 45 +++++++----
.../lighthouse/util/disassembler/ida_api.py | 80 +++++++++++++++++--
plugin/lighthouse/util/misc.py | 11 +++
3 files changed, 113 insertions(+), 23 deletions(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 06440bb1..98bdf359 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -103,7 +103,9 @@ def __init__(self):
# initialize the user theme directory
self._populate_user_theme_dir()
- self.warmup()
+ # load a placeholder theme (unhinted) for inital Lighthoue bring-up
+ self._load_preferred_theme(True)
+ self._initialized = False
#----------------------------------------------------------------------
# Properties
@@ -162,6 +164,8 @@ def warmup(self):
if self._initialized:
return
+ logger.debug("Warming up theme subsystem...")
+
#
# attempt to load the user's preferred (or hinted) theme. if we are
# successful, then there's nothing else to do!
@@ -170,6 +174,7 @@ def warmup(self):
self._refresh_theme_hints()
if self._load_preferred_theme():
self._initialized = True
+ logger.debug(" - warmup complete, using preferred theme!")
return
#
@@ -197,6 +202,7 @@ def warmup(self):
lmsg("Could not load Lighthouse fallback theme!") # this is a bad place to be...
return
+ logger.debug(" - warmup complete, using hint-recommended theme!")
self._initialized = True
def interactive_change_theme(self):
@@ -249,15 +255,17 @@ def interactive_change_theme(self):
self._refresh_theme_hints()
- # load & apply theme from disk
- if self._load_theme(filename):
+ # if the selected theme fails to load, throw a visible warning
+ if not self._load_theme(filename):
+ disassembler.warning(
+ "Failed to load Lighthouse user theme!\n\n"
+ "Please check the console for more information..."
+ )
return
- # if the selected theme failed to load, throw a visible warning
- disassembler.warning(
- "Failed to load Lighthouse user theme!\n\n"
- "Please check the console for more information..."
- )
+ # since everthing looks like it loaded okay, save this as the preferred theme
+ with open(os.path.join(get_user_theme_dir(), ".active_theme"), "w") as f:
+ f.write(filename)
def refresh_theme(self):
"""
@@ -311,6 +319,7 @@ def _load_required_fields(self):
"""
Load the required theme fields from a donor in-box theme.
"""
+ logger.debug("Loading required theme fields from disk...")
# load a known-good theme from the plugin's in-box themes
filepath = os.path.join(get_plugin_theme_dir(), self._default_themes["dark"])
@@ -327,13 +336,14 @@ def _load_preferred_theme(self, fallback=False):
"""
Load the user's preferred theme, or the one hinted at by the theme subsystem.
"""
+ logger.debug("Loading preferred theme from disk...")
user_theme_dir = get_user_theme_dir()
# attempt te read the name of the user's active / preferred theme name
active_filepath = os.path.join(user_theme_dir, ".active_theme")
try:
theme_name = open(active_filepath).read().strip()
- logger.debug("Got '%s' from .active_theme" % theme_name)
+ logger.debug(" - Got '%s' from .active_theme" % theme_name)
except (OSError, IOError):
theme_name = None
@@ -354,7 +364,7 @@ def _load_preferred_theme(self, fallback=False):
if self._user_qt_hint == self._user_disassembly_hint:
theme_name = self._default_themes[self._user_qt_hint]
- logger.debug("No preferred theme, hints suggest theme '%s'" % theme_name)
+ logger.debug(" - No preferred theme, hints suggest theme '%s'" % theme_name)
#
# the UI hints don't match, so the user is using some ... weird
@@ -383,6 +393,7 @@ def _validate_theme(self, theme):
"""
Pefrom rudimentary theme validation.
"""
+ logger.debug(" - Validating theme fields for '%s'..." % theme["name"])
user_fields = theme.get("fields", None)
if not user_fields:
lmsg("Could not find theme 'fields' definition")
@@ -428,10 +439,6 @@ def _load_theme(self, filepath):
lmsg("Failed to load Lighthouse user theme\n%s" % e)
return False
- # since everthing looks like it loaded okay, save this as the preferred theme
- with open(os.path.join(get_user_theme_dir(), ".active_theme"), "w") as f:
- f.write(filepath)
-
# return success
self._notify_theme_changed()
return True
@@ -440,7 +447,7 @@ def _read_theme(self, filepath):
"""
Parse the Lighthouse theme file from the given filepath.
"""
- logging.debug("Opening theme '%s'..." % filepath)
+ logger.debug(" - Reading theme file '%s'..." % filepath)
# attempt to load the theme file contents from disk
raw_theme = open(filepath, "r").read()
@@ -455,7 +462,7 @@ def _apply_theme(self, theme):
"""
Apply the given theme definition to Lighthouse.
"""
- logging.debug("Applying theme '%s'..." % theme["name"])
+ logger.debug(" - Applying theme '%s'..." % theme["name"])
colors = theme["colors"]
for field_name, color_entry in theme["fields"].items():
@@ -500,6 +507,7 @@ def _pick_best_color(self, field_name, color_entry):
# the rest of the fields should be considered 'qt' fields
if self._user_qt_hint == "dark":
return dark
+
return light
#--------------------------------------------------------------------------
@@ -511,7 +519,7 @@ def _refresh_theme_hints(self):
Peek at the UI context to infer what kind of theme the user might be using.
"""
self._user_qt_hint = self._qt_theme_hint()
- self._user_disassembly_hint = self._disassembly_theme_hint()
+ self._user_disassembly_hint = self._disassembly_theme_hint() or "dark"
def _disassembly_theme_hint(self):
"""
@@ -529,6 +537,9 @@ def _disassembly_theme_hint(self):
#
bg_color = disassembler.get_disassembly_background_color()
+ if not bg_color:
+ logger.debug(" - Failed to get hint for disassembly background...")
+ return None
# return 'dark' or 'light'
return test_color_brightness(bg_color)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 32e6639e..fb92104c 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -1,7 +1,9 @@
+import os
import sys
import time
import logging
import binascii
+import tempfile
import functools
import idaapi
@@ -13,7 +15,7 @@
from .api import DisassemblerCoreAPI, DisassemblerContextAPI
from ..qt import *
-from ..misc import is_mainthread
+from ..misc import is_mainthread, get_string_between
logger = logging.getLogger("Lighthouse.API.IDA")
@@ -118,7 +120,19 @@ def get_disassembly_background_color(self):
disassembly view, and take a screenshot of said widget. It will then
attempt to extract the color of a single background pixel (hopefully).
"""
- return self._get_ida_bg_color_ida7()
+
+ # method one
+ color = self._get_ida_bg_color_from_file()
+ if color:
+ return color
+
+ # method two, fallback
+ color = self._get_ida_bg_color_from_view()
+ if not color:
+ return None
+
+ # return the found background color
+ return color
def is_msg_inited(self):
return idaapi.is_msg_inited()
@@ -188,11 +202,64 @@ def show_dockable(self, dockable_name):
# Theme Prediction Helpers (Internal)
#--------------------------------------------------------------------------
- def _get_ida_bg_color_ida7(self):
+ def _get_ida_bg_color_from_file(self):
"""
- Get the background color of the IDA disassembly view. (IDA 7+)
+ Get the background color of the IDA disassembly views via HTML export.
"""
- return QtGui.QColor(0,0,0) # TODO/IDA
+ logger.debug("Attempting to get IDA disassembly background color from HTML...")
+
+ #
+ # TODO/IDA: we need better early detection for if IDA is fully ready,
+ # this isn't effective and this func theme func can crash IDA if
+ # called too early (eg, during db load...)
+ #
+
+ imagebase = idaapi.get_imagebase()
+ #if imagebase == idaapi.BADADDR:
+ # logger.debug(" - No imagebase...")
+ # return None
+
+ # create a temp file that we can write to
+ handle, path = tempfile.mkstemp()
+ os.close(handle)
+
+ # 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)
+
+ # read the dumped text
+ with open(path, "r") as fd:
+ html = fd.read()
+
+ # delete the temp file from disk
+ try:
+ os.remove(path)
+ except OSError:
+ pass
+
+ # attempt to parse the user's disassembly background color from the html
+ bg_color_text = get_string_between(html, '')
+ if bg_color_text:
+ logger.debug(" - Extracted bgcolor '%s' from regex!" % bg_color_text)
+ return QtGui.QColor(bg_color_text)
+
+ # sometimes the above one isn't present... so try this one
+ 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)
+ return QtGui.QColor(bg_color_text)
+
+ logger.debug(" - HTML color regex failed...")
+ logger.debug(html)
+ return None
+
+ def _get_ida_bg_color_from_view(self):
+ """
+ Get the background color of the IDA disassembly views via widget inspection.
+ """
+ logger.debug("Attempting to get IDA disassembly background color from view...")
+
names = ["Enums", "Structures"]
names += ["Hex View-%u" % i for i in range(5)]
names += ["IDA View-%c" % chr(ord('A') + i) for i in range(5)]
@@ -203,7 +270,8 @@ def _get_ida_bg_color_ida7(self):
if twidget:
break
else:
- raise RuntimeError("Failed to find donor view")
+ logger.debug(" - Failed to find donor view...")
+ return None
# touch the target form so we know it is populated
self._touch_ida_window(twidget)
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index d3803b84..024b525d 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -1,4 +1,5 @@
import os
+import re
import weakref
import datetime
import threading
@@ -81,6 +82,16 @@ def human_timestamp(timestamp):
dt = datetime.datetime.fromtimestamp(timestamp)
return dt.strftime("%b %d %Y %H:%M:%S")
+def get_string_between(text, before, after):
+ """
+ Get the string between two strings.
+ """
+ pattern = "%s(.*)%s" % (before, after)
+ result = re.search(pattern, text)
+ if not result:
+ return None
+ return result.group(1)
+
#------------------------------------------------------------------------------
# Python Callback / Signals
#------------------------------------------------------------------------------
From 481c624b7e5a166bc825648cc531faa000d05652 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 13 Apr 2020 08:59:02 -0400
Subject: [PATCH 119/154] improve theme reactivity for binja
---
plugin/lighthouse/integration/core.py | 1 +
plugin/lighthouse/ui/coverage_overview.py | 13 +++++++------
plugin/lighthouse/util/disassembler/binja_api.py | 8 ++++----
3 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index b853d9a7..011dcb54 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -42,6 +42,7 @@ def load(self):
self.palette.theme_changed(self.refresh_theme)
def create_coverage_overview(name, parent, dctx):
+ self.palette.warmup()
widget = disassembler.create_dockable_widget(parent, name)
overview = CoverageOverview(self, dctx, widget)
return widget
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 84fbcd0d..3dfecc8f 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -259,6 +259,7 @@ class EventProxy(QtCore.QObject):
EventShow = 17
EventDestroy = 16
EventLayoutRequest = 76
+ EventUpdateLater = 78
def __init__(self, target):
super(EventProxy, self).__init__()
@@ -286,21 +287,21 @@ def eventFilter(self, source, event):
# every database was opened ...
#
- elif int(event.type()) == self.EventLayoutRequest and not self._target.initialized:
+ elif int(event.type()) == self.EventLayoutRequest:
self._target.initialized = True
#
- # this is used to hook the 'show' event of the coverage overview. in
- # particular, we use this event to ensure the metadata cache is built
- # and available for use.
+ # this is used to hook a little bit after the 'show' event of the
+ # coverage overview. in particular, we use this event to ensure the
+ # metadata cache is built and available for use.
#
# this should only ever kick off a run if the user attempts to open the
# coverage overview before loading a coverage file. this is useful,
# because the overview does have some utility even without coverage...
#
- elif int(event.type()) == self.EventShow and self._target.initialized:
- if not self._target.director.metadata.cached:
+ elif int(event.type()) == self.EventUpdateLater:
+ if self._target.visible not self._target.director.metadata.cached:
self._target.director.refresh()
#
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index a375b0c1..2aa7122d 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
import os
import sys
+
import logging
import functools
import threading
import collections
-
import binaryninja
+import binaryninjaui
from binaryninja import PythonScriptingInstance, binaryview
from binaryninjaui import DockHandler, DockContextHandler, UIContext, UIActionHandler
from binaryninja.plugin import BackgroundTaskThread
@@ -147,9 +148,8 @@ def wrapper(*args, **kwargs):
def get_disassembler_user_directory(self):
return os.path.split(binaryninja.user_plugin_path())[0]
- def get_disassembly_background_color(self): # TODO/BINJA - use theme apis...
- palette = QtGui.QPalette()
- return palette.color(QtGui.QPalette.Button)
+ def get_disassembly_background_color(self):
+ return binaryninjaui.getThemeColor(binaryninjaui.ThemeColor.LinearDisassemblyBlockColor)
def is_msg_inited(self):
return True
From 6e6084a058828fbb4ee2c96d36fb27d966486a7a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 13 Apr 2020 10:04:25 -0400
Subject: [PATCH 120/154] adds minimal update check
---
plugin/lighthouse/integration/core.py | 38 ++++++++------
plugin/lighthouse/ui/coverage_overview.py | 35 ++++++++++---
plugin/lighthouse/util/update.py | 64 +++++++++++++++++++++++
3 files changed, 113 insertions(+), 24 deletions(-)
create mode 100644 plugin/lighthouse/util/update.py
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 011dcb54..5f8eb395 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -12,14 +12,6 @@
logger = logging.getLogger("Lighthouse.Core")
-#------------------------------------------------------------------------------
-# Plugin Metadata
-#------------------------------------------------------------------------------
-
-PLUGIN_VERSION = "0.9.0-DEV"
-AUTHORS = "Markus Gaasedelen"
-DATE = "2020"
-
#------------------------------------------------------------------------------
# Lighthouse Plugin Core
#------------------------------------------------------------------------------
@@ -27,6 +19,14 @@
class LighthouseCore(object):
__metaclass__ = abc.ABCMeta
+ #--------------------------------------------------------------------------
+ # Plugin Metadata
+ #--------------------------------------------------------------------------
+
+ PLUGIN_VERSION = "0.9.0-DEV"
+ AUTHORS = "Markus Gaasedelen"
+ DATE = "2020"
+
#--------------------------------------------------------------------------
# Initialization
#--------------------------------------------------------------------------
@@ -63,27 +63,20 @@ def unload(self):
"""
self._uninstall_ui()
- # spin donw any active contexts (stop threads, cleanup qt state, etc)
+ # spin down any active contexts (stop threads, cleanup qt state, etc)
for lctx in self.lighthouse_contexts.values():
lctx.terminate()
logger.info("-"*75)
logger.info("Plugin terminated")
- @abc.abstractmethod
- def get_context(self, dctx):
- """
- Get the LighthouseContext object for a given disassembler context.
- """
- pass
-
def print_banner(self):
"""
Print the plugin banner.
"""
# build the main banner title
- banner_params = (PLUGIN_VERSION, AUTHORS, DATE)
+ banner_params = (self.PLUGIN_VERSION, self.AUTHORS, self.DATE)
banner_title = "Lighthouse v%s - (c) %s - %s" % banner_params
# print plugin banner
@@ -93,6 +86,17 @@ def print_banner(self):
lmsg("-"*75)
lmsg("")
+ #--------------------------------------------------------------------------
+ # Disassembler / Database Context Selector
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def get_context(self, dctx):
+ """
+ Get the LighthouseContext object for a given disassembler context.
+ """
+ pass
+
#--------------------------------------------------------------------------
# UI Integration (Internal)
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 3dfecc8f..b5dd82b9 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -4,7 +4,9 @@
from lighthouse.util.qt import *
from lighthouse.util.misc import plugin_resource
+from lighthouse.util.update import check_for_update
from lighthouse.util.disassembler import disassembler
+
from lighthouse.composer import ComposingShell
from lighthouse.ui.coverage_table import CoverageTableView, CoverageTableModel, CoverageTableController
from lighthouse.ui.coverage_combobox import CoverageComboBox
@@ -292,17 +294,36 @@ def eventFilter(self, source, event):
#
# this is used to hook a little bit after the 'show' event of the
- # coverage overview. in particular, we use this event to ensure the
- # metadata cache is built and available for use.
+ # coverage overview. this is the most universal signal that the
+ # user is *actually* trying to use lighthouse in a meaningful way...
+ #
+ # we will use this moment first to check if they skipped straight to
+ # 'go' and opened the coverage overview without the metadata cache
+ # getting built.
#
- # this should only ever kick off a run if the user attempts to open the
- # coverage overview before loading a coverage file. this is useful,
- # because the overview does have some utility even without coverage...
+ # we also want to send off a one-time (per session) update check to
+ # see if lighthoue has a plugin update available...
#
elif int(event.type()) == self.EventUpdateLater:
- if self._target.visible not self._target.director.metadata.cached:
- self._target.director.refresh()
+
+ # we should only care to attempt these actions if the UI is visible
+ if self._target.visible:
+
+ # simple async request to github to see if we're up to date
+ check_for_update(self._target._core.PLUGIN_VERSION, disassembler.warning)
+
+ #
+ # this should only ever kick off a run if the user attempts to open the
+ # coverage overview before loading a coverage file. this is useful,
+ # because the overview does have some utility even without coverage...
+ #
+ # this case should only happen if the user does 'Show Coverage
+ # Overview' from the binja-controlled Window menu entry...
+ #
+
+ if not self._target.director.metadata.cached:
+ self._target.director.refresh()
#
# this is an unknown event, but it seems to fire when the widget is
diff --git a/plugin/lighthouse/util/update.py b/plugin/lighthouse/util/update.py
new file mode 100644
index 00000000..7ac5acde
--- /dev/null
+++ b/plugin/lighthouse/util/update.py
@@ -0,0 +1,64 @@
+import re
+import json
+import logging
+import threading
+import urllib.request
+
+logger = logging.getLogger("Lighthouse.Util.Update")
+
+UPDATE_URL = "https://api.github.com/repos/gaasedelen/lighthouse/releases/latest"
+update_checked = False
+
+def check_for_update(current_version, callback):
+ """
+ Perform a plugin update check.
+ """
+ global update_checked
+
+ # only ever perform the update check once per session...
+ if update_checked:
+ return
+
+ update_thread = threading.Thread(
+ target=async_update_check,
+ args=(current_version, callback,),
+ name="UpdateChecker"
+ )
+ update_thread.start()
+
+ # burn the update checking code
+ update_checked = True
+
+def async_update_check(current_version, callback):
+ """
+ An async worker thread to check for an plugin update.
+ """
+ logger.debug("Checking for update...")
+
+ try:
+ response = urllib.request.urlopen(UPDATE_URL, timeout=5.0)
+ html = response.read()
+ info = json.loads(html)
+ remote_version = info["tag_name"]
+ except Exception as e:
+ logger.exception(" - Failed to reach GitHub for update check...")
+ return
+
+ # convert vesrion #'s to integer for easy compare...
+ version_remote = int(re.findall('\d+', remote_version)[0])
+ version_local = int(re.findall('\d+', current_version)[0])
+
+ # no updates available...
+ logger.debug(" - Local: '%s' vs Remote: '%s'" % (current_version, remote_version))
+ if version_remote <= version_local:
+ logger.debug(" - No update needed...")
+ return
+
+ # 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) + \
+ "Please go download the update from GitHub."
+
+ callback(update_message)
+
From 4aa116580ef0fffb76af5e5fb80653983d934a19 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 13 Apr 2020 10:04:53 -0400
Subject: [PATCH 121/154] minor bugfixs / QOL
---
plugin/lighthouse/exceptions.py | 2 ++
plugin/lighthouse/util/disassembler/binja_api.py | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py
index 2a61a080..b70b9b77 100644
--- a/plugin/lighthouse/exceptions.py
+++ b/plugin/lighthouse/exceptions.py
@@ -115,6 +115,8 @@ def warn_errors(errors):
"""
Warn the user of any encountered errors with a messagebox.
"""
+ if not errors:
+ return
for error_type, error_list in iteritems(errors):
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 2aa7122d..740b5cfc 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -376,4 +376,4 @@ def notifyViewChanged(self, view_frame):
self._active_view = shiboken.getCppPointer(view_frame)[0]
if self.visible:
dock_handler = DockHandler.getActiveDockHandler()
- dock_handler.setVisible(self.name, True)
+ dock_handler.setVisible(self.m_name, True)
From ea8cd6112214a0056f6f9ba3fa396266e7621fbe Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 13 Apr 2020 20:52:35 -0400
Subject: [PATCH 122/154] improve robustness of update check
---
plugin/lighthouse/context.py | 2 +-
plugin/lighthouse/integration/core.py | 24 +++++++++++++++--
plugin/lighthouse/ui/coverage_overview.py | 33 +++++------------------
plugin/lighthouse/util/update.py | 28 +++++++++----------
4 files changed, 43 insertions(+), 44 deletions(-)
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index 5d117bb7..9bdeed6f 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -22,8 +22,8 @@ class LighthouseContext(object):
def __init__(self, core, dctx):
disassembler[self] = DisassemblerContextAPI(dctx)
- self.dctx = dctx
self.core = core
+ self.dctx = dctx
# the database metadata cache
self.metadata = DatabaseMetadata(self)
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 5f8eb395..c39fbb9f 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -2,11 +2,12 @@
import abc
import logging
-from lighthouse.ui import *
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.ui import *
from lighthouse.metadata import DatabaseMetadata, metadata_progress
from lighthouse.exceptions import *
@@ -35,6 +36,7 @@ def load(self):
"""
Load the plugin, and integrate its UI into the disassembler.
"""
+ self._update_checked = False
self.lighthouse_contexts = {}
# the plugin color palette
@@ -43,8 +45,9 @@ def load(self):
def create_coverage_overview(name, parent, dctx):
self.palette.warmup()
+ lctx = self.get_context(dctx)
widget = disassembler.create_dockable_widget(parent, name)
- overview = CoverageOverview(self, dctx, widget)
+ overview = CoverageOverview(lctx, widget)
return widget
# the coverage overview widget
@@ -202,6 +205,9 @@ def open_coverage_overview(self, dctx=None):
# show the coverage overview
disassembler.show_dockable("Coverage Overview")
+ # trigger an update check (this should only ever really 'check' once)
+ self.check_for_update()
+
def open_coverage_xref(self, address, dctx=None):
"""
Open the 'Coverage Xref' dialog for a given address.
@@ -391,6 +397,20 @@ def interactive_load_file(self, dctx=None):
# finally, emit any notable issues that occurred during load
warn_errors(errors)
+ def check_for_update(self):
+ """
+ Check if there is an update available for Lighthouse.
+ """
+ if self._update_checked:
+ return
+
+ # wrap the callback (a popup) to ensure it gets called from the UI
+ callback = disassembler.execute_ui(disassembler.warning)
+
+ # kick off the async update check
+ check_for_update(self.PLUGIN_VERSION, callback)
+ self._update_checked = True
+
#--------------------------------------------------------------------------
# Scheduled
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index b5dd82b9..3471aace 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -4,7 +4,6 @@
from lighthouse.util.qt import *
from lighthouse.util.misc import plugin_resource
-from lighthouse.util.update import check_for_update
from lighthouse.util.disassembler import disassembler
from lighthouse.composer import ComposingShell
@@ -23,14 +22,12 @@ class CoverageOverview(object):
The Coverage Overview Widget.
"""
- def __init__(self, core, dctx, widget):
- self._core = core
- self.dctx = dctx
+ def __init__(self, lctx, widget):
+ self.lctx = lctx
self.widget = widget
+ self.director = self.lctx.director
- self.lctx = self._core.get_context(self.dctx)
self.lctx.coverage_overview = self
- self.director = self.lctx.director
self.initialized = False
# see the EventProxy class below for more details
@@ -301,29 +298,13 @@ def eventFilter(self, source, event):
# 'go' and opened the coverage overview without the metadata cache
# getting built.
#
- # we also want to send off a one-time (per session) update check to
- # see if lighthoue has a plugin update available...
+ # this case should only happen if the user does 'Show Coverage
+ # Overview' from the binja-controlled Window menu entry...
#
elif int(event.type()) == self.EventUpdateLater:
-
- # we should only care to attempt these actions if the UI is visible
- if self._target.visible:
-
- # simple async request to github to see if we're up to date
- check_for_update(self._target._core.PLUGIN_VERSION, disassembler.warning)
-
- #
- # this should only ever kick off a run if the user attempts to open the
- # coverage overview before loading a coverage file. this is useful,
- # because the overview does have some utility even without coverage...
- #
- # this case should only happen if the user does 'Show Coverage
- # Overview' from the binja-controlled Window menu entry...
- #
-
- if not self._target.director.metadata.cached:
- self._target.director.refresh()
+ if self._target.visible and not self._target.director.metadata.cached:
+ self._target.director.refresh()
#
# this is an unknown event, but it seems to fire when the widget is
diff --git a/plugin/lighthouse/util/update.py b/plugin/lighthouse/util/update.py
index 7ac5acde..f46c4fa4 100644
--- a/plugin/lighthouse/util/update.py
+++ b/plugin/lighthouse/util/update.py
@@ -2,23 +2,24 @@
import json
import logging
import threading
-import urllib.request
+
+try:
+ from urllib2 import urlopen # Py2
+except ImportError:
+ from urllib.request import urlopen # Py3
logger = logging.getLogger("Lighthouse.Util.Update")
+#------------------------------------------------------------------------------
+# Update Checking
+#------------------------------------------------------------------------------
+
UPDATE_URL = "https://api.github.com/repos/gaasedelen/lighthouse/releases/latest"
-update_checked = False
def check_for_update(current_version, callback):
"""
Perform a plugin update check.
"""
- global update_checked
-
- # only ever perform the update check once per session...
- if update_checked:
- return
-
update_thread = threading.Thread(
target=async_update_check,
args=(current_version, callback,),
@@ -26,9 +27,6 @@ def check_for_update(current_version, callback):
)
update_thread.start()
- # burn the update checking code
- update_checked = True
-
def async_update_check(current_version, callback):
"""
An async worker thread to check for an plugin update.
@@ -36,7 +34,7 @@ def async_update_check(current_version, callback):
logger.debug("Checking for update...")
try:
- response = urllib.request.urlopen(UPDATE_URL, timeout=5.0)
+ response = urlopen(UPDATE_URL, timeout=5.0)
html = response.read()
info = json.loads(html)
remote_version = info["tag_name"]
@@ -45,12 +43,12 @@ def async_update_check(current_version, callback):
return
# convert vesrion #'s to integer for easy compare...
- version_remote = int(re.findall('\d+', remote_version)[0])
- version_local = int(re.findall('\d+', current_version)[0])
+ version_remote = int(''.join(re.findall('\d+', remote_version)))
+ version_local = int(''.join(re.findall('\d+', current_version)))
# no updates available...
logger.debug(" - Local: '%s' vs Remote: '%s'" % (current_version, remote_version))
- if version_remote <= version_local:
+ if version_local >= version_remote:
logger.debug(" - No update needed...")
return
From c5dbfb1062bdfae3ff7b5282b28e7012e64890a7 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 13 Apr 2020 21:11:53 -0400
Subject: [PATCH 123/154] more verbose painter output
---
plugin/lighthouse/painting/painter.py | 10 +++++++++-
plugin/lighthouse/util/log.py | 10 ++++++----
2 files changed, 15 insertions(+), 5 deletions(-)
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 1888e184..49fac236 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -294,6 +294,7 @@ def _paint_database(self):
"""
Repaint the current database based on the current state.
"""
+ lmsg("Painting database...")
# more code-friendly, readable aliases (db_XX == database_XX)
db_coverage = self.director.coverage
@@ -342,7 +343,7 @@ def _paint_database(self):
#------------------------------------------------------------------
end = time.time()
- logger.debug("Full Paint took %s seconds" % (end - start))
+ lmsg(" - Painting took %.2f seconds" % (end - start))
logger.debug(" stale_inst: %s" % "{:,}".format(len(stale_inst)))
logger.debug(" fresh inst: %s" % "{:,}".format(len(db_coverage.coverage)))
logger.debug(" stale_nodes: %s" % "{:,}".format(len(stale_nodes)))
@@ -355,6 +356,9 @@ def _clear_database(self):
"""
Clear all paint from the current database.
"""
+ lmsg("Clearing database paint...")
+ start = time.time()
+
db_metadata = self.director.metadata
instructions = db_metadata.instructions
nodes = viewvalues(db_metadata.nodes)
@@ -367,6 +371,9 @@ def _clear_database(self):
if not self._async_action(self._clear_nodes, nodes):
return False # a repaint was requested
+ end = time.time()
+ lmsg(" - Database paint cleared in %.2f seconds..." % (end-start))
+
# paint finished successfully
return True
@@ -432,6 +439,7 @@ def _async_database_painter(self):
try:
self._async_database_painter2()
except:
+ lmsg("PAINTER THREAD CRASHED :'(")
logger.exception("Painter crashed...")
def _async_database_painter2(self):
diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py
index 9d8de8fc..e232d382 100644
--- a/plugin/lighthouse/util/log.py
+++ b/plugin/lighthouse/util/log.py
@@ -67,7 +67,7 @@ def isatty(self):
# Initialize Logging
#------------------------------------------------------------------------------
-MAX_LOGS = 5
+MAX_LOGS = 10
def cleanup_log_directory(log_directory):
"""
Retain only the last 15 logs.
@@ -109,10 +109,12 @@ 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()
From a0b375064a70104aeb7eae102450d1a5b8a51108 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 15 Apr 2020 21:49:03 -0400
Subject: [PATCH 124/154] improve painting perf
---
plugin/lighthouse/painting/ida_painter.py | 52 +++++++++++--------
plugin/lighthouse/painting/painter.py | 45 +++++++---------
.../lighthouse/util/disassembler/ida_api.py | 1 +
3 files changed, 51 insertions(+), 47 deletions(-)
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 6763951a..85591276 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -1,8 +1,10 @@
+import struct
import logging
import functools
import idc
import idaapi
+from idaapi import clr_abits, set_abits, netnode
from lighthouse.util import *
from lighthouse.util.disassembler import disassembler
@@ -172,13 +174,13 @@ def _clear_instructions(self, instructions):
self._action_complete.set()
@execute_paint
- def _paint_nodes(self, nodes_coverage):
- self.paint_nodes(nodes_coverage)
+ def _paint_nodes(self, node_addresses):
+ self.paint_nodes(node_addresses)
self._action_complete.set()
@execute_paint
- def _clear_nodes(self, nodes_metadata):
- self.clear_nodes(nodes_metadata)
+ def _clear_nodes(self, node_addresses):
+ self.clear_nodes(node_addresses)
self._action_complete.set()
@execute_paint
@@ -202,8 +204,11 @@ def paint_instructions(self, instructions):
"""
Paint instruction level coverage defined by the current database mapping.
"""
+ color = struct.pack("I", self.palette.coverage_paint+1)
for address in instructions:
- idaapi.set_item_color(address, self.palette.coverage_paint)
+ set_abits(address, 0x40000)
+ nn = netnode(address)
+ nn.supset(20, color, 'A')
self._painted_instructions |= set(instructions)
def clear_instructions(self, instructions):
@@ -211,47 +216,48 @@ def clear_instructions(self, instructions):
Clear paint from the given instructions.
"""
for address in instructions:
- idaapi.set_item_color(address, idc.DEFCOLOR)
+ clr_abits(address, 0x40000)
self._painted_instructions -= set(instructions)
- def paint_nodes(self, nodes_coverage):
+ def paint_nodes(self, node_addresses):
"""
Paint node level coverage defined by the current database mappings.
"""
+ db_coverage = self.director.coverage
db_metadata = self.director.metadata
# create a node info object as our vehicle for setting the node color
node_info = idaapi.node_info_t()
+ node_info.bg_color = self.palette.coverage_paint
+ node_flags = idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
#
# loop through every node that we have coverage data for, painting them
# in the IDA graph view as applicable.
#
- for node_coverage in nodes_coverage:
- node_metadata = db_metadata.nodes[node_coverage.address]
+ for node_address in node_addresses:
+ node_coverage = db_coverage.nodes[node_address]
+ node_metadata = db_metadata.nodes[node_address]
# ignore nodes that are only partially executed
if node_coverage.instructions_executed != node_metadata.instruction_count:
continue
# get the function address for this node (there should only be one...)
- function_metadata = db_metadata.get_functions_by_node(node_coverage.address)[0]
-
- # assign the background color we would like to paint to this node
- node_info.bg_color = self.palette.coverage_paint
+ function_metadata = db_metadata.get_functions_by_node(node_address)[0]
# do the *actual* painting of a single node instance
- idaapi.set_node_info(
+ set_node_info(
function_metadata.address,
node_metadata.id,
node_info,
- idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
+ node_flags
)
- self._painted_nodes.add(node_metadata.address)
+ self._painted_nodes |= set(node_addresses)
- def clear_nodes(self, nodes_metadata):
+ def clear_nodes(self, node_addresses):
"""
Clear paint from the given graph nodes.
"""
@@ -260,26 +266,28 @@ def clear_nodes(self, nodes_metadata):
# create a node info object as our vehicle for resetting the node color
node_info = idaapi.node_info_t()
node_info.bg_color = idc.DEFCOLOR
+ node_flags = idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
#
# loop through every node that we have metadata data for, clearing
# their paint (color) in the IDA graph view as applicable.
#
- for node_metadata in nodes_metadata:
+ for node_address in node_addresses:
+ node_metadata = db_metadata.nodes[node_address]
# get the function address for this node (there should only be one...)
- function_metadata = db_metadata.get_functions_by_node(node_metadata.address)[0]
+ function_metadata = db_metadata.get_functions_by_node(node_address)[0]
# do the *actual* painting of a single node instance
- idaapi.set_node_info(
+ set_node_info(
function_metadata.address,
node_metadata.id,
node_info,
- idaapi.NIF_BG_COLOR | idaapi.NIF_FRAME_COLOR
+ node_flags
)
- self._painted_nodes.discard(node_metadata.address)
+ self._painted_nodes -= set(node_addresses)
#------------------------------------------------------------------------------
# Painting - HexRays (Decompilation / Source)
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 49fac236..4f123334 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -13,7 +13,7 @@ class DatabasePainter(object):
"""
__metaclass__ = abc.ABCMeta
- PAINTER_SLEEP = 0.001
+ PAINTER_SLEEP = 0.01
MSG_TERMINATE = 0
MSG_REPAINT = 1
@@ -316,38 +316,39 @@ def _paint_database(self):
return False # a repaint was requested
# compute the painted instructions that will not get painted over
- stale_partial_inst = self._painted_instructions - db_coverage.partial_instructions
- stale_inst = self._painted_instructions - db_coverage.coverage
- stale_inst |= stale_partial_inst
-
- # compute the painted nodes that will not get painted over
- stale_nodes_ea = self._painted_nodes - viewkeys(db_coverage.nodes)
- stale_nodes_ea |= db_coverage.partial_nodes
- stale_nodes = [db_metadata.nodes[ea] for ea in stale_nodes_ea]
+ stale_partial_inst = self._painted_instructions & db_coverage.partial_instructions
+ stale_instr = self._painted_instructions - db_coverage.coverage
+ stale_instr |= stale_partial_inst
# clear old instruction paint
- if not self._async_action(self._clear_instructions, stale_inst):
+ if not self._async_action(self._clear_instructions, stale_instr):
return False # a repaint was requested
+ # compute the painted nodes that will not get painted over
+ stale_nodes = self._painted_nodes - viewkeys(db_coverage.nodes)
+ stale_nodes |= db_coverage.partial_nodes
+
# clear old node paint
if not self._async_action(self._clear_nodes, stale_nodes):
return False # a repaint was requested
# paint new instructions
- if not self._async_action(self._paint_instructions, db_coverage.coverage):
+ new_instr = sorted(db_coverage.coverage - self._paint_instructions)
+ if not self._async_action(self._paint_instructions, new_instr):
return False # a repaint was requested
# paint new nodes
- if not self._async_action(self._paint_nodes, itervalues(db_coverage.nodes)):
+ new_nodev = sorted(viewkeys(db_coverage.nodes) - self._paint_nodes)
+ if not self._async_action(self._paint_nodes, new_nodes):
return False # a repaint was requested
#------------------------------------------------------------------
end = time.time()
lmsg(" - Painting took %.2f seconds" % (end - start))
- logger.debug(" stale_inst: %s" % "{:,}".format(len(stale_inst)))
- logger.debug(" fresh inst: %s" % "{:,}".format(len(db_coverage.coverage)))
+ logger.debug(" stale_instr: %s" % "{:,}".format(len(stale_instr)))
+ logger.debug(" fresh instr: %s" % "{:,}".format(len(new_instr)))
logger.debug(" stale_nodes: %s" % "{:,}".format(len(stale_nodes)))
- logger.debug(" fresh_nodes: %s" % "{:,}".format(len(db_coverage.nodes)))
+ logger.debug(" fresh_nodes: %s" % "{:,}".format(len(new_nodes)))
# paint finished successfully
return True
@@ -360,8 +361,8 @@ def _clear_database(self):
start = time.time()
db_metadata = self.director.metadata
- instructions = db_metadata.instructions
- nodes = viewvalues(db_metadata.nodes)
+ instructions = sorted(db_metadata.instructions)
+ nodes = sorted(db_metadata.nodes.keys())
# clear all instructions
if not self._async_action(self._clear_instructions, instructions):
@@ -490,7 +491,7 @@ def _async_action(self, paint_action, work_iterable):
Internal routine for asynchrnous painting.
"""
- CHUNK_SIZE = 800 # somewhat arbitrary
+ CHUNK_SIZE = 1500 # somewhat arbitrary
# split the given nodes into multiple paints
for work_chunk in chunks(list(work_iterable), CHUNK_SIZE):
@@ -515,7 +516,7 @@ def _async_action(self, paint_action, work_iterable):
# we should end this thread (via end_threads)
#
- while not (self._action_complete.wait(timeout=0.1) or self._end_threads):
+ while not (self._action_complete.wait(timeout=0.2) or self._end_threads):
continue
#
@@ -539,11 +540,5 @@ def _async_action(self, paint_action, work_iterable):
if not self._msg_queue.empty():
return False
- #
- # sleep some so we don't choke the main IDA thread
- #
-
- time.sleep(self.PAINTER_SLEEP)
-
# operation completed successfully
return True
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index fb92104c..ecd95735 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -140,6 +140,7 @@ def is_msg_inited(self):
def warning(self, text):
idaapi.warning(text)
+ @execute_ui.__func__
def message(self, message):
print(message)
From 68f8c884f9f2e32daef7e1c0f8058aeccacc2c00 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 16 Apr 2020 23:46:17 -0400
Subject: [PATCH 125/154] make theme util function more accessible
---
plugin/lighthouse/coverage.py | 2 +-
plugin/lighthouse/ui/palette.py | 92 ++++++++++---------------------
plugin/lighthouse/util/misc.py | 20 +++++++
plugin/lighthouse/util/qt/util.py | 19 +++++++
4 files changed, 68 insertions(+), 65 deletions(-)
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index 5e6f9fcd..2ba2b9cb 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -7,8 +7,8 @@
import collections
from lighthouse.util import *
+from lighthouse.util.qt import compute_color_on_gradiant
from lighthouse.metadata import DatabaseMetadata
-from lighthouse.ui.palette import compute_color_on_gradiant
logger = logging.getLogger("Lighthouse.Coverage")
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 98bdf359..74a6270b 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -7,65 +7,11 @@
from lighthouse.util.qt import *
from lighthouse.util.log import lmsg
+from lighthouse.util.misc import *
from lighthouse.util.disassembler import disassembler
-from lighthouse.util.misc import plugin_resource, register_callback, notify_callback
logger = logging.getLogger("Lighthouse.UI.Palette")
-#------------------------------------------------------------------------------
-# Theme Util
-#------------------------------------------------------------------------------
-
-def swap_rgb(i):
- """
- Swap RRGGBB (integer) to BBGGRR.
- """
- return struct.unpack("I", i))[0] >> 8
-
-def test_color_brightness(color):
- """
- Test the brightness of a color.
- """
- if color.lightness() > 255.0/2:
- return "light"
- else:
- return "dark"
-
-def compute_color_on_gradiant(percent, color1, color2):
- """
- Compute the color specified by a percent between two colors.
-
- TODO/PERF: This is silly, heavy, and can be refactored.
- """
-
- # dump the rgb values from QColor objects
- 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)
-
- # return the new color
- return QtGui.QColor(r,g,b)
-
-def get_plugin_theme_dir():
- """
- Return the Lighthouse plugin theme directory.
- """
- return plugin_resource("themes")
-
-def get_user_theme_dir():
- """
- Return the Lighthouse user theme directory.
- """
- theme_directory = os.path.join(
- disassembler.get_disassembler_user_directory(),
- "lighthouse_themes"
- )
- return theme_directory
-
#------------------------------------------------------------------------------
# Plugin Color Palette
#------------------------------------------------------------------------------
@@ -107,6 +53,24 @@ def __init__(self):
self._load_preferred_theme(True)
self._initialized = False
+ @staticmethod
+ def get_plugin_theme_dir():
+ """
+ Return the Lighthouse plugin theme directory.
+ """
+ return plugin_resource("themes")
+
+ @staticmethod
+ def get_user_theme_dir():
+ """
+ Return the Lighthouse user theme directory.
+ """
+ theme_directory = os.path.join(
+ disassembler.get_disassembler_user_directory(),
+ "lighthouse_themes"
+ )
+ return theme_directory
+
#----------------------------------------------------------------------
# Properties
#----------------------------------------------------------------------
@@ -183,7 +147,7 @@ def warmup(self):
#
try:
- os.remove(os.path.join(get_user_theme_dir(), ".active_theme"))
+ os.remove(os.path.join(self.get_user_theme_dir(), ".active_theme"))
except:
pass
@@ -230,7 +194,7 @@ def interactive_change_theme(self):
#
file_dir = os.path.abspath(os.path.dirname(filename))
- user_dir = os.path.abspath(get_user_theme_dir())
+ user_dir = os.path.abspath(self.get_user_theme_dir())
if file_dir != user_dir:
text = "Please install your Lighthouse theme into the user theme directory:\n\n" + user_dir
disassembler.warning(text)
@@ -264,7 +228,7 @@ def interactive_change_theme(self):
return
# since everthing looks like it loaded okay, save this as the preferred theme
- with open(os.path.join(get_user_theme_dir(), ".active_theme"), "w") as f:
+ with open(os.path.join(self.get_user_theme_dir(), ".active_theme"), "w") as f:
f.write(filename)
def refresh_theme(self):
@@ -287,7 +251,7 @@ def _populate_user_theme_dir(self):
"""
# create the user theme directory if it does not exist
- user_theme_dir = get_user_theme_dir()
+ user_theme_dir = self.get_user_theme_dir()
if not os.path.exists(user_theme_dir):
os.makedirs(user_theme_dir)
@@ -305,7 +269,7 @@ def _populate_user_theme_dir(self):
continue
# copy the in-box themes to the user theme directory
- plugin_theme_file = os.path.join(get_plugin_theme_dir(), theme_name)
+ plugin_theme_file = os.path.join(self.get_plugin_theme_dir(), theme_name)
shutil.copy(plugin_theme_file, user_theme_file)
#
@@ -322,7 +286,7 @@ def _load_required_fields(self):
logger.debug("Loading required theme fields from disk...")
# load a known-good theme from the plugin's in-box themes
- filepath = os.path.join(get_plugin_theme_dir(), self._default_themes["dark"])
+ filepath = os.path.join(self.get_plugin_theme_dir(), self._default_themes["dark"])
theme = self._read_theme(filepath)
#
@@ -337,7 +301,7 @@ def _load_preferred_theme(self, fallback=False):
Load the user's preferred theme, or the one hinted at by the theme subsystem.
"""
logger.debug("Loading preferred theme from disk...")
- user_theme_dir = get_user_theme_dir()
+ user_theme_dir = self.get_user_theme_dir()
# attempt te read the name of the user's active / preferred theme name
active_filepath = os.path.join(user_theme_dir, ".active_theme")
@@ -382,9 +346,9 @@ def _load_preferred_theme(self, fallback=False):
#
if fallback:
- theme_path = os.path.join(get_plugin_theme_dir(), theme_name)
+ theme_path = os.path.join(self.get_plugin_theme_dir(), theme_name)
else:
- theme_path = os.path.join(get_user_theme_dir(), theme_name)
+ theme_path = os.path.join(self.get_user_theme_dir(), theme_name)
# finally, attempt to load & apply the theme -- return True/False
return self._load_theme(theme_path)
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index 024b525d..f99adf6f 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -1,5 +1,6 @@
import os
import re
+import struct
import weakref
import datetime
import threading
@@ -54,6 +55,25 @@ def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
+#------------------------------------------------------------------------------
+# Theme Util
+#------------------------------------------------------------------------------
+
+def swap_rgb(i):
+ """
+ Swap RRGGBB (integer) to BBGGRR.
+ """
+ return struct.unpack("I", i))[0] >> 8
+
+def test_color_brightness(color):
+ """
+ Test the brightness of a color.
+ """
+ if color.lightness() > 255.0/2:
+ return "light"
+ else:
+ return "dark"
+
#------------------------------------------------------------------------------
# Python Util
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 1225e0b0..40ace0a5 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -71,6 +71,25 @@ 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):
+ """
+ Compute the color specified by a percent between two colors.
+
+ TODO/PERF: This is silly, heavy, and can be refactored.
+ """
+
+ # dump the rgb values from QColor objects
+ 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)
+
+ # return the new color
+ return QtGui.QColor(r,g,b)
+
def move_mouse_event(mouse_event, position):
"""
Move the given mouse event to a different position.
From 61b8fb7668732260ded337a99c80d7ce9c623218 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 01:37:21 -0400
Subject: [PATCH 126/154] better handling of subsystem lifetimes
---
plugin/lighthouse/context.py | 13 ++++
plugin/lighthouse/director.py | 16 +++-
.../integration/binja_integration.py | 74 +++++++++++++++++--
plugin/lighthouse/integration/core.py | 8 +-
.../lighthouse/integration/ida_integration.py | 23 ++++--
plugin/lighthouse/metadata.py | 10 +++
plugin/lighthouse/painting/painter.py | 40 +++++++---
plugin/lighthouse/ui/coverage_overview.py | 2 +
8 files changed, 158 insertions(+), 28 deletions(-)
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index 9bdeed6f..5ba650ec 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -24,6 +24,7 @@ def __init__(self, core, dctx):
disassembler[self] = DisassemblerContextAPI(dctx)
self.core = core
self.dctx = dctx
+ self._started = False
# the database metadata cache
self.metadata = DatabaseMetadata(self)
@@ -44,6 +45,18 @@ def __init__(self, core, dctx):
# expose the live CoverageDirector object instance for external scripts
#lighthouse.coverage_director = self.director
+ def start(self):
+ """
+ One-time activation a Lighthouse context and its subsystems.
+ """
+ if self._started:
+ return
+ self.core.palette.warmup()
+ self.metadata.start()
+ self.director.start()
+ self.painter.start()
+ self._started = True
+
def terminate(self):
"""
Spin down any session subsystems before the session is deleted.
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 04a572f1..d0a0c805 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -182,7 +182,6 @@ def __init__(self, metadata, palette):
target=self._async_evaluate_ast,
name="EvaluateAST"
)
- self._composition_worker.start()
#----------------------------------------------------------------------
# Callbacks
@@ -207,12 +206,25 @@ def __init__(self, metadata, palette):
# director callbacks
self._refreshed_callbacks = []
+ #--------------------------------------------------------------------------
+ # Subsystem Lifetime
+ #--------------------------------------------------------------------------
+
+ def start(self):
+ """
+ Start the metadata subsystem.
+ """
+ self._composition_worker.start()
+
def terminate(self):
"""
Cleanup & terminate the director.
"""
self._ast_queue.put(None)
- self._composition_worker.join()
+ try:
+ self._composition_worker.join()
+ except RuntimeError:
+ pass
#--------------------------------------------------------------------------
# Properties
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
index fe75abae..2bd03b4c 100644
--- a/plugin/lighthouse/integration/binja_integration.py
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -22,18 +22,48 @@ class LighthouseBinja(LighthouseCore):
def __init__(self):
super(LighthouseBinja, self).__init__()
- def get_context(self, dctx):
+ def get_context(self, dctx, startup=True):
"""
Get the LighthouseContext object for a given disassembler context.
"""
dctx_id = ctypes.addressof(dctx.handle.contents)
- # create a new LighthouseContext if this is a new disassembler ctx / bv
+ #
+ # create a new LighthouseContext if this is the first time a context
+ # has been requested for this BNDB / bv
+ #
+
if dctx_id not in self.lighthouse_contexts:
- self.lighthouse_contexts[dctx_id] = LighthouseContext(self, dctx)
+
+ # create a new 'context' representing this BNDB / bv
+ lctx = LighthouseContext(self, dctx)
+ if startup:
+ lctx.start()
+
+ # save the created ctx for future calls
+ self.lighthouse_contexts[dctx_id] = lctx
+
+ #
+ # for binja, we basically *never* want to start the lighthouse ctx
+ # when it is first created. this is because binja will *immediately*
+ # create a coverage overview widget for every database when it is
+ # first opened.
+ #
+ # this is annoying, because we don't want to actually start up all
+ # of the lighthouse threads and subsystems unless the user actually
+ # 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
+ # below... any subsequent call to start() is effectively a nop!
+ #
+
+ else:
+ lctx = self.lighthouse_contexts[dctx_id]
+ lctx.start()
# return the lighthouse context object for this disassembler ctx / bv
- return self.lighthouse_contexts[dctx_id]
+ return lctx
#--------------------------------------------------------------------------
# UI Integration (Internal)
@@ -68,6 +98,34 @@ def _interactive_load_batch(self, context):
def _open_coverage_xref(self, dctx, addr):
super(LighthouseBinja, self).open_coverage_xref(addr, dctx)
+ def _is_xref_valid(self, dctx, addr):
+
+ #
+ # 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 indidication of 'is the user actually using lighthouse' so we
+ # do not want this to be one that creates lighthouse contexts
+ #
+
+ dctx_id = ctypes.addressof(dctx.handle.contents)
+ lctx = self.lighthouse_contexts.get(dctx_id, None)
+ if not lctx:
+ return False
+
+ # return True if there appears to be coverage loaded...
+ return bool(lctx.director.coverage_names)
+
+ def _open_coverage_overview(self, context):
+ dctx = disassembler.binja_get_bv_from_dock()
+ if not dctx:
+ disassembler.warning("Lighthouse requires an open BNDB to open the overview.")
+ return
+ super(LighthouseBinja, self).open_coverage_overview(dctx)
+
#--------------------------------------------------------------------------
# Binja Actions
#--------------------------------------------------------------------------
@@ -97,12 +155,16 @@ def _install_open_coverage_xref(self):
self.ACTION_COVERAGE_XREF,
"Open the coverage xref window",
self._open_coverage_xref,
- lambda bv, addr: bool(self.get_context(bv).director.aggregate.instruction_percent)
+ self._is_xref_valid
)
# NOTE/V35: Binja automatically creates View --> Show Coverage Overview
def _install_open_coverage_overview(self):
- pass
+ action = self.ACTION_COVERAGE_OVERVIEW
+ UIAction.registerAction(action)
+ UIActionHandler.globalActions().bindAction(action, UIAction(self._open_coverage_overview))
+ Menu.mainMenu("Tools").addAction(action, "Windows", 0)
+ logger.info("Installed the 'Open Coverage Overview' menu entry")
# NOTE/V35: Binja doesn't really 'unload' plugins, so whatever...
def _uninstall_load_file(self):
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index c39fbb9f..39b7e965 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -44,8 +44,7 @@ def load(self):
self.palette.theme_changed(self.refresh_theme)
def create_coverage_overview(name, parent, dctx):
- self.palette.warmup()
- lctx = self.get_context(dctx)
+ lctx = self.get_context(dctx, startup=False)
widget = disassembler.create_dockable_widget(parent, name)
overview = CoverageOverview(lctx, widget)
return widget
@@ -94,7 +93,7 @@ def print_banner(self):
#--------------------------------------------------------------------------
@abc.abstractmethod
- def get_context(self, dctx):
+ def get_context(self, dctx, startup=True):
"""
Get the LighthouseContext object for a given disassembler context.
"""
@@ -195,7 +194,6 @@ def open_coverage_overview(self, dctx=None):
"""
Open the dockable 'Coverage Overview' dialog.
"""
- self.palette.warmup()
lctx = self.get_context(dctx)
# the coverage overview is already open & visible, nothing to do
@@ -245,7 +243,6 @@ def interactive_load_batch(self, dctx=None):
"""
Perform the user-interactive loading of a coverage batch.
"""
- self.palette.warmup()
lctx = self.get_context(dctx)
#
@@ -330,7 +327,6 @@ def interactive_load_file(self, dctx=None):
"""
Perform the user-interactive loading of individual coverage files.
"""
- self.palette.warmup()
lctx = self.get_context(dctx)
#
diff --git a/plugin/lighthouse/integration/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
index bf94a206..525ab2f8 100644
--- a/plugin/lighthouse/integration/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -32,16 +32,29 @@ def __init__(self):
# run initialization
super(LighthouseIDA, self).__init__()
- def get_context(self, dctx):
+ def get_context(self, dctx, startup=True):
"""
TODO
"""
+ self.palette.warmup()
+
+ #
+ # there should only ever be 'one' disassembler / IDB context at any
+ # time for IDA. but if one does not exist yet, that means this is the
+ # first time the user has interacted with Lighthouse for this session
+ #
- # create a new LighthouseContext if this is a new disassembler ctx / bv
if dctx not in self.lighthouse_contexts:
- self.lighthouse_contexts[dctx] = LighthouseContext(self, dctx)
- # return the lighthouse context object for this disassembler ctx / bv
+ # create a new 'context' representing this IDB
+ lctx = LighthouseContext(self, dctx)
+ if startup:
+ lctx.start()
+
+ # save the created ctx for future calls
+ self.lighthouse_contexts[dctx] = lctx
+
+ # return the lighthouse context object for this IDB
return self.lighthouse_contexts[dctx]
#--------------------------------------------------------------------------
@@ -361,7 +374,7 @@ def finish_populating_widget_popup(self, widget, popup):
# inject any of lighthouse's right click context menu's into IDA
lctx = self.integration.get_context(None)
- if lctx.director.aggregate.instruction_percent:
+ if lctx.director.coverage_names:
self.integration._inject_ctx_actions(widget, popup, idaapi.get_widget_type(widget))
# must return 0 for ida...
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index cfee63ba..5ddb2ebe 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -108,6 +108,16 @@ def __init__(self, lctx=None):
self._function_renamed_callbacks = []
self._rebased_callbacks = []
+ #--------------------------------------------------------------------------
+ # Subsystem Lifetime
+ #--------------------------------------------------------------------------
+
+ def start(self):
+ """
+ Start the metadata subsystem.
+ """
+ pass # TODO: rebase scheduled task
+
def terminate(self):
"""
Cleanup & terminate the metadata object.
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 4f123334..e32ec130 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -29,7 +29,8 @@ def __init__(self, lctx, director, palette):
self.lctx = lctx
self.palette = palette
self.director = director
- self._enabled = True
+ self._enabled = False
+ self._started = False
#----------------------------------------------------------------------
# Painted State
@@ -66,7 +67,6 @@ def __init__(self, lctx, director, palette):
target=self._async_database_painter,
name="DatabasePainter"
)
- self._painting_worker.start()
#----------------------------------------------------------------------
# Callbacks
@@ -80,6 +80,20 @@ def __init__(self, lctx, director, palette):
self.director.coverage_modified(self.repaint)
self.director.refreshed(self.check_rebase)
+ def start(self):
+ """
+ Start the painter.
+ """
+ if self._started:
+ return
+
+ # start the painter thread
+ self._painting_worker.start()
+
+ # all done
+ self._started = True
+ self.set_enabled(True)
+
#--------------------------------------------------------------------------
# Status
#--------------------------------------------------------------------------
@@ -91,21 +105,26 @@ def enabled(self):
"""
return self._enabled
- def set_enabled(self, status):
+ def set_enabled(self, enabled):
"""
Enable or disable the painter.
"""
# enabled/disabled status is not changing, ignore...
- if status == self._enabled:
+ if enabled == self._enabled:
return
- lmsg("%s painting..." % ("Enabling" if status else "Disabling"))
- self._enabled = status
- self.repaint()
+ lmsg("%s painting..." % ("Enabling" if enabled else "Disabling"))
+ self._enabled = enabled
+
+ # paint or clear the database based on the change of status...
+ if enabled:
+ self._send_message(self.MSG_REPAINT)
+ else:
+ self._send_message(self.MSG_CLEAR)
# notify listeners that the painter has been enabled/disabled
- self._notify_status_changed(status)
+ self._notify_status_changed(enabled)
#--------------------------------------------------------------------------
# Commands
@@ -117,7 +136,10 @@ def terminate(self):
"""
self._end_threads = True
self._msg_queue.put(self.MSG_TERMINATE)
- self._painting_worker.join()
+ try:
+ self._painting_worker.join()
+ except RuntimeError: # thread was never started...
+ pass
def repaint(self):
"""
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 3471aace..e2fc7730 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -304,6 +304,8 @@ def eventFilter(self, source, event):
elif int(event.type()) == self.EventUpdateLater:
if self._target.visible and not self._target.director.metadata.cached:
+ if disassembler.NAME == "BINJA":
+ self._target.lctx.start()
self._target.director.refresh()
#
From 0d52ef50689f7bccb93f7187be043272a1c63737 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 01:37:46 -0400
Subject: [PATCH 127/154] misc fixes / tweaks
---
plugin/lighthouse/util/disassembler/binja_api.py | 8 +++++++-
plugin/lighthouse/util/disassembler/ida_api.py | 1 +
plugin/lighthouse/util/update.py | 4 ++--
3 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 740b5cfc..211e6531 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -327,7 +327,13 @@ def __symbol_handler(self, view, symbol):
func = self._bv.get_function_at(symbol.address)
if not func.start == symbol.address:
return
- self.renamed(symbol.address, symbol.name)
+ self.name_changed(symbol.address, symbol.name)
+
+ def name_changed(self, address, name):
+ """
+ A placeholder callback, which will get hooked / replaced once live.
+ """
+ pass
#------------------------------------------------------------------------------
# UI
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index ecd95735..59364c3b 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -338,6 +338,7 @@ def busy(self):
# API Shims
#--------------------------------------------------------------------------
+ @IDACoreAPI.execute_read
def get_current_address(self):
return idaapi.get_screen_ea()
diff --git a/plugin/lighthouse/util/update.py b/plugin/lighthouse/util/update.py
index f46c4fa4..c99c6a33 100644
--- a/plugin/lighthouse/util/update.py
+++ b/plugin/lighthouse/util/update.py
@@ -38,8 +38,8 @@ def async_update_check(current_version, callback):
html = response.read()
info = json.loads(html)
remote_version = info["tag_name"]
- except Exception as e:
- logger.exception(" - Failed to reach GitHub for update check...")
+ except Exception:
+ logger.debug(" - Failed to reach GitHub for update check...")
return
# convert vesrion #'s to integer for easy compare...
From c2ceb47b847b1c2efea7dab53495494c4745ccc1 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 01:38:40 -0400
Subject: [PATCH 128/154] totally ovehaul painting to support notion of
'streaming'
---
plugin/lighthouse/painting/binja_painter.py | 43 +--
plugin/lighthouse/painting/ida_painter.py | 243 ++++++---------
plugin/lighthouse/painting/painter.py | 311 +++++++++++---------
plugin/lighthouse/ui/coverage_settings.py | 33 +--
4 files changed, 289 insertions(+), 341 deletions(-)
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index 46173535..d807fc87 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -17,7 +17,6 @@ class BinjaPainter(DatabasePainter):
"""
Asynchronous Binary Ninja database painter.
"""
- PAINTER_SLEEP = 0.01
def __init__(self, lctx, director, palette):
super(BinjaPainter, self).__init__(lctx, director, palette)
@@ -51,32 +50,39 @@ def _partial_paint(self, bv, instructions, color):
func.set_auto_instr_highlight(address, color)
self._painted_instructions |= set(instructions)
- def _paint_nodes(self, nodes_coverage):
+ def _paint_nodes(self, node_addresses):
bv = disassembler[self.lctx].bv
+ db_coverage = self.director.coverage
+ db_metadata = self.director.metadata
r, g, b, _ = self.palette.coverage_paint.getRgb()
color = HighlightColor(red=r, green=g, blue=b)
- for node_coverage in nodes_coverage:
- node_metadata = node_coverage.database._metadata.nodes[node_coverage.address]
+ for node_address in node_addresses:
+ node_metadata = db_metadata.nodes[node_address]
+ node_coverage = db_coverage.nodes[node_address]
# special case for nodes that are only partially executed...
if node_coverage.instructions_executed != node_metadata.instruction_count:
self._partial_paint(bv, node_coverage.executed_instructions.keys(), color)
continue
- for node in bv.get_basic_blocks_starting_at(node_metadata.address):
+ for node in bv.get_basic_blocks_starting_at(node_address):
node.highlight = color
- self._painted_nodes.add(node_metadata.address)
+ self._painted_nodes |= set(node_addresses)
self._action_complete.set()
- def _clear_nodes(self, nodes_metadata):
+ def _clear_nodes(self, node_addresses):
bv = disassembler[self.lctx].bv
- for node_metadata in nodes_metadata:
- for node in bv.get_basic_blocks_starting_at(node_metadata.address):
+ db_metadata = self.director.metadata
+
+ for node_address in node_addresses:
+ node_metadata = db_metadata.nodes[node_address]
+ for node in bv.get_basic_blocks_starting_at(node_address):
node.highlight = HighlightStandardColor.NoHighlightColor
- self._painted_nodes.discard(node_metadata.address)
+
+ self._painted_nodes -= set(node_addresses)
self._action_complete.set()
def _refresh_ui(self):
@@ -85,20 +91,3 @@ def _refresh_ui(self):
def _cancel_action(self, job):
pass
- #--------------------------------------------------------------------------
- # Priority Painting
- #--------------------------------------------------------------------------
-
- def _priority_paint(self):
- disassembler_ctx = disassembler[self.lctx]
- db_metadata = self.director.metadata
-
- current_address = disassembler_ctx.get_current_address()
- current_function = disassembler_ctx.bv.get_function_at(current_address)
- function_metadata = db_metadata.get_closest_function(current_address)
-
- if current_function and function_metadata:
- self._paint_function(current_function.start)
-
- return True
-
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 85591276..292253e6 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -1,10 +1,11 @@
import struct
+import ctypes
import logging
import functools
import idc
import idaapi
-from idaapi import clr_abits, set_abits, netnode
+from idaapi import clr_abits, set_abits, netnode, set_node_info
from lighthouse.util import *
from lighthouse.util.disassembler import disassembler
@@ -120,89 +121,77 @@ class IDAPainter(DatabasePainter):
"""
def __init__(self, lctx, director, palette):
+ super(IDAPainter, self).__init__(lctx, director, palette)
+ self._streaming_instructions = True
+
+ # see the MFF_NOWAIT workaround details above
+ self._signal = ToMainthread()
#----------------------------------------------------------------------
# HexRays Hooking
#----------------------------------------------------------------------
-
+ #
+ # TODO/COMMENT
#
# we attempt to hook hexrays the *first* time a repaint request is
# made. the assumption being that IDA is fully loaded and if hexrays is
# present, it will definitely be available (for hooking) by this time
#
+ self._idp_hooks = InstructionPaintHooks(director, palette)
self._attempted_hook = False
- # see the MFF_NOWAIT workaround details above
- self._signal = ToMainthread()
+ def terminate(self):
- # continue normal painter initialization
- super(IDAPainter, self).__init__(lctx, director, palette)
+ #
+ # IDA is either closing or simply switching databases... we should try
+ # to unhook our processor hooks so that artifacts of this painter do
+ # not carry over to the next IDB / session.
+ #
+ # if we don't do this, our current 'IDP' hooks will continue to fire
+ # once the next IDB is open. we don't want this, because a new painter
+ # will be spun up an it will install its own instance of hooks...
+ #
+
+ if self._idp_hooks:
+ self._idp_hooks.unhook()
+ self._idp_hooks = None
+
+ # spin down the painter as usual
+ super(IDAPainter, self).terminate()
def repaint(self):
"""
Paint coverage defined by the current database mappings.
"""
-
- # attempt to hook hexrays *once*
if not self._attempted_hook:
- self._init_hexrays_hooks()
+ self._init_ida_hooks()
self._attempted_hook = True
# execute underlying repaint function
super(IDAPainter, self).repaint()
- #------------------------------------------------------------------------------
- # Paint Actions
- #------------------------------------------------------------------------------
-
- #
- # NOTE:
- # these are 'internal' functions meant only to be used by the painter.
- # they are decorated with @execute_paint to force execution into the
- # mainthread, where it is safe to paint (in IDA)
- #
-
- @execute_paint
- def _paint_instructions(self, instructions):
- self.paint_instructions(instructions)
- self._action_complete.set()
-
- @execute_paint
- def _clear_instructions(self, instructions):
- self.clear_instructions(instructions)
- self._action_complete.set()
-
- @execute_paint
- def _paint_nodes(self, node_addresses):
- self.paint_nodes(node_addresses)
- self._action_complete.set()
-
- @execute_paint
- def _clear_nodes(self, node_addresses):
- self.clear_nodes(node_addresses)
- self._action_complete.set()
+ def _notify_status_changed(self, status):
- @execute_paint
- def _refresh_ui(self):
- """
- Note that this has been decorated with @execute_paint (vs @execute_ui)
- to help avoid deadlocking on exit.
- """
- idaapi.refresh_idaview_anyway()
+ # enable / disable hook based on the painter being enabled or disabled
+ if status:
+ self._idp_hooks.hook()
+ else:
+ self._idp_hooks.unhook()
- def _cancel_action(self, job_id):
- if idaapi.IDA_SDK_VERSION < 710:
- return
- idaapi.cancel_exec_request(job_id)
+ # send the status changed signal...
+ super(IDAPainter, self)._notify_status_changed(status)
#------------------------------------------------------------------------------
# Paint Actions
#------------------------------------------------------------------------------
- def paint_instructions(self, instructions):
+ @execute_paint
+ def _paint_instructions(self, instructions):
"""
Paint instruction level coverage defined by the current database mapping.
+
+ NOTE: we now use 'streaming' mode for instructions rather than this.
"""
color = struct.pack("I", self.palette.coverage_paint+1)
for address in instructions:
@@ -210,16 +199,22 @@ def paint_instructions(self, instructions):
nn = netnode(address)
nn.supset(20, color, 'A')
self._painted_instructions |= set(instructions)
+ self._action_complete.set()
- def clear_instructions(self, instructions):
+ @execute_paint
+ def _clear_instructions(self, instructions):
"""
Clear paint from the given instructions.
+
+ NOTE: we now use 'streaming' mode for instructions rather than this.
"""
for address in instructions:
clr_abits(address, 0x40000)
self._painted_instructions -= set(instructions)
+ self._action_complete.set()
- def paint_nodes(self, node_addresses):
+ @execute_paint
+ def _paint_nodes(self, node_addresses):
"""
Paint node level coverage defined by the current database mappings.
"""
@@ -256,8 +251,10 @@ def paint_nodes(self, node_addresses):
)
self._painted_nodes |= set(node_addresses)
+ self._action_complete.set()
- def clear_nodes(self, node_addresses):
+ @execute_paint
+ def _clear_nodes(self, node_addresses):
"""
Clear paint from the given graph nodes.
"""
@@ -288,22 +285,37 @@ def clear_nodes(self, node_addresses):
)
self._painted_nodes -= set(node_addresses)
+ self._action_complete.set()
+
+ @execute_paint
+ def _refresh_ui(self):
+ """
+ Note that this has been decorated with @execute_paint (vs @execute_ui)
+ to help avoid deadlocking on exit.
+ """
+ idaapi.refresh_idaview_anyway()
+
+ def _cancel_action(self, job_id):
+ if idaapi.IDA_SDK_VERSION < 710:
+ return
+ idaapi.cancel_exec_request(job_id)
#------------------------------------------------------------------------------
# Painting - HexRays (Decompilation / Source)
#------------------------------------------------------------------------------
- def _init_hexrays_hooks(self):
+ def _init_ida_hooks(self):
"""
- Install Hex-Rays hooks (when available).
+ Install IDA-specific painting hooks.
"""
result = False
+ # install hexrays hooks (if available)
if idaapi.init_hexrays_plugin():
logger.debug("HexRays present, installing hooks...")
result = idaapi.install_hexrays_callback(self._hxe_callback)
-
logger.debug("HexRays hooked: %r" % result)
+ #self._idp_hooks.hook()
def paint_hexrays(self, cfunc, db_coverage):
"""
@@ -409,106 +421,19 @@ def _hxe_callback(self, event, *args):
return 0
- #------------------------------------------------------------------------------
- # Priority Painting
- #------------------------------------------------------------------------------
-
- def _priority_paint(self):
- """
- Immediately repaint regions of the database visible to the user.
- """
- cursor_address = disassembler.execute_read(idaapi.get_screen_ea)()
-
- # paint functions around the cursor address
- if not self._priority_paint_functions(cursor_address):
- return False # a repaint was requested
-
- # paint instructions around the cursor address
- #if not self._priority_paint_instructions(cursor_address):
- # return False # a repaint was requested
-
- # refresh the view
- self._refresh_ui()
-
- # successful completion
- return True
-
- def _priority_paint_functions(self, target_address):
- """
- Paint functions in the immediate vicinity of the given address.
-
- This will paint both the instructions & graph nodes of defined functions.
- """
- db_metadata = self.director.metadata
- db_coverage = self.director.coverage
-
- # the number of functions before and after the cursor to paint
- FUNCTION_BUFFER = 1
-
- # get the function metadata for the function closest to our cursor
- function_metadata = db_metadata.get_closest_function(target_address)
- if not function_metadata:
- return False # a repaint was requested
-
- # select the range of functions around us that we would like to paint
- func_num = db_metadata.get_function_index(function_metadata.address)
- func_num_start = max(func_num - FUNCTION_BUFFER, 0)
- func_num_end = min(func_num + FUNCTION_BUFFER + 1, len(db_metadata.functions))
-
- # repaint the specified range of functions
- for current_num in xrange(func_num_start, func_num_end):
-
- # get the next function to paint
- function_metadata = db_metadata.get_function_by_index(current_num)
- if not function_metadata:
- continue
- function_address = function_metadata.address
-
- # get the function coverage data for the target address
- function_coverage = db_coverage.functions.get(function_address, None)
-
- # if there is no function coverage, clear the function
- if not function_coverage:
- if not self._clear_function(function_address):
- return False # a repaint was requested
- continue
-
- # there is coverage, so repaint the function
- if not self._paint_function(function_address):
- return False # a repaint was requested
-
- # paint finished successfully
- return True
-
- def _priority_paint_instructions(self, target_address):
- """
- Paint instructions in the immediate vicinity of the given address.
- """
- db_metadata = self.director.metadata
- db_coverage = self.director.coverage
-
- # the number of instruction bytes before and after the cursor to paint
- INSTRUCTION_BUFFER = 200
-
- # determine range of instructions to repaint
- start_address = max(target_address - INSTRUCTION_BUFFER, 0)
- end_address = target_address + INSTRUCTION_BUFFER
- instructions = set(db_metadata.get_instructions_slice(start_address, end_address))
-
- # mask only the instructions with coverage data in this region
- instructions_coverage = instructions & db_coverage.coverage
-
- #
- # clear all instructions in this region, repaint the coverage data
- #
-
- # clear instructions
- if not self._async_action(self._clear_instructions, instructions):
- return False # a repaint was requested
-
- # paint instructions
- if not self._async_action(self._paint_instructions, instructions_coverage):
- return False # a repaint was requested
+class InstructionPaintHooks(idaapi.IDP_Hooks):
+ """
+ TODO/COMMENT
+ """
- # paint finished successfully
- return True
+ def __init__(self, director, palette):
+ super(InstructionPaintHooks, self).__init__()
+ self.director = director
+ self.palette = palette
+
+ def ev_get_bg_color(self, pcolor, ea):
+ if ea not in self.director.coverage.coverage:
+ return 0
+ bgcolor = ctypes.cast(int(pcolor), ctypes.POINTER(ctypes.c_int))
+ bgcolor[0] = self.palette.coverage_paint
+ return 1
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index e32ec130..add01946 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -4,6 +4,7 @@
import threading
from lighthouse.util import *
+from lighthouse.coverage import FunctionCoverage
logger = logging.getLogger("Lighthouse.Painting")
@@ -13,12 +14,11 @@ class DatabasePainter(object):
"""
__metaclass__ = abc.ABCMeta
- PAINTER_SLEEP = 0.01
-
MSG_TERMINATE = 0
MSG_REPAINT = 1
MSG_CLEAR = 2
- MSG_REBASE = 3
+ MSG_FORCE_CLEAR = 3
+ MSG_REBASE = 4
def __init__(self, lctx, director, palette):
@@ -45,6 +45,21 @@ def __init__(self, lctx, director, palette):
self._painted_nodes = set()
self._painted_instructions = set()
+ #
+ # these toggles will let the core painter (this class) know that it
+ # does not have to order explicit paints of instructions or nodes.
+ #
+ # this is because a disassembler-specific painter may be able to hook
+ # unique callbacks for painting graphs nodes or instructions
+ # 'on-the-fly' as they are rendered.
+ #
+ # these types of paints are ephermal and the most performant, they
+ # also will not need to be tracked by the painter.
+ #
+
+ self._streaming_nodes = False
+ self._streaming_instructions = False
+
#----------------------------------------------------------------------
# Async
#----------------------------------------------------------------------
@@ -145,33 +160,29 @@ def repaint(self):
"""
Paint coverage defined by the current database mappings.
"""
- if not self.enabled:
- return
- self._msg_queue.put(self.MSG_REPAINT)
+ self._send_message(self.MSG_REPAINT)
- def clear_paint(self):
+ def force_clear(self):
"""
Clear all paint from the current database (based on metadata)
"""
-
- #
- # we should only disable the painter (as a result of clear_paint()) if
- # the user has coverage open & in use. for example, there is no reason
- # to *preemptively* disable painting if no other coverage is loaded.
- #
-
- if self.enabled and len(self.director.coverage_names):
- self.set_enabled(False)
-
- # trigger the database clear
- self._msg_queue.put(self.MSG_CLEAR)
+ self._send_message(self.MSG_FORCE_CLEAR)
+ self.set_enabled(False)
def check_rebase(self):
"""
Perform a rebase on the painted data cache (if necessary).
"""
- self._msg_queue.put(self.MSG_REBASE)
- self._msg_queue.put(self.MSG_REPAINT)
+ self._send_message(self.MSG_REBASE)
+ self._send_message(self.MSG_REPAINT)
+
+ def _send_message(self, message):
+ """
+ Queue a painter command for execution.
+ """
+ if not self._started:
+ return
+ self._msg_queue.put(message)
#--------------------------------------------------------------------------
# Commands
@@ -239,75 +250,86 @@ def _cancel_action(self, job):
# Painting - High Level
#------------------------------------------------------------------------------
- def _paint_function(self, address):
+ def _priority_paint(self):
"""
- Paint function instructions & nodes with the current database mappings.
+ Immediately repaint regions of the database visible to the user.
+
+ Return True upon completion, or False if interrupted.
"""
- function_metadata = self.director.metadata.functions[address]
- function_coverage = self.director.coverage.functions.get(address, None)
- if not function_coverage:
- return False
+ if self._streaming_instructions and self._streaming_nodes:
+ return True
- #
- # ~ compute paint job ~
- #
+ # get current function / user location in the database
+ cursor_address = disassembler[self.lctx].get_current_address()
- # compute the painted instructions within this function
- painted = self._painted_instructions & function_metadata.instructions
+ # attempt to paint the functions in the immediate cursor vicinity
+ result = self._priority_paint_functions(cursor_address)
- # compute the painted instructions that will not get painted over
- stale_instructions = painted - function_coverage.instructions
+ # force a refresh *now* as this is a prority painting
+ self._refresh_ui()
- # compute the painted nodes within this function
- painted = self._painted_nodes & viewkeys(function_metadata.nodes)
+ # all done
+ return result
- # compute the painted nodes that will not get painted over
- stale_nodes_ea = painted - viewkeys(function_coverage.nodes)
- stale_nodes_ea |= (painted & function_coverage.database.partial_nodes)
- stale_nodes = [function_metadata.nodes[ea] for ea in stale_nodes_ea]
+ def _priority_paint_functions(self, target_address, neighbors=1):
+ """
+ Paint functions in the immediate vicinity of the given address.
- # active instructions
- instructions = function_coverage.instructions
- nodes = itervalues(function_coverage.nodes)
+ This will paint both the instructions & graph nodes of defined functions.
+ """
+ db_metadata = self.director.metadata
+ db_coverage = self.director.coverage
+ blank_coverage = FunctionCoverage(BADADDR)
- #
- # ~ painting ~
- #
+ # get the function metadata for the function closest to our cursor
+ function_metadata = db_metadata.get_closest_function(target_address)
+ if not function_metadata:
+ return False
- # clear instructions
- if not self._async_action(self._clear_instructions, stale_instructions):
- return False # a repaint was requested
+ # select the range of functions around us that we would like to paint
+ func_num = db_metadata.get_function_index(function_metadata.address)
+ func_num_start = max(func_num - neighbors, 0)
+ func_num_end = min(func_num + neighbors + 1, len(db_metadata.functions) - 1)
- # clear nodes
- if not self._async_action(self._clear_nodes, stale_nodes):
- return False # a repaint was requested
+ # repaint the specified range of functions
+ for current_num in xrange(func_num_start, func_num_end):
- # paint instructions
- if not self._async_action(self._paint_instructions, instructions):
- return False # a repaint was requested
+ # get the next function to paint
+ function_metadata = db_metadata.get_function_by_index(current_num)
+ if not function_metadata:
+ continue
- # paint nodes
- if not self._async_action(self._paint_nodes, nodes):
- return False # a repaint was requested
+ # get the function coverage data for the target address
+ function_address = function_metadata.address
+ function_coverage = db_coverage.functions.get(function_address, blank_coverage)
- # paint finished successfully
- return True
+ if not self._streaming_nodes:
- def _clear_function(self, address):
- """
- Clear paint from the given function.
- """
- function_metadata = self.director.metadata.functions[address]
- instructions = function_metadata.instructions
- nodes = itervalues(function_metadata.nodes)
+ # clear nodes
+ must_clear = sorted(set(function_metadata.nodes) - set(function_coverage.nodes))
+ self._action_complete.clear()
+ self._clear_nodes(must_clear)
+ self._action_complete.wait()
- # clear instructions
- if not self._async_action(self._clear_instructions, instructions):
- return False # a repaint was requested
+ # paint nodes
+ must_paint = sorted(function_coverage.nodes)
+ self._action_complete.clear()
+ self._paint_nodes(must_paint)
+ self._action_complete.wait()
- # clear nodes
- if not self._async_action(self._clear_nodes, nodes):
- return False # a repaint was requested
+ if not self._streaming_instructions:
+
+ # clear instructions
+ must_clear = sorted(function_metadata.instructions - function_coverage.instructions)
+ self._action_complete.clear()
+ self._clear_instructions(must_clear)
+ self._action_complete.wait()
+
+ # paint instructions
+ must_paint = sorted(function_coverage.instructions)
+ self._action_complete.clear()
+ self._paint_instructions(must_paint)
+ self._action_complete.wait()
# paint finished successfully
return True
@@ -329,71 +351,108 @@ def _paint_database(self):
if self._imagebase == BADADDR:
self._imagebase = db_metadata.imagebase
- # abandon painting early if it appears a rebase has occurred
- elif self._imagebase != db_metadata.imagebase:
- return False
-
# immediately paint user-visible regions of the database
if not self._priority_paint():
return False # a repaint was requested
- # compute the painted instructions that will not get painted over
- stale_partial_inst = self._painted_instructions & db_coverage.partial_instructions
- stale_instr = self._painted_instructions - db_coverage.coverage
- stale_instr |= stale_partial_inst
+ #
+ # if the painter is not capable of 'streaming' the coverage paint,
+ # then we must explicitly paint the instructions & nodes here
+ #
+
+ if not self._streaming_instructions:
- # clear old instruction paint
- if not self._async_action(self._clear_instructions, stale_instr):
- return False # a repaint was requested
+ # compute the painted instructions that will not get painted over
+ stale_partial_inst = self._painted_instructions & db_coverage.partial_instructions
+ stale_instr = self._painted_instructions - db_coverage.coverage
+ stale_instr |= stale_partial_inst
- # compute the painted nodes that will not get painted over
- stale_nodes = self._painted_nodes - viewkeys(db_coverage.nodes)
- stale_nodes |= db_coverage.partial_nodes
+ # clear old instruction paint
+ if not self._async_action(self._clear_instructions, stale_instr):
+ return False # a repaint was requested
- # clear old node paint
- if not self._async_action(self._clear_nodes, stale_nodes):
- return False # a repaint was requested
+ # paint new instructions
+ new_instr = sorted(db_coverage.coverage - self._painted_instructions)
+ if not self._async_action(self._paint_instructions, new_instr):
+ return False # a repaint was requested
- # paint new instructions
- new_instr = sorted(db_coverage.coverage - self._paint_instructions)
- if not self._async_action(self._paint_instructions, new_instr):
- return False # a repaint was requested
+ if not self._streaming_nodes:
- # paint new nodes
- new_nodev = sorted(viewkeys(db_coverage.nodes) - self._paint_nodes)
- if not self._async_action(self._paint_nodes, new_nodes):
- return False # a repaint was requested
+ # compute the painted nodes that will not get painted over
+ stale_nodes = self._painted_nodes - viewkeys(db_coverage.nodes)
+ stale_nodes |= db_coverage.partial_nodes
+
+ # clear old node paint
+ if not self._async_action(self._clear_nodes, stale_nodes):
+ return False # a repaint was requested
+
+ # paint new nodes
+ new_nodes = sorted(viewkeys(db_coverage.nodes) - self._painted_nodes)
+ if not self._async_action(self._paint_nodes, new_nodes):
+ return False # a repaint was requested
#------------------------------------------------------------------
end = time.time()
lmsg(" - Painting took %.2f seconds" % (end - start))
- logger.debug(" stale_instr: %s" % "{:,}".format(len(stale_instr)))
- logger.debug(" fresh instr: %s" % "{:,}".format(len(new_instr)))
- logger.debug(" stale_nodes: %s" % "{:,}".format(len(stale_nodes)))
- logger.debug(" fresh_nodes: %s" % "{:,}".format(len(new_nodes)))
+ #logger.debug(" stale_instr: %s" % "{:,}".format(len(stale_instr)))
+ #logger.debug(" fresh instr: %s" % "{:,}".format(len(new_instr)))
+ #logger.debug(" stale_nodes: %s" % "{:,}".format(len(stale_nodes)))
+ #logger.debug(" fresh_nodes: %s" % "{:,}".format(len(new_nodes)))
# paint finished successfully
return True
def _clear_database(self):
"""
- Clear all paint from the current database.
+ Clear all paint from the current database using the known paint state.
"""
lmsg("Clearing database paint...")
start = time.time()
+ #------------------------------------------------------------------
db_metadata = self.director.metadata
- instructions = sorted(db_metadata.instructions)
- nodes = sorted(db_metadata.nodes.keys())
# clear all instructions
- if not self._async_action(self._clear_instructions, instructions):
- return False # a repaint was requested
+ if not self._streaming_instructions:
+ if not self._async_action(self._clear_instructions, self._painted_instructions):
+ return False # a repaint was requested
# clear all nodes
- if not self._async_action(self._clear_nodes, nodes):
- return False # a repaint was requested
+ if not self._streaming_nodes:
+ if not self._async_action(self._clear_nodes, self._painted_nodes):
+ return False # a repaint was requested
+ #------------------------------------------------------------------
+ end = time.time()
+ lmsg(" - Database paint cleared in %.2f seconds..." % (end-start))
+
+ # sanity checks...
+ assert self._painted_nodes == set()
+ assert self._painted_instructions == set()
+
+ # paint finished successfully
+ return True
+
+ @not_mainthread
+ def _force_clear_database(self):
+ """
+ Forcibly clear the paint from all known database addresses.
+ """
+ lmsg("Forcibly clearing all paint from database...")
+ db_metadata = self.director.metadata
+
+ start = time.time()
+ #------------------------------------------------------------------
+
+ self._action_complete.clear()
+ self._clear_instructions(sorted(db_metadata.instructions))
+ self._action_complete.wait()
+
+ self._action_complete.clear()
+ self._clear_nodes(sorted(db_metadata.nodes))
+ self._action_complete.wait()
+
+ #------------------------------------------------------------------
end = time.time()
lmsg(" - Database paint cleared in %.2f seconds..." % (end-start))
@@ -426,34 +485,6 @@ def _rebase_database(self):
# a rebase has been observed
return True
- #--------------------------------------------------------------------------
- # Priority Painting
- #--------------------------------------------------------------------------
-
- def _priority_paint(self):
- """
- Immediately repaint regions of the database visible to the user.
-
- Return True upon completion, or False if interrupted.
- """
- return True # NOTE: optional, but recommended
-
- def _priority_paint_functions(self, target_address):
- """
- Paint functions in the immediate vicinity of the given address.
-
- This will paint both the instructions & graph nodes of defined functions.
- """
- pass # NOTE: optional, organizational
-
- def _priority_paint_instructions(self, target_address, ignore=set()):
- """
- Paint instructions in the immediate vicinity of the given address.
-
- Optionally, one can provide a set of addresses to ignore while painting.
- """
- pass # NOTE: optional, organizational
-
#--------------------------------------------------------------------------
# Asynchronous Painting
#--------------------------------------------------------------------------
@@ -484,10 +515,14 @@ def _async_database_painter2(self):
if action == self.MSG_REPAINT:
result = self._paint_database()
- # clear all possible database paint
+ # clear database base on the current state
elif action == self.MSG_CLEAR:
result = self._clear_database()
+ # clear all possible database paint
+ elif action == self.MSG_FORCE_CLEAR:
+ result = self._force_clear_database()
+
# check for a rebase of the painted data
elif action == self.MSG_REBASE:
result = self._rebase_database()
diff --git a/plugin/lighthouse/ui/coverage_settings.py b/plugin/lighthouse/ui/coverage_settings.py
index ada6098a..6fa39789 100644
--- a/plugin/lighthouse/ui/coverage_settings.py
+++ b/plugin/lighthouse/ui/coverage_settings.py
@@ -53,30 +53,29 @@ def _ui_init_actions(self):
self.addSeparator()
# painting
- self._action_pause_paint = QtWidgets.QAction("Pause painting", None)
- self._action_pause_paint.setCheckable(True)
- self._action_pause_paint.setToolTip("Disable the coverage painting subsystem")
- self.addAction(self._action_pause_paint)
-
- # misc
- self._action_clear_paint = QtWidgets.QAction("Clear database paint", None)
- self._action_clear_paint.setToolTip("Forcefully clear all paint from the database")
- self.addAction(self._action_clear_paint)
+ self._action_force_clear = QtWidgets.QAction("Force clear paint (slow!)", None)
+ self._action_force_clear.setToolTip("Attempt to forcefully clear stuck paint from the database")
+ self.addAction(self._action_force_clear)
+
+ self._action_disable_paint = QtWidgets.QAction("Disable painting", None)
+ self._action_disable_paint.setCheckable(True)
+ self._action_disable_paint.setToolTip("Disable the coverage painting subsystem")
+ self.addAction(self._action_disable_paint)
self.addSeparator()
# table actions
- self._action_refresh_metadata = QtWidgets.QAction("Full table refresh", None)
+ self._action_refresh_metadata = QtWidgets.QAction("Rebuild coverage mappings", None)
self._action_refresh_metadata.setToolTip("Refresh the database metadata and coverage mapping")
self.addAction(self._action_refresh_metadata)
- 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)
-
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)
+
self._action_hide_zero = QtWidgets.QAction("Hide 0% coverage", None)
self._action_hide_zero.setToolTip("Hide table entries with no coverage data")
self._action_hide_zero.setCheckable(True)
@@ -89,8 +88,8 @@ def connect_signals(self, controller, lctx):
self._action_change_theme.triggered.connect(lctx.core.palette.interactive_change_theme)
self._action_refresh_metadata.triggered.connect(lctx.director.refresh)
self._action_hide_zero.triggered[bool].connect(controller._model.filter_zero_coverage)
- self._action_pause_paint.triggered[bool].connect(lambda x: lctx.painter.set_enabled(not x))
- self._action_clear_paint.triggered.connect(lctx.painter.clear_paint)
+ 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)
@@ -104,4 +103,4 @@ def _ui_painter_changed_status(self, painter_enabled):
"""
Handle an event from the painter being enabled/disabled.
"""
- self._action_pause_paint.setChecked(not painter_enabled)
+ self._action_disable_paint.setChecked(not painter_enabled)
From b2c6695042c86cde08c1866e80e7872a28566150 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 02:03:03 -0400
Subject: [PATCH 129/154] improve double click jump precision to go to first
block with coverage in function
---
plugin/lighthouse/ui/coverage_table.py | 26 ++++++++++++++++++-
.../lighthouse/util/disassembler/binja_api.py | 17 ++++++++++--
.../lighthouse/util/disassembler/ida_api.py | 2 +-
3 files changed, 41 insertions(+), 4 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index afa97634..09e94f9d 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -537,8 +537,32 @@ def navigate_to_function(self, row):
Navigate to the function depicted by the given row.
"""
lctx = self._model._director.metadata.lctx # TODO dirty
+
+ # get the clicked function address
function_address = self._model.row2func[row]
- disassembler[lctx].navigate_to_function(function_address, function_address)
+
+ #
+ # if there is actually coverage in the function, attempt to locate the
+ # first block (or any block) with coverage and set that as our target
+ #
+
+ function_coverage = lctx.director.coverage.functions.get(function_address, None)
+ if function_coverage:
+ if function_address in function_coverage.nodes:
+ target_address = function_address
+ else:
+ target_address = sorted(function_coverage.nodes)[0]
+
+ #
+ # if the user clicked a function with no coverage, we should just
+ # navigate to the top of the function... nothing fancy
+ #
+
+ else:
+ target_address = function_address
+
+ # navigate to the target function + block
+ disassembler[lctx].navigate_to_function(function_address, target_address)
def toggle_column_alignment(self, column):
"""
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 211e6531..8253ef62 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -270,8 +270,21 @@ def navigate_to_function(self, function_address, address):
# an address/node that is shared between two functions
#
- func = self.bv.get_function_at(address)
- if not func:
+ funcs = self.bv.get_functions_containing(address)
+ if not funcs:
+ return False
+
+ #
+ # try to find the function that contains our target (address) and has
+ # a matching function start...
+ #
+
+ for func in funcs:
+ if func.start == function_address:
+ break
+
+ # no matching function ???
+ else:
return False
dh = DockHandler.getActiveDockHandler()
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 59364c3b..ebf60faa 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -364,7 +364,7 @@ def navigate(self, address):
return idaapi.jumpto(address)
def navigate_to_function(self, function_address, address):
- return self.navigate(function_address)
+ return self.navigate(address)
def set_function_name_at(self, function_address, new_name):
idaapi.set_name(function_address, new_name, idaapi.SN_NOWARN)
From 4d94680b946ad1b62ddc7da291147a22cd980bce Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 14:01:04 -0400
Subject: [PATCH 130/154] better resource cleanup when unloading (helps IDA
close faster...)
---
plugin/lighthouse/director.py | 9 +++++++++
plugin/lighthouse/integration/ida_loader.py | 12 +++++++++---
plugin/lighthouse/metadata.py | 6 ++++++
plugin/lighthouse/painting/painter.py | 5 +++++
4 files changed, 29 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index d0a0c805..a31eb1e4 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -226,6 +226,15 @@ def terminate(self):
except RuntimeError:
pass
+ # best effort to free up resources & improve interpreter spindown
+ del self._special_coverage
+ del self._database_coverage
+ del self._coverage_switched_callbacks
+ del self._coverage_modified_callbacks
+ del self._coverage_created_callbacks
+ del self._coverage_deleted_callbacks
+ del self._composition_cache
+
#--------------------------------------------------------------------------
# Properties
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/integration/ida_loader.py b/plugin/lighthouse/integration/ida_loader.py
index 2ddf669c..32506397 100644
--- a/plugin/lighthouse/integration/ida_loader.py
+++ b/plugin/lighthouse/integration/ida_loader.py
@@ -1,3 +1,4 @@
+import time
import logging
import idaapi
@@ -65,9 +66,6 @@ def init(self):
except Exception as e:
lmsg("Failed to initialize Lighthouse")
logger.exception("Exception details:")
- return idaapi.PLUGIN_SKIP
-
- # tell IDA to keep the plugin loaded (everything is okay)
return idaapi.PLUGIN_KEEP
def run(self, arg):
@@ -80,9 +78,17 @@ def term(self):
"""
This is called by IDA when it is unloading the plugin.
"""
+ logger.debug("IDA term started...")
+
+ start = time.time()
+ logger.debug("-"*50)
try:
self._lighthouse.unload()
self._lighthouse = None
except Exception as e:
logger.exception("Failed to cleanly unload Lighthouse from IDA.")
+ end = time.time()
+ print("-"*50)
+
+ logger.debug("IDA term done... (%.3f seconds...)" % (end-start))
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 5ddb2ebe..4db071c0 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -126,6 +126,12 @@ def terminate(self):
if self._rename_hooks:
self._rename_hooks.unhook()
+ # best effort to free up resources & improve interpreter spindown
+ del self._metadata_modified_callbacks
+ del self._function_renamed_callbacks
+ del self._rebased_callbacks
+ self._clear_cache()
+
#--------------------------------------------------------------------------
# Providers
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index add01946..166f9ca3 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -156,6 +156,11 @@ def terminate(self):
except RuntimeError: # thread was never started...
pass
+ # best effort to free up resources & improve interpreter spindown
+ del self._painted_nodes
+ del self._painted_instructions
+ del self._status_changed_callbacks
+
def repaint(self):
"""
Paint coverage defined by the current database mappings.
From bc77c0ece25c0fc8dbc1101e6a46e5f42e910f5a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 23:01:27 -0400
Subject: [PATCH 131/154] wrap force clearing with a waitbox...
---
plugin/lighthouse/painting/painter.py | 39 ++++++++++++++++------
plugin/lighthouse/util/disassembler/api.py | 7 ++--
plugin/lighthouse/util/qt/waitbox.py | 3 +-
3 files changed, 35 insertions(+), 14 deletions(-)
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 166f9ca3..72267bf1 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -343,7 +343,7 @@ def _paint_database(self):
"""
Repaint the current database based on the current state.
"""
- lmsg("Painting database...")
+ logger.debug("Painting database...")
# more code-friendly, readable aliases (db_XX == database_XX)
db_coverage = self.director.coverage
@@ -398,11 +398,7 @@ def _paint_database(self):
#------------------------------------------------------------------
end = time.time()
- lmsg(" - Painting took %.2f seconds" % (end - start))
- #logger.debug(" stale_instr: %s" % "{:,}".format(len(stale_instr)))
- #logger.debug(" fresh instr: %s" % "{:,}".format(len(new_instr)))
- #logger.debug(" stale_nodes: %s" % "{:,}".format(len(stale_nodes)))
- #logger.debug(" fresh_nodes: %s" % "{:,}".format(len(new_nodes)))
+ logger.debug(" - Painting took %.2f seconds" % (end - start))
# paint finished successfully
return True
@@ -411,7 +407,7 @@ def _clear_database(self):
"""
Clear all paint from the current database using the known paint state.
"""
- lmsg("Clearing database paint...")
+ logger.debug("Clearing database paint...")
start = time.time()
#------------------------------------------------------------------
@@ -429,7 +425,7 @@ def _clear_database(self):
#------------------------------------------------------------------
end = time.time()
- lmsg(" - Database paint cleared in %.2f seconds..." % (end-start))
+ logger.debug(" - Database paint cleared in %.2f seconds..." % (end-start))
# sanity checks...
assert self._painted_nodes == set()
@@ -438,14 +434,33 @@ def _clear_database(self):
# paint finished successfully
return True
- @not_mainthread
def _force_clear_database(self):
"""
Forcibly clear the paint from all known database addresses.
"""
- lmsg("Forcibly clearing all paint from database...")
db_metadata = self.director.metadata
+ text = "Forcibly clearing all paint from database..."
+ logger.debug(text)
+
+ #
+ # NOTE: forcefully clearing the database of paint can take a long time
+ # in certain cases, so we want to block the user from doing anything
+ # to the database while we're working.
+ #
+ # we will pop up a waitbox to block them, but we have to be careful as
+ # a *modal* waitbox will conflict with IDA's processing of MFF_WRITE
+ # requests, where it waits for the waitbox to close before processing
+ #
+ # therefore, we put in a little bodge wire here to make sure the
+ # waitbox is *not* modal for IDA... but will be in the normal case.
+ # it also helps that IDA will be busy processing our 'write' requests,
+ # so the UI will be mostly frozen to the user anyway!
+ #
+
+ is_modal = bool(disassembler.NAME != "IDA")
+ disassembler.execute_ui(disassembler.show_wait_box)(text, is_modal)
+
start = time.time()
#------------------------------------------------------------------
@@ -459,7 +474,9 @@ def _force_clear_database(self):
#------------------------------------------------------------------
end = time.time()
- lmsg(" - Database paint cleared in %.2f seconds..." % (end-start))
+
+ logger.debug(" - Database paint cleared in %.2f seconds..." % (end-start))
+ disassembler.execute_ui(disassembler.hide_wait_box)()
# paint finished successfully
return True
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index d1322562..d169767d 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -1,7 +1,10 @@
import abc
+import logging
from ..qt import QT_AVAILABLE, QtGui
+logger = logging.getLogger("Lighthouse.API")
+
#------------------------------------------------------------------------------
# Disassembler API
#------------------------------------------------------------------------------
@@ -180,13 +183,13 @@ def show_dockable(self, dockable_name):
# WaitBox API
#------------------------------------------------------------------------------
- def show_wait_box(self, text):
+ def show_wait_box(self, text, modal=True):
"""
Show the disassembler universal WaitBox.
"""
assert QT_AVAILABLE, "This function can only be used in a Qt runtime"
self._waitbox.set_text(text)
- self._waitbox.show()
+ self._waitbox.show(modal)
def hide_wait_box(self):
"""
diff --git a/plugin/lighthouse/util/qt/waitbox.py b/plugin/lighthouse/util/qt/waitbox.py
index e32b804d..8fb33005 100644
--- a/plugin/lighthouse/util/qt/waitbox.py
+++ b/plugin/lighthouse/util/qt/waitbox.py
@@ -35,7 +35,8 @@ def set_text(self, text):
qta = QtCore.QCoreApplication.instance()
qta.processEvents()
- def show(self):
+ def show(self, modal=True):
+ self.setModal(modal)
result = super(WaitBox, self).show()
qta = QtCore.QCoreApplication.instance()
qta.processEvents()
From a789220b56eb51478d30c1bc99d6a52471f28d5a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Fri, 17 Apr 2020 23:46:34 -0400
Subject: [PATCH 132/154] improve metadata collection
---
plugin/lighthouse/metadata.py | 34 +++++++---------------------------
1 file changed, 7 insertions(+), 27 deletions(-)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 4db071c0..ada25dcc 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -547,27 +547,17 @@ def _sync_collect_metadata(self, function_addresses, progress_callback, progress
while function_addresses:
# split off a chunk of functions to process metadata for
- try:
- addresses_chunk = function_addresses[:CHUNK_SIZE]
- del function_addresses[:CHUNK_SIZE]
-
- # reached the end of the function_addresses list... take whatever is left
- except IndexError:
- addresses_chunk = function_addresses[:]
- function_addresses.clear()
- CHUNK_SIZE = len(addresses_chunk)
+ addresses_chunk = function_addresses[:CHUNK_SIZE]
+ del function_addresses[:CHUNK_SIZE]
# collect metadata from the database
self._cache_functions(addresses_chunk)
# report incremental progress to an optional progress_callback
if progress_callback:
- completed += CHUNK_SIZE
+ completed += CHUNK_SIZE if function_addresses else len(addresses_chunk)
progress_callback(completed, total)
- # sleep some so we don't choke the mainthread
- time.sleep(.001)
-
@not_mainthread
def _async_collect_metadata(self, function_addresses, progress_callback):
"""
@@ -587,27 +577,17 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
# to operate on it if needed
#
- try:
- addresses_chunk = function_addresses[:CHUNK_SIZE]
- del function_addresses[:CHUNK_SIZE]
-
- # reached the end of the function_addresses list... take whatever is left
- except IndexError:
- addresses_chunk = function_addresses[:]
- function_addresses.clear()
- CHUNK_SIZE = len(addresses_chunk)
-
+ addresses_chunk = function_addresses[:CHUNK_SIZE]
+ del function_addresses[:CHUNK_SIZE]
# collect metadata from the database
self._async_cache_functions(addresses_chunk)
-
# report incremental progress to an optional progress_callback
if progress_callback:
- completed += CHUNK_SIZE
+ completed += CHUNK_SIZE if function_addresses else len(addresses_chunk)
progress_callback(completed, total)
-
# if the refresh was canceled, stop collecting metadata and bail
if self._stop_threads:
logger.debug("Async metadata collection is bailing!")
@@ -618,7 +598,7 @@ def _async_collect_metadata(self, function_addresses, progress_callback):
break
# sleep some so we don't choke the mainthread
- time.sleep(.0015)
+ time.sleep(.015)
# the refresh either completed, or it is going synchronous!
return False
From 4708422c6a2d03defc3dc199ca5bc436c391576b Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 Apr 2020 02:19:42 -0400
Subject: [PATCH 133/154] improve the stability of the IDA painter
---
plugin/lighthouse/painting/ida_painter.py | 49 +++++++++++++++++++----
plugin/lighthouse/painting/painter.py | 7 +++-
2 files changed, 47 insertions(+), 9 deletions(-)
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 292253e6..b92dbad4 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -232,16 +232,37 @@ def _paint_nodes(self, node_addresses):
#
for node_address in node_addresses:
- node_coverage = db_coverage.nodes[node_address]
- node_metadata = db_metadata.nodes[node_address]
+
+ # 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)
+
+ #
+ # if we did not get *everything* that we needed, then it is
+ # possible the database changesd, 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):
+ 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
- # get the function address for this node (there should only be one...)
- function_metadata = db_metadata.get_functions_by_node(node_address)[0]
-
# do the *actual* painting of a single node instance
set_node_info(
function_metadata.address,
@@ -271,10 +292,22 @@ def _clear_nodes(self, node_addresses):
#
for node_address in node_addresses:
- node_metadata = db_metadata.nodes[node_address]
- # get the function address for this node (there should only be one...)
- function_metadata = db_metadata.get_functions_by_node(node_address)[0]
+ # 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
+ #
+
+ if not (node_metadata and functions):
+ 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(
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index 72267bf1..b9d7a52b 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -14,6 +14,7 @@ class DatabasePainter(object):
"""
__metaclass__ = abc.ABCMeta
+ MSG_ABORT = -1
MSG_TERMINATE = 0
MSG_REPAINT = 1
MSG_CLEAR = 2
@@ -450,7 +451,7 @@ def _force_clear_database(self):
#
# we will pop up a waitbox to block them, but we have to be careful as
# a *modal* waitbox will conflict with IDA's processing of MFF_WRITE
- # requests, where it waits for the waitbox to close before processing
+ # requests making it wait for the waitbox to close before processing
#
# therefore, we put in a little bodge wire here to make sure the
# waitbox is *not* modal for IDA... but will be in the normal case.
@@ -549,6 +550,10 @@ def _async_database_painter2(self):
elif action == self.MSG_REBASE:
result = self._rebase_database()
+ # thrown internally to escape a stale paint, just ignore
+ elif action == self.MSG_ABORT:
+ continue
+
# spin down the painting thread (this thread)
elif action == self.MSG_TERMINATE:
break
From a0367a85da8dadfff841bbf796b249872d448d87 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 Apr 2020 03:29:09 -0400
Subject: [PATCH 134/154] refresh hexrays views automatically
---
plugin/lighthouse/painting/ida_painter.py | 56 ++++++-----------------
plugin/lighthouse/painting/painter.py | 6 +--
2 files changed, 18 insertions(+), 44 deletions(-)
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index b92dbad4..030954c6 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -123,24 +123,12 @@ class IDAPainter(DatabasePainter):
def __init__(self, lctx, director, palette):
super(IDAPainter, self).__init__(lctx, director, palette)
self._streaming_instructions = True
+ self._idp_hooks = InstructionPaintHooks(director, palette)
+ self._vduis = {}
# see the MFF_NOWAIT workaround details above
self._signal = ToMainthread()
- #----------------------------------------------------------------------
- # HexRays Hooking
- #----------------------------------------------------------------------
- #
- # TODO/COMMENT
- #
- # we attempt to hook hexrays the *first* time a repaint request is
- # made. the assumption being that IDA is fully loaded and if hexrays is
- # present, it will definitely be available (for hooking) by this time
- #
-
- self._idp_hooks = InstructionPaintHooks(director, palette)
- self._attempted_hook = False
-
def terminate(self):
#
@@ -160,24 +148,17 @@ def terminate(self):
# spin down the painter as usual
super(IDAPainter, self).terminate()
- def repaint(self):
- """
- Paint coverage defined by the current database mappings.
- """
- if not self._attempted_hook:
- self._init_ida_hooks()
- self._attempted_hook = True
-
- # execute underlying repaint function
- super(IDAPainter, self).repaint()
-
def _notify_status_changed(self, status):
# enable / disable hook based on the painter being enabled or disabled
if status:
self._idp_hooks.hook()
+ if idaapi.init_hexrays_plugin():
+ idaapi.install_hexrays_callback(self._hxe_callback)
else:
self._idp_hooks.unhook()
+ if idaapi.init_hexrays_plugin():
+ idaapi.remove_hexrays_callback(self._hxe_callback)
# send the status changed signal...
super(IDAPainter, self)._notify_status_changed(status)
@@ -326,6 +307,9 @@ def _refresh_ui(self):
Note that this has been decorated with @execute_paint (vs @execute_ui)
to help avoid deadlocking on exit.
"""
+ for vdui in self._vduis.values():
+ if vdui.valid():
+ vdui.refresh_ctext(False)
idaapi.refresh_idaview_anyway()
def _cancel_action(self, job_id):
@@ -337,19 +321,6 @@ def _cancel_action(self, job_id):
# Painting - HexRays (Decompilation / Source)
#------------------------------------------------------------------------------
- def _init_ida_hooks(self):
- """
- Install IDA-specific painting hooks.
- """
- result = False
-
- # install hexrays hooks (if available)
- if idaapi.init_hexrays_plugin():
- logger.debug("HexRays present, installing hooks...")
- result = idaapi.install_hexrays_callback(self._hxe_callback)
- logger.debug("HexRays hooked: %r" % result)
- #self._idp_hooks.hook()
-
def paint_hexrays(self, cfunc, db_coverage):
"""
Paint decompilation text for the given HexRays Window.
@@ -430,9 +401,6 @@ def paint_hexrays(self, cfunc, db_coverage):
decompilation_text[line_number].bgcolor = self.palette.coverage_paint
lines_painted += 1
- # finally, refresh the view
- self._refresh_ui()
-
def _hxe_callback(self, event, *args):
"""
HexRays event handler.
@@ -444,6 +412,7 @@ def _hxe_callback(self, event, *args):
# more code-friendly, readable aliases
vdui = args[0]
cfunc = vdui.cfunc
+ self._vduis[vdui.view_idx] = vdui
# if there's no coverage data for this function, there's nothing to do
if not cfunc.entry_ea in self.director.coverage.functions:
@@ -452,6 +421,11 @@ def _hxe_callback(self, event, *args):
# paint the decompilation text for this function
self.paint_hexrays(cfunc, self.director.coverage)
+ # stop tracking vdui's if they close...
+ elif event == idaapi.hxe_close_pseudocode:
+ vdui = args[0]
+ self._vduis.pop(vdui.view_idx, None)
+
return 0
class InstructionPaintHooks(idaapi.IDP_Hooks):
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index b9d7a52b..ef928736 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -133,15 +133,15 @@ def set_enabled(self, enabled):
lmsg("%s painting..." % ("Enabling" if enabled else "Disabling"))
self._enabled = enabled
+ # notify listeners that the painter has been enabled/disabled
+ self._notify_status_changed(enabled)
+
# paint or clear the database based on the change of status...
if enabled:
self._send_message(self.MSG_REPAINT)
else:
self._send_message(self.MSG_CLEAR)
- # notify listeners that the painter has been enabled/disabled
- self._notify_status_changed(enabled)
-
#--------------------------------------------------------------------------
# Commands
#--------------------------------------------------------------------------
From 9c0ecbc81e2762760706b61ca4da7d40743f8267 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 Apr 2020 20:21:50 -0400
Subject: [PATCH 135/154] a few fixes and tweaks for robustness, performance
---
plugin/lighthouse/director.py | 2 +-
plugin/lighthouse/metadata.py | 17 +++++++++++++----
plugin/lighthouse/painting/binja_painter.py | 19 ++++++++++++++++---
3 files changed, 30 insertions(+), 8 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index a31eb1e4..0f7e0b2e 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1377,7 +1377,7 @@ def _refresh(self):
#
else:
- future = self.metadata.refresh_async(metadata_progress)
+ future = self.metadata.refresh_async(metadata_progress, force=True)
self.metadata.go_synchronous()
await_future(future)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index ada25dcc..f1beae04 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -6,6 +6,7 @@
import threading
import collections
+from lighthouse.util.log import lmsg
from lighthouse.util.misc import *
from lighthouse.util.python import *
from lighthouse.util.disassembler import disassembler
@@ -831,6 +832,7 @@ def _binja_refresh_nodes(self, disassembler_ctx):
function_metadata = self
function_metadata.nodes = {}
bv = disassembler_ctx.bv
+ count = ctypes.c_ulonglong(0)
# get the function from the Binja database
function = bv.get_function_at(self.address)
@@ -860,12 +862,12 @@ def _binja_refresh_nodes(self, disassembler_ctx):
edge_src = node_metadata.edge_out
- count = ctypes.c_ulonglong(0)
+ count.value = 0
edges = core.BNGetBasicBlockOutgoingEdges(node.handle, count)
for i in range(0, count.value):
if edges[i].target:
- function_metadata.edges[edge_src].append(node._create_instance(core.BNNewBasicBlockReference(edges[i].target), bv).start)
+ function_metadata.edges[edge_src].append(node._create_instance(BNNewBasicBlockReference(edges[i].target), bv).start)
core.BNFreeBasicBlockEdgeList(edges, count.value)
# NOTE/PERF ~28% of metadata collection time alone...
@@ -1006,7 +1008,7 @@ def _ida_cache_node(self, _):
#
while current_address < node_end:
- instruction_size = idaapi.get_item_end(current_address) - current_address
+ instruction_size = get_item_end(current_address) - current_address
self.instructions[current_address] = instruction_size
current_address += instruction_size
@@ -1034,7 +1036,7 @@ def _binja_cache_node(self, disassembler_ctx):
#
while current_address < node_end:
- instruction_size = core.BNGetInstructionLength(bh, ah, current_address) or 1
+ instruction_size = BNGetInstructionLength(bh, ah, current_address) or 1
self.instructions[current_address] = instruction_size
current_address += instruction_size
@@ -1115,6 +1117,9 @@ def metadata_progress(completed, total):
FunctionMetadata._refresh_nodes = FunctionMetadata._ida_refresh_nodes
NodeMetadata._cache_node = NodeMetadata._ida_cache_node
+ # pull hot funcs out of module for faster access... (perf)
+ from idaapi import get_item_end
+
elif disassembler.NAME == "BINJA":
import ctypes
import binaryninja
@@ -1122,5 +1127,9 @@ def metadata_progress(completed, total):
FunctionMetadata._refresh_nodes = FunctionMetadata._binja_refresh_nodes
NodeMetadata._cache_node = NodeMetadata._binja_cache_node
+ # pull hot funcs out of module for faster access... (perf)
+ BNGetInstructionLength = core.BNGetInstructionLength
+ BNNewBasicBlockReference = core.BNNewBasicBlockReference
+
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index d807fc87..084ae8d8 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -59,8 +59,14 @@ def _paint_nodes(self, node_addresses):
color = HighlightColor(red=r, green=g, blue=b)
for node_address in node_addresses:
- node_metadata = db_metadata.nodes[node_address]
- node_coverage = db_coverage.nodes[node_address]
+ node_metadata = db_metadata.nodes.get(node_address, None)
+ node_coverage = db_coverage.nodes.get(node_address, None)
+
+ # read comment in ida_painter.py (self._paint_nodes)
+ if not (node_coverage and node_metadata):
+ self._msg_queue.put(self.MSG_ABORT)
+ node_addresses = node_addresses[:node_addresses.index(node_address)]
+ break
# special case for nodes that are only partially executed...
if node_coverage.instructions_executed != node_metadata.instruction_count:
@@ -78,7 +84,14 @@ def _clear_nodes(self, node_addresses):
db_metadata = self.director.metadata
for node_address in node_addresses:
- node_metadata = db_metadata.nodes[node_address]
+ node_metadata = db_metadata.nodes.get(node_address, None)
+
+ # read comment in ida_painter.py (self._paint_nodes)
+ if not node_metadata:
+ self._msg_queue.put(self.MSG_ABORT)
+ node_addresses = node_addresses[:node_addresses.index(node_address)]
+ break
+
for node in bv.get_basic_blocks_starting_at(node_address):
node.highlight = HighlightStandardColor.NoHighlightColor
From 3f33c3cb45b7bf1cbc88f7f8d275ab539a3d0dae Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 Apr 2020 22:15:57 -0400
Subject: [PATCH 136/154] enable live rebasing (at least... for IDA)
---
plugin/lighthouse/director.py | 4 +++
plugin/lighthouse/integration/core.py | 21 --------------
plugin/lighthouse/metadata.py | 42 ++++++++++++++++++++++++++-
3 files changed, 45 insertions(+), 22 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 0f7e0b2e..2412a5d2 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1345,6 +1345,10 @@ def refresh(self):
"""
Complete refresh of the director and mapped coverage.
"""
+ if disassembler[self.metadata.lctx].busy:
+ disassembler.warning("Cannot refresh Lighthouse while the disassembler is busy...")
+ return
+
disassembler.show_wait_box("Refreshing Lighthouse...")
self._refresh()
disassembler.hide_wait_box()
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 39b7e965..b6e26bcb 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -406,24 +406,3 @@ def check_for_update(self):
# kick off the async update check
check_for_update(self.PLUGIN_VERSION, callback)
self._update_checked = True
-
- #--------------------------------------------------------------------------
- # Scheduled
- #--------------------------------------------------------------------------
-
- # TODO/REBASING
- @disassembler.execute_read
- def scheduled(self):
- metadata = self.director.metadata
-
- # get current imagebase
- base = disassembler.get_imagebase()
- lmsg("Imagebase: 0x%08x" % base)
-
- # detect an image rebase
- if (metadata.cached and base != metadata.imagebase) and not disassembler.busy:
- lmsg("Image rebase detected, rebasing Lighthouse metadata...")
- self.director.refresh()
-
- # schedule the next update
- self._scheduled.start(1000)
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index f1beae04..4c96be82 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -6,6 +6,7 @@
import threading
import collections
+from lighthouse.util.qt import QtCore
from lighthouse.util.log import lmsg
from lighthouse.util.misc import *
from lighthouse.util.python import *
@@ -101,6 +102,13 @@ def __init__(self, lctx=None):
self._stop_threads = False
self._go_synchronous = False
+ # a scheduled callback to watch for specific database changes
+ self._scheduled_interval = 2000 # ms
+ self._scheduled_timer = QtCore.QTimer()
+ self._scheduled_timer.setInterval(self._scheduled_interval)
+ self._scheduled_timer.setSingleShot(True)
+ self._scheduled_timer.timeout.connect(self._scheduled_worker)
+
#----------------------------------------------------------------------
# Callbacks
#----------------------------------------------------------------------
@@ -117,7 +125,8 @@ def start(self):
"""
Start the metadata subsystem.
"""
- pass # TODO: rebase scheduled task
+ if self._scheduled_timer:
+ self._scheduled_timer.start()
def terminate(self):
"""
@@ -127,6 +136,12 @@ def terminate(self):
if self._rename_hooks:
self._rename_hooks.unhook()
+ # attempt to stop the scheduled callback... semi-safely :S
+ if self._scheduled_timer:
+ stopping = self._scheduled_timer
+ self._scheduled_timer = None
+ stopping.stop()
+
# best effort to free up resources & improve interpreter spindown
del self._metadata_modified_callbacks
del self._function_renamed_callbacks
@@ -712,6 +727,31 @@ def _notify_rebased(self, old_imagebase, new_imagebase):
"""
notify_callback(self._rebased_callbacks)
+ #--------------------------------------------------------------------------
+ # Scheduled
+ #--------------------------------------------------------------------------
+
+ @disassembler.execute_read
+ def _scheduled_worker(self):
+ """
+ A timed callback to watch for metadata-relevant database changes.
+ """
+ logger.debug("In timed metadata callback...")
+ disassembler_ctx = disassembler[self.lctx]
+
+ # watch for rebase events
+ current_imagebase = disassembler_ctx.get_imagebase()
+ if (self.cached and current_imagebase != self.imagebase):
+
+ # only attempt a rebase if the disassembler seems idle...
+ if not disassembler_ctx.busy:
+ lmsg("Rebasing Lighthouse (0x%X --> 0x%X)" % (self.imagebase, current_imagebase))
+ self.lctx.director.refresh()
+
+ # schedule the next update (ms)
+ if self._scheduled_timer:
+ self._scheduled_timer.start(self._scheduled_interval)
+
#------------------------------------------------------------------------------
# Function Metadata
#------------------------------------------------------------------------------
From feb83fc5d54ca61325f007f9fbd4afcffbf6922d Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sat, 18 Apr 2020 23:13:23 -0400
Subject: [PATCH 137/154] re-enable headless access via
lighthouse.get_context(...)
---
plugin/lighthouse/__init__.py | 2 +-
plugin/lighthouse/context.py | 10 ----------
plugin/lighthouse/integration/core.py | 7 +++++++
plugin/lighthouse/integration/ida_integration.py | 2 +-
plugin/lighthouse_plugin.py | 2 --
5 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/plugin/lighthouse/__init__.py b/plugin/lighthouse/__init__.py
index 8879317c..79ecc714 100644
--- a/plugin/lighthouse/__init__.py
+++ b/plugin/lighthouse/__init__.py
@@ -1 +1 @@
-coverage_director = None
+get_context = lambda x: None
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index 5ba650ec..60891cb7 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -41,10 +41,6 @@ def __init__(self, core, dctx):
# the directory to start the coverage file dialog in
self._last_directory = None
- # TODO/HEADLESS: re-enable
- # expose the live CoverageDirector object instance for external scripts
- #lighthouse.coverage_director = self.director
-
def start(self):
"""
One-time activation a Lighthouse context and its subsystems.
@@ -61,12 +57,6 @@ def terminate(self):
"""
Spin down any session subsystems before the session is deleted.
"""
-
- # TODO/HEADLESS: re-enable
- # remove access to the exposed CoverageDirector
- #lighthouse.coverage_director = None
-
- # spin down the rest of the session subsystems
self.painter.terminate()
self.director.terminate()
self.metadata.terminate()
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index b6e26bcb..bfcd92bc 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -2,6 +2,7 @@
import abc
import logging
+import lighthouse
from lighthouse.util import lmsg
from lighthouse.util.qt import *
from lighthouse.util.update import check_for_update
@@ -55,6 +56,9 @@ def create_coverage_overview(name, parent, dctx):
# install disassembler UI
self._install_ui()
+ # install entry point for headless / terminal access...
+ lighthouse.get_context = self.get_context
+
# plugin loaded successfully, print the plugin banner
self.print_banner()
logger.info("Successfully loaded plugin")
@@ -65,6 +69,9 @@ def unload(self):
"""
self._uninstall_ui()
+ # remove headless entry point
+ lighthouse.get_context = lambda x: None
+
# spin down any active contexts (stop threads, cleanup qt state, etc)
for lctx in self.lighthouse_contexts.values():
lctx.terminate()
diff --git a/plugin/lighthouse/integration/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
index 525ab2f8..40a91065 100644
--- a/plugin/lighthouse/integration/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -32,7 +32,7 @@ def __init__(self):
# run initialization
super(LighthouseIDA, self).__init__()
- def get_context(self, dctx, startup=True):
+ def get_context(self, dctx=None, startup=True):
"""
TODO
"""
diff --git a/plugin/lighthouse_plugin.py b/plugin/lighthouse_plugin.py
index 16611590..1160be95 100644
--- a/plugin/lighthouse_plugin.py
+++ b/plugin/lighthouse_plugin.py
@@ -17,12 +17,10 @@
elif disassembler.NAME == "IDA":
logger.info("Selecting IDA loader...")
from lighthouse.integration.ida_loader import *
- #from lighthouse import coverage_director
elif disassembler.NAME == "BINJA":
logger.info("Selecting Binary Ninja loader...")
from lighthouse.integration.binja_loader import *
- #from lighthouse import coverage_director
else:
raise NotImplementedError("DISASSEMBLER-SPECIFIC SHIM MISSING")
From 7c1573bfd62e91d5a9165e5a968f7cc7ca3b71a6 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 19 Apr 2020 02:33:56 -0400
Subject: [PATCH 138/154] cleanup of TODO's
---
plugin/lighthouse/composer/shell.py | 6 +-
plugin/lighthouse/context.py | 6 +-
plugin/lighthouse/coverage.py | 12 ----
plugin/lighthouse/director.py | 57 ++++++++++++-------
.../integration/binja_integration.py | 6 +-
plugin/lighthouse/integration/core.py | 3 +-
.../lighthouse/integration/ida_integration.py | 5 +-
plugin/lighthouse/metadata.py | 2 -
plugin/lighthouse/painting/ida_painter.py | 6 +-
plugin/lighthouse/reader/coverage_reader.py | 23 +++++---
plugin/lighthouse/ui/coverage_overview.py | 7 ++-
plugin/lighthouse/ui/coverage_table.py | 45 +++++++--------
plugin/lighthouse/util/disassembler/api.py | 31 ++++++++--
.../lighthouse/util/disassembler/binja_api.py | 6 --
.../lighthouse/util/disassembler/ida_api.py | 20 ++-----
plugin/lighthouse/util/qt/util.py | 4 --
16 files changed, 130 insertions(+), 109 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index 6fe13773..b49ae1fd 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -21,13 +21,13 @@ class ComposingShell(QtWidgets.QWidget):
independent, but obviously must communicate with the director.
"""
- def __init__(self, director, table_model, table_view=None):
+ def __init__(self, lctx, table_model, table_view=None):
super(ComposingShell, self).__init__()
self.setObjectName(self.__class__.__name__)
# external entities
- self._director = director
- self._palette = director.palette
+ self._director = lctx.director
+ self._palette = lctx.palette
self._table_model = table_model
self._table_view = table_view
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index 60891cb7..c6eaf50d 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -17,7 +17,7 @@
class LighthouseContext(object):
"""
- TODO/COMMENT
+ A database/binary-unique instance of Lighthouse and its subsystems.
"""
def __init__(self, core, dctx):
@@ -41,6 +41,10 @@ def __init__(self, core, dctx):
# the directory to start the coverage file dialog in
self._last_directory = None
+ @property
+ def palette(self):
+ return self.core.palette
+
def start(self):
"""
One-time activation a Lighthouse context and its subsystems.
diff --git a/plugin/lighthouse/coverage.py b/plugin/lighthouse/coverage.py
index 2ba2b9cb..f3ac1411 100644
--- a/plugin/lighthouse/coverage.py
+++ b/plugin/lighthouse/coverage.py
@@ -560,18 +560,6 @@ def _map_nodes(self):
node_coverage.executed_instructions[address] = self._hitmap[address]
self._unmapped_data.discard(address)
- ##
- ## if the given address allegedly falls within this node's
- ## address range, but doesn't line up with the known
- ## instructions, log it as 'misaligned' / suspicious
- ##
- ## TODO/COV: This will need to be moved as instruction to
- ## node mapping is now guaranteed
- ##
-
- #else:
- # self._misaligned_data.add(address)
-
# get the next address to attempt mapping on
try:
address = coverage_addresses.popleft()
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index 2412a5d2..aa201cf2 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -74,7 +74,14 @@ def __init__(self, metadata, palette):
# a map of loaded or composed database coverages
self._database_coverage = collections.OrderedDict()
- # TODO/COMMENT
+ #
+ # the owners map is used in block/coverage blame operations. it
+ # contains the mapping of node_address --> [ coverage filepaths ]
+ #
+ # given any node (basic block) address, we can use this mapping to do
+ # a reverse lookup to find which loaded coverage sets hit the block.
+ #
+
self.owners = collections.defaultdict(set)
#
@@ -311,8 +318,6 @@ def coverage_created(self, callback):
def _notify_coverage_created(self):
"""
Notify listeners of a coverage creation event.
-
- TODO/FUTURE: send list of names created?
"""
notify_callback(self._coverage_created_callbacks)
@@ -325,8 +330,6 @@ def coverage_deleted(self, callback):
def _notify_coverage_deleted(self):
"""
Notify listeners of a coverage deletion event.
-
- TODO/FUTURE: send list of names deleted?
"""
notify_callback(self._coverage_deleted_callbacks)
@@ -590,17 +593,27 @@ def _extract_coverage_data(self, coverage_file):
def _optimize_coverage_data(self, coverage_addresses):
"""
- Internal routine to optimize raw coverage data to the current metadata.
+ Optimize exploded coverage data to the current metadata cache.
"""
logger.debug("Optimizing coverage data...")
addresses = set(coverage_addresses)
- # bucketize 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 addresses
+ # bucketize the uncategorized exploded addresses
undefined, misaligned = [], []
for address in unknown:
@@ -611,18 +624,15 @@ def _optimize_coverage_data(self, coverage_addresses):
# size == 0 (misaligned inst)
else:
misaligned.append(address)
+ """
#
- # TODO/LOADING: what if there are no defined instructions?
- # TODO/LOADING: display undefined/misaligned data somehow
+ # here we attempt to compute the ratio between basic block addresses,
+ # and instruction addresses in the incoming coverage data.
#
-
- if not instructions:
- logger.debug("No mappable instruction addresses in coverage data")
- return []
-
- #
- # TODO/COMMENT
+ # 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))
@@ -631,7 +641,8 @@ def _optimize_coverage_data(self, coverage_addresses):
#
# a low basic block to instruction ratio implies the data is probably
- # from an instruction trace or has been flattened already.
+ # from an instruction trace, or a basic block trace has been flattened
+ # exploded already (eg, a drcov log)
#
if block_ratio < block_trace_confidence:
@@ -639,15 +650,19 @@ def _optimize_coverage_data(self, coverage_addresses):
return list(instructions)
#
- # take each basic block address, and expand it into a list of
- # presumably executed 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)
- # DONE
logger.debug("Optimized as basic block trace...")
return list(block_instructions | instructions)
diff --git a/plugin/lighthouse/integration/binja_integration.py b/plugin/lighthouse/integration/binja_integration.py
index 2bd03b4c..61fe0826 100644
--- a/plugin/lighthouse/integration/binja_integration.py
+++ b/plugin/lighthouse/integration/binja_integration.py
@@ -24,7 +24,7 @@ def __init__(self):
def get_context(self, dctx, startup=True):
"""
- Get the LighthouseContext object for a given disassembler context.
+ Get the LighthouseContext object for a given database context.
"""
dctx_id = ctypes.addressof(dctx.handle.contents)
@@ -62,7 +62,7 @@ def get_context(self, dctx, startup=True):
lctx = self.lighthouse_contexts[dctx_id]
lctx.start()
- # return the lighthouse context object for this disassembler ctx / bv
+ # return the lighthouse context object for this database ctx / bv
return lctx
#--------------------------------------------------------------------------
@@ -75,7 +75,7 @@ def get_context(self, dctx, startup=True):
#
# this is problematic, because if the user 'clicks' onto the termial, and
# then tries to execute our UIActions (like 'Load Coverage File'), the
- # given 'contxet.binaryView' will be None
+ # 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.
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index bfcd92bc..b1240941 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -3,6 +3,7 @@
import logging
import lighthouse
+
from lighthouse.util import lmsg
from lighthouse.util.qt import *
from lighthouse.util.update import check_for_update
@@ -102,7 +103,7 @@ def print_banner(self):
@abc.abstractmethod
def get_context(self, dctx, startup=True):
"""
- Get the LighthouseContext object for a given disassembler context.
+ Get the LighthouseContext object for a given database context.
"""
pass
diff --git a/plugin/lighthouse/integration/ida_integration.py b/plugin/lighthouse/integration/ida_integration.py
index 40a91065..f89c4ace 100644
--- a/plugin/lighthouse/integration/ida_integration.py
+++ b/plugin/lighthouse/integration/ida_integration.py
@@ -34,7 +34,10 @@ def __init__(self):
def get_context(self, dctx=None, startup=True):
"""
- TODO
+ Get the LighthouseContext object for a given database context.
+
+ NOTE: since IDA can only have one binary / IDB open at a time, the
+ dctx (database context) should always be 'None'.
"""
self.palette.warmup()
diff --git a/plugin/lighthouse/metadata.py b/plugin/lighthouse/metadata.py
index 4c96be82..0a2c9d98 100644
--- a/plugin/lighthouse/metadata.py
+++ b/plugin/lighthouse/metadata.py
@@ -722,8 +722,6 @@ def rebased(self, callback):
def _notify_rebased(self, old_imagebase, new_imagebase):
"""
Notify listeners of a database rebasing event.
-
- TODO/FUTURE: send old / new imagebases
"""
notify_callback(self._rebased_callbacks)
diff --git a/plugin/lighthouse/painting/ida_painter.py b/plugin/lighthouse/painting/ida_painter.py
index 030954c6..3301e3ea 100644
--- a/plugin/lighthouse/painting/ida_painter.py
+++ b/plugin/lighthouse/painting/ida_painter.py
@@ -428,9 +428,13 @@ def _hxe_callback(self, event, *args):
return 0
+#------------------------------------------------------------------------------
+# Instruction Paint Streaming (Processor Hooks)
+#------------------------------------------------------------------------------
+
class InstructionPaintHooks(idaapi.IDP_Hooks):
"""
- TODO/COMMENT
+ Hook IDA's processor callbacks to paint instructions on the fly.
"""
def __init__(self, director, palette):
diff --git a/plugin/lighthouse/reader/coverage_reader.py b/plugin/lighthouse/reader/coverage_reader.py
index 2c95d287..9f580e3b 100644
--- a/plugin/lighthouse/reader/coverage_reader.py
+++ b/plugin/lighthouse/reader/coverage_reader.py
@@ -4,8 +4,8 @@
import logging
import traceback
-from lighthouse.util.python import iteritems
from .coverage_file import CoverageFile
+from lighthouse.util.python import iteritems
from lighthouse.exceptions import CoverageParsingError
logger = logging.getLogger("Lighthouse.Reader")
@@ -14,7 +14,14 @@
class CoverageReader(object):
"""
- TODO/COMMENT
+ Middleware to automatically parse and load different coverage file formats.
+
+ This class will dynamically load and make use of coverage file parsers
+ that subclass from the CoverageFile abstraction and live within the
+ reader's 'parsers' folder.
+
+ This should allow end-users to write parsers for custom coverage file
+ format without having to modify any of Lighthouse's existing code (ideally)
"""
def __init__(self):
@@ -59,7 +66,7 @@ def open(self, filepath):
def _import_parsers(self):
"""
- Scan and import coverage file parsers.
+ Scan and import coverage file parsers from the 'parsers' directory.
"""
target_subclass = CoverageFile
ignored_files = ["__init__.py"]
@@ -93,12 +100,12 @@ def _locate_subclass(self, module_file, target_subclass):
"""
Return the first matching target_subclass in module_file.
- This function is used to scan a specific file (module_file)
- in the Lighthouse parsers directory for class definitions that
- subclass from target_subclass.
+ This function is used to scan a specific file (module_file) in the
+ Lighthouse parsers directory for class definitions that subclass from
+ target_subclass.
- We use this to dynmically import, locate, and return objects
- that are utilizing our CoverageFile abstraction.
+ We use this to dynmically import, locate, and return objects that are
+ utilizing our CoverageFile abstraction.
"""
module = None
module_class = None
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index e2fc7730..84c6b27f 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -92,8 +92,8 @@ def _ui_init_table(self):
"""
Initialize the coverage table.
"""
- self._table_model = CoverageTableModel(self.director, self.widget)
- self._table_controller = CoverageTableController(self._table_model)
+ self._table_model = CoverageTableModel(self.lctx, self.widget)
+ self._table_controller = CoverageTableController(self.lctx, self._table_model)
self._table_view = CoverageTableView(self._table_controller, self._table_model, self.widget)
def _ui_init_toolbar(self):
@@ -122,9 +122,10 @@ def _ui_init_toolbar_elements(self):
"""
Initialize the coverage toolbar UI elements.
"""
+
# the composing shell
self._shell = ComposingShell(
- self.director,
+ self.lctx,
weakref.proxy(self._table_model),
weakref.proxy(self._table_view)
)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 09e94f9d..de3bbe84 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -42,7 +42,7 @@ def refresh_theme(self):
"""
Refresh UI facing elements to reflect the current theme.
"""
- palette = self._model._director.palette
+ palette = self._model.lctx.palette
self.setStyleSheet(
"QTableView {"
" gridline-color: %s;" % palette.table_grid.name() +
@@ -411,7 +411,8 @@ class CoverageTableController(object):
The Coverage Table Controller (Logic)
"""
- def __init__(self, model):
+ def __init__(self, lctx, model):
+ self.lctx = lctx
self._model = model
self._last_directory = None
@@ -424,11 +425,10 @@ def rename_table_function(self, row):
"""
Interactive rename of a database function via the coverage table.
"""
- lctx = self._model._director.metadata.lctx # TODO dirty
# retrieve details about the function targeted for rename
function_address = self._model.row2func[row]
- original_name = disassembler[lctx].get_function_raw_name_at(function_address)
+ original_name = disassembler[self.lctx].get_function_raw_name_at(function_address)
# prompt the user for a new function name
ok, new_name = prompt_string(
@@ -446,14 +446,13 @@ def rename_table_function(self, row):
return
# rename the function
- disassembler[lctx].set_function_name_at(function_address, new_name)
+ disassembler[self.lctx].set_function_name_at(function_address, new_name)
@mainthread
def prefix_table_functions(self, rows):
"""
Interactive prefixing of database functions via the coverage table.
"""
- lctx = self._model._director.metadata.lctx # TODO dirty
# prompt the user for a new function name
ok, prefix = prompt_string(
@@ -468,16 +467,15 @@ def prefix_table_functions(self, rows):
# apply the user prefix to the functions depicted in the given rows
function_addresses = self._get_function_addresses(rows)
- disassembler[lctx].prefix_functions(function_addresses, prefix)
+ disassembler[self.lctx].prefix_functions(function_addresses, prefix)
@mainthread
def clear_function_prefixes(self, rows):
"""
Clear prefixes of database functions via the coverage table.
"""
- lctx = self._model._director.metadata.lctx # TODO dirty
function_addresses = self._get_function_addresses(rows)
- disassembler[lctx].clear_prefixes(function_addresses)
+ disassembler[self.lctx].clear_prefixes(function_addresses)
#---------------------------------------------------------------------------
# Copy-to-Clipboard
@@ -536,7 +534,6 @@ def navigate_to_function(self, row):
"""
Navigate to the function depicted by the given row.
"""
- lctx = self._model._director.metadata.lctx # TODO dirty
# get the clicked function address
function_address = self._model.row2func[row]
@@ -546,7 +543,7 @@ def navigate_to_function(self, row):
# first block (or any block) with coverage and set that as our target
#
- function_coverage = lctx.director.coverage.functions.get(function_address, None)
+ function_coverage = self.lctx.director.coverage.functions.get(function_address, None)
if function_coverage:
if function_address in function_coverage.nodes:
target_address = function_address
@@ -562,7 +559,7 @@ def navigate_to_function(self, row):
target_address = function_address
# navigate to the target function + block
- disassembler[lctx].navigate_to_function(function_address, target_address)
+ disassembler[self.lctx].navigate_to_function(function_address, target_address)
def toggle_column_alignment(self, column):
"""
@@ -584,12 +581,11 @@ def export_to_html(self):
"""
Export the coverage table to an HTML report.
"""
- lctx = self._model._director.metadata.lctx # TODO dirty
if not self._last_directory:
- self._last_directory = disassembler[lctx].get_database_directory()
+ self._last_directory = disassembler[self.lctx].get_database_directory()
# build filename for the coverage report based off the coverage name
- name, _ = os.path.splitext(self._model._director.coverage_name)
+ name, _ = os.path.splitext(self.lctx.director.coverage_name)
filename = name + ".html"
suggested_filepath = os.path.join(self._last_directory, filename)
@@ -693,9 +689,10 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
""
]
- def __init__(self, director, parent=None):
+ def __init__(self, lctx, parent=None):
super(CoverageTableModel, self).__init__(parent)
- self._director = director
+ self.lctx = lctx
+ self._director = lctx.director
# convenience mapping from row_number --> function_address
self.row2func = {}
@@ -708,7 +705,7 @@ def __init__(self, director, parent=None):
# a fallback coverage object for functions with no coverage
self._blank_coverage = FunctionCoverage(BADADDR)
- self._blank_coverage.coverage_color = director.palette.table_coverage_none
+ self._blank_coverage.coverage_color = lctx.palette.table_coverage_none
# set the default column text alignment for each column (centered)
self._default_alignment = QtCore.Qt.AlignCenter
@@ -761,7 +758,7 @@ def refresh_theme(self):
Does not require @disassembler.execute_ui decorator, data_changed() has its own.
"""
- self._blank_coverage.coverage_color = self._director.palette.table_coverage_none
+ self._blank_coverage.coverage_color = self.lctx.palette.table_coverage_none
self._data_changed()
#--------------------------------------------------------------------------
@@ -821,7 +818,7 @@ def data(self, index, role=QtCore.Qt.DisplayRole):
# lookup the function info for this row
try:
function_address = self.row2func[index.row()]
- function_metadata = self._director.metadata.functions[function_address]
+ function_metadata = self.lctx.metadata.functions[function_address]
#
# if we hit a KeyError, it is probably because the database metadata
@@ -921,7 +918,7 @@ def sort(self, column, sort_order):
# column has not been enlightened to sorting
except KeyError as e:
- logger.warning("TODO/FUTURE: implement column %u sorting?" % column)
+ logger.error("ERROR: Sorting not implemented for column %u" % column)
self.layoutChanged.emit()
return
@@ -1023,7 +1020,7 @@ def to_html(self):
"""
Generate an HTML representation of the coverage table.
"""
- palette = self._director.palette
+ palette = self.lctx.palette
# table summary
summary_html, summary_css = self._generate_html_summary()
@@ -1063,7 +1060,7 @@ def _generate_html_summary(self):
"""
Generate the HTML table summary.
"""
- palette = self._director.palette
+ palette = self.lctx.palette
metadata = self._director.metadata
coverage = self._director.coverage
@@ -1107,7 +1104,7 @@ def _generate_html_table(self):
"""
Generate the HTML coverage table.
"""
- palette = self._director.palette
+ palette = self.lctx.palette
table_rows = []
# generate the table's column title row
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index d169767d..19f8c70d 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -22,7 +22,7 @@
class DisassemblerCoreAPI(object):
"""
- An abstract implementation of the required disassembler API.
+ An abstract implementation of the core disassembler APIs.
"""
__metaclass__ = abc.ABCMeta
@@ -158,24 +158,45 @@ def message(self, function_address, new_name):
# UI APIs
#--------------------------------------------------------------------------
+ #
+ # NOTE: please note, these APIs and their usage is a little ... obtuse.
+ # this is primarily because the IDA & Binja dockable widget management
+ # system is rather different.
+ #
+ # these APIs make a best effort in unifiying the systems in a manner that
+ # works for this project. it may not be ideal for the universal use case
+ # but is good enough for our purposes.
+ #
+
@abc.abstractmethod
def register_dockable(self, dockable_name, create_widget_callback):
"""
- TODO/COMMENT
+ Register a callback with the disassembler to generate dockable widgets.
+
+ - dockable_name: the name of the window / dockable to be created
+ - create_widget_callback: a static function that return a new dockable widget
+
+ The registered callback will be called automatically in certain events
+ that will preclude the display of the dockable_name. These events
+ may include a new databse being opened, or show_dockable being called.
+
"""
pass
@abc.abstractmethod
def create_dockable_widget(self, parent, dockable_name):
"""
- TODO/COMMENT
+ Creates a dockable widget.
+
+ This function should generally be called within the create_widget_callback
+ described in register_dockable(...).
"""
pass
@abc.abstractmethod
def show_dockable(self, dockable_name):
"""
- TODO/COMMENT
+ Show the named dockable widget.
"""
pass
@@ -211,7 +232,7 @@ def replace_wait_box(self, text):
class DisassemblerContextAPI(object):
"""
- An abstract implementation of the required binary-specific disassembler API.
+ An abstract implementation of database/contextual disassembler APIs.
"""
__metaclass__ = abc.ABCMeta
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index 8253ef62..ced1369b 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -79,9 +79,6 @@ def run(self):
#------------------------------------------------------------------------------
class BinjaCoreAPI(DisassemblerCoreAPI):
- """
- The Binary Ninja implementation of the disassembler API abstraction.
- """
NAME = "BINJA"
def __init__(self):
@@ -191,9 +188,6 @@ def binja_get_bv_from_dock(self):
return bv
class BinjaContextAPI(DisassemblerContextAPI):
- """
- TODO/COMMENT
- """
def __init__(self, dctx):
super(BinjaContextAPI, self).__init__(dctx)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index ebf60faa..6dc13795 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -58,9 +58,6 @@ def thunk():
#------------------------------------------------------------------------------
class IDACoreAPI(DisassemblerCoreAPI):
- """
- The IDA implementation of the disassembler API abstraction.
- """
NAME = "IDA"
def __init__(self):
@@ -172,13 +169,8 @@ def show_dockable(self, dockable_name):
except KeyError:
return False
- # TODO/IDA remove try/cacth after cleanup
- try:
- parent, dctx = None, None # not used for IDA's integration
- widget = make_dockable(dockable_name, parent, dctx)
- except Exception:
- logger.exception("Error showing dockable...")
- return False
+ parent, dctx = None, None # not used for IDA's integration
+ widget = make_dockable(dockable_name, parent, dctx)
# get the original twidget, so we can use it with the IDA API's
#twidget = idaapi.TWidget__from_ptrval__(widget) NOTE: IDA 7.2+ only...
@@ -212,7 +204,10 @@ def _get_ida_bg_color_from_file(self):
#
# TODO/IDA: we need better early detection for if IDA is fully ready,
# this isn't effective and this func theme func can crash IDA if
- # called too early (eg, during db load...)
+ # called too early (eg, during db load...).
+ #
+ # this isn't a problem now... but I don't want us to be at risk of
+ # hard crashing people's IDA in the future should we change something.
#
imagebase = idaapi.get_imagebase()
@@ -323,9 +318,6 @@ def _touch_ida_window(self, target):
#------------------------------------------------------------------------------
class IDAContextAPI(DisassemblerContextAPI):
- """
- TODO/COMMENT
- """
def __init__(self, dctx):
super(IDAContextAPI, self).__init__(dctx)
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 40ace0a5..768ed914 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -74,11 +74,7 @@ def get_dpi_scale():
def compute_color_on_gradiant(percent, color1, color2):
"""
Compute the color specified by a percent between two colors.
-
- TODO/PERF: This is silly, heavy, and can be refactored.
"""
-
- # dump the rgb values from QColor objects
r1, g1, b1, _ = color1.getRgb()
r2, g2, b2, _ = color2.getRgb()
From 75572aed3355a87d199e392ee4cb156c0bcbb3f9 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 19 Apr 2020 18:00:15 -0400
Subject: [PATCH 139/154] fix so the 'prefix' delim does not show in the table
/ matches what IDA renders...
---
plugin/lighthouse/util/disassembler/ida_api.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 6dc13795..ad898c04 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -389,8 +389,10 @@ def renamed(self, address, new_name, local_name):
if local_name or new_name.startswith("loc_"):
return 0
+ rendered_name = idaapi.get_short_name(address)
+
# call the 'renamed' callback, that will get hooked by a listener
- self.name_changed(address, new_name)
+ self.name_changed(address, rendered_name)
# must return 0 to keep IDA happy...
return 0
From 112e5d6a3b6d734893664a0d92a53f3028f81807 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 19 Apr 2020 18:00:32 -0400
Subject: [PATCH 140/154] fix partial painting in binja...
---
plugin/lighthouse/painting/binja_painter.py | 6 +++++-
plugin/lighthouse/painting/painter.py | 9 +++++++--
2 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/painting/binja_painter.py b/plugin/lighthouse/painting/binja_painter.py
index 084ae8d8..996153e1 100644
--- a/plugin/lighthouse/painting/binja_painter.py
+++ b/plugin/lighthouse/painting/binja_painter.py
@@ -41,6 +41,7 @@ def _clear_instructions(self, instructions):
for address in instructions:
for func in bv.get_functions_containing(address):
func.set_auto_instr_highlight(address, HighlightStandardColor.NoHighlightColor)
+ self._painted_partial -= set(instructions)
self._painted_instructions -= set(instructions)
self._action_complete.set()
@@ -48,6 +49,7 @@ def _partial_paint(self, bv, instructions, color):
for address in instructions:
for func in bv.get_functions_containing(address):
func.set_auto_instr_highlight(address, color)
+ self._painted_partial |= set(instructions)
self._painted_instructions |= set(instructions)
def _paint_nodes(self, node_addresses):
@@ -58,6 +60,7 @@ def _paint_nodes(self, node_addresses):
r, g, b, _ = self.palette.coverage_paint.getRgb()
color = HighlightColor(red=r, green=g, blue=b)
+ partial_nodes = set()
for node_address in node_addresses:
node_metadata = db_metadata.nodes.get(node_address, None)
node_coverage = db_coverage.nodes.get(node_address, None)
@@ -70,13 +73,14 @@ def _paint_nodes(self, node_addresses):
# special case for nodes that are only partially executed...
if node_coverage.instructions_executed != node_metadata.instruction_count:
+ partial_nodes.add(node_address)
self._partial_paint(bv, node_coverage.executed_instructions.keys(), color)
continue
for node in bv.get_basic_blocks_starting_at(node_address):
node.highlight = color
- self._painted_nodes |= set(node_addresses)
+ self._painted_nodes |= (set(node_addresses) - partial_nodes)
self._action_complete.set()
def _clear_nodes(self, node_addresses):
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index ef928736..b2fc9745 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -44,6 +44,7 @@ def __init__(self, lctx, director, palette):
self._imagebase = BADADDR
self._painted_nodes = set()
+ self._painted_partial = set()
self._painted_instructions = set()
#
@@ -368,10 +369,14 @@ def _paint_database(self):
if not self._streaming_instructions:
+ #
+ # TODO: 'partially painted nodes' might be a little funny / not
+ # working correctly in IDA if we ever disable instruction streaming...
+ #
+
# compute the painted instructions that will not get painted over
- stale_partial_inst = self._painted_instructions & db_coverage.partial_instructions
stale_instr = self._painted_instructions - db_coverage.coverage
- stale_instr |= stale_partial_inst
+ stale_instr |= (self._painted_partial - db_coverage.partial_instructions)
# clear old instruction paint
if not self._async_action(self._clear_instructions, stale_instr):
From a943580b2cc8113e89c3eb6bedbc9f21d9c0b1f9 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Sun, 19 Apr 2020 20:52:43 -0400
Subject: [PATCH 141/154] improved warning dialogs a bit...
---
plugin/lighthouse/exceptions.py | 11 ++++++-----
plugin/lighthouse/util/disassembler/api.py | 17 ++++++++++++++---
.../lighthouse/util/disassembler/binja_api.py | 3 ++-
plugin/lighthouse/util/disassembler/ida_api.py | 3 ++-
4 files changed, 24 insertions(+), 10 deletions(-)
diff --git a/plugin/lighthouse/exceptions.py b/plugin/lighthouse/exceptions.py
index b70b9b77..eb35490a 100644
--- a/plugin/lighthouse/exceptions.py
+++ b/plugin/lighthouse/exceptions.py
@@ -79,7 +79,7 @@ class CoverageMappingAbsent(CoverageException):
" Possible reasons:\n" \
" - The loaded coverage data does not fall within defined functions.\n" \
" - You loaded an absolute address trace with a different imagebase.\n" \
- " - The coverage file might be corrupt or malformed.\n\n" \
+ " - The coverage data might be corrupt or malformed.\n\n" \
"Please see the disassembler console for more info..."
def __init__(self, coverage):
@@ -94,13 +94,14 @@ class CoverageMappingSuspicious(CoverageException):
description = \
"One or more of the loaded coverage files appears to be badly mapped.\n\n" \
" Possible reasons:\n" \
- " - You selected a coverage file that was collected against a\n" \
- " slightly different version of the binary.\n" \
+ " - You selected the wrong binary/module to load coverage from.\n" \
+ " - Your coverage file/data is for a different version of the\n" \
+ " binary that does not match what the disassembler has open.\n" \
" - You recorded self-modifying code or something with very\n" \
" abnormal control flow (obfuscated code, malware, packers).\n" \
- " - The coverage file might be corrupt or malformed.\n\n" \
+ " - The coverage data might be corrupt or malformed.\n\n" \
"This means that any coverage displayed by Lighthouse is PROBABLY\n" \
- "WRONG and is not be trusted because the coverage data does not\n." \
+ "WRONG and is not be trusted because the coverage data does not\n" \
"appear to match the disassembled binary."
def __init__(self, coverage):
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index 19f8c70d..822c4dbb 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -1,7 +1,7 @@
import abc
import logging
-from ..qt import QT_AVAILABLE, QtGui
+from ..qt import QT_AVAILABLE, QtGui, QtWidgets
logger = logging.getLogger("Lighthouse.API")
@@ -140,12 +140,23 @@ def is_msg_inited(self):
"""
pass
- @abc.abstractmethod
def warning(self, text):
"""
Display a warning dialog box with the given text.
"""
- pass
+ msgbox = QtWidgets.QMessageBox()
+ msgbox.setIcon(QtWidgets.QMessageBox.Critical)
+ msgbox.setWindowTitle("Lighthouse Warning")
+ msgbox.setInformativeText(text)
+
+ # don't ask...
+ spacer = QtWidgets.QSpacerItem(int(msgbox.sizeHint().width()*1.1), 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ layout = msgbox.layout()
+ layout.addItem(spacer, layout.rowCount(), 0, 1, layout.columnCount())
+ msgbox.setLayout(layout)
+
+ # show the dialog
+ msgbox.exec_()
@abc.abstractmethod
def message(self, function_address, new_name):
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index ced1369b..e060e033 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -151,8 +151,9 @@ def get_disassembly_background_color(self):
def is_msg_inited(self):
return True
+ @execute_ui.__func__
def warning(self, text):
- binaryninja.interaction.show_message_box("Warning", text)
+ super(BinjaCoreAPI, self).warning(text)
def message(self, message):
print(message)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index ad898c04..2e5d1ab5 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -134,8 +134,9 @@ def get_disassembly_background_color(self):
def is_msg_inited(self):
return idaapi.is_msg_inited()
+ @execute_ui.__func__
def warning(self, text):
- idaapi.warning(text)
+ super(IDACoreAPI, self).warning(text)
@execute_ui.__func__
def message(self, message):
From 4e8f5d3bbc6a99907f6fc51237348c966d776a60 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 03:04:17 -0400
Subject: [PATCH 142/154] improve table header styling, dialogs
---
plugin/lighthouse/ui/coverage_table.py | 74 +++++++++++++------------
plugin/lighthouse/ui/coverage_xref.py | 6 ++
plugin/lighthouse/ui/module_selector.py | 8 +++
3 files changed, 54 insertions(+), 34 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index de3bbe84..35e36a33 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -50,6 +50,10 @@ def refresh_theme(self):
" color: %s;" % palette.table_text.name() +
" outline: none; "
"} " +
+ "QHeaderView::section { "
+ " padding: 1ex;" \
+ " margin: 0;" \
+ "} " +
"QTableView::item:selected {"
" color: white; "
" background-color: %s;" % palette.table_selection.name() +
@@ -128,23 +132,8 @@ def _ui_init_table(self):
# set the initial column widths based on their title or contents
for i in xrange(self._model.columnCount()):
-
- # determine the pixel width of the column header text
- title_text = self._model.headerData(i, QtCore.Qt.Horizontal)
- title_rect = title_fm.boundingRect(title_text)
-
- # determine the pixel width of sample column entry text
- entry_text = self._model.SAMPLE_CONTENTS[i]
- entry_rect = entry_fm.boundingRect(entry_text)
-
- # select the lager of the two potential column widths
- column_width = max(title_rect.width(), entry_rect.width())
-
- # pad the final column width to make the table less dense
- column_width = int(column_width * 1.2)
-
- # save the final column width
- self.setColumnWidth(i, column_width)
+ title_rect = self._model.headerData(i, QtCore.Qt.Horizontal, QtCore.Qt.SizeHintRole)
+ self.setColumnWidth(i, title_rect.width())
#
# Misc
@@ -161,7 +150,9 @@ def _ui_init_table(self):
vh.hide()
# stretch last table column (which is blank) to fill remaining space
- hh.setStretchLastSection(True)
+ #hh.setStretchLastSection(True)
+ #hh.setCascadingSectionResizes(True)
+ hh.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch)
# disable bolding of table column headers when table is selected
hh.setHighlightSections(False)
@@ -181,11 +172,6 @@ def _ui_init_table(self):
# force the table row heights to be fixed height
vh.setSectionResizeMode(QtWidgets.QHeaderView.Fixed)
- # specify the fixed pixel height for the table headers
- spacing = title_fm.height() - title_fm.xHeight()
- tweak = 26*get_dpi_scale() - spacing
- hh.setFixedHeight(entry_fm.height()+tweak)
-
# specify the fixed pixel height for the table rows
# NOTE: don't ask too many questions about this voodoo math :D
spacing = entry_fm.height() - entry_fm.xHeight()
@@ -646,7 +632,6 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
INST_HIT = 4
FUNC_SIZE = 5
COMPLEXITY = 6
- FINAL_COLUMN = 7
METADATA_ATTRIBUTES = [FUNC_NAME, FUNC_ADDR, FUNC_SIZE, COMPLEXITY]
COVERAGE_ATTRIBUTES = [COV_PERCENT, BLOCKS_HIT, INST_HIT]
@@ -666,27 +651,37 @@ class CoverageTableModel(QtCore.QAbstractTableModel):
# column headers of the table
COLUMN_HEADERS = \
{
- COV_PERCENT: "Coverage %",
- FUNC_NAME: "Function Name",
+ COV_PERCENT: "Cov %",
+ FUNC_NAME: "Func Name",
FUNC_ADDR: "Address",
BLOCKS_HIT: "Blocks Hit",
- INST_HIT: "Instructions Hit",
- FUNC_SIZE: "Function Size",
- COMPLEXITY: "Complexity",
- FINAL_COLUMN: ""
+ INST_HIT: "Instr. Hit",
+ FUNC_SIZE: "Func Size",
+ COMPLEXITY: "CC",
+ }
+
+ # column header tooltips
+ COLUMN_TOOLTIPS = \
+ {
+ COV_PERCENT: "Coverage Percent",
+ FUNC_NAME: "Function Name",
+ FUNC_ADDR: "Function Address",
+ BLOCKS_HIT: "Number of Basic Blocks Executed",
+ INST_HIT: "Number of Instructions Executed",
+ FUNC_SIZE: "Function Size (bytes)",
+ COMPLEXITY: "Cyclomatic Complexity",
}
# sample column
SAMPLE_CONTENTS = \
[
- " 100.00% ",
+ " 100.00 ",
" sub_140001B20 ",
" 0x140001b20 ",
" 100 / 100 ",
" 1000 / 1000 ",
- " 10000000 ",
- " 1000000 ",
- ""
+ " 100000 ",
+ " 1000 ",
]
def __init__(self, lctx, parent=None):
@@ -797,10 +792,21 @@ def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
# center align all columns
return self._column_alignment[column]
+ # tooltip request
+ elif role == QtCore.Qt.ToolTipRole:
+ return self.COLUMN_TOOLTIPS[column]
+
# font format request
elif role == QtCore.Qt.FontRole:
return self._title_font
+ if role == QtCore.Qt.SizeHintRole:
+ title_fm = QtGui.QFontMetricsF(self._title_font)
+ #title_rect = title_fm.boundingRect(self.COLUMN_HEADERS[column])
+ title_rect = title_fm.boundingRect(self.SAMPLE_CONTENTS[column])
+ padded = QtCore.QSize(int(title_rect.width()*1.45), int(title_rect.height()*1.75))
+ return padded
+
# unhandeled header request
return None
diff --git a/plugin/lighthouse/ui/coverage_xref.py b/plugin/lighthouse/ui/coverage_xref.py
index 26e621e6..d7bd4b86 100644
--- a/plugin/lighthouse/ui/coverage_xref.py
+++ b/plugin/lighthouse/ui/coverage_xref.py
@@ -45,6 +45,10 @@ def _ui_init(self):
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.setModal(True)
+ self._font = self.font()
+ self._font.setPointSizeF(normalize_to_dpi(10))
+ self._font_metrics = QtGui.QFontMetricsF(self._font)
+
# initialize coverage xref table
self._ui_init_table()
self._populate_table()
@@ -59,6 +63,8 @@ def _ui_init_table(self):
self._table = QtWidgets.QTableWidget()
self._table.verticalHeader().setVisible(False)
self._table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
+ self._table.horizontalHeader().setFont(self._font)
+ self._table.setFont(self._font)
self._table.setWordWrap(False)
# symbol, cov %, name, time
diff --git a/plugin/lighthouse/ui/module_selector.py b/plugin/lighthouse/ui/module_selector.py
index 2c90f451..badb8b23 100644
--- a/plugin/lighthouse/ui/module_selector.py
+++ b/plugin/lighthouse/ui/module_selector.py
@@ -47,6 +47,10 @@ def _ui_init(self):
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint)
self.setModal(True)
+ self._font = self.font()
+ self._font.setPointSizeF(normalize_to_dpi(10))
+ self._font_metrics = QtGui.QFontMetricsF(self._font)
+
# initialize module selector table
self._ui_init_header()
self._ui_init_table()
@@ -71,10 +75,12 @@ def _ui_init_header(self):
self._label_description = QtWidgets.QLabel(description_text)
self._label_description.setTextFormat(QtCore.Qt.RichText)
+ self._label_description.setFont(self._font)
#self._label_description.setWordWrap(True)
# a checkbox to save the user selected alias to the database
self._checkbox_remember = QtWidgets.QCheckBox("Remember target module alias for this session")
+ self._checkbox_remember.setFont(self._font)
def _ui_init_table(self):
"""
@@ -83,6 +89,8 @@ def _ui_init_table(self):
self._table = QtWidgets.QTableWidget()
self._table.verticalHeader().setVisible(False)
self._table.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
+ self._table.horizontalHeader().setFont(self._font)
+ self._table.setFont(self._font)
# Create a simple table / list
self._table.setColumnCount(1)
From 4eaca66caa5bac3b93d33b68693e989d5ca87d63 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 03:17:42 -0400
Subject: [PATCH 143/154] fix bug where refresh could run twice when opening
cov overview
---
plugin/lighthouse/director.py | 3 ++-
plugin/lighthouse/ui/coverage_overview.py | 10 ++++++++--
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/plugin/lighthouse/director.py b/plugin/lighthouse/director.py
index aa201cf2..7a1de3f9 100644
--- a/plugin/lighthouse/director.py
+++ b/plugin/lighthouse/director.py
@@ -1401,7 +1401,8 @@ def _refresh(self):
await_future(future)
# (re) map each set of loaded coverage data to the database
- self._refresh_database_coverage()
+ if self.coverage_names:
+ self._refresh_database_coverage()
# notify of full-refresh
self._notify_refreshed()
diff --git a/plugin/lighthouse/ui/coverage_overview.py b/plugin/lighthouse/ui/coverage_overview.py
index 84c6b27f..51eb1861 100644
--- a/plugin/lighthouse/ui/coverage_overview.py
+++ b/plugin/lighthouse/ui/coverage_overview.py
@@ -264,6 +264,7 @@ class EventProxy(QtCore.QObject):
def __init__(self, target):
super(EventProxy, self).__init__()
self._target = weakref.proxy(target)
+ self._first_hit = True
def eventFilter(self, source, event):
@@ -304,10 +305,15 @@ def eventFilter(self, source, event):
#
elif int(event.type()) == self.EventUpdateLater:
- if self._target.visible and not self._target.director.metadata.cached:
+
+ if self._target.visible and self._first_hit:
+ self._first_hit = False
+
if disassembler.NAME == "BINJA":
self._target.lctx.start()
- self._target.director.refresh()
+
+ if not self._target.director.metadata.cached:
+ self._target.director.refresh()
#
# this is an unknown event, but it seems to fire when the widget is
From 33ef4e47a24adcea66c3f8ee772dc926dbe05468 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 04:55:31 -0400
Subject: [PATCH 144/154] fix bug where drcov parser could fail to get correct
filename from a crossplatform path...
---
plugin/lighthouse/reader/parsers/drcov.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/plugin/lighthouse/reader/parsers/drcov.py b/plugin/lighthouse/reader/parsers/drcov.py
index 14b148e5..cb5d4cf5 100644
--- a/plugin/lighthouse/reader/parsers/drcov.py
+++ b/plugin/lighthouse/reader/parsers/drcov.py
@@ -386,7 +386,7 @@ def _parse_module_v1(self, data):
self.id = int(data[0])
self.size = int(data[1])
self.path = str(data[2])
- self.filename = os.path.basename(self.path)
+ self.filename = os.path.basename(self.path.replace('\\', os.sep))
def _parse_module_v2(self, data):
"""
@@ -401,7 +401,7 @@ def _parse_module_v2(self, data):
self.timestamp = int(data[5], 16)
self.path = str(data[-1])
self.size = self.end-self.base
- self.filename = os.path.basename(self.path)
+ self.filename = os.path.basename(self.path.replace('\\', os.sep))
def _parse_module_v3(self, data):
"""
@@ -417,7 +417,7 @@ def _parse_module_v3(self, data):
self.timestamp = int(data[6], 16)
self.path = str(data[-1])
self.size = self.end-self.base
- self.filename = os.path.basename(self.path)
+ self.filename = os.path.basename(self.path.replace('\\', os.sep))
def _parse_module_v4(self, data):
"""
@@ -434,7 +434,7 @@ def _parse_module_v4(self, data):
self.timestamp = int(data[7], 16)
self.path = str(data[-1])
self.size = self.end-self.base
- self.filename = os.path.basename(self.path)
+ self.filename = os.path.basename(self.path.replace('\\', os.sep))
#------------------------------------------------------------------------------
# drcov basic block parser
From b610b1ee329c8cdd06e3fadfd88781d11212d6af Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 04:56:26 -0400
Subject: [PATCH 145/154] fixes bug where combobox would immediately close on
binja linux
---
plugin/lighthouse/ui/coverage_combobox.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index dd106261..41c542e6 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -49,9 +49,9 @@ def __init__(self, director, parent=None):
# QComboBox Overloads
#--------------------------------------------------------------------------
- def mousePressEvent(self, e):
+ def mouseReleaseEvent(self, e):
"""
- Capture mouse click events to the QComboBox.
+ Capture mouse release events on the QComboBox.
"""
# get the widget currently beneath the mouse event being handled
From 859b994bf78676d2d351a72c953da67c4a6a0e21 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 15:42:08 -0400
Subject: [PATCH 146/154] improve styling cross-platform/DPI
---
plugin/lighthouse/composer/shell.py | 4 ++--
plugin/lighthouse/ui/coverage_combobox.py | 6 +++---
plugin/lighthouse/ui/coverage_table.py | 24 +++++++++++++++++-----
plugin/lighthouse/ui/palette.py | 8 +++++++-
plugin/lighthouse/util/disassembler/api.py | 12 +++++++++--
5 files changed, 41 insertions(+), 13 deletions(-)
diff --git a/plugin/lighthouse/composer/shell.py b/plugin/lighthouse/composer/shell.py
index b49ae1fd..2696c61f 100644
--- a/plugin/lighthouse/composer/shell.py
+++ b/plugin/lighthouse/composer/shell.py
@@ -70,7 +70,7 @@ def _ui_init(self):
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
- self._font.setPointSizeF(normalize_to_dpi(9))
+ self._font.setPointSizeF(normalize_to_dpi(10))
self._font_metrics = QtGui.QFontMetricsF(self._font)
# initialize our ui elements
@@ -1029,7 +1029,7 @@ def _ui_init(self):
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
- self._font.setPointSizeF(normalize_to_dpi(9))
+ self._font.setPointSizeF(normalize_to_dpi(10))
self._font_metrics = QtGui.QFontMetricsF(self._font)
self.setFont(self._font)
diff --git a/plugin/lighthouse/ui/coverage_combobox.py b/plugin/lighthouse/ui/coverage_combobox.py
index 41c542e6..0d4bafa9 100644
--- a/plugin/lighthouse/ui/coverage_combobox.py
+++ b/plugin/lighthouse/ui/coverage_combobox.py
@@ -88,7 +88,7 @@ def _ui_init(self):
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
- self._font.setPointSizeF(normalize_to_dpi(9))
+ self._font.setPointSizeF(normalize_to_dpi(10))
self._font_metrics = QtGui.QFontMetricsF(self._font)
self.setFont(self._font)
@@ -413,7 +413,7 @@ def _ui_init(self):
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
- self._font.setPointSizeF(normalize_to_dpi(9))
+ self._font.setPointSizeF(normalize_to_dpi(10))
self._font_metrics = QtGui.QFontMetricsF(self._font)
self.setFont(self._font)
@@ -526,7 +526,7 @@ def __init__(self, director, parent=None):
# initialize a monospace font to use with our widget(s)
self._font = MonospaceFont()
- self._font.setPointSizeF(normalize_to_dpi(9))
+ self._font.setPointSizeF(normalize_to_dpi(10))
self._font_metrics = QtGui.QFontMetricsF(self._font)
# load the raw 'X' delete icon from disk
diff --git a/plugin/lighthouse/ui/coverage_table.py b/plugin/lighthouse/ui/coverage_table.py
index 35e36a33..edcee16b 100644
--- a/plugin/lighthouse/ui/coverage_table.py
+++ b/plugin/lighthouse/ui/coverage_table.py
@@ -130,10 +130,25 @@ def _ui_init_table(self):
entry_font = self._model.data(0, QtCore.Qt.FontRole)
entry_fm = QtGui.QFontMetricsF(entry_font)
+ # get the font used by the table cell entries
+ entry_font = self._model.data(0, QtCore.Qt.FontRole)
+ entry_fm = QtGui.QFontMetricsF(entry_font)
+
# set the initial column widths based on their title or contents
for i in xrange(self._model.columnCount()):
+
+ # determine the pixel width of the column header text
title_rect = self._model.headerData(i, QtCore.Qt.Horizontal, QtCore.Qt.SizeHintRole)
- self.setColumnWidth(i, title_rect.width())
+
+ # determine the pixel width of sample column entry text
+ entry_text = self._model.SAMPLE_CONTENTS[i]
+ 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)
+
+ # save the final column width
+ self.setColumnWidth(i, column_width)
#
# Misc
@@ -714,11 +729,11 @@ def __init__(self, lctx, parent=None):
# initialize a monospace font to use for table row / cell text
self._entry_font = MonospaceFont()
self._entry_font.setStyleStrategy(QtGui.QFont.ForceIntegerMetrics)
- self._entry_font.setPointSizeF(normalize_to_dpi(9))
+ self._entry_font.setPointSizeF(normalize_to_dpi(10))
# use the default / system font for the column titles
self._title_font = QtGui.QFont()
- self._title_font.setPointSizeF(normalize_to_dpi(9))
+ self._title_font.setPointSizeF(normalize_to_dpi(10))
#----------------------------------------------------------------------
# Sorting
@@ -802,8 +817,7 @@ def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole):
if role == QtCore.Qt.SizeHintRole:
title_fm = QtGui.QFontMetricsF(self._title_font)
- #title_rect = title_fm.boundingRect(self.COLUMN_HEADERS[column])
- title_rect = title_fm.boundingRect(self.SAMPLE_CONTENTS[column])
+ title_rect = title_fm.boundingRect(self.COLUMN_HEADERS[column])
padded = QtCore.QSize(int(title_rect.width()*1.45), int(title_rect.height()*1.75))
return padded
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 74a6270b..9f4ff7a8 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -5,6 +5,12 @@
import logging
import traceback
+# NOTE: Py2/Py3 compat
+try:
+ from json.decoder import JSONDecodeError
+except ImportError:
+ JSONDecodeError = ValueError
+
from lighthouse.util.qt import *
from lighthouse.util.log import lmsg
from lighthouse.util.misc import *
@@ -387,7 +393,7 @@ def _load_theme(self, filepath):
return False
# JSON decoding failed
- except json.decoder.JSONDecodeError as e:
+ except JSONDecodeError as e:
lmsg("Failed to decode theme '%s' to json" % filepath)
lmsg(" - " + str(e))
return False
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index 822c4dbb..40f12ef7 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -145,12 +145,20 @@ def warning(self, text):
Display a warning dialog box with the given text.
"""
msgbox = QtWidgets.QMessageBox()
+ before = msgbox.sizeHint().width()
msgbox.setIcon(QtWidgets.QMessageBox.Critical)
+ after = msgbox.sizeHint().width()
+ icon_width = after - before
+
msgbox.setWindowTitle("Lighthouse Warning")
- msgbox.setInformativeText(text)
+ msgbox.setText(text)
+
+ font = msgbox.font()
+ fm = QtGui.QFontMetricsF(font)
+ text_width = fm.size(0, text).width()
# don't ask...
- spacer = QtWidgets.QSpacerItem(int(msgbox.sizeHint().width()*1.1), 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ spacer = QtWidgets.QSpacerItem(int(text_width*1.1 + icon_width), 0, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
layout = msgbox.layout()
layout.addItem(spacer, layout.rowCount(), 0, 1, layout.columnCount())
msgbox.setLayout(layout)
From 1df982ca4c0240941b7b69cdf2bf39734a96f194 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 18:04:39 -0400
Subject: [PATCH 147/154] ensure lighthouse stays disabled while using
disassemblers headlessly
---
dev_scripts/reload_IDA_7_batch.bat | 18 ++++
.../lighthouse/util/disassembler/binja_api.py | 84 ++++++++++---------
.../lighthouse/util/disassembler/ida_api.py | 2 +-
3 files changed, 62 insertions(+), 42 deletions(-)
create mode 100644 dev_scripts/reload_IDA_7_batch.bat
diff --git a/dev_scripts/reload_IDA_7_batch.bat b/dev_scripts/reload_IDA_7_batch.bat
new file mode 100644
index 00000000..db20893b
--- /dev/null
+++ b/dev_scripts/reload_IDA_7_batch.bat
@@ -0,0 +1,18 @@
+set LIGHTHOUSE_LOGGING=1
+REM - Close any running instances of IDA
+call close_IDA.bat
+
+REM - Purge old lighthouse log files
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\lighthouse_logs\*"
+
+REM - Delete the old plugin bits
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\*lighthouse_plugin.py"
+rmdir "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\lighthouse" /s /q
+
+REM - Copy over the new plugin bits
+xcopy /s/y "..\plugin\*" "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\"
+del /F /Q "C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins\.#lighthouse_plugin.py"
+
+REM - Launch a new IDA session
+"C:\tools\disassemblers\IDA 7.0\ida64.exe" "-B" "..\..\testcase\boombox.exe"
+
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index e060e033..aaf6ba6d 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -1,22 +1,19 @@
# -*- coding: utf-8 -*-
import os
import sys
-
import logging
import functools
import threading
import collections
-import binaryninja
-import binaryninjaui
-from binaryninja import PythonScriptingInstance, binaryview
-from binaryninjaui import DockHandler, DockContextHandler, UIContext, UIActionHandler
-from binaryninja.plugin import BackgroundTaskThread
-
from .api import DisassemblerCoreAPI, DisassemblerContextAPI
from ..qt import *
from ..misc import is_mainthread, not_mainthread
+import binaryninja
+from binaryninja import PythonScriptingInstance, binaryview
+from binaryninja.plugin import BackgroundTaskThread
+
logger = logging.getLogger("Lighthouse.API.Binja")
#------------------------------------------------------------------------------
@@ -347,47 +344,52 @@ def name_changed(self, address, name):
# UI
#------------------------------------------------------------------------------
-class DockableWidget(QtWidgets.QWidget, DockContextHandler):
- """
- A dockable Qt widget for Binary Ninja.
- """
+if QT_AVAILABLE:
- def __init__(self, parent, name):
- QtWidgets.QWidget.__init__(self, parent)
- DockContextHandler.__init__(self, self, name)
+ import binaryninjaui
+ from binaryninjaui import DockHandler, DockContextHandler, UIContext, UIActionHandler
- self.actionHandler = UIActionHandler()
- self.actionHandler.setupActionHandler(self)
+ class DockableWidget(QtWidgets.QWidget, DockContextHandler):
+ """
+ A dockable Qt widget for Binary Ninja.
+ """
- self._active_view = None
- self._visible_for_view = collections.defaultdict(lambda: False)
+ def __init__(self, parent, name):
+ QtWidgets.QWidget.__init__(self, parent)
+ DockContextHandler.__init__(self, self, name)
- @property
- def visible(self):
- return self._visible_for_view[self._active_view]
+ self.actionHandler = UIActionHandler()
+ self.actionHandler.setupActionHandler(self)
- @visible.setter
- def visible(self, is_visible):
- self._visible_for_view[self._active_view] = is_visible
+ self._active_view = None
+ self._visible_for_view = collections.defaultdict(lambda: False)
- def shouldBeVisible(self, view_frame):
- if not view_frame:
- return False
+ @property
+ def visible(self):
+ return self._visible_for_view[self._active_view]
- import shiboken2 as shiboken
- vf_ptr = shiboken.getCppPointer(view_frame)[0]
- return self._visible_for_view[vf_ptr]
+ @visible.setter
+ def visible(self, is_visible):
+ self._visible_for_view[self._active_view] = is_visible
- def notifyVisibilityChanged(self, is_visible):
- self.visible = is_visible
+ def shouldBeVisible(self, view_frame):
+ if not view_frame:
+ return False
- def notifyViewChanged(self, view_frame):
- if not view_frame:
- self._active_view = None
- return
+ import shiboken2 as shiboken
+ vf_ptr = shiboken.getCppPointer(view_frame)[0]
+ return self._visible_for_view[vf_ptr]
+
+ def notifyVisibilityChanged(self, is_visible):
+ self.visible = is_visible
+
+ def notifyViewChanged(self, view_frame):
+ if not view_frame:
+ self._active_view = None
+ return
- 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)
+ import shiboken2 as shiboken
+ self._active_view = shiboken.getCppPointer(view_frame)[0]
+ if self.visible:
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(self.m_name, True)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 2e5d1ab5..19d50705 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -83,7 +83,7 @@ def _init_version(self):
@property
def headless(self):
- return False
+ return QT_AVAILABLE
#--------------------------------------------------------------------------
# Synchronization Decorators
From 8cbfffe2e0f1a746b35bd83d474b5c8db80a1110 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 19:42:06 -0400
Subject: [PATCH 148/154] actually detect when IDA is is batch.....
---
plugin/lighthouse/util/disassembler/ida_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 19d50705..4f82f544 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -83,7 +83,7 @@ def _init_version(self):
@property
def headless(self):
- return QT_AVAILABLE
+ return idaapi.cvar.batch
#--------------------------------------------------------------------------
# Synchronization Decorators
From 22d48fa52b4776c29eecab242a4930041857954a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Mon, 20 Apr 2020 19:42:24 -0400
Subject: [PATCH 149/154] more robust path creation
---
plugin/lighthouse/ui/palette.py | 3 +--
plugin/lighthouse/util/log.py | 8 ++++++--
plugin/lighthouse/util/misc.py | 13 +++++++++++++
3 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/plugin/lighthouse/ui/palette.py b/plugin/lighthouse/ui/palette.py
index 9f4ff7a8..8bd6cc8f 100644
--- a/plugin/lighthouse/ui/palette.py
+++ b/plugin/lighthouse/ui/palette.py
@@ -258,8 +258,7 @@ def _populate_user_theme_dir(self):
# create the user theme directory if it does not exist
user_theme_dir = self.get_user_theme_dir()
- if not os.path.exists(user_theme_dir):
- os.makedirs(user_theme_dir)
+ makedirs(user_theme_dir)
# copy the default themes into the user directory if they don't exist
for theme_name in self._default_themes.values():
diff --git a/plugin/lighthouse/util/log.py b/plugin/lighthouse/util/log.py
index e232d382..f7674319 100644
--- a/plugin/lighthouse/util/log.py
+++ b/plugin/lighthouse/util/log.py
@@ -2,6 +2,7 @@
import sys
import logging
+from .misc import makedirs
from .disassembler import disassembler
#------------------------------------------------------------------------------
@@ -118,8 +119,11 @@ def start_logging():
# create a directory for lighthouse logs if it does not exist
log_dir = get_log_dir()
- if not os.path.exists(log_dir):
- os.makedirs(log_dir)
+ try:
+ makedirs(log_dir)
+ except Exception as e:
+ logger.disabled = True
+ return logger
# construct the full log path
log_path = os.path.join(log_dir, "lighthouse.%s.log" % os.getpid())
diff --git a/plugin/lighthouse/util/misc.py b/plugin/lighthouse/util/misc.py
index f99adf6f..e7ce56a3 100644
--- a/plugin/lighthouse/util/misc.py
+++ b/plugin/lighthouse/util/misc.py
@@ -1,5 +1,6 @@
import os
import re
+import errno
import struct
import weakref
import datetime
@@ -78,6 +79,18 @@ def test_color_brightness(color):
# Python Util
#------------------------------------------------------------------------------
+def makedirs(path, exists_ok=True):
+ """
+ Make a fully qualified path.
+ """
+ try:
+ os.makedirs(path)
+ except OSError as e:
+ if e.errno != errno.EEXIST:
+ raise e
+ if not exists_ok:
+ raise e
+
def chunks(l, n):
"""
Yield successive n-sized chunks from a list (l).
From a0c77f0f7230b2d70a1c0e3ebb953775e9ffa7b7 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Tue, 21 Apr 2020 23:13:44 -0400
Subject: [PATCH 150/154] lower the font size a bit for macos
---
plugin/lighthouse/util/qt/util.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugin/lighthouse/util/qt/util.py b/plugin/lighthouse/util/qt/util.py
index 768ed914..e87249f4 100644
--- a/plugin/lighthouse/util/qt/util.py
+++ b/plugin/lighthouse/util/qt/util.py
@@ -104,7 +104,7 @@ def normalize_to_dpi(font_size):
Normalize the given font size based on the system DPI.
"""
if sys.platform == "darwin": # macos is lame
- return font_size + 3
+ return font_size + 2
return font_size
def prompt_string(label, title, default=""):
From 8b9382d4149aad7d444fa16117ea8ab4021a72fd Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Wed, 22 Apr 2020 01:17:29 -0400
Subject: [PATCH 151/154] document the CoverageFile format a bit......
---
plugin/lighthouse/reader/coverage_file.py | 89 ++++++++++++++++++++---
1 file changed, 80 insertions(+), 9 deletions(-)
diff --git a/plugin/lighthouse/reader/coverage_file.py b/plugin/lighthouse/reader/coverage_file.py
index 7d441532..3d69a837 100644
--- a/plugin/lighthouse/reader/coverage_file.py
+++ b/plugin/lighthouse/reader/coverage_file.py
@@ -12,32 +12,103 @@ def __init__(self, filepath=None):
self.modules = {}
self._parse()
+ #--------------------------------------------------------------------------
+ # Parsing Routines
+ #--------------------------------------------------------------------------
+
+ @abc.abstractmethod
+ def _parse(self):
+ """
+ Load and parse coverage data from the file defined by self.filepath
+
+ Within this function, a custom CoverageFile is expected to attempt to
+ parse the coverage file from disk. If the coverage file does not appear
+ to match the format expected by this parser -- that is okay.
+
+ Should this parser crash and burn, the CoverageReader will simply move
+ on to the next available parser and discard this attempt.
+
+ This function should *only* parse & categorize the coverage data that
+ it loads from disk. If this function returns without error, the
+ CoverageReader will attempt to call one of the get() functions later
+ to retrieve the data you have loaded.
+
+ The best coverage file formats will contain some sort of mapping
+ for the coverage data that ties it to a module or binary that was in
+ the instrumented process space.
+
+ If this mapping in known, then this function should strive to store
+ the coverage data in the self.modules dictionary, where
+
+ self.modules[module_name] = [ coverage_addresses ]
+
+ """
+ raise NotImplementedError("Coverage parser not implemented")
+
#--------------------------------------------------------------------------
# Public
#--------------------------------------------------------------------------
+ #
+ # if you are writing a parser for a custom coverage file format, your
+ # parser is *REQUIRED* to implement one of the following routines.
+ #
+ # the CoverageReader well attempt to retrieve parsed data from this class
+ # using one of the function below.
+ #
+
def get_addresses(self, module_name=None):
"""
Return coverage data for the named module as absolute addresses.
+
+ If no name is given / available via self.modules, the trace is assumed
+ to be a an ABSOLUTE ADDRESS TRACE.
+
+ These are arugably the least flexible kind of traces available, but are
+ still provided as an option. This fuction should return a list of
+ integers representing absolute coverage addresses that match the open
+ disassembler database...
+
+ coverage_addresses = [address, address1, address2, ...]
+
"""
raise NotImplementedError("Absolute addresses not supported by this log format")
def get_offsets(self, module_name):
"""
Return coverage data for the named module as relative offets.
+
+ This function should return a list of integers representing the
+ relative offset of an executed instruction OR basic block from the
+ base of the requested module (module_name).
+
+ It is *okay* to return an instruction trace, OR a basic block trace
+ from thin function. Lighthoue will automatically detect basic block
+ based traces and 'explode' them into instruction traces.
+
+ coverage_data = [offset, offset2, offset3, ...]
+
"""
raise NotImplementedError("Relative addresses not supported by this log format")
def get_offset_blocks(self, module_name):
"""
- Return coverage data for the named module in block form (offset, size).
- """
- raise NotImplementedError("Block form not supported by this log format")
+ Return coverage data for the named module in block form.
- #--------------------------------------------------------------------------
- # Parsing Routines - Top Level
- #--------------------------------------------------------------------------
+ This function should return a list of tuples representing the coverage
+ for the requested module (module_name). The tuples must be in the form
+ of (offset, size).
- @abc.abstractmethod
- def _parse(self):
- raise NotImplementedError("Coverage parser not implemented")
+ offset - a relative offset from the module_name base address
+ size - the size of the instruction, block, or sequence executed
+
+ eg, if a basic block of 24 bytes in length at kernel32.dll+0x4182 was
+ executed, its tuple would be (0x4182, 24).
+
+ The complete list coverage data returned by thin function should be in
+ the following form:
+
+ coverage_data = [(offset, size), (offset1, size1), ...]
+
+ """
+ raise NotImplementedError("Block form not supported by this log format")
From 3f0cfa856e81460e151b7dc47460c6642b54c007 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 23 Apr 2020 04:20:38 -0400
Subject: [PATCH 152/154] bugfix where paint wouldn't fully refresh after
changing themes
---
plugin/lighthouse/integration/core.py | 2 +-
plugin/lighthouse/painting/painter.py | 62 ++++++++++++++++++++++-----
2 files changed, 52 insertions(+), 12 deletions(-)
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index b1240941..15685fa0 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -196,7 +196,7 @@ def refresh_theme(self):
for lctx in self.lighthouse_contexts.values():
lctx.director.refresh_theme()
lctx.coverage_overview.refresh_theme()
- lctx.painter.repaint()
+ lctx.painter.force_repaint()
def open_coverage_overview(self, dctx=None):
"""
diff --git a/plugin/lighthouse/painting/painter.py b/plugin/lighthouse/painting/painter.py
index b2fc9745..63b60034 100644
--- a/plugin/lighthouse/painting/painter.py
+++ b/plugin/lighthouse/painting/painter.py
@@ -4,6 +4,7 @@
import threading
from lighthouse.util import *
+from lighthouse.util.debug import catch_errors
from lighthouse.coverage import FunctionCoverage
logger = logging.getLogger("Lighthouse.Painting")
@@ -17,9 +18,10 @@ class DatabasePainter(object):
MSG_ABORT = -1
MSG_TERMINATE = 0
MSG_REPAINT = 1
- MSG_CLEAR = 2
- MSG_FORCE_CLEAR = 3
- MSG_REBASE = 4
+ MSG_FORCE_REPAINT = 2
+ MSG_CLEAR = 3
+ MSG_FORCE_CLEAR = 4
+ MSG_REBASE = 5
def __init__(self, lctx, director, palette):
@@ -169,6 +171,12 @@ def repaint(self):
"""
self._send_message(self.MSG_REPAINT)
+ def force_repaint(self):
+ """
+ Force a coverage repaint of the current database mappings.
+ """
+ self._send_message(self.MSG_FORCE_REPAINT)
+
def force_clear(self):
"""
Clear all paint from the current database (based on metadata)
@@ -440,6 +448,39 @@ def _clear_database(self):
# paint finished successfully
return True
+ def _force_paint_database(self):
+ """
+ Forcibly repaint the database.
+ """
+ db_metadata = self.director.metadata
+
+ text = "Repainting the database..."
+ logger.debug(text)
+
+ is_modal = bool(disassembler.NAME != "IDA")
+ disassembler.execute_ui(disassembler.show_wait_box)(text, False)
+
+ start = time.time()
+ #------------------------------------------------------------------
+
+ # discard current / known paint state
+ self._painted_nodes = set()
+ self._painted_partial = set()
+ self._painted_instructions = set()
+
+ # paint the database...
+ self._paint_database()
+
+ #------------------------------------------------------------------
+ end = time.time()
+ logger.debug(" - Database repainted in %.2f seconds..." % (end-start))
+
+ time.sleep(.2) # XXX: this seems to fix a bug where the waitbox doesn't close if the paint is too fast??
+ disassembler.execute_ui(disassembler.hide_wait_box)()
+
+ # paint finished successfully
+ return True
+
def _force_clear_database(self):
"""
Forcibly clear the paint from all known database addresses.
@@ -480,8 +521,9 @@ def _force_clear_database(self):
#------------------------------------------------------------------
end = time.time()
-
logger.debug(" - Database paint cleared in %.2f seconds..." % (end-start))
+
+ time.sleep(.2) # XXX: this seems to fix a bug where the waitbox doesn't close if the clear is too fast??
disassembler.execute_ui(disassembler.hide_wait_box)()
# paint finished successfully
@@ -517,14 +559,8 @@ def _rebase_database(self):
# Asynchronous Painting
#--------------------------------------------------------------------------
+ @catch_errors
def _async_database_painter(self):
- try:
- self._async_database_painter2()
- except:
- lmsg("PAINTER THREAD CRASHED :'(")
- logger.exception("Painter crashed...")
-
- def _async_database_painter2(self):
"""
Asynchronous database painting worker loop.
"""
@@ -543,6 +579,10 @@ def _async_database_painter2(self):
if action == self.MSG_REPAINT:
result = self._paint_database()
+ # forcibly repaint the database based on the current state
+ elif action == self.MSG_FORCE_REPAINT:
+ result = self._force_paint_database()
+
# clear database base on the current state
elif action == self.MSG_CLEAR:
result = self._clear_database()
From 4661517cb3dff465b33108465ef016b47ab73025 Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 23 Apr 2020 05:03:09 -0400
Subject: [PATCH 153/154] hack to hide binja featuremap by default, since it
will probably collide with the coverage overview dock position :-X
---
plugin/lighthouse/context.py | 5 +++++
plugin/lighthouse/util/disassembler/api.py | 7 +++++++
plugin/lighthouse/util/disassembler/binja_api.py | 4 ++++
plugin/lighthouse/util/disassembler/ida_api.py | 3 +++
4 files changed, 19 insertions(+)
diff --git a/plugin/lighthouse/context.py b/plugin/lighthouse/context.py
index c6eaf50d..3a229233 100644
--- a/plugin/lighthouse/context.py
+++ b/plugin/lighthouse/context.py
@@ -55,6 +55,11 @@ def start(self):
self.metadata.start()
self.director.start()
self.painter.start()
+
+ # TODO/BINJA remove this ASAP, or find a better workaround... I hate having this here
+ if disassembler.NAME == "BINJA":
+ disassembler.hide_dockable("Feature Map")
+
self._started = True
def terminate(self):
diff --git a/plugin/lighthouse/util/disassembler/api.py b/plugin/lighthouse/util/disassembler/api.py
index 40f12ef7..4c4f6855 100644
--- a/plugin/lighthouse/util/disassembler/api.py
+++ b/plugin/lighthouse/util/disassembler/api.py
@@ -219,6 +219,13 @@ def show_dockable(self, dockable_name):
"""
pass
+ @abc.abstractmethod
+ def hide_dockable(self, dockable_name):
+ """
+ Hide the named dockable widget.
+ """
+ pass
+
#------------------------------------------------------------------------------
# WaitBox API
#------------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/binja_api.py b/plugin/lighthouse/util/disassembler/binja_api.py
index aaf6ba6d..d58ea85a 100644
--- a/plugin/lighthouse/util/disassembler/binja_api.py
+++ b/plugin/lighthouse/util/disassembler/binja_api.py
@@ -170,6 +170,10 @@ def show_dockable(self, dockable_name):
dock_handler = DockHandler.getActiveDockHandler()
dock_handler.setVisible(dockable_name, True)
+ def hide_dockable(self, dockable_name):
+ dock_handler = DockHandler.getActiveDockHandler()
+ dock_handler.setVisible(dockable_name, False)
+
#--------------------------------------------------------------------------
# XXX Binja Specfic Helpers
#--------------------------------------------------------------------------
diff --git a/plugin/lighthouse/util/disassembler/ida_api.py b/plugin/lighthouse/util/disassembler/ida_api.py
index 4f82f544..bfa72c3f 100644
--- a/plugin/lighthouse/util/disassembler/ida_api.py
+++ b/plugin/lighthouse/util/disassembler/ida_api.py
@@ -192,6 +192,9 @@ def show_dockable(self, dockable_name):
idaapi.set_dock_pos(dockable_name, 'IDA View-A', idaapi.DP_RIGHT)
break
+ def hide_dockable(self, dockable_name):
+ pass # TODO/IDA: this should never actually be called by lighthouse right now
+
#--------------------------------------------------------------------------
# Theme Prediction Helpers (Internal)
#--------------------------------------------------------------------------
From 69a595a8758efb2ee99a91b11ea26f9bcf6bde2a Mon Sep 17 00:00:00 2001
From: gaasedelen
Date: Thu, 23 Apr 2020 05:41:53 -0400
Subject: [PATCH 154/154] updates readme and version #
---
README.md | 94 ++++++++++++++++++--------
coverage/README.md | 41 +++++++++++
plugin/lighthouse/integration/core.py | 2 +-
screenshots/overview.gif | Bin 4486275 -> 2053887 bytes
screenshots/themes.png | Bin 0 -> 208712 bytes
screenshots/xref.gif | Bin 0 -> 388976 bytes
6 files changed, 106 insertions(+), 31 deletions(-)
create mode 100644 screenshots/themes.png
create mode 100644 screenshots/xref.gif
diff --git a/README.md b/README.md
index 8dc847f2..6d9bd879 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
## Overview
-Lighthouse is a code coverage plugin for [IDA Pro](https://www.hex-rays.com/products/ida/), and [Binary Ninja](https://binary.ninja/). The plugin makes use of interactive disassemblers to map, explore, and visualize externally collected code coverage data when symbols or source may not be available for a given binary.
+Lighthouse is a 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.
This plugin is labeled only as a prototype & code resource for the community.
@@ -13,6 +13,7 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
## Releases
+* v0.9 -- Python 3 support, custom coverage formats, coverage cross-refs, theming subsystem, much more.
* v0.8 -- Binary Ninja support, HTML coverage reports, consistent styling, many tweaks, bugfixes.
* v0.7 -- Frida, C++ demangling, context menu, function prefixing, tweaks, bugfixes.
* v0.6 -- Intel pintool, cyclomatic complexity, batch load, bugfixes.
@@ -24,65 +25,81 @@ Special thanks to [@0vercl0k](https://twitter.com/0vercl0k) for the inspiration.
# Installation
-Lighthouse is a cross-platform (Windows, macOS, Linux) python plugin. It takes zero third party dependencies, making the code both portable and easy to install.
+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()`
2. Copy the contents of this repository's `/plugin/` folder to the listed directory.
-
-This project is primarily developed and tested with IDA for Windows, so that is where we expect the best experience. Support for Binary Ninja and other disassemblers is still considered exprimental at this time.
+3. Restart your disassembler.
# Usage
-Lighthouse loads automatically when a database is opened, installing a handful of menu entries into the disassembler.
+Once properly installed, there will be a few new menu entries available in the disassembler. These are the entry points for a user to load coverage data and start using Lighthouse.
-These are the entry points for a user to load and view coverage data. To generate coverage data that can be loaded into Lighthouse, please look at the [README](https://github.com/gaasedelen/lighthouse/tree/develop/coverage) in the coverage directory of this repository.
+Lighthouse is able to load a few different 'flavors' of coverage data. To generate coverage data that can be loaded into Lighthouse, please look at the [README](https://github.com/gaasedelen/lighthouse/tree/master/coverage) in the coverage directory of this repository.
## Coverage Painting
-Lighthouse 'paints' the active coverage data across the three major IDA views as applicable. Specifically, the Disassembly, Graph, and Pseudocode views.
+While Lighthouse is in use, it will 'paint' the active coverage data across all of the code viewers available in the disassembler. Specifically, this will apply to your linear disassembly, graph, and decompiler windows.
-In Binary Ninja, only the Disassembly and Graph views are supported.
+In Binary Ninja, only the linear disassembly, graph, and IL views are supported. Support for painting decompiler output in Binary Ninja will be added to Lighthouse in the *near future* as the feature stabilizes.
-## Coverage Overview
+# Coverage Overview
-The Coverage Overview is a dockable widget that provides a function level view of the active coverage data for the database.
+The Coverage Overview is a dockable widget that will open up once coverage has been loaded into Lighthouse.
-This table can be sorted by column, and entries can be double clicked to jump to their corresponding disassembly.
+This interactive widget provides a function level view of the loaded coverage data. It also houses a number of tools to manage loaded data and drive more advanced forms of coverage analysis.
## Context Menu
-Right clicking the table in the Coverage Overview will produce a context menu with a few basic amenities.
+Right clicking the table in the Coverage Overview will produce a context menu with a few basic amenities to extract information from the table, or manipulate the database as part of your reverse engineering process.
-These actions can be used to quickly manipulate or interact with entries in the table.
+If there are any other actions that you think might be useful to add to this context menu, please file an issue and they will be considered for a future release of Lighthouse.
+
+## Coverage ComboBox
+
+Loaded coverage data and user constructed compositions can be selected or deleted through the coverage combobox.
+
+
+
+
-## Coverage Composition
+## HTML Coverage Report
-Building relationships between multiple sets of coverage data often distills deeper meaning than their individual parts. The shell at the bottom of the [Coverage Overview](#coverage-overview) provides an interactive means of constructing these relationships.
+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).
+
+
+
+
+
+# Coverage Shell
+
+At the bottom of the coverage overview window is the coverage shell. This shell can be used to perform logic-based operations that combine or manipulate the loaded coverage sets.
-Pressing `enter` on the shell will evaluate and save a user constructed composition.
+This feature is extremely useful in exploring the relationships of program execution across multiple runs. In other words, the shell can be used to 'diff' execution between coverage sets and extract a deeper meaning that is otherwise obscured within the noise of their individual parts.
## Composition Syntax
@@ -90,15 +107,27 @@ Coverage composition, or _Composing_ as demonstrated above is achieved through a
### Grammar Tokens
* Logical Operators: `|, &, ^, -`
-* Coverage Symbol: `A, B, C, ..., Z`
+* Coverage Symbol: `A, B, C, ..., Z, *`
* Parenthesis: `(...)`
### Example Compositions
-* `A & B`
-* `(A & B) | C`
-* `(C & (A - B)) | (F,H & Q)`
-The evaluation of the composition may occur right to left, parenthesis are suggested for potentially ambiguous expressions.
+1. Executed code that is *shared* between coverage `A` and coverage `B`:
+```
+A & B
+```
+
+2. Executed code that is *unique* only to coverage `A`:
+```
+A - B
+```
+
+3. Executed code that is *unique* to `A` or `B`, but not `C`:
+```
+(A | B) - C
+```
+
+Expressions can be of arbitrary length or complexity, but the evaluation of the composition may occur right to left. So parenthesis are suggested for potentially ambiguous expressions.
## Hot Shell
@@ -112,7 +141,7 @@ The hot shell serves as a natural gateway into the unguided exploration of compo
## Search
-Using the shell, one can search and filter the functions listed in the coverage table by prefixing their query with `/`.
+Using the shell, you can search and filter the functions listed in the coverage table by prefixing their query with `/`.
@@ -128,23 +157,28 @@ Entering an address or function name into the shell can be used to jump to corre
-## Coverage ComboBox
+# Coverage Cross-references (Xref)
-Loaded coverage data and user constructed compositions can be selected or deleted through the coverage combobox.
+While using Lighthouse, you can right click any basic block (or instruction) and use the 'Coverage Xref' action to see which coverage sets executed the selected block. Double clicking any of the listed entries will instantly switch to that coverage set.
-
+
-## HTML Coverage Report
+This pairs well with the 'Coverage Batch' feature, which allows you to quickly load and aggregate thousands of coverage files into Lighthouse. Cross-referencing a block and selecting a 'set' will load the 'guilty' set from disk as a new coverage set for you to explore separate from the batch.
-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).
+# Themes
+
+Lighthouse ships with two default themes -- a 'light' theme, and a 'dark' one. Depending on the colors currently used by your disassembler, Lighthouse will attempt to select the theme that seems most appropriate.
-
+
+The theme files are stored as simple JSON on disk and are highly configurable. If you are not happy with the default themes or colors, you can create your own themes and simply drop them in the user theme directory.
+
+Lighthouse will remember your theme preference for future loads and uses.
+
# Future Work
Time and motivation permitting, future work may include:
@@ -159,7 +193,7 @@ Time and motivation permitting, future work may include:
* ~~Custom themes~~
* ~~Python 3 support~~
-I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repo.
+I welcome external contributions, issues, and feature requests. Please make any pull requests to the `develop` branch of this repository if you would like them to be considered for a future release.
# Authors
diff --git a/coverage/README.md b/coverage/README.md
index e27b2af7..02eb4fa3 100644
--- a/coverage/README.md
+++ b/coverage/README.md
@@ -36,3 +36,44 @@ Example usage:
sudo python frida-drcov.py bb-bench
```
+# Other Coverage Formats
+
+Lighthouse is flexible as to what kind of coverage or 'trace' file formats it can load. Below is an outline of these human-readable text formats that are arguably the easiest to output from a custom tracer.
+
+## Module + Offset (modoff)
+
+A 'Module+Offset' coverage file / trace is a highly recommended coverage format due to its simplicity and readability:
+
+```
+boombox+3a06
+boombox+3a09
+boombox+3a0f
+boombox+3a15
+...
+```
+
+Each line of the trace represents an executed instruction or basic block in the instrumented program. The line *must* name an executed module eg `boombox.exe` and a relative offset to the executed address from the imagebase.
+
+It is okay for hits from other modules (say, `kernel32.dll`) to exist in the trace. Lighthouse will not load coverage for them.
+
+## Address Trace (Instruction, or Basic Block)
+
+Perhaps the most primitive coverage format, Lighthouse can also consume an 'absolute address' style trace:
+
+```
+0x14000419c
+0x1400041a0
+0x1400045dc
+0x1400045e1
+0x1400045e2
+...
+```
+
+Note that these address traces can be either instruction addresses, or basic block addresses -- it does not matter. The main caveat is that addresses in the trace *must* match the address space within the disassembler database.
+
+If an address cannot be mapped into a function in the disassembler database, Lighthouse will simply discard it.
+
+## Custom Trace Formats
+
+If you are adamant to use a completely custom coverage format, you can try to subclass Lighthouse's `CoverageFile` parser interface. Once complete, simply drop your parser into the `parsers` folder.
+
diff --git a/plugin/lighthouse/integration/core.py b/plugin/lighthouse/integration/core.py
index 15685fa0..e7f33574 100644
--- a/plugin/lighthouse/integration/core.py
+++ b/plugin/lighthouse/integration/core.py
@@ -26,7 +26,7 @@ class LighthouseCore(object):
# Plugin Metadata
#--------------------------------------------------------------------------
- PLUGIN_VERSION = "0.9.0-DEV"
+ PLUGIN_VERSION = "0.9.0"
AUTHORS = "Markus Gaasedelen"
DATE = "2020"
diff --git a/screenshots/overview.gif b/screenshots/overview.gif
index f2c9cc12758c0a9376a3950b8f2ba6e47dfd26f3..f4ddf5c95bae3fbdab7d66fd1729ca488c1a228c 100644
GIT binary patch
literal 2053887
zcmV(;K-<4ZNk%w1VUq;Z0{8y_H8nL{TwJsyJxokYR8&+$Lqle2u^k;9gHVN~rKJT0
z1x7kHnISS(T$i`Ew+jmk3I+!h3=0|x78Dc|859`~4HGB`3MUvEI2IQ%5Dzpa91aR7
z4GAX^4Ji>0BNY-W85S)Z87>wUBpw_v7Zf%P8Z9dvCp;e{GaMx#9x*H!G9ev1G#xP=
zDK;i1CN3^6C?zO1B`r26DJ3H_C?+*4DmEn|JuNLdCni2HDmO4LIW{FXG&4FpJUlrz
zIy5se7$zD=85K?^8BH%MYA`cjAR!@QCsRi|OGG$yN;7j^9V{wBK0QG-H%2WnOgJ}c
z87f>uJ48=3Qh6&ye>hopG*xRcNm2u9Z4G^44Q^aKV_`LBJ4jPGL`(`zaWYGHCS-I+
zM@LsnL`YFeNm5EyR7yflR7FZxT})j@R#r`2T}f0}R#{hXQ%zfCT~BmcZDdzcQEqKW
zbZSX%S7m2XY;J>3ylW{ffEywAtRh*7K~;ViE0{=b0?Et7K$E5
zf-O~zE?KHQa=SQrwli$BCS{gaT9Zsyie*rNa!QnUOO|$5j8JQsP;jqkZnkuBv1(wS
zGk3-^TH#cC%~N;9M|Z|)aLY6_*M?Y#ky4C~XpydIjE!@wr&X4VOsNBfy)uQnc7lj+
zh>A^xv}}~Lb)TUNjKm9!%M_K+HI2+2sMr^!;~lByETz{~jm~G4&u66GT+!i>jgG09
zhp&^3jkk)6>)2+}z94)A!ZW^w`?y(a+}B<=5)-=jZA8
z`1tMZ?gIh>A^!_bMO0HmK~P09E-(WD0000X{uxhjbZKpAdSzrFb#rNMXCP&IXK7|G
zV{dH$A^8LZkOBVyEC2ui0FwmN0*3$qnE+LP0HWvs&;S9e`2xrT0|EjB6)*>q)d&U_
z2rL>31}O{@6AUV*4qP7(Fk2A~EE9Se6blLz4HFg+YzAe$s19yB4>I3*buC>ts%Cpan)2`f>HEG#T6Y^W@Q+AS?^Ei+3kRd6k1
z)h=(dF)ceWVwy2?%rYbzGQ;>Y9WFCC*)v#=Gic2;chxl)7&V34HIL^uHa9jlb2dj-
zHm>tGDknRU@;p32KtDr7M0-U=MMa0=MV;|SS6oOoC`lR-NiR)HnB7iAHBV=kP$(Bs
z94%2YE>VoaQJc?Es_RoWaZ_k$Q-!ru0B%(_TUJ(PSVuQmXmeUrNLv7RTU%RPUH@Eb
zpI>KdZvt~hUW@ct+PLOC*XlRYqX)+jUs_1M*b8Rv*
zZE0?8aL;XYv~DddZ%#>ZQxbC@9&<@DbE?>LyxnuW|8z=wb&RKWSYvivnRj=0cqv49
zWm9@if__nlfl`TvGzOdpvuR+`uSoSdAVWtyOEE}=GB
zp_H$qqob#KX{Xt!sgrZ6iFT@|>Z;tUs{jA1g=nkY#I1G1t=ZYHYaOslT(L1zvUydq
zk8!e-jI%C;vr(n9$@sL7QMB*P|CHK%DM2%xTeeY^30c!&1i(qv9HgOXwRy6
z{Zo()G{T(9mpQ(Op*3*R9jLr_>%J)VZ0|@zmCbQP;((*UXmK$G6*-rQF=y-0`~H
z{r%mTOx>t%-Mn+)x5VLOIqCiW>d@}%wX*J)V(!z)@1%C`vW4%_uJZBf`SjEK>$v>W
zn*RU)|GaYm000R80Eq}3NU)&6g9sBUT*$DYLxEf#N}TvGB1Iw=8D_k=@uEkJA466Y
zNs?m6k|;x-bht7l!j>*$%A85Frc0PMb2h}Ov!~CQJb!8wiVz9Wf*ZXkRim`27p6;_
zLXArGsne=dr)s@wwX4^!U8i1!Dt0T^uVlHV9qUwV+p1;NN|kH&ELOI2;o5!cwXRgZ
zS@+&`n>TM#ok4>Q>!ph5#DGcPMLZZQ
zVS!fpxLlC7ohaUk4SrW6k{VXUV_XetHQkD*%@}2p
z4lbx;j$7*Z)RRn#sil%p(WmB`Y_|6mer0<2B6uv)iRPVa+1KKQYBU+9j0!TPr(P0P
zd1sk^W@*@pHP%TWmrl-hSfWH$+U0ml+9+pm|B=`zpqkFvX@Qg)n3+;MZmQ|0W*&N`
zqp+6$s;Z?ZCOG4VCUWYkbzr&*s#7uk$mgP@{@UTApypa6t-hL-DwG83Y1W5rR%@-B
zZ3!f6v%V^-tCBhDXWX#a8Hy%h*#U|!mfjA@?sdn4xh=BVbt~JnuPXcMyVXIMD!1T<
zietbBiioAWc8-andi%P2U-
zvr(Pr>8yWxCEv9)tC<&^R1M0Rw8WlFT$2FeN^j5_qwM0otp2%fZ!F(yG0GgnEGfw&
zE5~cY0}l(~k5NlKs>3CES*d=#!W(wJzY@(akwU^MaLh)>X?DmqUOn!O%bKdS*M}zm
zN_NkF{(ULks}eqVy<4B#w6Ig2So7oaafLIU4D-D=)}q4Qo!_;px?8Xdx7)Uc+b->{
zgUOBVcHEnqUNPk&7Yi_PKza^0-ASK)>amAo&3Ws>xiv_@l&X!a>szLWXrQrX3~}vp
zWBzT@`1+ju^G?Uky4ZRFO}X(L=ian~keBZsX>d~9)D9*fVZskg=#43QN7w%-nQv#R
zzx1aQ=zX0o3(b|#*1za2&2{Oip5dm)w%|1oX`&Gy?yh$$?rm&**2`W26Nk8eT~1gW
zRMh`M2sYTM4r61ppU{S;r2&?#TV-?9#WWWtn=&Z#
z8BuaX0L~6Vpao6DKmrqh;tseVrx=QhRq88P1Si-r6B>(mX6lvk9QZsN8q17Vtejgs
z$d!LJ$b(&T;Jhwq#_fgfco>u+8SA98C>>BO|8k?ipkkU*H3~++Tia=>#JM@<5pg_Z
zo(9K=w={yNaf%y}^USupQ0Xg#&^nvK7DzScF$IXIGy!cM*N=XoJ}6>_9aEOv4+5T*6l8rx6r9Bl+M&uC-0Q1
zU$#hrh1;R;_!Y-3TFwi7++Z?ghqtB-@0{DgAvQ?~xRi+rbU!Ok&_PCIl6O5^D+)j!I3R
zrjn^%_$gG?;JzbR#eF8|sZ*qC7o#3wslt@xQ;cdaDE*JDvH9r_t?J7iuvJ1}g^Cum
zdX;oG=m%`|ADXzZRIxAts8!@jUm>W*isn@*ac#<9#RS$2HTG+%17u|W3Ph-?%CK*f
zRWXU^OC%iD3ylpIKRLxlpIY{EYOSnPetHD4xX&p+cpqJv!qs^HvaOsKRA>*InyGot
zD@cN?ibc7iO2U+{l^f+~QIpu!5ZFNz&RE4QED?fT?sB3v^&<=&nccR8Z-HaPpJvzj
zu?Hgds7#=1MNM$n^#(O?NQ&IVXb_98yiWY*(*B*dye-CcwH`%1Ql6VA=a!P1@>f#H{wX}QM%VSL&$u1RGn_VobR+++A_6lamItFl9FQq*5
zVvk*B9VQ;*rDfvUWw=){Uvgl$bWSKTM2_`zp^jDN@XjeBPG2}qe_j;
zs^e8K+ZG$wRViFp@{ASh-$O-Yw46O>M3n3mM~4Q=GCq_Rf)L=PPBhMy^TLT^%+z6b
z*<7ZMZy(t>>#ijGM9*#~SJD}g*0LI*zIrUCsd?fUsr9od#!I9p9kKuxO=1bOsWC;l
zr?fcIs=!thm>D*2s2mr$kdx(Jv>@G*IwP06G=LPL$HFOhx7?xL72!r$<+)jkyH9zi
zX{yjzw8~Y=zQikQ>9|z2s+h7_-Rci}%9N#IwFm3}m9!TI(QzTF+^Mp4DqA6~;);@*
z)l`J?Qw)n&Vzb)gB7YxLLC#c@3q7@P?E%mgJ93!^Iw~Ny>R)G$-nCx4EB*a6s@Hjk^jrSn3G-T@nu2`
zLcVGq*P5vyCu@Pn{a=?`7^`_rtyXWY^0)up1n8~1PtOi>CnGxSj|I6_quzO?2a)hl
z_#uH~YXTgX#`2cmamW4c)WLsT^-2xG$y09a*_&X{Ob0yK+4_Tz*Id$UUrNR0YxCr5
zQe);mFt!g4NrfLPl_f6tyw?{QjJi^z-&gMcR3;beL7)N{(GZ6?o{q9cHg!+8aiBGNp4WP6r&S8b
zfbW%SjkQ!5<^o=^X<#)2p9NOCMtWYySKx+d?n7V=h;haSfyP!!ZAAoQ7lt>7Q>GM9`4>Y-COy(uH?SoFsdOmVH$=u&Gs!h(
zZQ>T%5*mw;3C^Gm%jiTBU;qO51CTU-
zVQf`Mg{3HIvB-7C)?aT}hAJQ-8^?B4U|8!Ii#G>Wm?dDVrvhRpg~CQ|g;!|lIBE5m
zS6zr|;V5GWm~lryYU?&*ncXJbI8YsAR
zX!mk$g?K>~dM-I&RKbTur;11apeH6rSGksa6;*gp=#o6ePyCpC1o(oI)`2R?fn;}&
zJ_(R3*>-l|l3efvFd0>*Wr$-QxsSw`SLoPZBpH@Y
zS7=#>l`Zy5hIMllb(Sbei3IippG8(`6_;AobGQY7v{OIdm}r@!Ob3%eeFjF3V?^aM
z7UcI8gP;hn;0)Lx48l-d58VvBsaTW8byHE4{W*Io>3luIn2AJGDwA6Wbxv~gTe|f>Qz4m?
zsTF3XBbHDNz#t6Ma0{3q0Y88VpKwF`ca2=5G>>U#?37fbL5==2DVr1-4XAlAx}L%Z
zA}qI?JvwPeI)y_23YY0cpHP*kPVsWJwUPe?Sgg2ln}}6FL~_?gTSuh^i<+D*$yRKz
zkQ%9lkcuiCN>R$m1zc5xF{T0GX`3b_
zU^%FU)>FTFV-X2?Od5)v)u{JKg&Im|a~OFT_@E7Xl9IY}8JUr(x?U~$FEj~tDln0^
zx}%;dgbX^HQUm3@i0j%XRLOS)>Js&NcDo(w5<8!CcviHf6Gs_J2Y>S?K*+H}cD
zU<|rctk$dl|?o4(|T
zBhZh-cNWlzRvxBca|v|>I<0sawF+5z8;Y%?2elDtl#+I;E^vt~X@oA&0~t!C@knSc
zCZ2EGpe47iI>wa+xMMBGWM7D_@#<@Ml`2r?w3-)`;I=+7X?mJgt5PMNQYMrANrgN`
zMIB~fR@;Lhs)@TguxT54Zl$n?C8g~uh~)}fu85>W2%S%Ai16lo$Vs9WR-JI^J{LE-
ziZy6q+o%U?ca|Hrs8+Su=|L!|X!r?Lu!~UUhJWg&TRmj6H1fQE8YSuU6*Duk*MdY1
z@)=zJ5D99!nV*0MP=E<2R8}E7i`_VV2~{bBvYLd&N!n9#1E^k@C%EJ$Rg{7Rw3=}}
z@EH-dfvzWPps109DuV{RoEzzDftX%zMMO~8ot)U9jGDR$I+vFilCQODV0Eg5X=;_Y
zcN^TTKX#!n$6O@Hb(bgvk*jrSn4+Cq!B@)#ps2tqtZ6gHtxqR|pxDAr`IqR0zjTGe
z5;kKi9FQ_6s)O}cEI4!+c)CZ$K0Z~WUKg;o8dpb%aT~~j#+t4qh;b_g!Bbjvl*)7>
zM#Dx7#uq8Zju??#7qkF5V4Ii)Mx3-&$GLGxfD8OzRQDMV_MP(xS{XT)9jv^D2)1AU
z!MGOoh5`v&5nM`{byMudDT%on-&m*hRDZn1y|MIx%q1LLkO#JK4$~kE&>##hn+Lae
zvkphUa_YVh_h$^$Q3iB>D@4ouQ*k*;feI>EvsI-=g;kz7)ppad*sS
zc^W;E>=}H<#$;S)a)Wowg=)?rMyg8-T0}t1L8qRvsliiq!68bZzWle~hFkixSzan<
zWC0b~3p+{XHAqrSf8)u@bsx-CBZIIA!q5$CtqQ6T37=3GJ5^p(6Fg_A(WR$)0LXhB8;)o6m6(1rLf{+91&*_eq_chs
zna?3OYw@wvWH56i*XMRd>79+%NN`h8S{-ZKfCAy(Mc(4lIGf?)-!sdmY0H{K+;5TJ
zD)QMK9yWmd)W0Hnjk{pw@fk9n+SvU_%mUb#{Jty#*aT#<@%uGf)?k<1;=xVemYhi9
zGf(LYPXGkwCd(F;X&Yr;aOUV6g1teT#paP@Np;k4By`uV=}_qZgvl+WL)AUoG)v)<
z+-1+lXY{jBBV$`udYaV7MR=w{{$%7&LFD=J-sNQB%+p6=H0dyOr3rJ{*n~g-*RyUj
z-sTie{T$1)ohyPu$>OA^Q4>m}E>3%qK6*~)ybd#LKxYlaOo*=En7-wfUP6i#vtO=%
zlRQ(hxD^`qMG78GM|0lCo{Nj_ywINIzaG72-aJo!WmZm1TAuExbKYq_*9m9oG280!
zom21f=jTqzf8!@iUVX)WOkBP<+?}yf)9u#|Xv#i*B>pN@aNby6@U)ZVbBgP767Y>l
zNud<*>yqEzeevTY=kQ+KOw#YPftrVu=y8rQc6FHPMvHy_Gx3e7=oH`DNX|u;WQ{w|
z^8Ux@EU)r6KH+;|8jL-fK2IUFq45Z3-HBo!++~Yr!85PI25e;IY5-)L(bfMR;a~j}
zsLtdd`{^xpm|{;NIJ5Fd<>9wuC^6edOkeAqkwuEh@gkq}WB%a#V?(80u{8Bhq^YMB
zn^e`_JoNPCodors5#RKVOepS{TOTuxj`83QGdpfLU32y}-#R0C$3j8W$bWHB(Y8MU4pTHjv82vAdqLG@jseByGk(;Vav(~UE+NWPNmJxNiy#-~w8_(^7d0yn
z4(0eWJ0QKrmTCFV_10+%jdd}zdr4frSZ86vu_s++;-~G-6i+Nn?Cw^(g`h61Pd7<5iUo#
z;3skf|9&p^(=WgQ{UdO{{_;x@KmEWu5Ud6j95Af^00dA!0~HLAK?*0_&_D(W{I9?f
z9gMKQ5K+v}Ljx!DZ$tz!{I5a?`P(o*0XgL2s|YnbB10f&bTL2+Lj;jX|2%{cNgX5n
z(Z(WiOp?eYtJLsD0YB8SNg+4-T{K@|mUkwYG_ymG)#
zWz_OYFfUbbOd1yyG|LKuyiwFCXs1Wbjd7t4e`zgXVrGX
zVe4FU*dy7LG0$-~T~k|a&t#UrFX1)R!VT-iuuOlS~Ll$}DkD>UupU#?PuRj*LYny>48|kRemU?5Lv+h{!
zm3u}z2$5TUTkf#kw)*X_mEKusy!&3eX1)clT5F-}&O7JC4R@OG#^HAS>$4koIq<;2
zPP*`@kM7)V%hUa0Vsvt|9kW
z@|PnYS#Bs$E`I9F7+r@4H&x%Rz#urZBidu!hMLWj4R$?s|k{MqU3_dMA>uw>@*
z+W=$Mwgf6}f9%^|(fX(VLCM{$ej+>__ds{KlIiSo{Nvu?mc}#eL6CB38zJYK#=qQ2
zFnvX98TfVvL!DW0f*N!o4~JK|`0xaa)!BA|gE8o?G7(pj;jfPk3An>+V
zz|8H)KDHZ(#un(n9FnboI;^4Zd{((21Tk?{BwF?&NXIp5EssCknH#(2#;CC_i#Yor
z$?jIT9u}^EhtypXhvq}S1+9?{G@#F3hsZj<5QPSmp45!Dvif-vlcGZ({T#{0)Flmh
zm}FoDEq6r=3J{GWvtuSpIm9ipvXAD2rO|Hbzs>P3bJfe?;)JNcJ+{)AtyEqHMR`Y1
zdJKGA1YQbx*gQ!8J}`E=%vvaW=1h@glaR_3-Tb0i$)&|@XBaHyF9G>ZLnbhGZJZ?*
zDd|qnJu{T1JDEYiGMSldvYrB!9utYV%fsz}W>EBC5_bsBHx{#q-YY02FZeU}(KC0D
zgQwI^I7Rkpah~>bS@9D2ONeTeh^j=J3R@?(hI;a)f1BPpxmi+(0uzVkbY?||7}0YE
zka+J?;uG^Y(w~~plKY(L<8I1?JvP;dIbA0rqpDA^D3odn4JQ6XYQ>uobCT*5=;!K&
z!4AT)i;n}J`s&6(saiCsRTO79pN2if$?Sv-q$BDG7ts*jPoiFvqWzSaQDnL=1)zVIBFJFvtW(wEpr=I
zIZEKfp=|F{ATqAxHxz>Yd9K+X6#!{A}FjXB$
znQApA`qrAZ6kA=t=ic3^b+Bjsui~-^Nz*!1f{j$|Au*dHn3hNmGL^x5*1K`6%tqw#Wf6K?{S9`$&BI@WhquT;QuJe774QbPOV~(yE|C$W
z*>3BcWhUoWabhG083kg-kPSf==^!;Wn$U_&3;`CI*zzkwR)SHJ+7?n%btY=SjZECO
zskIiJYni&@pcT#mjcWm^zmbRZX;`?tRw5epfDM_oARA4RtoVcCaF-gpt2({*J3*6C!RVQ}e0srm)
zdY4&lf)9M&DXTR-t*h&v)t01cE3BN8I@aBxIoygqT2vKT-gHLVdmhs&C$Ie1{x0_1
zSd*}mx6CvjTD-9RH6+S0-BG$hvXJE*pul-7Wzo(Rt|#2)cxgLkzG6G$lio6YJ6lh;
zj?dcpB=eyuy~D>&hK%=dAk)n4vaeSSe_;oUxy_BzkC8paM6O>G)GdT2SOTXwL*(7e
zO~u2&6g&R5_=Crxq{=Eubas;XRH~yQzDDZ-|3^YXxM}ni12p!h=A!
zvx5?9xVxf09g`>8g0miAzdeS^DifWgoSJNbz{p!%35XomfW
z0#P8mAxMSW<30zhJ?*I}og2Nft3VkHH7(??NIEkwY#_zUK9Iq{zM`}LDci#30z#ns
znz~{x_G+}I!#^u4GT1V>NL#_&W2!;&wFYxD$-zI}@woF-D(VS0I65G!nxK@jE`C}+
zkt48^nIJPHLz|1l`TMIIqZ{tpLK>txpK+`X1GSk6!&-YlR=c%y_=YQ-t(W;acdLd~
zQ?*JM22vwD`m!olpp*rF)Vx(Z&g4`FK>#P0Y6EDb1YS@$UIT+)u!45z
z0V{w7av*{zfJRqq#B77WA+!Zo#7-&zPiiYk;#{@cv;`(mM&q0{1Z7Pk00Wfd&esaO
zhwK!T3Z8IAUrEzNa+#I>2$f*sR1bb7<;q@Q8NeVl+J%l&>nC*Ta$&o
zYk_VoJx!9Tq%=`G&;v^V19!;53T=mUREIqH&)lqs1Z9HgB!$vz0cBXdC@?i`$eNVg
zHYII1*xbCZ3J$yGB-kF=UFe8TRtU^Vck4yrTsIo#bJlvmhjaa;sChvYr33+8x1@Yg5=_DWi5*aT?ML!*!ffz2`do%7Rk;4FwHIK~
zAgHw$?3ko{LM@;Ka%F>Ut$|^%Po~qE){E74RY|M7y*((xcUn|EL`?g^CYGW_wcJ?3
zG{xbAE%>9kJS5e=>?4_5Lq5C9OT4K@JJhx0%;{>j@PV!}#VDj|ITqBojr-IyL#jeN
zOD1zIKs~M-I>bg)xdBq2qsq9eD#WPOGOxk7wUw)yK}CjmL#agBR)E1Xlu%adm>Mt#
zEErZ)YeimNp_UU?%U!h|5LlW-$GriIHfRTjWg52=gq(cGV;h?nMVf~tR}(~8hWr?w
zY=$gI1wd$q6r@XLOE>6zU4>JE6xBihls(babliM(*jPo&iObiIaYZP2*VF}8YPHvI
z5QHJv*1h3eAxO7HXt>|SSmVvN;;dJ&kOJ$}u=RREKbW;Av|eCk##t5KS$$nS;01eh
z1ldgmLudxXRYn6%IW|3Bu%O2mm{4B(Mq~x7vPvrqn;WKsvd|)0%ktX3884m-+8h%k
zX<9V=B3n1@o!nX^K(!v^C9+6rKeNrhOH;!UCY$WKKIVfS1WMdi>e@Klxf3q1t)$@2
z<*mKxL>Oja{M_13Nl?9sBJ{STYvm1uo#I>MA
zHK^&IdD=kIMaLFUQY!G`7Zo-CXw+S+iMz~u(AX4Xd1E&3Jl?yTODPx!``pghOuZ%P=0D&(cV
zEc3N%nmSVjL_Lf>8{Sm^b~eS3K}C#UgOw@VqHIYb^}-Cg($y=rbihSVHr84NOz6?J
zbtCC{m{p8bPgx)aS+h)^Ny#dW(O9zwdrXIO2xYKTA9qR%(Oi1Pe;g29t5#m<9t*v&OVM~?(UMm-3g_pz?hr>wL^p(wJNf_@;=jM8Th(Rq)p!9uIHd5
zG|u$kkm}Qcic7OZ9kj5@?H9O3(ZT9xTQ6^r2BBA>h(OaPu?
z;ojIPn&->vZz;>~pJH2!rdowQIL=y~jg1@w!|ixtpY<~5EqWbd(ki)KJ4mA{=4)Dz
z;csONG9VvJ;XALRvYZtkz!0|JED}sp#hclt=ws^Kt(kJ-eqf;jy@-l4`>HWr+glkv
zf`dT7g1{aB)Y`Uf1HpIx7&Rxg8y+xgQ**3|a3*{y9v*HYcyl;MVWQ#laUeq-?qDZZ
zB0f)tlhFg>O&m^e+}L`$t2uN%;zE%*bW$^Giu*O4vGg@Z@zybJuRW|mm%URKa6z5H
z;>+RfDsAu1^AvZ(`wCNrU9?A!^#%saCWLCJ{olXs^-P2*1w!J#op0)fE>IWRax_J^
zwAwd(A7gK};Mw6%51H)~JrPQ68HVnLqM-~jbyaGoEK*g0u$_%CO;1l(i+=FTlyK?F
z)R8gk3qMRq@1jRd;TP{`0PpEov;!#6cZPi2beCJj`^`~rcBISC#dUQ-7dH&V!7%qi
z4HKgOZ|6Aji{d)7=K(@u5Ki1IV#|(SQ}F&-JkEbys=57F#)YwMl
z>b521S$JHZCG>i(Rf6Huc5T`|`K|@8(E2=e}^+>}+x)zS;RmMcW|nq(o9Q`&;?*##$m;@i}rWrL_M&BTq!s!cb#;pG^hAr3DBgOa>AZ
zI8X?a9tj09Y^bmy5hM^NGC@e>;Y5KwEJDP{aAQV?13hXSDN>_Gj}l8VTuBjSNQEvz
zUL0w%M-M0)bB3&FFy=y_4|#^HNfIYVmM{xmgc1}eM4wKHAjFw)_(Y{nf$#a_kul`TxUao-9yTNftYy@~<j8hG&RGc|<+rX(o8(vB`BFV&q&+0r)kfi6+plko-
z8bq~PjGdz@Rn7bE?3XI7QXQ>OrEIcr$*S(%xNYs?xLKoyN;-5wy~B&T{JVL3(dmFM
zUsbOA>*kisIgYRC+_U!I#DDS~jGlXQ&oKpBX6gkdV0;FyCf`}?snt_;*7;`_eos-z
zT6^h@))QLwU6j*s!s$ode9?h4AAqcZnBr>y4#<{;vkj*ghWr8OKx@FUINNc49Wj{~
zYDflTb3*9#TSXe|uq2Z^{LmznQA#=Glt@r{C6-xYX{8BTF3F{rVO|OU=9f}FDJGg}
zs(GfGZAQtan{moHC!KZLc_)@|YN;oke0u38o^6sDD4}x#S}3B4D(dE;i#odHql*48
zXq}Ya=>U|LQb~jwL=pj6r(RSRDXFEFX)3COYHDhutET#Dn0huk<*bjo3T3UZ>gwjL
zyV{v+n!WnU>acHS>8G$-s!%MmmF6iYuvq>WWMrRG``8gd(8^@8sCL_Io6(XgF1Y2I
z3&pYKx}a{X?Cwe~mGHXDq`X${@a?>D;v1*DS@w#qzw2VD0>Fi0nlHjkT00q4p;D%;
zlnTr#0L2wsd@;ruQ_Kr5ws`EZ#vzM5GRY;Id@{-@tGqJHExY{xGR!f{j4{YFzkCiX
z*I*0_G3VI)GtfZ`Jv7lp8=bN(%DCdn#~&~KG}KW`JvG%;TYWXwSz9fDEm?Q1wbmwk
zto1pqD06nsXPbRC+t;v749{;9)ArnHvu$?5Sst9Xm|QsAsbrn9@fcN0o{cBawsh=q
zH?~}S3FHJEJvrr-TYfp_mDim4#cX4I&fJ`fK04{8o6d65OWV@8$Gm|3I_$B_4mQ_W
zcQbp{VtwS?rn$8Z5xVOkiiy0^MzpicmhyM*N8C&Ceuu!I)t1}{#hK8vl;
zgf4s`425QkbEr;nPTL<2bBH?u-0y1t)1TN}mo)_cwFu`t
zz!^zO*qag;ssI`8{Y-I-L*d0h5WX0av5aOUS_ehfLW-^NjBb3R8|SyWISNsScD&=&
z1W-r++0kU(=%LkmK|CW8GI&T#paKcCH!n4?PKzuR6g>sKBLGYp*YMI~I;DmRPAPGW
zV;uNE2***9vXmHWoC?o|u{N5rm98A096$C)JkqlNmQdqbFR=E*{D}>aRx2bh2iXb)
zBGLd?oKiuy^CIp9XGby0k!33LodtnsFHMSMkf<1{P;p@+E~p-pj`>9Bd4Y>ILt)dF
zfdGqr@Rja-rwKcjN@c84mGHbLJ~gK_SMY*$9kV4s@mM>o`4WgF8(Y@g7R-fSl1fP|
zW(BVF%#`$|MpW@oQ}Po8;rIkO$vKj103!qec~l5$5YdPxqA=(U$zg?S;*w$zrB7+m
zVVsl_&|cBHj-`;Dk;^AggE~6J&9k1Z5GqoW`m$J}bC&^q>g)#U!#*C=sZxvEAQx&t
zSlnw$3@By-8L+AN_=F}0vB;8AG#HR76GT4$*{DU{`c@ZJ1wG6`sZY~3acOH?O`6=K
zjB_#)gx*aLmn9}>kyeFeHu?^{=0$2Z_PIgUowmKKU9IY<
z8s36ZEw-|CC~d{sq`fhPMjNfjP9&0(xgrRq0p6=y#j_c*N<~1wh0RA%vK=Hz1PKY+
zNl5gX(#t$&rJ0oC-?nEsVk}XdWtyP>IcH|W#W~KWl|3(v!J9_Tk=LFvzVRz#*);d^
zn09-t8hl|FM6D^*zJxqvF_$Q%u)IV*Ac@btkiwIZ;H8h~!_-l*_6%E^#Z&=})7%GR4+h!ZeXKpJT$$zlRwig2l0GtX;a*lGtOL%iBa*5i%`dk
z#x}^aZHru9&Cxsb_7KBe#D;n`ky`SPg*9>>!jN?^E7^)$Lmd%LiyARRd5Ehi${sL3
zq^t|}w1CyH;C%F>=}zxQJ_K^4B~8nvZoVnc*hKCqt;&;4>juU`$nmk8adnzyp`gjG
zcGJxdor?>3$h8e*`Hrl#fs}>aR8BxUS;o|_w(?D61mSz_+`;qyjlXI12>X}AMa
zkFTA5*h`N5CIlo
zm^{XdRw9a}a3#Y*gemKWQlEwqK`J;M
zF#y+FrB$0n5Cu(-G6cgC$yAhlmCIp?3(k=j;UFKHPCuR9pvhk!4kF4>8y)o^qsblB
za2x&zAnz360CCmJ1sgIU$1(v0!2uU%@LLulN#jLFS^eFHt(8S^AVNf+nvL0+ffUn4
z-INs^Hfd5zJq8;T4!S5AE-r~3-eIORRu|owu@Ry$QVz3~jtveYG9J`E*$?9&;-dMV
zE(Kx8*xfHd;s9w0kW`6xR789n9nTpZ7b3<-O&Lyz#?LX?zNyqSjpB7sL=|{q!-xXF
z(Zov~L12mh-y~7VuYuLpEs32e4%j85Kn_jn8KeBs-#{j#j@^(mx|bqKO)u~u*0k3C
zJq@^7quiWB_CU|3=+^eZ#;pZL&8-`QL?QGAm_#TZkGL9^E!Vvv$EsaM8OFs*R3RHW
zSHpams1%NL#aSKlUp>5G5iR>IVcA;B1m|I}qkt82dsG)R#+NX6$Zs`Yr$yH)RS+{pap|Sc6&wWp<#lM
zYM}JXnDkwS9HX(ipDkQN{t?@#29)hZ#{cGGMrA?$m!YmXuG=q9#g$t
zw2~@zT4=^lW7YWHHBegIm<{OA&D-Q1XPJ%fmBX1wu+j;cfNRR?1*xoT%bu*3d`p(93CvcB@=yvA*eagz
ztfV;6nBqyeGSZsp%Fq%mt{RE~nhCVr$(bDOq6+P@xPYQgt)sB)l@wSc_Q~M7YTX8lm~5@x#t8(4%9nD11}KRMD2WD045iNhY@{?$+9u58
z2JWg@%H#@*-S+MB{B5(8E}FzGdlmV
z?Cr*G+cr_RxY^}~tl!3}*m9kfk_z&I3&ny9?1BrMXv+2y?w-I3>kjYFlJD=D?_rh7
z_$tr(*6yNwZ}o;PGGze?=40!IiI#FH^&0K{qAb!9Z`tyS?zsbFmYK|sF0
zoE&iIrpfLSO4?qm1OG|0bZ`Zu@cXvQxwtM4>o5?fO8%hANR2y12P}au^@K|rsQ!U|M49wavTfjk^}-IN3tYO
zG9_2CC0{ZoXR;=5GADPkCx0?1N3te_awSKGs9?)}5)Ky}!767#B)u{hKv68S@{bU~
zkF2sRyK)w=G6jjsEq@9G&GIS3GAlz-enyhDd`c`wk1qRiE8{XYJ@YAN3pp7x;jpN+
z@Uk>h5H8=cH8*o4X)|T?@+?=zGeeOri;66NbEAecFRL>+!*ldN|MN1#axb$e1r;+p
zkFzl^vo1?>G2`vJw|^EE4TKMV9izjHSiG&wKyE?YA?L-asX
zb1E~lM`JTC`?6r&vqHb}qf)dk6G22LRyYrIMDOxE2XsTv^fcFVL)UaPPxCbk);Uj$
zLX-0m{D?x!v_H2qPsj5}vovLtGdXFqN7u6{d+j}!v^+m^G7Gao^YlntwLK5>Ef0$e
zTMz=5OXZs1WE=r5#;~w-NeusOxF!oJ&oy1wwO!veUgz~Dm-IPTMo9-VOe=IYGxJx+
zG)*(LIL9(TFLgT4GAn<|LGQCSdo_!)^8`8ckgzl~tF&Wp|1?QAv@6FlM2qvA`E^JC
zvqcLQP`@EI54AbVa~3c)GB0*ze=}r1wr@5zR`a#54{D|RhQb7<3a
zWyf|vD|bAzv@$1jMo-HuvotM7Haf$!R;%_d>vLq7wp1%NOb>KV%l2b;^D3Kna!a)@
z-?m6gvvrHNPSJ$-cVR2GL+f{K2edK+H$G?fDc?3aJ9Sz&
zI8Yb&N+&pLBlATkc67J2Roik!LpGJ}K=!im{|>OJe9KsqUmNNq5je5nDlfC>HH^o&
zjL$fYgYtnl^up{jgVS<36E#mGwmH)_JU4c1mo$T~|2Hq^_;klNE$g>zCwGxocaInO
zXnS}~D|dMtI6@zIlV>-F8~I>YcQ12!MLRf@`}9zI_d3TmQ+Ky=n|68E^uiFhm&3O{
zC%I^&_JUKhYd3eE12mT#_?kyGGtcvDLwIHQ`A-uzk83!9EBcZDH&Lf|k*B$5XLxOA
zx+*96V0Sr^Kl)Zr_>C`mWZN{5i#oPg`AD<#l>>M#vpI!ZdYe0WlYcpYZ}?yL`DUxO
zXZ!O?<1%zJw=xe2R4+_?lQe$gqiu)zn@_N}^lzf3ufcig)v>q(o%pcO;Um{Lw|Bd@
ze>*AvdY|VuheNfiH#)2@vxf`zac4AqmpOV*|94kQ#*o0foTE2@+xna5XELL?GgG=R
z*ZQxEyD;~=RipX3!#TdE_ihXKxzo3G19y_O0hR
z&HFXbb9z3*xujG4g5S5kBf3`C`+6^RlE*yDqw_l-z0+ely6ZTl13b+CdDeUQrnfXN
zFL1QWZx`cjm?-fCts)bx?+^oT%3}N7ZacW|J>U1eCaVG@Lqgz#GO&-m7s!AQ$N=K&
z0ORWb3giIf-vA7teah#2eN(o<8$4Dk|8;W5H`&*BR!=>yTZ>=gvzA}I##?oLCqCl)
zfDbf&j4!{lJGdkQcty=ew(yK4fS9LzB8e8@K0&HepvhpHuiVvwrKpzT?Nf
z4buh!7mer63nC3W+G>a)b-Rg9ryUl=$$-!GaKp5b+^H
zh7XZCNaiqk10}?TStLTd$dctlhZ$uWtXUDKmx2isN=$g9=SGS$G2SdnFyh0X3Skn>
z;$lZssZ*&|wR#n6R;^NzIN16%|0)u&W2rJ7SdoaDvuqZcAWK%P+p#~cdi@&rD&Dty
z`O1wN2ykG*g9#Tld>CUh?Mh>J^yxUjzNRYYjCBE8f2)bpcZ;iJKSb7&qJUTtVyB{%Y!h!{{j?)G%2L0
z?m8HT`))f9k@65BiegfZ|GXR_q=_{U;S&+Qg?z%OIqt&i@I#J1YzRu>1_Dqt0!up$
zL8XGU5k-PT+!Dm&$V<*U2uo~E#~B?gkwi0#TduD-<>X7RIqghNqk_~r>j=N>{4*~=
z1)Zu+unL_jvP2bKl+i{Vg-jD+x}m2Udn~o|9(%g+Mi|C|98)LIfY1R2luSJ^HK9H=
zGpIE=WEDi)j##p|Am0SlOo$*cMU+!~{k2y}iewPV*R<1U$|`AINwhSgyFx*a*U
zg3gr~SW|>0c%xR|D)dmSy0$BVOARs4YHMsmZe^2J`U4smmJ8Q@BT4=TlBfRb6KF
z2uQ9`2KCfcCk7Iqrsa;>?i6>N@x~`_ymHYoKY8@lZT!Mo@mU8p_VNqXIg44K90Og51cq6|h-_3wt_C
zM}$}rKOK>WgZjatx{^dd#Q;%>kwf07SVb#7Dja<4#y#jUDRSJS8++^2V%A3#i*2wU
ztoQ;AzOW7LQ7%Ww3y*QCCqpK2=~nrn(EVh1q6dBHemhx>C{njPLoP2E$`Hkc*0h?R
z7zavd|J=p{D9MfPQSOgEQko3O^+=WdtBx=;V|M_#AB&{tJEth*Aq{yAQ7DOEYjc+#
zfigSn$)Ex8=s*Wpz&)b936%gll5liZE`k&@dPz9~7U*ccEv;~UOwx`jiIGTF8j^xX
z;|URpY03u`Ya6oYM?MrV&f9IzduehKgL1U9W}0xFuv1|)C4?ewi3nV*F~uscNsL9d
zsV0vUAjVAISrMTo(s7zXoIFwA5VGV<|T4+KD*-u0&G9pS)Rf8I0
z6}s#IZcfw++)mmyu8rkv0Ye+xqymzVjHDzdF;$?fB9@ef?T2z>DWHCe(yAoJ58Ft`
z|2Iq_i*!8o7l8qUEY6_`!T6yZng9bJU}1`?MrNvCJ&YcnnhtYZ@i3eyt2P+rhi_Q*
zF~R`jJ^G=KF6yHn^O#gPJjEM9B4l>k_`(-hk;cJFV{tY>o_2Cnq*`U{V%5srVl9?2
z0@BB1?XzGLOg0fiS6Moj?tr#$>!5eJ#N&I!*URX
z)=8`=@v$dBF-831x4hy~5SSOu<6pl71lnN$ov-blVvTtmcNRyWdIYAy@aRzSya{90
z)g6b9z}ez<7Mt~S(kKbG$8|U$6|IPX0>+_-C|Ci%O=v+AYVcPgH1Ll<;tW!D|HCaG
zU9L6|E#=8xBeFm?L=@aO-FZO^Qj5{#yWBl+^xAM=E1!a3iXLaH}$KF&J1xwakigt1NDD7)pEt
z6@a0HCIBIrJ}*WSQ$)<43v=gH1Y)Xe4GdfdL+HXdnlX^b;-os5j}F?E9P|*zp%rta
z4n0H%i=%-g`!|y2&OYx
z4JC&!brBaL=>ads%_hXUBc{DkLy(DK1py`B%?>%gN1n%WH8R5xX#>pr5eNmq_vMsp
zaBP@E$J{i58}fko%SOn+s1pI!Y+#v*`iQ|@3Jdu8e4({|KKC>hjy{#O1N!
zsS48;A{YGRvu1e=RW<+%nD>5{#$QFu-!Oz*Iqip59GVl+poB99vxjJ?IU86db6Hnm
z`B$7-tEu30U^2gX!vI3{gE@0L&VdR)sKcssfQ9YH@zh(ml@)KL1nJdW*0=hh_h^>t
zn*-z4;pQF|qfduAe*cX-KR*zoXoV?SA^miezN)uR{VN0$j(fm^9`NYkIK$WtdyG*r
z_+(}#u}y<*7`)WmQ#B?@Y8-^<+73Db-CB&X!fOa9A>shgTQn-7K&1`LDgYs?j5=bt
zmZ#DpCv{E%c}Poi6pXn-4V_j%7UDq`N}zW5&tf|1z}5g3|19AMYTy{qE*ds0Tp$VU
zfUd>3K%_3jwwCGbfFqzpL=>dK0?kR^PKSaZf?S-=x)uwNu#ItY3E|KQgbu8e07+a(
ziVJ=&0JQ^UI52u>E9f?43BmB9`fm)WY3arZg=oKmY_9Ar;CA
z02-hHXrOUkAU_`Jk3>o#T#XBVY?hV_YZl_?*kHykERYV2K_aXOEluBG!R{vUzXtK_
z)(0r8U~&4e0XX3l?hpiIVGl8hTYzT*vj6~X@Kwaik#>d)&Bs+PE8ZTY0+(+6lunUQ
zCVHY|K6nVp8qe>HYA;%Ko{iV}VSt2Uwg00FB;A^JF>
z6|e#J@{y}lp$*Jo6$I_7-pbF$0T=>-68JG20-+7WLHqbj6u#li(kw9aKpen87An%M
zU~d%EOd?s5(flC#BmvIE0U-0t`IOHhXVMhDfvart8?=^W7QyY#EgHHoj^0TP|B2xjS;-T(Y36RrVu%3S;vpX%z~Cr}
z8mLg-gdiz&CLx9Z78ZdrGh+DC0Uc5y_zdA2WQ_~S()T*^8i4KRV2u^jVG+Pi>>};~
zA@B@?PD_ZO7X=dov57Sp&bBZHyP{_?8$br2YnM7ex)cizGb@BVaSO}gp>$0Wp2`+z
z?f=T5-GdA
z!KFb&(b8UP1suQu6krcKs0w*!3?Ydl|K2j)xL^om!Q8+O63W4;(xE$H6G4A&K^s&G
z#11-%(P^-2nG%z;bdzxoX^~be?_}aDf{GdYVyNoErBo`W(4vTFDk{*(cJiSKhG4Z4
zs~Lri2^en&v_K64VI9x0Pq;Bn&2fjgpbFX)90$Q2Uk|G$Ppov0%-%}(3MVkYpcM#`
z%+6uBo@yL$j}l&i5=>zlunG`5GxV}5Bnx98BhsqeY83oH90Fk|lS(e1iu=f~_?`+M
zcJfl0s;r(0uCPilz~CE74;*rG_L6TW4^1HX@fPS{`@&LH6EzhW)lnC1Z!)dYAjhug
zVQ?7ZJCMgVazNC!!72?d9TsUi?2Yc@1LEYk
zp~&hUg6=E1p+LFt2Gf8Nc%eA>(>iYu4Y;8@hoBX>)f`+^*536uhtWTZ)I3o~1J_ds
z`j10L23(v~dpKYnYUkiE>6^Nao8)LGZtkhX0T3ki+{^(GfNnL#(hAbS60BfkN3*;%
zvm@p&N?y#t94s&=2N*3(661qvx#^UGj%K&P#BMepNYVaSE8`fpUaPe$n?M#ap#{#;
zX}Jn5tw8c*p*uGc?7EXL|Lx!{HPdAAQ)GrQ?>a7ZRyG(n3J$Gx0;2~F7>5+MAp(3w
zM5k-mv}^%0bP_Vax=B
z6Of@C4B=NrQqNLV8z4>1n6LRHQuJUCAj!-e7;PT=Y*L*st5}lq2xF>NPxJyIP`hu;
z{!CX_H5Iha5N5Q0{{v(50(kofSPmv_9v%n%I_MtA;r-+w9}QzSB=HDncGRfh{^VgB
zQf<|WY5uCmE;BY7z_P5=Appq=8@LN({~#|{a~s4i9q_>pOd%8e01&dF39{h`q@dh@
z;ud!oVP=JpPN&i;Edv?W3&dqZW0qez0t)@W3LKN$q*tNX_U+adEbpNQG}hQ!_y1af
zVhiHu(m>b#uNjV(G#_rM3PBc}_y|O*v|gq-5Z4!XQ;~Lpc5iD)8;nK-mVcym3cn3S
zdrL?$nPMIJA$09%b>im$0XkW<34(zvN3$@^C!cf<4)3*%LGB$W$Tqn$!M@NUP{45z
z$`5dMd+rYd|74c|F=$`AHP+f~3!4Gf5TXWPVSc%lC%*v_Z_o>BKo)pe2;k6>){_t^
zbl)%$7)6U-cgDbeOW+W~L+i5@_^q5I6FC2M11;tSaBw~zFp4P_Gk=XVTVa+3FxGZC
z-2x1}^4VhKZs`~^6N|at#>KmON*ldwPKwtm$doM5huXYAh|*CipzwAcfE%!pa;(=J
z!WX2qq6TWZZP+&)*O#Zg(WlcCP1{#0#H<dz=47_>nS=z17_;U%$J4k$r^0b>=85^%r>aJFbu(l3V%qX)R@
zO#to&7DuqYKoYh=5(dkSyr(@Uv?~>vn}H6yme$;?l@m+MDHA$A`^jxo7fsp;e7p$I*T8=xm_vpX?!L=g+(vuj5RQyGMy#1NO8
z#?JdVD0VfGc4T9%paB=?$kLLlb)Y%hZ8@~O-hp}Oj^9|;a0T3C+}mGwf@Za+1;&XK
z|5@PvEP!x_EjR_x9JblPx6o*TZK08tCvotV$x^-hwW2pSN94}9i%`Dno3wV2Mi54h
z%djCzJRbUx1KfxN>k|e0bpxLc#=i-+`4S;ma7O=+C&BZ)H(STyRkekb=<*4^H5|0=
zjH<48vc{usJj>~Nk6AmYI
z7-=>hEFh){)&c-}7_rM_8r{VK*L71t5Y7!jiJ&1ZmKQdg30}Z;dASX+^Sgg8~%@5JdL|n=nsjwL~FVEwX)p4Bba`Bn0y`X4~XMon!T%z|2c4w8T{*S
z`H@4G2+%+r3Vv@@w;A>`N5j6?NVjn_`RBc0pC(N0dknuZ2IkT5LFgV@@BZVHb86eZ
zBP4>ZHliTg55=w(#ZjyH_
z{O(Y82rH^|+6F=`xB!Y;`g*p<54Hf|X4=qsRUHb!dv}1*xgpeBz#OoE5<)%EF}-`s
zKo&wh3fe)_zuz5@A1uJp0Op_m>HmDsvHs`ZO-nu10V0sVfdmU0JosuLpo0tz$|>l_
zT*HY47g97BQJ}?(2Mcj52&SSYhJb{@>xPY<%9Sd;3HwNdOH3j&|7pg&(J!aYcsh03
zkXf_l5iCub*-=8INh>Z*(m6FpNER
KsMl7%1z1&2x#WIsFO)N)%3KlGv2+p32eF{OG*d~`Dl-^pRDJh*Z
zv_zuFOE;|<({$s~**c;p9X7Y1n*l=Gqv_K%U{$l78xt{7ny2vUy|x#xD7r@=)09bf
zH(f4;k1OtpVJBl7iIo7A2{fq9CfnZDgH(2*+#x9vn#m&XVVmdVuk30Z0&;SkFtYd*>^GVhN|A3OocFrcE6~$CAU~uD<
zI#)F{&Nu6AMbjSXRO6mdxaGE+fHsLD%YLu`x0fgu)-)j#T&Q&%an~Uj&w?~?@Sp|L
z?Py$fg30#QD6*VZ6(GYnWmQrjil`oPsj=u3Hrc(H*bx&pN5oyk;D}?6G+8&GjBM>d
z=MG5Ji6@;u<7jsia4kfM*Ul@x)UP
zrf?b$Pd4CLLo-SsS}GJfl)45oTCCd48m87+Y8L8j(@
zHU{HYO=C)&uw3;i_Ad+spG+MWfXN71#0S4<+*(8cIcA*yz5M3Qh(WR@UNxQr%YKk}
zeDHEA-*z9#{Zi74Bqjqq@V~eL2-(9yEi5w5GFlz7b7Tg#vJe?-44f>%49!-Bfqnh5
zOeUB7La6=3^WZwvJmB@kZR;IiQqiDD;g1Gqt@YY6sZA5lIBLva*KNlfGEgUkN_pKD
zL=AO-*&Us9%ZaU}USeJlJ~+Z#_gxdudMOjM|Hf$Njo4a^?f~rUjSg#jpST~oyY0O%
z%7vuGj!s0RkWPweotpAAH#G;WL%H0e(%LKarnpb3O8;u`8?wn7KC(1kGUm;;+sv4g#!1|?`B2P|NL2Sg}o`+5^`Ko~ltl}?2M
zs@Ml3Ba0~GE2c4ziA??7>sv2V
zOBH#jHNio1ng*n%7C~skXoB&X*6gM2HJ
zgjM`zINO;{9?tWC@to#9ZPHH=GLVr3UF4t$iqL}Msd$g1)bS#9sZ7m{Q&oV)CPh^z
zB-pY}Rv5=Pu5pZC!RmV;O2;bq|1b_`^r{A>`G&2`amqOGpcOK0MIL6_JwEAjr_Bnc
zvVJ*C^c~B6!>nI2kBZc!Diwb;2`D_(8P0!#6QB9y=Qyj1)tQ;|nj>^7HLqI7rzXa$
zNjs-h9eCDmk~OQ{gxp&1$=0<}HHd62Tw3e;GDRM0Pk1uu?EHGjgbJ%sj(jMk9yv*g
z<|7_k=z$zAx{iKKPq36V6i?q5RGtd+S?ufO{OCugrHXd6q%CbAz{v!%zBQby{VHBx
z>%i911gy=Z?E!78HQB-ztGbP1nr0ncCVG
z7n=FSZEJ;luz&*TxjY1LIAyln0BaSU{gsP>UAy1JAVIhNOmKdK5aA3jIKu=U=Y*@;
z+y@uVt84A23r}3Z6r)(hEMBpTUkqaxws^)drg4pH{3zi;(hHDcgCr@rgSQD8WdLh|
zcr&^v>n_T?K;`T%)0C%g~^qw27=}&7Q)L-^a;hs0>Q5_M*0}~Yt#b`&TsPa<%{H{K|D5MMM_bw^
zfOVg_-D_|U`qlAxyc(<
zaoqu&ETmgG2n0YB;R%etdD3``dRS-?z{2{qubXA?WLq`N@xt^rTByO?#51cy5pTsUe3Fr?C$r#3m#BN;Cjmkk9c_-p1LOgdg32%I}bA;Z^SPtjj*;v!i|N
zb7%X?7Tk8hPh|1=bo{G3-sA>fe$K0YdPc(B_l=4{x^;Zmhb9*3(a%%pwe|gCG2#0Q
zS9-;%pZnXFKcmSX{mBboecto#<6}R4@!_v{MYSLJCGY-`(eFF(rdJ=ge|{HynkRVO
zSAdMv|2zJtCzFSE3>7Ip#&(crP~q2a;pYWAPz9lPWxew!(T6+abtn7xI}Z4JP-jpa
z2UrFdcFsp~Mn`%m=zcXAT_ct}8d
zg<^+V=YIvLk93GT>Nt;v#C2geeDl^q&@#7l&>r17aYNVlV|hAO%ZM1SZo0C~$aL
zSb*)9i%mFyA_Zo8NhjzX^8U$$f^0hD=GG
zPe+j8X(++ddB+)v##tv^P@h4khi6xn+I5xVgkchfnVmUS&IAR{RGYSY2FbiMRN4$Z4PWIfXkQ
zh4YzRIi`J&l3N5?nmu5e7v^rCb3mjirdG9JT&WgZnxFy7re*q|rCB@pM~$>&D9lMY
zR?rVy00@8}3ei&oqyi$lQX3|HRfpr3gbYiD-69st+27b_|l#`po(gGfp4%uLh1!VOkW9TqTxB9!e)n@TohX1JEO?bF*0SkPr154}aQ8umlAfH4dw^D%a2o
zkFZdbzzDY#i%eMq09&v&siRl-1K|gHm`Z-@3S{ppH>27Q@<6W%0zLT3QTWh3MBoRJ
zilTr*O0U8LQc;g?$$*Zz1C}~?aCx2$n@H4GjnOr6FKC#|%COScf`L_fnU{JH8?Da@
zg;z>*{#jt;8eHG%pr;u!4)B?|Vqx6srJ9Mg-l~~<)jJi)|0fn3DtHI
zMY>MGxjaA?)YAl@i&L*FBBjC!>QD$S1qCz3E8i0$d%IF5^0y@GvmnQCatfJ1nxr#G
zvrT7=!P>HjB(pCYz0&2la^Qh0x`)*Iy)q#O&D5s3Wn0grl^&)wv{PWU#k3oST#h54
z2%1Qhm;=xgwsWJgi?t2Z5DyrTwkw6GJAhN0djjTA|2`+s4m+R+q);lN+dU6VQ&A8M
zq`(7>x&u8xuq`max%z%dhJ_ACZ@%Pt?e$Y7Omv2V0+E%!^BOAY(6OWvN_dj8t7HWW
z{06Jk0_}iPOu#C`;ID|%0`5@5t0E2%90Y6Is5sTYb?XjDj6MBICwrj7*&x9Yj7l~o
z1*c%a7;Kcw`gHyHyi135f*HahNtg!-#>J{}FiWB?OR*z-!cG=HBuvNL8+W^CJZZ;^
z*-8XMFsSd5wJ-C&L;y-zwqms?2BRhQbkYOzLKm})BCG{a=(-V1`|~UO#lfL
z^uGbDuXJ*|y&}5;TPQPC2B)9}*i%yJ0J3_({|J5n2)|GRt{MeSaksc~%0=0R+qrND
zsCzw2cok=Ga-47ASI2YQQ@tc*oYkw!gTL|60`gk9X8S1sOe-C{Du@b7d9cF_yth|^
zC57NgHBcL+G(NF1sIT-%OH5NcYyz_Ux>yptcM{I@d>$n=4zWDTYhcT+%A>&Q%Wj-^
zn`g-{YfuJlqDuP4>zBBL$tQq^%sZa(h6%DHL&e|g)u!0`iqYX{a!Kqx=g_0^tpx1ou*CRzI
zf4kR1ZAx;h30Yj#zU-?(Ina!^yu-Y>4=t>4tl3N1R}5`+?Ld1|(hkPp<5
zBtnh1s#3#->aQI9*mX;&4=hWoxhk!|3p-${cFo;%!og0BxCV%rZ8z3JFTyITQ-%@7LdJMkow%Z|17C;i*y)DwFKpKL~1Lm>{wP6pzUC*{~
z(pC-^m0aavq86!<4(?#curLbUVGCF-$S<%ZfFoeP&BzWWGv8X;UR$xD0zGX_H_+3+
z*WJ14uq)h98i9?sxYPqBPQx}eQuPc|y?nsmT`R0KQ!$Q{Z5h@g?AlLu$M!8P_`Poy
zZDoCO18hAvbu;J?!Y3W&{}k@9DqE6M-~37Btj>P@2BZ-Uz0v}2;kt;P4u_4^g!)M{
zyeg$J?3HaNw$8UD1*w$I&50@FUfq!)NbQPJ+FyeCKB8&aPT3P$ZD#{$fd5{PAA*PCoLdk(XFvt
zpr?NDDHbpSCdqK*9ij_(e;H@Smi6gOrqN0c+xm^)fFfi!@B;ntAe{^d6toRAKXT#h
z?x0B0xmR37DXOBSuv$W2P;Ru
zOsGbtOan7**0d>eM@}_3Y4V)8Gp7NcJ&URVxo`yr3=u6rqzF^$RH{{XxRhGb>eP~0
zLvDSk6{c6PWzBvxE3oTUu~o^MY@62V+mUoJzO^eAuiTA8fgaV#vnb%8f(IWZqQ=YO
z#YBiOX6#rE6URefWpdo(2qjxRz|_lR88em3LntlFESiYuA+mT0Y4XjEYSm17(7xP4
z9F`{B7~#guTGlPcj*$?7lDT*fBt427w++SlbLZ2a7e@<|$yOD7z325F*ZcUiDc52g
zN>sgi|DZx|t|uHA==`AMuiAeJzARn+TfJtdsy~qWBM`r_5)yEr1n<)*uf0$*usjGO
zR7t#y7DSLif>y$iuk_#}sy)Njqt8A1fZ_rUz7_4K5NZ&N4=X$gH<|*wmA%3sbU<6}GAK&W(K+C{k6ZbT6PoDbpQzw<^JTNQ?Su)TrIHkIiQ2#*s
z;Xy|myvV|YEd6RyP2W5eEV?kAYr?xY0yI!LkTO-oRA)MZF~z)a46?zNGOw
z|Le#Eb5~t&%@x?^k|RP1UXSH9O=Opyt~oWQB0@t_$y;>JYB##pK2Tw5X`oeeD%I3*
zPxN9_bI-jHUAHVPh3`&YhiQ^B(
z)!FMDwsluu#|&24>1+#=Vq_=2)z^rb6+*g>!^AjFkl1vqUzI&tncjc}{&(CH_04Eo
zud3D0-IjOe8D4wo)Vbw-3q{DPp?kL0qL)#9S!VjUXtj!iVSP;27bjabYciG9I#-dq
zE-uYog$6rs2y576rbrgxC_4=FU|H1)>
z*3$u{g~@JgF&cdF!;#Lr@_z}2%;18t##LC)&m1;lvJC=OScaEnLb_nXmiY5rIUZX%
zkHy3oWjR-Nd~&ODkC5(-4!&IP68WxM!To?n{P4(Y7HQGt34ipv-Q{|oai4X!&v(U*
zm%e7XUwXORm#Yw-C(I*DOjcSs&lvWHdzhWf)uGEJ)|Y*fZkIV1-o~=lZ}diRnZ^^xo16QqQ%ES(ILU12tgqXjrO~*N(JDu6W|8}-t6)`yg
zJP1OhIK>!H(FAV#h!ssxMJ;ksi(dp|7!l&dF^bUy8ZhG)%a}$sP7#Y~B%>JP*v2)s
zagKAGA`8VO5EBrg3ag41LiFgTK}1oCZ{*?|>!?OLDw2y{bRQlysvRLrKX}b`l{9h=C$GX~{Ci5{#U5lPO1eNLRiRj%`#x
zM-th~Q?l`nJ3!~g3_%l#)edrl^1YmKZb%WemOLv>u|Nyg#t8d{IlP^R_fB;QPo`6
zvZ+VsCF$hSN3e~47
zG;2O=>O%RdwZR6~R%bivUkm%37cI7HNKC9@ZPQrCXhCa~bqp7FH9)mO4_#>$RcA#z
zsgb&rKVTW_+}>6=%*h0(>IhGe``h74ZU?se
zD`uI<*vJ}Ixsqj~a%mMpoHhL65Lc4U|7pB48}nRe5Kkqq0;%zki$mG-
zYFE754KJYsYh=e5nbBM@Kma5?X-Zd`(gs-Wi)`E4Nrhj
zotC&n)FQ{%UDCt?qdlDW`9?ic@fPuwLlp5v5@`C
z1n&2&`JLY+cNR{LBiO63_ERs=YS<1(c0c^!2Wq6F;Wp?w#x)K`6;J{ktpGVfWNr>9
zqyX_5e}v=X0C^syq7INR{NZEXBPl}O1Tt^O=N~_Kxh8I{KgRl@!AAAeuh;+pfPL&`
zKYQ65;PlAyHtSp8=-mspa3aB!yTaP`>verUaLc#a1BV}do7E*BG<&ZN$LaGep0lUb@!kop*KMkmT?03%b)%Tk-y>*|9R5?
z!1E@kfsebt_{%wtvoq7XJ)`Tr1za)RIl2=GOj?o5?sL+{2TCi3ks4l)ykfW3bQF=vVJ+ReF`eOLx}A=wi-Bq
z14sZrShk9jw!#Avn;SgJgN0F0l9rP;^#j0PC;|7gf|jGd{4;?q)I#`EzyI5T|0_IF
z;KCDd2R#chsTi>Y%)mEs7|TXKUd`#|88@aYU#%#6S%!ol!doeR>tm
znHtBCq8#8pP3(bF@Wf9{Apqotmh(I(+&B|>15(JMDN?^!Y{gjY!daBXS9G~B>_0N3
zghz15@}st49LJ2zwq#Vs32a7<1j%pQh#r^>b6l=3Ar|>DsO#8?1yrJwTrQ2FA8(|t
zf0`KQXvuGstb*D}m~1!p|7o#|nX#8t$tKcCkpxO}G)dNRs+#nltjWn^@u#Dt$*1(m
zsGPBcnlZ;>N#Z-Mi`YSgV~jtNM}(lqhI_aK2tQ9mLM@7dn)^qM^Sm;|xK?n3o1-~#
z0E7CQKfWA5g@nj}6h(w6fp2hwzm!5cQk1q)8?Xt<)p5*Xh%&ZNo}!%7;{pTnxI#8M*bC>b(=tjMfN$n?qTz{={H%Fuk6WvPOI(v{EbNn~k`
z(fpX#Tq3RXNyIvq#Oi?`0LtLx6}0J1Z%nt#G%Tsa$>r=T&mp3flLdDkMx(96eU-&%&HM(hJPa
zYeHEdNB|W>534nc7`4{(n5E3ju9-~A1ON&YI++xc+5|@kB~9KGO6F7(x~P@RFiF4?
z&Yj^&!aV)cnsd3GBgk63MV!02aiC9~15}?|
zBm|UBqdb<>{~;pmOpX=>(F}aivdKy!m{7{Jy^lmrB5fiqMUygdOb><4j1d{X`p_C3
z$;sd$=9CT-eF`L$O%qP9g4<$_?)y*(Hj?+9xthCkY@Sz$-%^x))
zvGIdOpae<)7Hoqb_#suJ6q)0gjw`*@49!Xt?N(fUopnT3?o5-B0nVND(QJK5nRL-G
z6;^X4*T|erVdc_hk&bpH9ivn#)jU;lbxsrLRe`FX9okL*(W%qRg1TeJwF1kaa*SZh
z96q{%W!r+paL@NFJ&VLMktNxZebkd82w#2H9$F|>b(nyge%DxZZ?mSoalwOSb4(jR?IqfJ@Hz?=nY
zM@jT4v9v@f+Sp@L-D4xaV7xfIOb7g&Hj~{mJ%iN4%~W0C)?hK!6ZOr%qFEb_T>8OT
zWenAU(zd(3+D@I)<*hM^iN=JjT4c4!pn0
zO;P8dT%lCoU|o(~z2DR!R@ouaCcTbZshuNQ-~hfK7`>QLW#00Q7NFeVe6?EmNy+TJ
z7#IsWfq;x7-~!AEx``c#^I4JfF*Aup-8YM~9|qz$n_b!+Vveg_s%XPV%^IIwBHol>
zp?yQ51l}nw-a4G58;Y}
zFyIeOR`iu0e`=QSC1K*^8mKa3u@xwb5gW4^lf=DXy6vIJQsQ$&&E~33&i%$e|6XKc
zrHo0ASMP12|J~b{bxsX#WB^n^tG^n-V5`hjCSI(4&D+k&;{=XdvEf{PEn#nsQu6&F
z&jl%=3aT4Q*^5Y@gM&wfDPkga-C+J@VZ=xWT_O*D%)?ErDwaUYjL?RC%;>dO5Jlu1
za#}csO2#EBf+`auvdL@4Tb?9iJ${Z!KHne0=5v1K6E+V1-I(vBN#z~mU-4oGI97vD
z+Us@YcE+|R-eYjyV;m@F$YkP8rjg-g=4A{4bN|j=
z-tFB}Zr(hUW`|B-r0Q3825L3o9Kmtg9^y?6o=GuP+$6PML%!doCNWsDLRk_&BKUxYjqxB
zubJhV&6U*7Y1(t5YbD+MeyyHmdO)-IPU&CS?v*9?pyARE_y#sAg+T
zhR~J8nBr#RzNTb=F3RfVYM9)KAZ1~qmg-oJ?ce@uq|R2Jrrf}-X1l&_PX8w773NOU
zwquZK$;VCWFtO)-7R~_`VVYbWT{{q8t!V)u
zv4!K30@NsWe2x)ZOb2X`Yn#UH)-VSxIHKBMS1=!66^8fRbr#W1C=I4-14pQ`hRsw)
z7D;G_XTObBzziNbbL@uFL|)?+@AKt=_v$`sdEXLp_YoZt_^wrRnpN|SxA=7a@xZQG
zb@i1RXoW*XxSz6JvV9Y6se{^1rl_1RT*RJTYn
z5u9(5&d+&bQRU@;{Kr@O
z694CKKAp`{gCy7UD6fHONCj;Fl8h$;C?S0SMjJqvTk{3(5)RwF8X_(+2QE;J?iCFW
zUjET94&GPmjHc>AzYZ$s5j)=%clS|_@BA9*c!NT4c>fM-ML!nqFIUer^xi0VKREiF
zzh3BG=UQ(KoNoe45CWUGfBc7XfFRO?2@!z>iR?(Y!;2Rp3>|vmlEuprh6)iX%($Z>
z!;S?zdPHc#gp7VB)@$l(_#%Q5Q$O7h1U{s{CIKMs6sUoQFD|aQl&~LVVkNaT{?4i
zi0Hw_xP`emWvMjdf+dwBN{l#jD1Znu>K?E2Vo<<*G^KCX2XKvvaSr7b;xEXO8
z#^+4t98$i`NC`9ckKpsFado$tM^=Oxkypi#@D(9FgBy$HZvqs1{u`
z9Tr5K6x;E6orJ=f7@}pGc}3Q4oZP}$bN|bL)#zkI?I2M{no88E7Z+WYDMM<6ujw&g?bsLAnsSterWZlR(#zqC8bzjU0P|n^_{C5xVy#q)K`JIH7^ly*_Cfy
z258`=$Fbxl6FcVo@S=Gg=h1a~wUiF(DH>vmJMX
zHe=zGBuF;Vj2$&pIb7iCnQIt9P{ErN6L1dfVGl!Q(u06Ztmt+R9Ty~pIa**52ymoG
z;W5Tj$y}!Cw>2&)dnPdI-(X?84kZ<-EHcC}rY&vd;4}-@^n>ggZIGP}21o8*iHdmQ
znh3QXCFFk9Ddg=><8xXYF$Qv2dFJH3)Cvej)r2?@V%|4|`5nc&L0{81#NdKwTxooQ
z<+V{3o@xrxrwL(-nf{D+gbfTif=EoTs#c+d4t`iiI^uy6ms~3*x5|}GR6q%Fq@q_N
zNr5cH0fiK_q7EQ@1PDcltN#*Cum+?s!30r=D--B}7cX%k1tYkV`b{h-mH`T20tUQr
z0nuz+Lto42<3oZOZ!qi=5yB3FFMUCgUnAMf*OEAym4(HM-b&Z}wD>SZ!A^BqL{WPr
zCKaIoB_m8|-e86Xs6Zj;I#pUjEAl|c_-RKSqiDvCn6o@dDTQ?dp%a!!V~gk!uXm=)
zgc8`aqU+h=d7K$u5S~XI9v}z{Y5U_6wc@qIEi6QZqs(B)w!HWpj$SC*THow&1YD5`
zb#{{FBH<{U@-U)wh#I9B88g4fsBe%;Ld$QkR1b3qLI{);!tAURN1B=)O;9jMp+QXmH*|J;pC>AS|JETZZml4Bm&zG0yJz+NSkp4m^ZyeOi2avY}Y9y
zcMw@8Zsc;FO^KUnC>KXW5&@U!I-4nDC!!jBLlJ2iT87r7JlF*?A^D4l{vHCV%WU)^
z4mlM=QZ*7C%t|HkcmOvXh!R@S1Sb=$RS92W1za=(3MXjCNTRTeo%ZynJT*beUSh#V
zWMHT%OF>Ta0#u^PFoH3iSQ<6L1vLsrc_3;cSNrodbBWVY4i(}ThX}9}ZACw0c_I|^
zg_j?|XDmz9ix)$=#kU+UuR#>dZVcuXt(s`AXTvKKckqLRI6x?w8LXTHD#v67Zx6O;
zfyaw}+P`4ON5lK?
zUmU{~P>Pi+Y;4Mk2GX?1QL?;+i9*g?qz7^ogujH^C3rmxMn}HS!KuiD@`U-Au_>ga
z9FYh|I`UD)lC-1{5uiwGg3_1vqXq}q@d6zPQ