mirror of
https://github.com/gaasedelen/lighthouse
synced 2026-06-08 14:19:10 +00:00
467 lines
15 KiB
Python
467 lines
15 KiB
Python
import abc
|
|
import logging
|
|
|
|
from ..qt import QT_AVAILABLE, QtGui, QtWidgets
|
|
|
|
logger = logging.getLogger("Lighthouse.API")
|
|
|
|
#------------------------------------------------------------------------------
|
|
# Disassembler API
|
|
#------------------------------------------------------------------------------
|
|
#
|
|
# the purpose of this file is to provide an abstraction layer for the more
|
|
# generic disassembler APIs required by the plugin codebase. we strive to
|
|
# use (or extend) this API for the bulk of our disassembler operations,
|
|
# making the plugin as disassembler-agnostic as possible.
|
|
#
|
|
# by subclassing the templated classes below, the plugin can support other
|
|
# disassembler plaforms relatively easily. at the moment, implementing these
|
|
# subclasses is ~50% of the work that is required to add lighthouse support
|
|
# to any given interactive disassembler.
|
|
#
|
|
|
|
class DisassemblerCoreAPI(object):
|
|
"""
|
|
An abstract implementation of the core disassembler APIs.
|
|
"""
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
# the name of the disassembler framework, eg 'IDA' or 'BINJA'
|
|
NAME = NotImplemented
|
|
|
|
@abc.abstractmethod
|
|
def __init__(self):
|
|
self._ctxs = {}
|
|
|
|
# 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...")
|
|
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
|
|
#--------------------------------------------------------------------------
|
|
|
|
def version_major(self):
|
|
"""
|
|
Return the major version number of the disassembler framework.
|
|
"""
|
|
assert self._version_major != NotImplemented
|
|
return self._version_major
|
|
|
|
def version_minor(self):
|
|
"""
|
|
Return the minor version number of the disassembler framework.
|
|
"""
|
|
assert self._version_patch != NotImplemented
|
|
return self._version_patch
|
|
|
|
def version_patch(self):
|
|
"""
|
|
Return the patch version number of the disassembler framework.
|
|
"""
|
|
assert self._version_patch != NotImplemented
|
|
return self._version_patch
|
|
|
|
@abc.abstractproperty
|
|
def headless(self):
|
|
"""
|
|
Return a bool indicating if the disassembler is running without a GUI.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Synchronization Decorators
|
|
#--------------------------------------------------------------------------
|
|
|
|
@staticmethod
|
|
def execute_read(function):
|
|
"""
|
|
Thread-safe function decorator to READ from the disassembler database.
|
|
"""
|
|
raise NotImplementedError("execute_read() has not been implemented")
|
|
|
|
@staticmethod
|
|
def execute_write(function):
|
|
"""
|
|
Thread-safe function decorator to WRITE to the disassembler database.
|
|
"""
|
|
raise NotImplementedError("execute_write() has not been implemented")
|
|
|
|
@staticmethod
|
|
def execute_ui(function):
|
|
"""
|
|
Thread-safe function decorator to perform UI disassembler actions.
|
|
|
|
This function is generally used for executing UI (Qt) events from
|
|
a background thread. as such, your implementation is expected to
|
|
transfer execution to the main application thread where it is safe to
|
|
perform Qt actions.
|
|
"""
|
|
raise NotImplementedError("execute_ui() has not been implemented")
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Disassembler Universal APIs
|
|
#--------------------------------------------------------------------------
|
|
|
|
@abc.abstractmethod
|
|
def get_disassembler_user_directory(self):
|
|
"""
|
|
Return the 'user' directory for the disassembler.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_disassembly_background_color(self):
|
|
"""
|
|
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
|
|
|
|
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.setText(text)
|
|
|
|
font = msgbox.font()
|
|
fm = QtGui.QFontMetricsF(font)
|
|
text_width = fm.size(0, text).width()
|
|
|
|
# don't ask...
|
|
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)
|
|
|
|
# show the dialog
|
|
msgbox.exec_()
|
|
|
|
@abc.abstractmethod
|
|
def message(self, function_address, new_name):
|
|
"""
|
|
Print a message to the disassembler console.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# 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):
|
|
"""
|
|
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):
|
|
"""
|
|
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):
|
|
"""
|
|
Show the named dockable widget.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def hide_dockable(self, dockable_name):
|
|
"""
|
|
Hide the named dockable widget.
|
|
"""
|
|
pass
|
|
|
|
#------------------------------------------------------------------------------
|
|
# WaitBox API
|
|
#------------------------------------------------------------------------------
|
|
|
|
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(modal)
|
|
|
|
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
|
|
#------------------------------------------------------------------------------
|
|
|
|
class DisassemblerContextAPI(object):
|
|
"""
|
|
An abstract implementation of database/contextual disassembler APIs.
|
|
"""
|
|
__metaclass__ = abc.ABCMeta
|
|
|
|
@abc.abstractmethod
|
|
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
|
|
#--------------------------------------------------------------------------
|
|
|
|
@abc.abstractmethod
|
|
def get_current_address(self):
|
|
"""
|
|
Return the current cursor address in the open database.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_database_directory(self):
|
|
"""
|
|
Return the directory for the open database.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_function_addresses(self):
|
|
"""
|
|
Return all defined function addresses in the open database.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_function_name_at(self, address):
|
|
"""
|
|
Return the name of the function at the given address.
|
|
|
|
This is generally the user-facing/demangled name seen throughout the
|
|
disassembler and is probably what you want to use for almost everything.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_function_raw_name_at(self, address):
|
|
"""
|
|
Return the raw (eg, unmangled) name of the function at the given address.
|
|
|
|
On the backend, most disassemblers store what is called the 'true' or
|
|
'raw' (eg, unmangled) function name.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_imagebase(self):
|
|
"""
|
|
Return the base address of the open database.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def get_root_filename(self):
|
|
"""
|
|
Return the root executable (file) name used to generate the database.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
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):
|
|
"""
|
|
Set the function name at given address.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# Hooks API
|
|
#--------------------------------------------------------------------------
|
|
|
|
@abc.abstractmethod
|
|
def create_rename_hooks(self, function_address, new_name):
|
|
"""
|
|
Returns a hooking object that can capture rename events for this context.
|
|
"""
|
|
pass
|
|
|
|
#--------------------------------------------------------------------------
|
|
# 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
|