Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
5d9a689
Draft of BinaryNinja extension
0cyn May 13, 2026
c902dc5
Headless export script for binaryninja
0cyn May 13, 2026
b368704
Use package-relative import in BinaryNinja plugin entry point
patacca Jun 3, 2026
6e85fe9
Avoid sys.path pollution in headless export script
patacca Jun 3, 2026
b9dafe4
Move annotation-only BinaryNinja imports behind TYPE_CHECKING
patacca Jun 3, 2026
d3620d0
Replace optional-import fallbacks with a test-side BinaryNinja stub
patacca Jun 4, 2026
4f861d5
Stop checking in the generated BinaryNinja protobuf module
patacca Jun 4, 2026
b3b2031
Align BinaryNinja extension on the repository protobuf release line
patacca Jun 4, 2026
802e462
Commit generated BinaryNinja protobuf module with a CI freshness guard
patacca Jun 4, 2026
782522e
Fix style inconsistencies in the BinaryNinja extension
patacca Jun 4, 2026
b23f29b
Remove empty .gitignore from the BinaryNinja extension
patacca Jun 4, 2026
f96f81d
Drop dead import fallbacks in bn_quokka
patacca Jun 4, 2026
be107cb
Run the UI export in a BackgroundTaskThread
patacca Jun 4, 2026
b08757d
Split bn_quokka export module into phase packages
patacca Jun 4, 2026
d4a0acc
Forward bn_quokka logging to the BinaryNinja log pane
patacca Jun 4, 2026
262dd54
Synthesize extern segments for unmapped external symbols
patacca Jun 4, 2026
f6e3343
Export CFG edge types from BinaryNinja branch kinds
patacca Jun 4, 2026
1ded015
Resolve named primitive aliases to their TYPEDEF entries
patacca Jun 4, 2026
043b241
Emit dependency-ordered headers via BinaryNinja's TypePrinter
patacca Jun 4, 2026
52ca7f3
Replace text-matching heuristics with structural BinaryNinja signals
patacca Jun 4, 2026
ea3ee7b
Index reference endpoints for data xref population
patacca Jun 4, 2026
c891d02
Add Python-bindings load tests for BinaryNinja exports
patacca Jun 4, 2026
085faeb
Centralize the extension version and drop dead ExportContext state
patacca Jun 5, 2026
8504e1c
Document the BinaryNinja extension in the user-facing docs
patacca Jun 5, 2026
ae5d9a6
Polish BinaryNinja documentation
patacca Jun 5, 2026
33b93f6
Fix plugin.json metadata and document distribution constraints
patacca Jun 5, 2026
4f5d86b
Fix CC_UNK handling found by real BinaryNinja validation
patacca Jun 5, 2026
ba203e7
Add BinaryNinja-exported qb-crackme fixture
patacca Jun 5, 2026
b14ad2d
Condense the calling convention mapping
patacca Jun 5, 2026
9c154e2
Move BinaryNinja integration tests into the root test suite
patacca Jun 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/binaryninja.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: BinaryNinja Extension

on:
push:
branches:
- main
paths:
- "binaryninja_extension/**"
- "proto/**"
- ".github/workflows/binaryninja.yml"
pull_request:
branches: ["**"]
paths:
- "binaryninja_extension/**"
- "proto/**"
- ".github/workflows/binaryninja.yml"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions: {}

jobs:
binaryninja-test:
name: "Check generated protobuf and test BinaryNinja extension"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
python-version: '3.12'
enable-cache: false

- name: Install codegen and test dependencies
run: uv pip install -r binaryninja_extension/requirements-dev.txt pytest

- name: Regenerate quokka_pb2.py with the pinned toolchain
run: python binaryninja_extension/generate_proto.py

- name: Check the committed generated module is up to date
run: |
git diff --exit-code -- binaryninja_extension/bn_quokka/quokka_pb2.py || {
echo "::error::binaryninja_extension/bn_quokka/quokka_pb2.py is stale." \
"Regenerate it with binaryninja_extension/generate_proto.py" \
"using the grpcio-tools version pinned in binaryninja_extension/requirements-dev.txt."
exit 1
}

- name: Run extension tests
run: python -m pytest binaryninja_extension/tests/ -v
46 changes: 33 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Table of Contents

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

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

