Skip to content

Commit

Permalink
Implement BugHouse components plugin. (#179)
Browse files Browse the repository at this point in the history
Also with the following improvements:

- Plugins now support headless mode.
- URL scheme handling now only requires implementing a single handler
(instead of three).
  • Loading branch information
ltfish committed May 19, 2020
1 parent a646a5a commit 8bde8b6
Show file tree
Hide file tree
Showing 23 changed files with 659 additions and 60 deletions.
4 changes: 4 additions & 0 deletions angrmanagement/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,10 @@ def main():
run_daemon_process()
time.sleep(1)

# initialize plugins
from .plugins import PluginManager
PluginManager(None).discover_and_initialize_plugins()

action = handle_url(args.url, act=False)
action.act(daemon_conn())
return
Expand Down
16 changes: 16 additions & 0 deletions angrmanagement/daemon/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,28 @@ def exposed_openbitmap(self, bitmap_path, base):
_l.critical("TracePlugin is probably not installed.")
# TODO: Open a message box

def exposed_custom_binary_aware_action(self, action, kwargs):
kwargs_copy = dict(kwargs.items()) # copy it to local
DaemonClient.invoke(action, kwargs_copy)


class DaemonClientCls:
"""
Implements logic that the client needs to talk to the daemon service.
"""

def __init__(self, custom_handlers=None):
self.custom_handlers = {}

def register_handler(self, action:str, handler):
self.custom_handlers[action] = handler

def invoke(self, action, kwargs):
if action not in self.custom_handlers:
_l.critical("Unregistered URL action \"%s\"." % action)
return
self.custom_handlers[action](kwargs)

@property
def conn(self):
return GlobalInfo.daemon_conn
Expand Down
13 changes: 12 additions & 1 deletion angrmanagement/daemon/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ def exposed_openbitmap(self, bitmap_path, base, md5, sha256):
def exposed_exit(self):
pass

def exposed_custom_binary_aware_action(self, md5, sha256, action, kwargs):
conn = self._get_conn(md5, sha256)
conn.root.custom_binary_aware_action(action, kwargs)


def monitor_thread(server):
"""
Expand Down Expand Up @@ -128,7 +132,13 @@ def start_daemon(port=DEFAULT_PORT):
except SingleInstanceException:
return

server = ThreadedServer(ManagementService, port=port)
# load plugins in headless mode
from ..plugins import PluginManager
GlobalInfo.headless_plugin_manager = PluginManager(None)
GlobalInfo.headless_plugin_manager.discover_and_initialize_plugins()

# start the server
server = ThreadedServer(ManagementService, port=port, protocol_config={'allow_public_attrs': True})
threading.Thread(target=monitor_thread, args=(server, ), daemon=True).start()
server.start()

Expand Down Expand Up @@ -163,5 +173,6 @@ def daemon_conn(port=DEFAULT_PORT, service=None):
kwargs = { }
if service is not None:
kwargs['service'] = service
kwargs['config'] = {'allow_public_attrs': True}
conn = rpyc.connect("localhost", port, **kwargs)
return conn
38 changes: 38 additions & 0 deletions angrmanagement/daemon/url_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,40 @@ def _from_params(cls, params):
)


class UrlActionBinaryAware(UrlActionBase):
"""
The base class of all binary-aware URl actions.
"""
def __init__(self, md5=None, sha256=None, action=None, kwargs=None):
super().__init__(md5, sha256)
self.action = action
self.kwargs = kwargs

if not self.md5 and not self.sha256:
raise TypeError("You must provide either MD5 or SHA256 of the target binary.")
if not self.action:
raise TypeError("You must provide action.")

def act(self, daemon_conn=None):
daemon_conn.root.custom_binary_aware_action(
self.md5, self.sha256, self.action, self.kwargs
)

@classmethod
def _from_params(cls, params):
sha256 = cls._one_param(params, 'sha256')
md5 = cls._one_param(params, 'md5')
action = cls._one_param(params, 'action')
kwargs = {}
for k, v in params.items():
if k not in {'sha256', 'md5', 'action'}:
if isinstance(v, (list, tuple)):
kwargs[k] = v[0]
else:
kwargs[k] = v
return cls(md5=md5, sha256=sha256, action=action, kwargs=kwargs)


_ACT2CLS = {
'open': UrlActionOpen,
'jumpto': UrlActionJumpTo,
Expand All @@ -158,3 +192,7 @@ def handle_url(url, act=True):
if act:
action.act()
return action


def register_url_action(action: str, action_handler: UrlActionBase):
_ACT2CLS[action] = action_handler
2 changes: 1 addition & 1 deletion angrmanagement/data/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def __init__(self, project=None):
#

@property
def project(self):
def project(self) -> Optional[angr.Project]:
return self._project_container.am_obj

@project.setter
Expand Down
10 changes: 6 additions & 4 deletions angrmanagement/data/jobs/variable_recovery.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

from typing import TYPE_CHECKING
import time

from angr.analyses import CallingConventionAnalysis

from .job import Job

if TYPE_CHECKING:
from ..instance import Instance


class VariableRecoveryJob(Job):
"""
Expand All @@ -16,10 +17,11 @@ def __init__(self, on_finish=None):

self._last_progress_callback_triggered = None

def run(self, inst):
def run(self, inst: 'Instance'):
inst.project.analyses.CompleteCallingConventions(
recover_variables=True,
low_priority=True,
cfg=inst.cfg,
progress_callback=self._progress_callback,
)

Expand Down
1 change: 1 addition & 0 deletions angrmanagement/logic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ class GlobalInfo:
main_window = None
daemon_inst = None
daemon_conn = None
headless_plugin_manager = None
16 changes: 14 additions & 2 deletions angrmanagement/plugins/base_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@


class BasePlugin:
REQUIRE_WORKSPACE = True
__i_hold_this_abstraction_token = True


def __init__(self, workspace):
self.workspace = workspace

from angrmanagement.ui.workspace import Workspace

self.workspace: Optional[Workspace] = workspace
_l.info("Loaded plugin {}".format(self.__class__.__name__))

# valid things that we want you do be able to do in __init__:
Expand Down Expand Up @@ -60,16 +63,19 @@ def handle_click_block(self, qblock, event: QGraphicsSceneMouseEvent):

# iterable of tuples (icon, tooltip)
TOOLBAR_BUTTONS = [] # type: List[Tuple[QIcon, str]]

def handle_click_toolbar(self, idx):
pass

# Iterable of button texts
MENU_BUTTONS = [] # type: List[str]

def handle_click_menu(self, idx):
pass

# Iterable of column names
FUNC_COLUMNS = [] # type: List[str]

def extract_func_column(self, func, idx) -> Tuple[Any, str]:
# should return a tuple of the sortable column data and the rendered string
return 0, ''
Expand All @@ -85,3 +91,9 @@ def build_context_menu_block(self, item) -> Iterator[Union[None, Tuple[str, Call
Use None to insert a MenuSeparator(). The tuples are: (menu entry text, callback)
"""
return []

# Iterable of URL actions
URL_ACTIONS: List[str] = []

def handle_url_action(self, action, kwargs):
pass
1 change: 1 addition & 0 deletions angrmanagement/plugins/bughouse/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .components import ComponentsPlugin
90 changes: 90 additions & 0 deletions angrmanagement/plugins/bughouse/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
from typing import Optional

from angrmanagement.logic.threads import is_gui_thread, gui_thread_schedule_async
from angrmanagement.plugins.base_plugin import BasePlugin

from .ui import LoadComponentsDialog, ComponentsView


class ComponentsPlugin(BasePlugin):
REQUIRE_WORKSPACE = False

"""
Implement a component viewer that takes JSON messages on function clustering from the server side and visualizes
the clusters in a treeview.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

if self.workspace is not None:
self.view: ComponentsView = ComponentsView(self.workspace, "left")

# register a new view
self.workspace.view_manager.add_view(self.view, self.view.caption, self.view.category)

def teardown(self):
pass

#
# Menus
#

MENU_BUTTONS = [
'Load components...',
'Reset components',
]
LOAD_COMPONENTS = 0
RESET_COMPONENTS = 1

def handle_click_menu(self, idx):

if idx < 0 or idx >= len(self.MENU_BUTTONS):
return

if self.workspace.instance.project is None:
return

mapping = {
self.LOAD_COMPONENTS: self.load_components,
self.RESET_COMPONENTS: self.reset_components,
}
mapping.get(idx)()

def load_components(self, url: Optional[str]=None):
"""
Open a new dialog and take a JSON URL or a file path. Then load components from that URL.
"""
dialog = LoadComponentsDialog(workspace=self.workspace, url=url)
dialog.exec_()
if dialog.tree is not None:
self.view.load(dialog.tree)

def reset_components(self):
"""
Clear existing components information.
"""
self.view.reset()

#
# URLs
#

# register actions
URL_ACTIONS = [
'bughouse_component',
]

def handle_url_action(self, action, kwargs):
mapping = {
'bughouse_component': self.handle_url_action_bughouse_component,
}

func = mapping.get(action)
if is_gui_thread():
func(**kwargs)
else:
gui_thread_schedule_async(func, kwargs=kwargs)

def handle_url_action_bughouse_component(self, url=None):
self.load_components(url)
1 change: 1 addition & 0 deletions angrmanagement/plugins/bughouse/data/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .component_tree import ComponentTreeNode, ComponentTree, ComponentFunction
43 changes: 43 additions & 0 deletions angrmanagement/plugins/bughouse/data/component_tree.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import List, Optional


class ComponentFunction:

__slots__ = ('mapped_base', 'virtual_addr', 'symbol_name', )

def __init__(self, mapped_base: int, virtual_addr: int, symbol_name: Optional[str]=None):
self.mapped_base = mapped_base
self.virtual_addr = virtual_addr
self.symbol_name = symbol_name

def __eq__(self, other):
return isinstance(other, ComponentFunction) and \
self.mapped_base == other.mapped_base and \
self.virtual_addr == other.virtual_addr

def __hash__(self):
return hash((ComponentFunction, self.mapped_base, self.virtual_addr))


class ComponentTreeNode:
def __init__(self, name=None):
self.name = name
self.components: List['ComponentTreeNode'] = [ ]
self.functions: List[ComponentFunction] = [ ]

def __eq__(self, other):
return isinstance(other, ComponentTreeNode) \
and self.components == other.components \
and set(self.functions) == set(other.functions)

def __hash__(self):
return hash((ComponentTreeNode,
hash(tuple(self.components)),
hash(tuple(sorted((f.mapped_base + f.virtual_addr) for f in self.functions))),
)
)


class ComponentTree:
def __init__(self, root: Optional[ComponentTreeNode]=None):
self.root = root
2 changes: 2 additions & 0 deletions angrmanagement/plugins/bughouse/ui/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .load_components_dialog import LoadComponentsDialog
from .components_treeview import ComponentsView
Loading

0 comments on commit 8bde8b6

Please sign in to comment.