Skip to content

Commit 9a66643

Browse files
authored
Merge pull request #112 from 0cyn/binaryninja_support
Binary Ninja support draft
2 parents 49d4a9e + 9c154e2 commit 9a66643

39 files changed

Lines changed: 4741 additions & 13 deletions

.github/workflows/binaryninja.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: BinaryNinja Extension
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- "binaryninja_extension/**"
9+
- "proto/**"
10+
- ".github/workflows/binaryninja.yml"
11+
pull_request:
12+
branches: ["**"]
13+
paths:
14+
- "binaryninja_extension/**"
15+
- "proto/**"
16+
- ".github/workflows/binaryninja.yml"
17+
18+
concurrency:
19+
group: ${{ github.workflow }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
permissions: {}
23+
24+
jobs:
25+
binaryninja-test:
26+
name: "Check generated protobuf and test BinaryNinja extension"
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
with:
31+
persist-credentials: false
32+
33+
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
34+
with:
35+
python-version: '3.12'
36+
enable-cache: false
37+
38+
- name: Install codegen and test dependencies
39+
run: uv pip install -r binaryninja_extension/requirements-dev.txt pytest
40+
41+
- name: Regenerate quokka_pb2.py with the pinned toolchain
42+
run: python binaryninja_extension/generate_proto.py
43+
44+
- name: Check the committed generated module is up to date
45+
run: |
46+
git diff --exit-code -- binaryninja_extension/bn_quokka/quokka_pb2.py || {
47+
echo "::error::binaryninja_extension/bn_quokka/quokka_pb2.py is stale." \
48+
"Regenerate it with binaryninja_extension/generate_proto.py" \
49+
"using the grpcio-tools version pinned in binaryninja_extension/requirements-dev.txt."
50+
exit 1
51+
}
52+
53+
- name: Run extension tests
54+
run: python -m pytest binaryninja_extension/tests/ -v

README.md

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Table of Contents
2323

2424
Quokka is a binary exporter: from the disassembly of a program, it generates
2525
an export file that can be used without the disassembler. It currently supports
26-
**IDA Pro** and **Ghidra** as disassembly backends.
26+
**IDA Pro**, **Ghidra** and **Binary Ninja** as disassembly backends.
2727

2828
The main objective of **Quokka** is to enable to completely manipulate the
2929
binary without ever opening a disassembler after the initial export. Moreover, it
@@ -35,18 +35,18 @@ the binary exporter used by BinDiff.
3535
## Architecture
3636

3737
```
38-
IDA Pro Ghidra
39-
│ │
40-
IDA Plugin (C++) Ghidra Plugin (Java)
41-
│ │
42-
└─── quokka.proto ─────┘
43-
(protobuf schema)
44-
45-
.quokka files
46-
47-
Python bindings (quokka.Program)
48-
├── Capstone backend (primary)
49-
└── Pypcode backend (optional)
38+
IDA Pro Ghidra Binary Ninja
39+
│ │
40+
IDA Plugin (C++) Ghidra Plugin (Java) BinaryNinja Plugin (Python)
41+
│ │
42+
└────────────── quokka.proto ─────────────────┘
43+
(protobuf schema)
44+
45+
.quokka files
46+
47+
Python bindings (quokka.Program)
48+
├── Capstone backend (primary)
49+
└── Pypcode backend (optional)
5050
```
5151

5252
## Installation
@@ -83,6 +83,14 @@ library can load.
8383
For build instructions, installation, and usage details see the
8484
[Ghidra extension README](ghidra_extension/README.md).
8585

86+
### BinaryNinja Extension
87+
88+
Quokka also supports exporting from **Binary Ninja** via a Python plugin. It
89+
produces the same `.quokka` protobuf files that the Python library can load.
90+
91+
For installation and usage details see the
92+
[BinaryNinja extension README](binaryninja_extension/README.md).
93+
8694
## Usage
8795

8896
### Exporting via GUI
@@ -127,6 +135,18 @@ $ analyzeHeadless /tmp/proj Test \
127135

128136
See the [Ghidra extension README](ghidra_extension/README.md) for more details.
129137

138+
#### Binary Ninja
139+
140+
Note: headless usage of the Binary Ninja API requires a commercial license.
141+
Without one, use the export command inside the Binary Ninja UI instead.
142+
143+
```commandline
144+
$ python binaryninja_extension/export_headless.py /path/to/binary \
145+
-o /path/to/output.quokka --mode LIGHT
146+
```
147+
148+
See the [BinaryNinja extension README](binaryninja_extension/README.md) for more details.
149+
130150
### Exporting in CLI
131151

132152
Quokka provides a CLI utility tool to automatically export one or more files

binaryninja_extension/README.md

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Quokka BinaryNinja Extension
2+
3+
## Distribution
4+
5+
The extension is installed manually: symlink or copy this directory into the
6+
Binary Ninja user plugin folder (`install_dev.py` automates the symlink). It
7+
cannot be listed in the official plugin manager from this repository, because
8+
the plugin manager requires `plugin.json` at the root of the repository it
9+
fetches; publishing a dedicated distribution repository would lift that
10+
limitation.
11+
12+
`plugin.json` declares Binary Ninja 4.0 (build 4911) as the minimum version:
13+
all APIs used are available there and the protobuf>=6.31 runtime requires the
14+
Python >= 3.9 bundled with modern builds. Development and testing happen
15+
against current stable releases.
16+
17+
## Code layout
18+
19+
```
20+
bn_quokka/
21+
├── export.py # public API: pipeline orchestration and entry points
22+
├── context.py # ExportContext state shared by all pipeline phases
23+
├── util.py # BinaryNinja primitives: segments, addresses, type mapping
24+
├── quokka_pb2.py # generated protobuf module (see below)
25+
└── exporters/ # one module per semantic cluster of the schema
26+
├── binary.py # program image: metadata, segments, layout, data items
27+
├── types.py # type table and C header collection
28+
├── cfg.py # functions, basic blocks, and edges
29+
├── instructions.py # instruction/operand encoding from disassembly tokens
30+
└── references.py # cross-references between code and data
31+
```
32+
33+
`bn_quokka.export` is the stable import surface; everything the plugin, the
34+
headless CLI, and external scripts need is importable from there.
35+
36+
## Protobuf module
37+
38+
`bn_quokka/quokka_pb2.py` is generated from the shared schema
39+
`proto/quokka.proto` at the repository root, using the grpcio-tools version
40+
pinned in `requirements-dev.txt` (which keeps the generated code on the same
41+
protobuf release line as the other exporters).
42+
43+
Unlike the Python bindings, which generate the module at wheel build time,
44+
the generated module is committed here: a BinaryNinja plugin is distributed
45+
as a plain git tree, so there is no build or install step where generation
46+
could run on the user's machine. End users therefore only need the protobuf
47+
runtime declared in `plugin.json`. CI regenerates the module with the pinned
48+
toolchain and fails if the committed copy is stale.
49+
50+
After changing `proto/quokka.proto`, regenerate it with:
51+
52+
```bash
53+
pip install -r binaryninja_extension/requirements-dev.txt
54+
python binaryninja_extension/generate_proto.py
55+
```
56+
57+
`install_dev.py` also runs the generation automatically before symlinking the
58+
extension into the BinaryNinja user plugin directory.
59+
60+
## Headless Export
61+
62+
Headlessly using the BinaryNinja API requires a commercial license currently.
63+
The UI plugin can still be used to export .quokka files.
64+
65+
Use `export_headless.py` with a Python environment that can import the Binary Ninja
66+
Python API:
67+
68+
```bash
69+
python binaryninja_extension/export_headless.py /path/to/binary --out /tmp/output.quokka --mode LIGHT
70+
```
71+
72+
The output path defaults to `<input>.quokka`.

binaryninja_extension/__init__.py

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
from pathlib import Path
5+
6+
from binaryninja import ( # type: ignore
7+
BackgroundTaskThread,
8+
PluginCommand,
9+
core_ui_enabled,
10+
execute_on_main_thread,
11+
log_debug,
12+
log_error,
13+
log_info,
14+
log_warn,
15+
)
16+
from binaryninja.enums import MessageBoxIcon # type: ignore
17+
from binaryninja.interaction import ( # type: ignore
18+
SaveFileNameField,
19+
get_form_input,
20+
show_message_box,
21+
)
22+
23+
from .bn_quokka.export import ExportCancelled, export_binary_view
24+
25+
26+
class _BinaryNinjaLogHandler(logging.Handler):
27+
"""Forward stdlib logging records to the BinaryNinja log.
28+
29+
bn_quokka deliberately uses Python's logging (so headless runs can
30+
configure it normally); inside the UI those records would otherwise never
31+
reach the BinaryNinja log pane.
32+
"""
33+
34+
def emit(self, record: logging.LogRecord) -> None:
35+
try:
36+
message = self.format(record)
37+
if record.levelno >= logging.ERROR:
38+
log_error(message)
39+
elif record.levelno >= logging.WARNING:
40+
log_warn(message)
41+
elif record.levelno >= logging.INFO:
42+
log_info(message)
43+
else:
44+
log_debug(message)
45+
except Exception:
46+
self.handleError(record)
47+
48+
49+
def _install_log_forwarder() -> None:
50+
"""Route this package's loggers to the BinaryNinja log pane (UI only)."""
51+
if not core_ui_enabled():
52+
return
53+
54+
logger = logging.getLogger(__name__)
55+
if any(isinstance(handler, _BinaryNinjaLogHandler) for handler in logger.handlers):
56+
return
57+
58+
handler = _BinaryNinjaLogHandler()
59+
handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
60+
logger.addHandler(handler)
61+
# Surface INFO diagnostics (skipped types, ...) in the log pane; the pane
62+
# has its own per-level filtering.
63+
logger.setLevel(logging.INFO)
64+
65+
66+
def _default_output_path(bv) -> Path:
67+
source = bv.file.original_filename or bv.file.filename or "binary"
68+
return Path(source).with_name(f"{Path(source).name}.quokka")
69+
70+
71+
class _ExportTask(BackgroundTaskThread):
72+
"""Run the export off the UI thread, with progress text and cancellation."""
73+
74+
def __init__(self, bv, output_path: Path, mode: str):
75+
super().__init__(f"Quokka: exporting {output_path.name} ({mode})", True)
76+
self.bv = bv
77+
self.output_path = output_path
78+
self.mode = mode
79+
80+
def _progress(self, text: str) -> None:
81+
if self.cancelled:
82+
raise ExportCancelled(f"Quokka export of {self.output_path} cancelled")
83+
self.progress = f"Quokka: {text}"
84+
85+
def run(self) -> None:
86+
try:
87+
proto = export_binary_view(
88+
self.bv, self.output_path, self.mode, progress=self._progress
89+
)
90+
except ExportCancelled as exc:
91+
log_info(str(exc))
92+
return
93+
except Exception as exc:
94+
message = f"Failed to export {self.output_path}: {exc}"
95+
log_error(message)
96+
execute_on_main_thread(
97+
lambda: show_message_box(
98+
"Quokka export failed", message, icon=MessageBoxIcon.ErrorIcon
99+
)
100+
)
101+
return
102+
103+
message = (
104+
f"Exported {self.output_path}\n"
105+
f"Functions: {len(proto.functions)}\n"
106+
f"Segments: {len(proto.segments)}\n"
107+
f"Types: {len(proto.types)}"
108+
)
109+
log_info(message)
110+
execute_on_main_thread(
111+
lambda: show_message_box(
112+
"Quokka export complete", message, icon=MessageBoxIcon.InformationIcon
113+
)
114+
)
115+
116+
117+
def _export_with_dialog(bv, mode: str) -> None:
118+
default_output = _default_output_path(bv)
119+
output_field = SaveFileNameField(
120+
"Output file",
121+
"Quokka files (*.quokka)",
122+
str(default_output),
123+
)
124+
125+
if not get_form_input([output_field], f"Quokka Export ({mode})"):
126+
return
127+
128+
output_path = Path(output_field.result or default_output)
129+
_ExportTask(bv, output_path, mode).start()
130+
131+
132+
def export_light(bv) -> None:
133+
_export_with_dialog(bv, "LIGHT")
134+
135+
136+
def export_self_contained(bv) -> None:
137+
_export_with_dialog(bv, "SELF_CONTAINED")
138+
139+
140+
_install_log_forwarder()
141+
142+
PluginCommand.register(
143+
"Quokka\\Export LIGHT",
144+
"Export this binary to a light-mode Quokka protobuf",
145+
export_light,
146+
)
147+
#
148+
# PluginCommand.register(
149+
# "Quokka\\Export SELF_CONTAINED",
150+
# "Export this binary to a self-contained Quokka protobuf",
151+
# export_self_contained,
152+
# )
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from .export import ExportCancelled, export_binary_view, export_file, run_export_pipeline
2+
from .quokka_pb2 import Quokka
3+
from .version import __version__
4+
5+
__all__ = [
6+
"ExportCancelled",
7+
"Quokka",
8+
"__version__",
9+
"export_binary_view",
10+
"export_file",
11+
"run_export_pipeline",
12+
]

0 commit comments

Comments
 (0)