Skip to content

Commit

Permalink
Showing 22 changed files with 3,575 additions and 16 deletions.
5 changes: 4 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -32,4 +32,7 @@ console_scripts =
test =
angr-management
pytest-qt
pyside6

ghidra =
ghidra_bridge
PySide6-Essentials>=6.4.2
58 changes: 57 additions & 1 deletion yodalib/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,61 @@
import argparse
import sys
import logging
from pathlib import Path
import importlib
import importlib.resources

from yodalib.installer import YODAInstaller

_l = logging.getLogger(__name__)


def run_ghidra_ui():
yodalib_path = Path(str(importlib.resources.files("yodalib"))).absolute()
decompilers_path = yodalib_path / "decompilers"
if not decompilers_path.exists():
_l.error("Known plugins path does not exist, which means BinSync did not install correctly!")
return False

sys.path.insert(1, str(decompilers_path))
plugin = importlib.import_module(f"ghidra")
_l.debug(f"Executing Ghidra UI...")
return plugin.start_ui()


def install():
YODAInstaller().install()


def main():
pass
parser = argparse.ArgumentParser(
description="""
The YODA Command Line Util. This is the script interface to YODA that allows you to install and run
the Ghidra UI for running plugins.
""",
epilog="""
Examples:
yodalib --install
"""
)
parser.add_argument(
"--install", action="store_true", help="""
Install all the YODA plugins to every decompiler.
"""
)
parser.add_argument(
"--run-ghidra-ui", action="store_true", help="""
Execute the Ghidra file selector UI for running YODA scripts.
"""
)
args = parser.parse_args()

if args.install:
install()

if args.run_ghidra_ui:
return run_ghidra_ui()


