Skip to content

Commit

Permalink
Add IDA artifact change watchers and add example plugin (#13)
Browse files Browse the repository at this point in the history
* Add IDA change watchers and add example plugin

* Make the readme better

* Fix an initialization bug
  • Loading branch information
mahaloz authored Dec 21, 2023
1 parent 836ed29 commit 16db598
Show file tree
Hide file tree
Showing 15 changed files with 672 additions and 483 deletions.
18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,24 @@
The decompiler API that works everywhere!

LibBS is an abstracted decompiler API that enables you to write plugins/scripts that work, with minimal edit,
in every decompiler supported by LibBS. LibBS was originally designed to work with [BinSync](https://binsync.net).
in every decompiler supported by LibBS. LibBS was originally designed to work with [BinSync](https://binsync.net), and is the backbone
for all BinSync based plugins.

## Install
```bash
pip install -e .
pip install libbs
```

You can optionally also do `libbs --install` after to install generic plugins, but it's not required, since `libbs`
files will be installed with plugins that use it.

## Usage
LibBS exposes all decompiler API through the abstract class `DecompilerInterface`. The `DecompilerInterface`
can be used in either the default mode, which assumes a GUI, or `headless` mode. In `headless` mode, the interface will
start a new process using a specified decompiler.
start a new process using a specified decompiler.

You can find various examples using LibBS in the [examples](./examples) folder. Examples that are plugins show off
more of the complicated API that allows you to use an abstracted UI, artifacts, and more.

### UI Mode (default)
To use the same script everywhere, use the convenience function `DecompilerInterface.discover_interface()`, which will
Expand Down Expand Up @@ -53,12 +60,7 @@ info. This also means using `keys`, `values`, or `list` on an artifact dictionar
G/S: Getters/Setters
- [ ] Add all decompilers to auto-detect interface

### ALL
- [ ] Move hook-inits to inside the `Interface` creation for all decompilers?
- This could cause issues. What happens when this is done twice?

### IDA
- [ ] Change Callbacks
- [ ] G/S Comments

### Binja
Expand Down
11 changes: 11 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# LibBS Examples
This directory contains a series of example uses of LibBS in both plugins and as scripting library utilities.
When used as a plugin, LibBS requires a bit more setup to both init the UI components and start the artifact
watching backend.

## Plugins
### change_watcher_plugins
This plugin shows off a few things:
1. Passing a generic function to be called on Artifact changes
2. Initing a context menu in any decompiler
3. Generally setting up a plugin as a package with its own installer
13 changes: 13 additions & 0 deletions examples/change_watcher_plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Example BS Change Watcher Plugin
The example plugin to show of LibBS for watching artifact changes.

## Install
```
pip3 install -e . && python3 -m bs_change_watcher --install
```

## Usage
Open the decompiler:
1. If you are in Ghidra, use the menu to start the BS backend first
2. Right click on any function and select the `ArtifactChangeWatcher` and start the change watcher backend
3. Change any stack variable (as an example), you should see a printout that it was changed
89 changes: 89 additions & 0 deletions examples/change_watcher_plugin/bs_change_watcher/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
from pathlib import Path

from libbs.plugin_installer import LibBSPluginInstaller


def create_plugin(*args, **kwargs):
"""
This is the entry point that all decompilers will call in various ways. To remain agnostic,
always pass the args and kwargs to the ui_init_args and ui_init_kwargs of DecompilerInterface, inited
through the discover api.
"""

from libbs.api import DecompilerInterface
from libbs.data import (
FunctionHeader, StackVariable, Enum, Struct, GlobalVariable, Comment
)

generic_printer = lambda *x, **y: print(f"Changed {x}{y}")
callback_handlers = {
typ: [generic_printer] for typ in (FunctionHeader, StackVariable, Enum, Struct, GlobalVariable, Comment,)
}
deci = DecompilerInterface.discover_interface(
plugin_name="ArtifactChangeWatcher",
init_plugin=True,
artifact_write_callbacks=callback_handlers,
ui_init_args=args,
ui_init_kwargs=kwargs
)

# register a ctx_menu_item late since we want the callback to be inside the deci
deci.register_ctx_menu_item(
"StartArtifactChangeWatcher",
"Start watching artifact changes",
lambda: deci.start_artifact_watchers(),
category="ArtifactChangeWatcher"
)

# return a plugin so the decompiler sets up the UI
return deci.gui_plugin


class BSChangeWatcherInstaller(LibBSPluginInstaller):
"""
This acts as a simple installer for the plugin
"""

def __init__(self):
super().__init__()
self.pkg_path = self.find_pkg_files("bs_change_watcher")

def _copy_plugin_to_path(self, path):
src = self.pkg_path / "bs_change_watcher_plugin.py"
dst = Path(path) / "bs_change_watcher_plugin.py"
self.link_or_copy(src, dst, symlink=True)

def display_prologue(self):
print("Now installing BSChangeWatcher plugin...")

def install_ida(self, path=None, interactive=True):
path = super().install_ida(path=path, interactive=interactive)
if not path:
return

self._copy_plugin_to_path(path)
return path

def install_ghidra(self, path=None, interactive=True):
path = super().install_ghidra(path=path, interactive=interactive)
if not path:
return

self._copy_plugin_to_path(path)
return path

def install_binja(self, path=None, interactive=True):
path = super().install_binja(path=path, interactive=interactive)
if not path:
return

self._copy_plugin_to_path(path)
return path

def install_angr(self, path=None, interactive=True):
path = super().install_angr(path=path, interactive=interactive)
if not path:
return

self._copy_plugin_to_path(path)
return path
26 changes: 26 additions & 0 deletions examples/change_watcher_plugin/bs_change_watcher/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import argparse

from . import BSChangeWatcherInstaller, create_plugin


def main():
parser = argparse.ArgumentParser(description="An example CLI for the example change watcher plugin")
parser.add_argument(
"-i", "--install", action="store_true", help="Install plugin into your decompilers"
)
parser.add_argument(
"-s", "--server", help="Run a a headless server for the watcher plugin", choices=["ghidra"]
)
args = parser.parse_args()

if args.install:
BSChangeWatcherInstaller().install()
elif args.server:
if args.server != "ghidra":
raise NotImplementedError("Only Ghidra is supported for now")

create_plugin(force_decompiler="ghidra")


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# An example LibBS plugin that will print when every artifact is changed inside the decompiler
# @author BinSync
# @category BinSync
# @menupath Tools.ArtifactChangeWatcher.Start the BS backed for watcher

library_command = "bs_change_watcher --run"
def create_plugin(*args, **kwargs):
from bs_change_watcher import create_plugin as _create_plugin
return _create_plugin(*args, **kwargs)


# =============================================================================
# LibBS generic plugin loader (don't touch)
# =============================================================================

import sys
# Python 2 has special requirements for Ghidra, which forces us to use a different entry point
# and scope for defining plugin entry points
if sys.version[0] == "2":
# Do Ghidra Py2 entry point
import subprocess
from libbs_vendored.ghidra_bridge_server import GhidraBridgeServer
full_command = "python3 -m " + library_command

GhidraBridgeServer.run_server(background=True)
process = subprocess.Popen(full_command.split(" "))
if process.poll() is not None:
raise RuntimeError(
"Failed to run the Python3 backed. It's likely Python3 is not in your Path inside Ghidra.")
else:
# Try plugin discovery for other decompilers
try:
import idaapi
has_ida = True
except ImportError:
has_ida = False

if not has_ida:
create_plugin()


def PLUGIN_ENTRY(*args, **kwargs):
"""
This is the entry point for IDA to load the plugin.
"""
print("[+] Loading callback_watcher plugin (1/2)")
return create_plugin(*args, **kwargs)
20 changes: 20 additions & 0 deletions examples/change_watcher_plugin/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[metadata]
name = bs_change_watcher
version = 0.0.0
url = https://github.com/binsync/libbs/tree/main/examples/change_watcher_plugin
classifiers =
License :: OSI Approved :: BSD License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.8
license = BSD 2 Clause
description = An example plugin using LibBS to watch and report changes to artifacts
long_description = file: README.md
long_description_content_type = text/markdown

[options]
install_requires =
libbs

python_requires = >= 3.8
include_package_data = True
packages = find:
2 changes: 2 additions & 0 deletions examples/change_watcher_plugin/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from setuptools import setup
setup()
8 changes: 8 additions & 0 deletions examples/retype_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from libbs.api import DecompilerInterface

deci = DecompilerInterface.discover_interface()
for function in deci.functions:
if function.header.type == "void *":
function.header.type = "long long"

deci.functions[function.addr] = function
49 changes: 49 additions & 0 deletions examples/template_plugin_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# REPLACE_ME: with the description of the plugin you want displayed in Ghidra, and update below items
# @author YourNameHere
# @category YourCategoryHere
# @menupath Tools.MyPlugin.Replace me with short desc shown in Tools>MyPlugin menu

# REPLACE_ME: replace the command to run your plugin from Ghidra Python2 side
library_command = "my_library_name --run"


def create_plugin(*args, **kwargs):
# REPLACE_ME this import with an import of your plugin's create_plugin function
from my_library_name import create_plugin as _create_plugin
return _create_plugin(*args, **kwargs)

# =============================================================================
# LibBS generic plugin loader (don't touch things below this)
# =============================================================================

import sys
# Python 2 has special requirements for Ghidra, which forces us to use a different entry point
# and scope for defining plugin entry points
if sys.version[0] == "2":
# Do Ghidra Py2 entry point
import subprocess
from libbs_vendored.ghidra_bridge_server import GhidraBridgeServer
full_command = "python3 -m " + library_command

GhidraBridgeServer.run_server(background=True)
process = subprocess.Popen(full_command.split(" "))
if process.poll() is not None:
raise RuntimeError(
"Failed to run the Python3 backed. It's likely Python3 is not in your Path inside Ghidra.")
else:
# Try plugin discovery for other decompilers
try:
import idaapi
has_ida = True
except ImportError:
has_ida = False

if not has_ida:
create_plugin()


def PLUGIN_ENTRY(*args, **kwargs):
"""
This is the entry point for IDA to load the plugin.
"""
return create_plugin(*args, **kwargs)
2 changes: 1 addition & 1 deletion libbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.6.11"
__version__ = "0.7.0"
Loading

0 comments on commit 16db598

Please sign in to comment.