```
IDA Pro Ghidra
│ │
IDA Plugin (C++) Ghidra Plugin (Java)
│ │
└─── quokka.proto ─────┘
(protobuf schema)
.quokka files
Python bindings (quokka.Program)
├── Capstone backend (primary)
└── Pypcode backend (optional)
IDA Pro Ghidra Binary Ninja
│ │
IDA Plugin (C++) Ghidra Plugin (Java) BinaryNinja Plugin (Python)
│ │
└────────────── quokka.proto ─────────────────┘
(protobuf schema)
.quokka files
Python bindings (quokka.Program)
├── Capstone backend (primary)
└── Pypcode backend (optional)
```

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

### BinaryNinja Extension

Quokka also supports exporting from **Binary Ninja** via a Python plugin. It
produces the same `.quokka` protobuf files that the Python library can load.

For installation and usage details see the
[BinaryNinja extension README](binaryninja_extension/README.md).

## Usage

### Exporting via GUI
Expand Down Expand Up @@ -127,6 +135,18 @@ $ analyzeHeadless /tmp/proj Test \

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

#### Binary Ninja

Note: headless usage of the Binary Ninja API requires a commercial license.
Without one, use the export command inside the Binary Ninja UI instead.

```commandline
$ python binaryninja_extension/export_headless.py /path/to/binary \
-o /path/to/output.quokka --mode LIGHT
```

See the [BinaryNinja extension README](binaryninja_extension/README.md) for more details.

### Exporting in CLI

Quokka provides a CLI utility tool to automatically export one or more files
Expand Down
72 changes: 72 additions & 0 deletions binaryninja_extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Quokka BinaryNinja Extension

## Distribution

The extension is installed manually: symlink or copy this directory into the
Binary Ninja user plugin folder (`install_dev.py` automates the symlink). It
cannot be listed in the official plugin manager from this repository, because
the plugin manager requires `plugin.json` at the root of the repository it
fetches; publishing a dedicated distribution repository would lift that
limitation.

`plugin.json` declares Binary Ninja 4.0 (build 4911) as the minimum version:
all APIs used are available there and the protobuf>=6.31 runtime requires the
Python >= 3.9 bundled with modern builds. Development and testing happen
against current stable releases.

## Code layout

```
bn_quokka/
├── export.py # public API: pipeline orchestration and entry points
├── context.py # ExportContext state shared by all pipeline phases
├── util.py # BinaryNinja primitives: segments, addresses, type mapping
├── quokka_pb2.py # generated protobuf module (see below)
└── exporters/ # one module per semantic cluster of the schema
├── binary.py # program image: metadata, segments, layout, data items
├── types.py # type table and C header collection
├── cfg.py # functions, basic blocks, and edges
├── instructions.py # instruction/operand encoding from disassembly tokens
└── references.py # cross-references between code and data
```

`bn_quokka.export` is the stable import surface; everything the plugin, the
headless CLI, and external scripts need is importable from there.

## Protobuf module

`bn_quokka/quokka_pb2.py` is generated from the shared schema
`proto/quokka.proto` at the repository root, using the grpcio-tools version
pinned in `requirements-dev.txt` (which keeps the generated code on the same
protobuf release line as the other exporters).

Unlike the Python bindings, which generate the module at wheel build time,
the generated module is committed here: a BinaryNinja plugin is distributed
as a plain git tree, so there is no build or install step where generation
could run on the user's machine. End users therefore only need the protobuf
runtime declared in `plugin.json`. CI regenerates the module with the pinned
toolchain and fails if the committed copy is stale.

After changing `proto/quokka.proto`, regenerate it with:

```bash
pip install -r binaryninja_extension/requirements-dev.txt
python binaryninja_extension/generate_proto.py
```

`install_dev.py` also runs the generation automatically before symlinking the
extension into the BinaryNinja user plugin directory.

## Headless Export

Headlessly using the BinaryNinja API requires a commercial license currently.
The UI plugin can still be used to export .quokka files.

Use `export_headless.py` with a Python environment that can import the Binary Ninja
Python API:

```bash
python binaryninja_extension/export_headless.py /path/to/binary --out /tmp/output.quokka --mode LIGHT
```

The output path defaults to `<input>.quokka`.
152 changes: 152 additions & 0 deletions binaryninja_extension/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
from __future__ import annotations

import logging
from pathlib import Path