if __name__ == "__main__":
main()
41 changes: 32 additions & 9 deletions yodalib/api/decompiler_interface.py
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@
from yodalib.api.artifact_dict import ArtifactDict
from yodalib.api.type_parser import CTypeParser, CType
from yodalib.data import (
State, Artifact,
Artifact,
Function, FunctionHeader, StackVariable,
Comment, GlobalVariable, Patch,
Enum, Struct
@@ -171,10 +171,34 @@ def _decompile(self, function: Function) -> Optional[str]:
raise NotImplementedError

#
# Optional Artifact API:
# A series of functions that allow public access to live artifacts in the decompiler. As an example,
# `function(addr)` will return the current Function at addr that the user would be seeing. This is useful
# for having a common interface of reading data from other decompilers.
# Override Optional API:
# There are API that provide extra introspection for plugins that may rely on YODA Interface
#

def local_variable_names(self, func: Function) -> List[str]:
"""
Returns a list of local variable names for a function. Note, these also include register variables
that are normally not liftable in YODA.
@param func: Function to get local variable names for
@return: List of local variable names
"""
return []

def rename_local_variables_by_names(self, func: Function, name_map: Dict[str, str]) -> bool:
"""
Renames local variables in a function by a name map. Note, these also include register variables
that are normally not liftable in YODA.
@param func: Function to rename local variables in
@param name_map: Dictionary of old name to new name
@return: True if any local variables were renamed, False if otherwise
"""
return False

#
# Artifact API:
# These functions are the main API for interacting with the decompiler artifacts. Generally, these functions
# should all be implemented by the decompiler interface, but in the case that they are not, they should not
# crash the YODA Interface.
#

# functions
@@ -305,7 +329,6 @@ def _comments(self) -> Dict[int, Comment]:
def _set_function_header(self, fheader: FunctionHeader, **kwargs) -> bool:
return False


#
# special
#
@@ -527,7 +550,7 @@ def discover_interface(force_decompiler: str = None, **ctrl_kwargs) -> Optional[
try:
import angr
import angrmanagement
has_angr_man = _find_global_in_call_frames('workspace') is not None
has_angr_man = DecompilerInterface._find_global_in_call_frames('workspace') is not None
except ImportError:
pass
if has_angr_man or force_decompiler == ANGR_DECOMPILER:
@@ -540,8 +563,8 @@ def discover_interface(force_decompiler: str = None, **ctrl_kwargs) -> Optional[
# TODO: make this check do a check to see if a remote port is open and it can connect
is_ghidra = True
if is_ghidra or force_decompiler == GHIDRA_DECOMPILER:
from binsync.decompilers.ghidra.server.controller import GhidraBSController
dec_controller = GhidraBSController(**ctrl_kwargs)
from yodalib.decompilers.ghidra.interface import GhidraDecompilerInterface
dec_controller = GhidraDecompilerInterface(**ctrl_kwargs)
else:
raise ValueError("Please use YODALib with our supported decompiler set!")

1 change: 0 additions & 1 deletion yodalib/data/__init__.py
Original file line number Diff line number Diff line change
@@ -2,4 +2,3 @@
Artifact, Comment, Enum, FunctionHeader, Function, FunctionArgument,
GlobalVariable, Patch, StackVariable, Struct, StructMember
)
from .state import State
4 changes: 0 additions & 4 deletions yodalib/decompiler_stubs/ghidra_yodalib.py

This file was deleted.

16 changes: 16 additions & 0 deletions yodalib/decompiler_stubs/ghidra_yodalib/ghidra_yodalib_run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Starts the YODA backend and selects a generic YODA Python script to run
# @author YODALib
# @category YODALib
# @menupath Tools.YODALib.Run YODA Backend

import subprocess
from yodalib_vendored.ghidra_bridge_server import GhidraBridgeServer


def start_bs_ui():
subprocess.Popen("yodalib --run-ghidra-ui".split(" "))


if __name__ == "__main__":
GhidraBridgeServer.run_server(background=True)
start_bs_ui()
15 changes: 15 additions & 0 deletions yodalib/decompiler_stubs/ghidra_yodalib/ghidra_yodalib_shutdown.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Shutdown the YODALib backend server.
# @author YODALib
# @category YODALib
# @menupath Tools.YODALib.Shutdown YODA Backend

from yodalib_vendored.jfx_bridge import bridge
from yodalib_vendored.ghidra_bridge_port import DEFAULT_SERVER_PORT

if __name__ == "__main__":
print("Requesting server shutdown...")
b = bridge.BridgeClient(
connect_to_host="127.0.0.1", connect_to_port=DEFAULT_SERVER_PORT
)

print(b.remote_shutdown())
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_SERVER_PORT = 4768
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import logging
import subprocess
import sys
from jfx_bridge import bridge
from ghidra_bridge_port import DEFAULT_SERVER_PORT

# NOTE: we definitely DON'T want to exclude ghidra from ghidra_bridge :P
import ghidra


class GhidraBridgeServer(object):
""" Class mostly used to collect together functions and variables that we don't want contaminating the global namespace
variables set in remote clients
NOTE: this class needs to be excluded from ghidra_bridge - it doesn't need to be in the globals, if people want it and
know what they're doing, they can get it from the BridgedObject for the main module
"""

class PrintAccumulator(object):
""" Class to handle capturing print output so we can send it across the bridge, by hooking sys.stdout.write().
Not multithreading aware, it'll just capture whatever is printed from the moment it hooks to the moment
it stops.
"""

output = None
old_stdout = None

def __init__(self):
self.output = ""

def write(self, output):
self.output += output

def get_output(self):
return self.output

def hook(self):
self.old_stdout = sys.stdout
sys.stdout = self

def unhook(self):
if self.old_stdout is not None:
sys.stdout = self.old_stdout

def __enter__(self):
self.hook()

return self

def __exit__(self, type, value, traceback):
self.unhook()

@staticmethod
def ghidra_help(param=None):
""" call the ghidra help method, capturing the print output with PrintAccumulator, and return it as a string """
with GhidraBridgeServer.PrintAccumulator() as help_output:
help(param)

return help_output.get_output()

class InteractiveListener(ghidra.framework.model.ToolListener):
""" Class to handle registering for plugin events associated with the GUI
environment, and sending them back to clients running in interactive mode
so they can update their variables
We define the interactive listener on the server end, so it can
cleanly recover from bridge failures when trying to send messages back. If we
let it propagate exceptions up into Ghidra, the GUI gets unhappy and can stop
sending tool events out
"""

tool = None
callback_fn = None

def __init__(self, tool, callback_fn):
""" Create with the tool to listen to (from state.getTool() - won't change during execution)
and the callback function to notify on the client end (should be the update_vars function) """
self.tool = tool
self.callback_fn = callback_fn

# register the listener against the remote tool
tool.addToolListener(self)

def stop_listening(self):
# we're done, make sure we remove the tool listener
self.tool.removeToolListener(self)

def processToolEvent(self, plugin_event):
""" Called by the ToolListener interface """
try:
self.callback_fn._bridge_conn.logger.debug(
"InteractiveListener got event: " + str(plugin_event)
)

event_name = plugin_event.getEventName()
if "Location" in event_name:
self.callback_fn(
currentProgram=plugin_event.getProgram(),
currentLocation=plugin_event.getLocation(),
)
elif "Selection" in event_name:
self.callback_fn(
currentProgram=plugin_event.getProgram(),
currentSelection=plugin_event.getSelection(),
)
elif "Highlight" in event_name:
self.callback_fn(
currentProgram=plugin_event.getProgram(),
currentHighlight=plugin_event.getHighlight(),
)
except Exception as e:
# any exception, we just want to bail and shut down the listener.
# most likely case is the bridge connection has gone down.
self.stop_listening()
self.callback_fn._bridge_conn.logger.error(
"InteractiveListener failed trying to callback client: " + str(e)
)

@staticmethod
def run_server(
server_host=bridge.DEFAULT_HOST,
server_port=DEFAULT_SERVER_PORT,
response_timeout=bridge.DEFAULT_RESPONSE_TIMEOUT,
background=True,
):
""" Run a ghidra_bridge_server (forever)
server_host - what address the server should listen on
server_port - what port the server should listen on
response_timeout - default timeout in seconds before a response is treated as "failed"
background - false to run the server in this thread (script popup will stay), true for a new thread (script popup disappears)
"""
server = bridge.BridgeServer(
server_host=server_host,
server_port=server_port,
loglevel=logging.INFO,
response_timeout=response_timeout,
)

if background:
server.start()
server.logger.info(
"Server launching in background - will continue to run after launch script finishes..."
)
else:
server.run()

@staticmethod
def run_script_across_ghidra_bridge(script_file, python="python", argstring=""):
""" Spin up a ghidra_bridge_server and spawn the script in external python to connect back to it. Useful in scripts being triggered from
inside ghidra that need to use python3 or packages that don't work in jython
The called script needs to handle the --connect_to_host and --connect_to_port command-line arguments and use them to start
a ghidra_bridge client to talk back to the server.
Specify python to control what the script gets run with. Defaults to whatever python is in the shell - if changing, specify a path
or name the shell can find.
Specify argstring to pass further arguments to the script when it starts up.
"""

# spawn a ghidra bridge server - use server port 0 to pick a random port
server = bridge.BridgeServer(
server_host="127.0.0.1", server_port=0, loglevel=logging.INFO
)
# start it running in a background thread
server.start()

try:
# work out where we're running the server
server_host, server_port = server.server.bridge.get_server_info()

print("Running " + script_file)

# spawn an external python process to run against it

try:
output = subprocess.check_output(
"{python} {script} --connect_to_host={host} --connect_to_port={port} {argstring}".format(
python=python,
script=script_file,
host=server_host,
port=server_port,
argstring=argstring,
),
stderr=subprocess.STDOUT,
shell=True,
)
print(output)
except subprocess.CalledProcessError as exc:
print("Failed ({}):{}".format(exc.returncode, exc.output))

print(script_file + " completed")

finally:
# when we're done with the script, shut down the server
server.shutdown()


if __name__ == "__main__":
# legacy version - run the server in the foreground, so we don't break people's expectations
GhidraBridgeServer.run_server(
response_timeout=bridge.DEFAULT_RESPONSE_TIMEOUT, background=False
)

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .bridge import __version__
2,233 changes: 2,233 additions & 0 deletions yodalib/decompiler_stubs/ghidra_yodalib/yodalib_vendored/jfx_bridge/bridge.py

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions yodalib/decompilers/ghidra/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Installation
To install our Ghidra backend you need to do the following steps:
1. Install BinSync with the extra Ghidra dependencies: `pip install binsync[ghidra]`
2. Install the BinSync python stubs: `binsync --install`
3. Open a binary in Ghidra, then open `Window -> Script Manager` tab
4. Click on the left hand side `BinSync`, then click the check marks next to both scripts in the folder

BinSync is now under your `Tools -> BinSync`. Use the `Start BinSync` to connect and go!
1 change: 1 addition & 0 deletions yodalib/decompilers/ghidra/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .file_selector import start_ui
32 changes: 32 additions & 0 deletions yodalib/decompilers/ghidra/artifact_lifter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import logging

from yodalib.api import ArtifactLifter

l = logging.getLogger(name=__name__)


class GhidraArtifactLifter(ArtifactLifter):
lift_map = {}

def __init__(self, controller):
super(GhidraArtifactLifter, self).__init__(controller)

def lift_addr(self, addr: int) -> int:
#return self.controller.rebase_addr(addr)
return addr

def lift_type(self, type_str: str) -> str:
return type_str

def lift_stack_offset(self, offset: int, func_addr: int) -> int:
return offset

def lower_addr(self, addr: int) -> int:
#return self.controller.rebase_addr(addr, up=True)
return addr

def lower_type(self, type_str: str) -> str:
return type_str

def lower_stack_offset(self, offset: int, func_addr: int) -> int:
return offset
79 changes: 79 additions & 0 deletions yodalib/decompilers/ghidra/file_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import sys
from pathlib import Path
import subprocess

from yodalib.ui.version import set_ui_version
set_ui_version("PySide6")
from yodalib.ui.qt_objects import (
QApplication, QWidget, QLabel, QMainWindow, QVBoxLayout, QPushButton, QFileDialog, QGridLayout, QDialog
)

class FileSelectorDialog(QDialog):
def __init__(self):
super(FileSelectorDialog, self).__init__()
self.setWindowTitle("Run a YODA Python script")

self._init_widgets()

#
# Private methods
#

def _init_widgets(self):
self._layout = QGridLayout()
row = 0

# make a button to open a dialog that selects a file
self._button = QPushButton("File Path")
self._button.clicked.connect(self._select_file)
self._layout.addWidget(self._button, row, 0)

# make a label to show the selected file
self._label = QLabel()
self._layout.addWidget(self._label, row, 1)
row += 1

# make a run and cancel button
self._run_button = QPushButton("Run")
self._run_button.clicked.connect(self._run)
self._layout.addWidget(self._run_button, row, 0)

self._cancel_button = QPushButton("Cancel")
self._cancel_button.clicked.connect(self._cancel)
self._layout.addWidget(self._cancel_button, row, 1)

self.setLayout(self._layout)

def _select_file(self):
# open a dialog to select a file
file_name, _ = QFileDialog.getOpenFileName(self, "Select a YODA Python script")

# update the label to show the selected file
self._label.setText(file_name)

def _run(self):
file_path = self._label.text()
if not file_path:
return

file_path = Path(file_path).absolute()
if not file_path.exists():
return

subprocess.Popen(f"python3 {file_path}".split(" "))
self.close()

def _cancel(self):
self.close()

def closeEvent(self, event):
sys.exit(0)


def start_ui():
app = QApplication()
file_selector = FileSelectorDialog()
file_selector.show()
file_selector.exec_()
app.exec_()

56 changes: 56 additions & 0 deletions yodalib/decompilers/ghidra/ghidra_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import time
import logging

import ghidra_bridge

l = logging.getLogger(__name__)

class GhidraAPIWrapper:
def __init__(self, controller, connection_timeout=10):
self._controller = controller
self._connection_timeout = connection_timeout

self.bridge = None
self._ghidra_bridge_attrs = {}
self._imports = {}

self.connected = self._connect_ghidra_bridge()
if not self.connected:
return

def __getattr__(self, item):
if item in self._ghidra_bridge_attrs:
return self._ghidra_bridge_attrs[item]
else:
return self.__getattribute__(item)

def import_module_object(self, module_name: str, obj_name: str):
module = self.import_module(module_name)
try:
module_obj = getattr(module, obj_name)
except AttributeError:
l.critical(f"Failed to import {module}.{obj_name}")
module_obj = None

return module_obj

def import_module(self, module_name: str):
if module_name not in self._imports:
self._imports[module_name] = self.bridge.remote_import(module_name)

return self._imports[module_name]

def _connect_ghidra_bridge(self):
start_time = time.time()
successful = False
while time.time() - start_time < self._connection_timeout:
try:
self.bridge = ghidra_bridge.GhidraBridge(namespace=self._ghidra_bridge_attrs, interactive_mode=True)
successful = True
except ConnectionError:
time.sleep(1)

if successful:
break

return successful
418 changes: 418 additions & 0 deletions yodalib/decompilers/ghidra/interface.py

Large diffs are not rendered by default.

305 changes: 305 additions & 0 deletions yodalib/installer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
import os
from pathlib import Path
import textwrap
import pkg_resources
import shutil

from prompt_toolkit import prompt
from prompt_toolkit.completion.filesystem import PathCompleter

class Color:
"""
Used to colorify terminal output.
Taken from: https://github.com/hugsy/gef/blob/dev/tests/utils.py
"""
NORMAL = "\x1b[0m"
GRAY = "\x1b[1;38;5;240m"
LIGHT_GRAY = "\x1b[0;37m"
RED = "\x1b[31m"
GREEN = "\x1b[32m"
YELLOW = "\x1b[33m"
BLUE = "\x1b[34m"
PINK = "\x1b[35m"
CYAN = "\x1b[36m"
BOLD = "\x1b[1m"
UNDERLINE = "\x1b[4m"
UNDERLINE_OFF = "\x1b[24m"
HIGHLIGHT = "\x1b[3m"
HIGHLIGHT_OFF = "\x1b[23m"
BLINK = "\x1b[5m"
BLINK_OFF = "\x1b[25m"


class Installer:
DECOMPILERS = (
"ida",
"binja",
"ghidra",
"angr"
)

DEBUGGERS = (
"gdb",
)

def __init__(self, targets=None, target_install_paths=None):
self.targets = targets or self.DECOMPILERS+self.DEBUGGERS
self._home = Path(os.getenv("HOME") or "~/").expanduser().absolute()
self.target_install_paths = target_install_paths or {} #or self._populate_installs_from_config()

def _populate_installs_from_config(self):
config = GlobalConfig.update_or_make(self._home)
if not config:
return {}

return {
attr: getattr(config, attr) for attr in config.__slots__
}

def install(self):
self.display_prologue()
try:
self.install_all_targets()
except Exception as e:
print(f"Stopping Install... because: {e}")
self.display_epilogue()

def display_prologue(self):
pass

def display_epilogue(self):
self.good("Install completed! If anything was skipped by mistake, please manually install it.")

@staticmethod
def info(msg):
print(f"{Color.BLUE}{msg}{Color.NORMAL}")

@staticmethod
def good(msg):
print(f"{Color.GREEN}[+] {msg}{Color.NORMAL}")

@staticmethod
def warn(msg):
print(f"{Color.YELLOW}[!] {msg}{Color.NORMAL}")

@staticmethod
def ask_path(question):
Installer.info(question)
filepath = prompt("", completer=PathCompleter(expanduser=True))

if not filepath:
return None

filepath = Path(filepath).expanduser().absolute()
if not filepath.exists():
Installer.warn(f"Provided filepath {filepath} does not exist.")
return None

return filepath

@staticmethod
def link_or_copy(src, dst, is_dir=False, symlink=False):
# clean the install location
shutil.rmtree(dst, ignore_errors=True)
try:
os.unlink(dst)
except:
pass

if not symlink:
# copy if symlinking is not available on target system
if is_dir:
shutil.copytree(src, dst)
else:
shutil.copy(src, dst)
else:
# first attempt a symlink, if it works, exit early
try:
os.symlink(src, dst, target_is_directory=is_dir)
return
except:
pass

def install_all_targets(self):
for target in self.targets:
try:
installer = getattr(self, f"install_{target}")
except AttributeError:
continue

path = self.target_install_paths.get(f"{target}_path", None)
if path:
path = Path(path).expanduser().absolute()

res = installer(path=path)
if res is None:
self.warn(f"Skipping or failed install for {target}... {Color.NORMAL}\n")
else:
self.good(f"Installed {target} to {res}\n")
#GlobalConfig.update_or_make(self._home, **{f"{target}_path": res.parent})

def install_ida(self, path=None):
ida_path = path or self._home.joinpath(".idapro")
ida_plugin_path = ida_path.joinpath("plugins").expanduser()
default_str = f" [default = {ida_plugin_path}]"
if not ida_plugin_path.exists():
if ida_path.exists():
os.makedirs(ida_plugin_path)
else:
ida_plugin_path = None
default_str = ""

path = self.ask_path(f"IDA Plugins Path{default_str}:")
if not path:
if not ida_plugin_path:
return None

path = ida_plugin_path

if not path.absolute().exists():
return None

return path

def install_ghidra(self, path=None):
path = self._home.joinpath('ghidra_scripts').expanduser()
default_str = f" [default = {path}]"
ghidra_default_path = path
path = self.ask_path(f"Ghidra Scripts Path{default_str}:")
if path:
path = path.joinpath("Extensions").joinpath("Ghidra")
elif ghidra_default_path:
path = ghidra_default_path
else:
return None

if not path.absolute().exists():
return None

return path

def install_binja(self, path=None):
binja_install_path = path or self._home.joinpath(".binaryninja").joinpath("plugins").expanduser()
default_str = f" [default = {binja_install_path}]"
if not binja_install_path.exists():
binja_install_path = None
default_str = ""

path = self.ask_path(f"Binary Ninja Plugins Path{default_str}:")
if not path:
if not binja_install_path:
return None

path = binja_install_path

return path

def install_angr(self, path=None):
# look for a default install
# attempt to resolve through packaging
angr_resolved = True
try:
import angrmanagement
except ImportError:
angr_resolved = False

default_str = ""
angr_install_path = None
if angr_resolved:
angr_install_path = Path(angrmanagement.__file__).parent
default_str = f" [default = {angr_install_path}]"
elif path:
angr_install_path = path
default_str = f" [default = {angr_install_path}]"

# use the default if possible
# TODO: this needs to be changed so we can still provide the install path if non-interactive,
# but also introduce a default_path kwarg to all these installers
path = self.ask_path(f"angr-management Install Path{default_str}:") if path is None else path
if not path:
if not angr_install_path:
return None

path = angr_install_path

path = path.joinpath("plugins")
if not path.absolute().exists():
return None

return path

def install_gdb(self, path=None):
default_gdb_path = path or self._home.joinpath(".gdbinit").expanduser()
default_str = f" [default = {default_gdb_path}]"
path = self.ask_path(f"gdbinit path{default_str}:") if path is None else path
if not path:
if not default_gdb_path:
return None

path = default_gdb_path

return path


class YODAInstaller(Installer):
def __init__(self):
super().__init__(targets=Installer.DECOMPILERS)
self.plugins_path = Path(
pkg_resources.resource_filename("yodalib", f"decompiler_stubs")
)

def display_prologue(self):
print(textwrap.dedent("""
Now installing YODALib plugins for all supported decompilers...
Please input decompiler/debugger install paths as prompted. Enter nothing to either use
the default install path if one exists, or to skip.
"""))

def install_ida(self, path=None):
ida_plugin_path = super().install_ida(path=path)
if ida_plugin_path is None:
return None

src_ida_yodalib_py = self.plugins_path.joinpath("ida_yodalib.py")
dst_ida_yodalib_py = ida_plugin_path.joinpath("ida_yodalib.py")
self.link_or_copy(src_ida_yodalib_py, dst_ida_yodalib_py)
return dst_ida_yodalib_py

def install_angr(self, path=None):
angr_plugin_path = super().install_angr(path=path)
if angr_plugin_path is None:
return None

src_angr_yodalib_pkg = self.plugins_path.joinpath("angr_yodalib")
dst_angr_yodalib_pkg = angr_plugin_path.joinpath("angr_yodalib")
self.link_or_copy(src_angr_yodalib_pkg, dst_angr_yodalib_pkg, is_dir=True)
return dst_angr_yodalib_pkg

def install_ghidra(self, path=None):
ghidra_path = super().install_ghidra(path=path)
if ghidra_path is None:
return None

src_ghidra_yodalib_pkg = self.plugins_path.joinpath("ghidra_yodalib")
src_vendored = src_ghidra_yodalib_pkg.joinpath("yodalib_vendored")
src_script = src_ghidra_yodalib_pkg.joinpath("ghidra_yodalib_run.py")
src_script_shutdown = src_ghidra_yodalib_pkg.joinpath("ghidra_yodalib_shutdown.py")

dst_ghidra_yodalib_pkg = ghidra_path.joinpath("yodalib_vendored")
dst_ghidra_script = ghidra_path.joinpath("ghidra_yodalib_run.py")
dst_script_shutdown = ghidra_path.joinpath("ghidra_yodalib_shutdown.py")

self.link_or_copy(src_vendored, dst_ghidra_yodalib_pkg, is_dir=True)
self.link_or_copy(src_script, dst_ghidra_script)
self.link_or_copy(src_script_shutdown, dst_script_shutdown)
return ghidra_path

def install_binja(self, path=None):
binja_plugin_path = super().install_binja(path=path)
if binja_plugin_path is None:
return None

src_path = self.plugins_path.joinpath("binja_yodalib")
dst_path = binja_plugin_path.joinpath("binja_yodalib")
self.link_or_copy(src_path, dst_path, is_dir=True)
return binja_plugin_path
Empty file added yodalib/ui/__init__.py
Empty file.
100 changes: 100 additions & 0 deletions yodalib/ui/qt_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from yodalib.ui.version import ui_version

if ui_version == "PySide6":
from PySide6.QtCore import (
QDir, Qt, Signal, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, QPersistentModelIndex,
QEvent, QThread, Slot, QObject, QPropertyAnimation, QAbstractAnimation, QParallelAnimationGroup
)
from PySide6.QtWidgets import (
QAbstractItemView,
QCheckBox,
QComboBox,
QDialog,
QFileDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMenu,
QMessageBox,
QPushButton,
QStatusBar,
QTableWidget,
QTableWidgetItem,
QTabWidget,
QVBoxLayout,
QWidget,
QDialogButtonBox,
QTableView,
QFontDialog,
QCheckBox,
QMainWindow,
QApplication,
QFrame,
QWidget,
QSizePolicy,
QScrollArea,
QToolButton,
QProgressBar
)
from PySide6.QtGui import (
QFontDatabase,
QColor,
QKeyEvent,
QFocusEvent,
QIntValidator,
QAction,
QImage,
)
else:
from PyQt5.QtCore import (
QDir, Qt, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, QPersistentModelIndex,
QEvent, QThread, QObject, QPropertyAnimation, QAbstractAnimation, QParallelAnimationGroup
)
from PyQt5.QtCore import pyqtSignal as Signal
from PyQt5.QtCore import pyqtSlot as Slot
from PyQt5.QtWidgets import (
QAbstractItemView,
QCheckBox,
QComboBox,
QDialog,
QFileDialog,
QGridLayout,
QGroupBox,
QHBoxLayout,
QHeaderView,
QLabel,
QLineEdit,
QMenu,
QMessageBox,
QPushButton,
QStatusBar,
QTableWidget,
QTableWidgetItem,
QTabWidget,
QVBoxLayout,
QWidget,
QDialogButtonBox,
QTableView,
QAction,
QFontDialog,
QCheckBox,
QMainWindow,
QApplication,
QFrame,
QWidget,
QSizePolicy,
QScrollArea,
QToolButton,
QProgressBar,
)
from PyQt5.QtGui import (
QFontDatabase,
QColor,
QKeyEvent,
QFocusEvent,
QIntValidator,
QImage
)
14 changes: 14 additions & 0 deletions yodalib/ui/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ui_version = "PySide6"


def set_ui_version(version):
global ui_version
valid_version = [
"PyQt5",
"PySide6"
]

if version in valid_version:
ui_version = version
else:
raise Exception("Failed to set BinSync UI version")

0 comments on commit 06f8605

Please sign in to comment.