Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 46 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ This repository contains ToCode, a Python-only binary exporter. ToCode takes one

## Scope

- Keep the project focused on the exporter CLI and Python library modules.
- Do not add web UI, server, container wrapper, shortcut, installer, or background service behavior.
- Keep the project focused on the exporter CLI, the live MCP server (`tocode mcp`), and Python library modules.
- Do not add web UI, network servers, container wrappers, shortcuts, installers, or background services. The one long-running mode is the stdio MCP server, started by an MCP client and ending with it.
- Do not add chat, report generation, or secondary analysis stages.
- Export one project tree from one binary or IDA database: raw decompiler output, assembly, summaries, section data, JSON metadata, optional IDA database, and optional scanner-friendly C.
- Generated export trees must include their own `AGENTS.md` for agents analyzing that exported binary.

## Project Layout

- `src/tocode/cli.py`: command-line entry point for `tocode`.
- `src/tocode/cli.py`: command-line entry point for `tocode` (subcommands `mcp`, `sync`, `skill` are dispatched before the flat parser).
- `src/tocode/live/`: the live layer. `jsonrpc.py` stdio MCP transport (standard library), `server.py` `tocode mcp` entry, `tools.py` the tool table (schemas, annotations, timeouts), `sessions.py` worker lifecycle (one spawned process per session, cap, idle close, timeouts, crash recovery), `targets.py` what `open` resolves to, `worker.py` the per-session process and query/eval operations, `edits.py` edit tools per backend and saving, `sync.py` incremental export updates and the `export` operation, `setup.py` `tocode sync`, `tocode skill install`, and MCP client registration. .NET sessions: `dotnet_live.py` (`DotnetLiveSession`, picked by `worker.open_session` for `backend == "dotnet"`), `dotnet_edits.py` (the edit log and renames), `dotnet_sync.py` (rewrites the touched types' `.cs`/`.il` and the documents).
- `src/tocode/syncstate.py`: what an export keeps for `sync` (`.tocode/sync/manifest.json` with the file layout, `.tocode/sync/renders/` with each function's render).
- `src/tocode/skill/`: the agent skill (`SKILL.md`, `references/*.md`), shipped as package data.
- `src/tocode/__init__.py`: package version and `export_from_binaryview()`, the in-UI Binary Ninja library entry.
- `src/tocode/analysis.py`: backend-neutral binary inventory and call graph normalization.
- `src/tocode/backends/`: IDA Domain, radare2, angr, and Binary Ninja (`binja_headless.py` over the in-process `binaryninja` API, `binja_mcp.py` over Binary Ninja's MCP server, `binja.py` over RPyc or an injected UI view) session adapters, plus the ASC/droidasc APK backend (`asc.py`: APK set discovery, DEX inventory, worker-side class decompilation).
Expand All @@ -27,6 +30,7 @@ This repository contains ToCode, a Python-only binary exporter. ToCode takes one
- `src/tocode/parallel.py`: worker-count selection.
- `src/tocode/schema.py`: dataclasses shared across the exporter.
- `tests/`: unit tests for algorithms, CLI helpers, and export tree generation.
- `eval/`: the blind source-recovery benchmark (`recover.py`, `corpus/`): a headless agent with only ToCode recovers a stripped program's source and a judge scores it. Not shipped in the package; run it after changing the live tools or the skill.

## Export Contract

Expand Down Expand Up @@ -54,6 +58,7 @@ The CLI accepts a regular binary file or IDA database and writes a project direc
- `tocode.log`
- generated export `AGENTS.md`
- generated export `CLAUDE.md`
- `.tocode/sync/manifest.json` and `.tocode/sync/renders/*.json` (what `sync` updates from)

When the IDA backend is used, the export also contains:

Expand Down Expand Up @@ -120,6 +125,44 @@ Framework/runtime assemblies and native runtime libraries inside bundles and pac
- Pin new dependencies exactly (`==`) and capture them in `uv.lock`.
- Keep dependencies minimal and tied to exporting a project.

## Live server (MCP) and sync

- `tocode mcp` speaks MCP over stdio. Nothing but protocol may reach fd 1:
`server.protect_stdout` moves it to stderr before backends load, and workers
log to `<export>/.tocode/live.log`.
- Every session is a spawned worker process (`sessions.SessionManager`): a
backend crash, OOM kill, or a call past its timeout stops only that worker,
which reopens on the next call (the reply says unsaved edits were lost).
Keep backend calls out of the supervisor process.
- Database edits go through `live/edits.py` only, so each edit records the
functions it makes stale (`LiveSession.dirty`); add a backend method there
rather than calling backend APIs from tools. angr is query-only.
- `sync` never renumbers or rewrites a source file whose content did not
change; existing functions keep their file, new ones join a caller's file
(else `discovered.c`). It rewrites metadata through the exporter's own
writers (`_write_metadata(publish=False)`), and refreshes folder links when
the export belongs to a folder export.
- `CheckpointStore.complete()` keeps the renders for sync and removes only the
checkpoint's own files; `.tocode/` also holds sync state and live logs.
- .NET live: the database is the edit log `<export>/.tocode/dotnet/edits.json`
(edits addressed by assembly and metadata token), replayed onto the
original input on open; `patched/<Assembly>.dll` are renamed copies written
from it (tokens preserved). Renames change dnlib objects and update only the
affected inventory records and name-index entries; patched copies are
written when a query decompiles an edited assembly; the inventory reloads
once per sync. Local renames and comments are text edits applied when C# is
rendered (neither lives in metadata). Keep edits O(what they touch): use the
reverse indexes (`member_owner`, `field_users`, the word index) rather than
scanning all methods or files.
- `src/tocode/skill/references/tools.md` is generated from `live/tools.py`
(`python -m tocode.live.tools > src/tocode/skill/references/tools.md`);
`tests/test_live_setup.py` fails when it is stale.
- The skill, tool descriptions, and server instructions must never use names
from `eval/corpus` (the benchmark would no longer be blind); a test checks.
- Tests drive the real server through `tests/live_client.py` against the
on-disk fake Binary Ninja (`tests/fake_binja/`, which supports the edit
API); the `export` fixture in `tests/conftest.py` builds a fake export.

## Binary Ninja backend

Three transports (`--binja-transport auto|headless|mcp|rpyc`); `auto` tries them
Expand Down
5 changes: 4 additions & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ Write tests so they do not depend on the machine running them: no network,
no installed backend or runtime, and no particular amount of free memory.
`tests/dotnet_fixtures.py` builds synthetic .NET files, and
`tests/test_apk_export.py` / `tests/test_dotnet_export.py` show how to drive a
whole export with fake sessions.
whole export with fake sessions. The live server tests (`tests/test_live_*.py`)
run `tocode mcp` in a subprocess through `tests/live_client.py` against the
fake Binary Ninja API; `python tests/live_client.py <export> 'decompile={"target":"main"}'`
drives a real server by hand.

## Checking a change on real inputs

Expand Down
138 changes: 81 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,81 @@ Four backends are supported, selected with `--backend` (default `auto`, which pr

Other disassemblers may be added in the future.

# Using

ToCode supports Windows, Linux, and macOS with Python 3.10 or newer.

Install it from PyPI:

```bash
pip install tocode-cli # or: uv tool install tocode-cli
pip install "tocode-cli[angr]" # also install the angr fallback backend
```

The command is `tocode`. You still need a backend (IDA, radare2, angr, or Binary Ninja); see [Supported backends](https://github.com/buzzer-re/ToCode#supported-backends).

To install from a clone of the repository instead, on Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1
```

On Linux or macOS:

```bash
bash ./install.sh
```

Add `--all` or `--full` (PowerShell: `-Full`) to also install the angr fallback backend, so ToCode works out-of-the-box even without IDA or radare2:

```bash
bash ./install.sh --all
```

Manual setup (requires [uv](https://docs.astral.sh/uv/)):

```bash
git clone https://github.com/buzzer-re/ToCode
cd ToCode
uv sync --locked
uv tool install --force --editable .
```

## Example

```
tocode firmwareX.bin -o firmwareX_decompiled/
cd firmwareX_decompiled/
codex

# Inside your agent shell, type your goals, e.g.: "Give me a brief overview of the boot process of this firmware."
```

### From an ongoing RE work

```
tocode firmwareX.bin.i64 -o firmwareX_decompiled/
```

Interrupted exports save progress automatically. Rerun the same command to resume, or add `--restart` to start over.

# Live analysis for agents (MCP)

`tocode mcp` keeps the decompiler open so an agent can query it, record what it learns (names, types, structs, comments, or scripts run in the database), and `sync` the export: changed functions and the code that uses them are re-rendered, and only those files change. It works on .NET exports too: renames are kept as an edit log, and the renamed assemblies are written to `patched/`.

```bash
tocode mcp --install claude # or --install codex, or --config for other MCP clients
tocode skill install # teach Claude Code and Codex the ToCode workflow
tocode sync program_decompiler/ # after editing the database in IDA or Binary Ninja
```

### .NET assemblies

ToCode decompiles .NET assemblies (`.dll`, `.exe`), single-file bundles, apphost launchers, and NuGet packages to C#, and writes the IL of each type next to it. You need a .NET 9 or newer runtime.

<details>
<summary>More on .NET</summary>

Decompilation uses [dnlib](https://github.com/0xd4d/dnlib) and [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy), which ToCode doesn't include. The first .NET export asks once whether to download them from nuget.org and checks them against pinned SHA-256 hashes. Scripts and agents can't answer that prompt, so run `tocode --setup-dotnet` beforehand. The files are stored per user (`~/.local/share/tocode/dotnet` on Linux, or `TOCODE_DOTNET_LIB_DIR`); delete that folder to reset.

Native code in the program (mixed-mode assemblies, bundled libraries, P/Invoke libraries next to the input) goes to the native backend unless you pass `--no-native`. Bundles and packages carry the .NET runtime itself, which is skipped unless you add `--include-framework`. `--as-native` treats a .NET file as a plain PE.
Expand All @@ -123,10 +194,15 @@ tocode publish/App # single-file bundle or apphost
tocode Vendor.Lib.1.0.0.nupkg
```

</details>

### Android APKs

`tocode app.apk` (also `.apks`/`.xapk` bundles) uses [ASC](https://github.com/MG1937/ASC) (`droidasc`, a core dependency) for the DEX side and the regular native backends for every shared object in the package:

<details>
<summary>More on APKs</summary>

- `src/raw/<package>/<Class>.java`: one decompiled Java file per class, folders follow Java packages (no clustering, no summaries).
- `AndroidManifest.xml` + `manifest.json`: decoded and parsed manifest (permissions, components with intent filters and exported state, application attributes, split manifests).
- `classes.json`, `functions.json`, `function-index.json`, `strings.json`, `imports.json`, `exports.json` (exported components + JNI methods), `reachable.json` (from manifest components), `package-graph.json`, `sections.json`, `triage.json`.
Expand All @@ -140,11 +216,15 @@ tocode base.apk # DEX + all splits + native libs (IDA/r2/ang
tocode app.apks --no-native -j 4 # bundle, DEX/Android side only
```

</details>

### Folders

Point `tocode` at a directory (a game folder, a Linux app, a macOS `.app` bundle, a .NET publish folder) and it exports every binary in it, then links them together.

<details>
<summary>More on folders</summary>

The links come from the binaries themselves: import and load tables, resolved with each platform's loader rules (`@rpath`/`@loader_path` on macOS, `RPATH`/`RUNPATH` and symbol versions on Linux, the application directory and forwarders on Windows, AssemblyRef/MemberRef signatures and P/Invoke for .NET). ToCode never matches by name similarity. When the executable calls `foo` from `libfoo.so`, the link tells you which file defines it and the file and line range of that function in the library's export.

```text
Expand All @@ -169,63 +249,7 @@ tocode Game/ --links-only # recompute links over existing exp
tocode Game/ -pf 8 -j 2 # 8 binaries at once (--parallel-files), 2 workers each
```

### Using

ToCode supports Windows, Linux, and macOS with Python 3.10 or newer.

Install it from PyPI:

```bash
pip install tocode-cli # or: uv tool install tocode-cli
pip install "tocode-cli[angr]" # also install the angr fallback backend
```

The command is `tocode`. You still need a backend (IDA, radare2, angr, or Binary Ninja); see [Supported backends](https://github.com/buzzer-re/ToCode#supported-backends).

To install from a clone of the repository instead, on Windows PowerShell:

```powershell
powershell -ExecutionPolicy Bypass -File .\install.ps1
```

On Linux or macOS:

```bash
bash ./install.sh
```

Add `--all` or `--full` (PowerShell: `-Full`) to also install the angr fallback backend, so ToCode works out-of-the-box even without IDA or radare2:

```bash
bash ./install.sh --all
```

Manual setup (requires [uv](https://docs.astral.sh/uv/)):

```bash
git clone https://github.com/buzzer-re/ToCode
cd ToCode
uv sync --locked
uv tool install --force --editable .
```

### Example

```
tocode firmwareX.bin -o firmwareX_decompiled/
cd firmwareX_decompiled/
codex

# Inside your agent shell, type your goals, e.g.: "Give me a brief overview of the boot process of this firmware."
```

#### From an ongoing RE work

```
tocode firmwareX.bin.i64 -o firmwareX_decompiled/
```

Interrupted exports save progress automatically. Rerun the same command to resume, or add `--restart` to start over.
</details>

### Binary Ninja

Expand Down
38 changes: 38 additions & 0 deletions eval/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Blind source recovery

The benchmark for ToCode as an agentic RE tool: an agent that sees only a
stripped binary and ToCode (its export, the `tocode` MCP server, and the
skill) tries to recover the original C source. A judge then scores the result
against the real source.

```bash
uv run python eval/recover.py run inventory --backend ida # one program
uv run python eval/recover.py all --backend binja # the whole corpus
uv run python eval/recover.py run calc --prepare-only --no-llm # baseline: the plain export, no agent
uv run python eval/recover.py judge /tmp/tocode-eval/<run> # grade a run again
uv run python eval/recover.py report # table of every judged run
```

Each program in `corpus/` has its sources and a `program.json` (compiler
flags, and test inputs for the behaviour check). A run builds it twice: with
debug info (the ground truth, kept in the run's `private/` folder) and
stripped (the agent's input, in `work/`). The agent runs headless (`claude -p`)
in `work/` with only the ToCode MCP server and file tools; reading the
corpus, this repository, or `private/` is denied, and it has no shell or web
access.

The judge reports:

- function names, prototypes, and parameter names, against the source;
- struct, union, and enum recovery (members in order) and global names;
- an LLM grade per function (semantics, naming, types, 0 to 5) and a leakage flag;
- behaviour: `recovered/*.c` is compiled and run against the original on the
test inputs (programs that run forever are scored on names and types only);
- leakage, from evidence: every tool call that named a forbidden path, and the
provenance of each original name (did the agent write it first, or did a
tool show it to the agent without the binary itself containing it). The
LLM judge's own suspicion is shown separately; it sees the binary's file
name and strings so it does not mistake good inference for leakage.

Runs live in `$TOCODE_EVAL_RUNS` (default `/tmp/tocode-eval`), outside the
repository so the agent cannot reach the corpus through it.
Loading
Loading