from binaryninja import ( # type: ignore
BackgroundTaskThread,
PluginCommand,
core_ui_enabled,
execute_on_main_thread,
log_debug,
log_error,
log_info,
log_warn,
)
from binaryninja.enums import MessageBoxIcon # type: ignore
from binaryninja.interaction import ( # type: ignore
SaveFileNameField,
get_form_input,
show_message_box,
)

from .bn_quokka.export import ExportCancelled, export_binary_view


class _BinaryNinjaLogHandler(logging.Handler):
"""Forward stdlib logging records to the BinaryNinja log.

bn_quokka deliberately uses Python's logging (so headless runs can
configure it normally); inside the UI those records would otherwise never
reach the BinaryNinja log pane.
"""

def emit(self, record: logging.LogRecord) -> None:
try:
message = self.format(record)
if record.levelno >= logging.ERROR:
log_error(message)
elif record.levelno >= logging.WARNING:
log_warn(message)
elif record.levelno >= logging.INFO:
log_info(message)
else:
log_debug(message)
except Exception:
self.handleError(record)


def _install_log_forwarder() -> None:
"""Route this package's loggers to the BinaryNinja log pane (UI only)."""
if not core_ui_enabled():
return

logger = logging.getLogger(__name__)
if any(isinstance(handler, _BinaryNinjaLogHandler) for handler in logger.handlers):
return

handler = _BinaryNinjaLogHandler()
handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))
logger.addHandler(handler)
# Surface INFO diagnostics (skipped types, ...) in the log pane; the pane
# has its own per-level filtering.
logger.setLevel(logging.INFO)


def _default_output_path(bv) -> Path:
source = bv.file.original_filename or bv.file.filename or "binary"
return Path(source).with_name(f"{Path(source).name}.quokka")


class _ExportTask(BackgroundTaskThread):
"""Run the export off the UI thread, with progress text and cancellation."""

def __init__(self, bv, output_path: Path, mode: str):
super().__init__(f"Quokka: exporting {output_path.name} ({mode})", True)
self.bv = bv
self.output_path = output_path
self.mode = mode

def _progress(self, text: str) -> None:
if self.cancelled:
raise ExportCancelled(f"Quokka export of {self.output_path} cancelled")
self.progress = f"Quokka: {text}"

def run(self) -> None:
try:
proto = export_binary_view(
self.bv, self.output_path, self.mode, progress=self._progress
)
except ExportCancelled as exc:
log_info(str(exc))
return
except Exception as exc:
message = f"Failed to export {self.output_path}: {exc}"
log_error(message)
execute_on_main_thread(
lambda: show_message_box(
"Quokka export failed", message, icon=MessageBoxIcon.ErrorIcon
)
)
return

message = (
f"Exported {self.output_path}\n"
f"Functions: {len(proto.functions)}\n"
f"Segments: {len(proto.segments)}\n"
f"Types: {len(proto.types)}"
)
log_info(message)
execute_on_main_thread(
lambda: show_message_box(
"Quokka export complete", message, icon=MessageBoxIcon.InformationIcon
)
)


def _export_with_dialog(bv, mode: str) -> None:
default_output = _default_output_path(bv)
output_field = SaveFileNameField(
"Output file",
"Quokka files (*.quokka)",
str(default_output),
)

if not get_form_input([output_field], f"Quokka Export ({mode})"):
return

output_path = Path(output_field.result or default_output)
_ExportTask(bv, output_path, mode).start()


def export_light(bv) -> None:
_export_with_dialog(bv, "LIGHT")


def export_self_contained(bv) -> None:
_export_with_dialog(bv, "SELF_CONTAINED")


_install_log_forwarder()

PluginCommand.register(
"Quokka\\Export LIGHT",
"Export this binary to a light-mode Quokka protobuf",
export_light,
)
#
# PluginCommand.register(
# "Quokka\\Export SELF_CONTAINED",
# "Export this binary to a self-contained Quokka protobuf",
# export_self_contained,
# )
12 changes: 12 additions & 0 deletions binaryninja_extension/bn_quokka/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from .export import ExportCancelled, export_binary_view, export_file, run_export_pipeline
from .quokka_pb2 import Quokka
from .version import __version__

__all__ = [
"ExportCancelled",
"Quokka",
"__version__",
"export_binary_view",
"export_file",
"run_export_pipeline",
]
Loading
Loading