Skip to main content
Glama
README.md
<p align="center">
  <a href="https://borer.lol">
    <picture>
      <source media="(prefers-color-scheme: dark)" srcset="brand/banner-dark.png">
      <img alt="borer: reverse engineering, rebuilt for AI agents" src="brand/banner-light.png" width="100%">
    </picture>
  </a>
</p>

<p align="center">
  <a href="https://github.com/imjustprism/borer/releases/latest"><img alt="Release" src="https://img.shields.io/github/v/release/imjustprism/borer?style=flat-square&labelColor=0b0c0e&color=26272b&label=release"></a>
  <a href="https://github.com/imjustprism/borer/actions/workflows/ci.yml"><img alt="CI" src="https://img.shields.io/github/actions/workflow/status/imjustprism/borer/ci.yml?branch=main&style=flat-square&labelColor=0b0c0e&color=26272b&label=ci"></a>
  <a href="LICENSE"><img alt="AGPL-3.0 license" src="https://img.shields.io/badge/license-AGPL--3.0-26272b?style=flat-square&labelColor=0b0c0e&color=26272b"></a>
  <img alt="MCP server" src="https://img.shields.io/badge/MCP-server-26272b?style=flat-square&labelColor=0b0c0e&color=26272b">
</p>

<p align="center">
  <a href="https://borer.lol"><b>borer.lol</b></a> &nbsp;·&nbsp;
  <a href="https://borer.lol/docs">Docs</a> &nbsp;·&nbsp;
  <a href="#what-it-does">Cases</a> &nbsp;·&nbsp;
  <a href="#install">Install</a> &nbsp;·&nbsp;
  <a href="https://borer.lol/docs/tools">Tools</a> &nbsp;·&nbsp;
  <a href="https://borer.lol/llms.txt">llms.txt</a>
</p>

borer is a reverse-engineering engine whose user is an AI agent. One Rust
binary: an MCP server when run with no arguments, a command-line tool with
them. It owns its loader, lifters, emulator and decompiler, and it has no GUI.

Everything below is real output of borer 0.2.12 (release build, commit
`7d9bb42c`) on two files of Windows 11: `notepad.exe` (sha256
`2d0b3948...2efb158f`) and `ntdll.dll` (sha256 `b072efae...ce5aed21`). Lines cut for length are marked `...`. Addresses
differ on another build: `borer symbols ntdll.dll RtlComputeCrc32` finds the
export, and its decompile names the two functions it forwards to.

> [!WARNING]
> borer is early-stage. Tool names, output and resource URIs still change
> between releases. Please report anything that looks wrong.

## What it does

### One call says what a file is and where to read

```console
$ borer triage notepad.exe
# PE x86-64, 360448 bytes, entry 0x1400019c0, 8 sections, 340 imports, 0 exports, 523 functions
# built: MSVC, cl 19.38 build 33145, Visual Studio 2022 17.8 (high: Rich header linker entry 0x102.33145; ...)
# protection: plain: no packer marks
# dynamic imports: 13 functions resolved at runtime, 1 library loaded, from 16 call site groups
# capabilities: 6 of 16 capabilities present across 340 imports
#   filesystem                CreateFileW, DeleteFileW, FindFirstFileW, ReadFile, WriteFile
#   persistence               RegCreateKeyExW, RegCreateKeyW, RegSetKeyValueW, RegSetValueExW
...
# blind spot: 4 of 18 GetProcAddress calls pass a name the decompiler cannot read (computed, hashed or decrypted): the imports resolved there stay unseen
# read first: no protection signal fired, so the ranking is by the strings and imports each function references
0x140013150  sub_140013150                references 13 strings and 47 imports
0x140012a28  sub_140012a28                references 38 strings and 3 imports
...
# next: function_summary(addr), then decompile(addr); emulate/<addr> runs one
```

The whole answer is 2,408 bytes. It ends with what to call next, and it names
its own blind spot instead of hiding it.

### The decompiler checks its own work

On x86, x86-64, arm64 and 32-bit ARM, every function's header says how much
of the text was checked against the machine code it came from:

```console
$ borer decompile ntdll.dll 0x180102184
// sub_180102184 @ 0x180102184: 6 instructions, 3 blocks
// checked vs binary: statements as emitted, 3/3 blocks: 3 proved vs lift; unchecked: C spelling, later text passes
// signature: uint64_t sub_180102184(uint64_t a1, uint8_t *a2, uint32_t a3)
...
if (a2 == nullptr) {
  return 0;
}
return sub_1800ed890(a1, a2, a3);
```

`certify` goes one step further: it runs the final text as C beside the
binary on the same inputs and grades the result.

```console
$ borer certify ntdll.dll 0x180102184
# certify for sub_180102184 (0x180102184): what of its decompilation was checked against the binary; agreement is evidence, not proof
lift	statements as emitted, 3/3 blocks: 3 proved vs lift; unchecked: C spelling, later text passes
text	20 of 64 inputs agree as ISO C, 44 unchecked, 0 differ (seed 1, 20000 steps each)
unchecked	44	the binary did not return: fault at 0x500000010050
reached	2 of 3 blocks; never reached: 0x180102191
grade	agrees	every input checked agrees (they reached 2 of 3 blocks) and no lifted block is contradicted
```

The block check covers x86, x86-64, arm64 and 32-bit ARM; RISC-V output has
no check header yet. `certify` runs on x86 and x86-64. The header always says
what was not checked (here the C spelling and the later text passes). An
instruction borer cannot model stays in the text as `/* asm: ... */`. It is
never replaced by a guess.

### It runs code without running the file

borer has its own emulator, so an agent can call a function on typed inputs
and read the result. Here the function `RtlComputeCrc32` forwards to gets the
standard CRC-32 test string, and answers the published check value
`0xcbf43926`. The last column says the decompiled text, run on the same
inputs, gave the same results:

```console
$ borer run ntdll.dll '0x1800ed890/0,"123456789",9;0,"",0;0,"a",1'
# run uint32_t sub_1800ed890(uint32_t a1, uint8_t *a2, uint64_t a3): 3 inputs, at most 1000000 steps each; the text agrees on 3, differs on 0, unchecked on 0
# args	returns	stop	steps	writes	text
0 "123456789" 9	0xcbf43926	returned	126	-	agrees
0 "" 0	0	returned	38	-	agrees
0 "a" 1	0xe8b7be43	returned	47	-	agrees
```

The same emulator drives input solving (`solve`), unpacking (`unpack`),
devirtualization (`devirt`), taint (`taint`) and struct recovery (`layout`).
The file itself is never executed and never modified.

### It says what it does not know

Ask the exported wrapper the same question and borer does not round its
answer up:

```console
$ borer run ntdll.dll 'RtlComputeCrc32/0,"123456789",9'
# run uint64_t RtlComputeCrc32(uint64_t a1, uint64_t a2, uint64_t a3): 1 input, at most 1000000 steps each; the text agrees on 0, differs on 0, unchecked on 1
...
0 "123456789" 9	0xcbf43926	returned	131	-	unchecked
# input 1 unchecked at line 6 `return sub_180102184(a1, a2, a3);`: the text states only the low 8 bits of the result; the bits above are the machine's own
```

A heuristic answer names its evidence. `borer why <address>` lists where
each fact about an address came from:

```console
$ borer why ntdll.dll 0x1800ed890
# 0x1800ed890 is the entry of sub_1800ed890: 864 bytes, 36 blocks, 215 instructions
# section .text at 0x180001000, r-x and code
# name: nothing names it, so the name is the address
# discovered by: an entry in the exception or symbol table, then 1 call site(s)
# referenced from 1 site(s): 0x18010218c in sub_180102184
```

### It finds code by what it is

```console
$ borer crypto ntdll.dll
# 17 crypto/hash constant hits: 6 algorithm(s) across 16 function(s)
# algorithm        sites  functions  where
CRC-32            8      3          data in .rdata, read by sub_18015431c, sub_180154538, crc32_fn
MD5               3      9          data in .rdata, read by sha1_or_ripemd160_fn, MD5Init, MD5Final, ...
SHA-256           2      2          data in .rdata, read by sha256_fn, sha256_or_blake2s_fn
...
```

```console
$ borer sig ntdll.dll 0x180102184
# signature of sub_180102184 at 0x180102184: 8 bytes, 1 wildcarded, unique in the image
pattern  48 85 D2 74 ?? 45 8B C0
...
```

Classes, vtables, message formats, opcode dispatchers, decoded and generated
strings, IOCs and YARA rules come from the same engine: `borer help` lists
every command, and `borer://help` every resource.

### Answers sized for a context window

Every list is filtered and paged on the server, and says how much it left
out:

```console
$ borer functions notepad.exe "" 0 3
# showing 3 of 481 functions; 39 import thunks left out, filter=thunk lists them; 3 exception funclets left out, filter=funclet lists them
# entry size blocks instrs callers callees src name
0x140001008 133 3 26 5 2 - sub_140001008
0x140001094 288 12 67 2 2 - sub_140001094
0x1400011bc 149 3 27 5 2 - sub_1400011bc
```

Over MCP an answer can also be asked again as a diff: see "Ask again, get
only what changed" below. Every answer ends with an evidence id; the session
keeps the text behind the last 256 ids, so a recent answer can be fetched
again exactly.

### Same question, same bytes

The same binary and the same query give the same bytes, on one core or all of
them, in any order. That is a tested contract, not a habit: the gate decompiles
a corpus twice and in parallel and compares.

```console
$ borer decompile notepad.exe 0x140013150 full 0 100000 | sha256sum
b45d12089ef2280f...
$ borer decompile notepad.exe 0x140013150 full 0 100000 | sha256sum
b45d12089ef2280f...
```

### A hostile file gets an error, not a crash

Reads are bounds-checked, scans capped and walks fuel-limited, and the
loaders are fuzzed in CI. The first 300 bytes of notepad.exe:

```console
$ borer triage cut.exe
Error: loading cut.exe

Caused by:
    the file starts like a PE but its headers do not parse, even leniently: it is truncated or damaged (300 bytes; ...)
```

### Fast enough to read everything

On notepad.exe (523 functions), `borer triage` loads, discovers and ranks the
whole file in about 2.3 seconds (release build `7d9bb42c`, three runs: 2.24,
2.41, 2.29 s). A cold decompile takes under 7 ms per function at the 95th
percentile (`tests/bench.rs`, release build `70b5db2c`, three runs: 6.64,
6.48, 6.49 ms). Both on one desktop machine. The time bars, 25 ms at p95 and
5 s at worst per function, are part of the gate. An agent can read every
function instead of guessing where to look.

## Platforms

| Architecture | Disassemble | Decompile | Emulation tools |
| --- | --- | --- | --- |
| x86, x86-64 | yes | yes | yes |
| arm64 | yes | yes | yes |
| 32-bit ARM (A32, Thumb-2) | yes | yes | no |
| RISC-V (RV32, RV64 GC) | yes | yes | no |

Formats: PE, ELF, Mach-O, raw code and memory dumps.

## Install

macOS and Linux:

```bash
curl -fsSL https://borer.lol/install | sh
```

Windows (PowerShell):

```powershell
irm https://borer.lol/install.ps1 | iex
```

The installer downloads the latest release, checks it against the release's
SHA256SUMS and registers borer with every MCP client it finds: Claude Code,
Claude Desktop, Cursor, VS Code, Windsurf, Zed, Codex CLI, Gemini CLI and 20
more. Restart the clients it names afterwards.

Package managers install the same release binary:

```bash
cargo binstall borer                  # or cargo install borer
brew install imjustprism/tap/borer    # macOS, Linux x64
npm install -g @imjustprism/borer     # or npx -y @imjustprism/borer
pipx install borer                    # or uvx borer
```

On Windows with Scoop: `scoop bucket add imjustprism
https://github.com/imjustprism/scoop-bucket`, then `scoop install
imjustprism/borer`. In Claude Code: `/plugin marketplace add imjustprism/borer`,
then `/plugin install borer@borer`. From source, with the toolchain
`rust-toolchain.toml` pins: `cargo build --release`.

Register an installed binary with `borer setup --apply` (`borer setup` alone
shows each file and entry it would write, in each client's own format). Most
clients (Claude Desktop, Cursor, Windsurf, Gemini CLI and others) read
`mcpServers`, with the binary's absolute path:

```json
{ "mcpServers": { "borer": { "command": "/path/to/borer", "args": [] } } }
```

VS Code reads `servers` (with `"type": "stdio"`), Zed `context_servers`, and
Codex CLI `[mcp_servers.borer]` in its TOML; `borer setup` writes each in its
own format.

To uninstall, run `borer setup --apply --remove`, then delete the binary and
its cache (`%LOCALAPPDATA%\borer` or `~/.cache/borer`). The installers do all
three with `--uninstall` (`-Uninstall` in PowerShell).

## Quick start

From an agent:

1. `load_binary`
2. read `borer://triage`: what the file is, how it is protected, where to start
3. `find_function_by_string`, `strings` or `symbols`
4. `function_summary`, then `decompile`
5. `xrefs`, `cfg` or `callgraph` to go wider

Any address argument takes `0x` hex, decimal or a name.

| Area | Tools |
| --- | --- |
| Load | `load_binary`, `binary_info`, `fetch_pdb`, `reload` |
| Find | `functions`, `function`, `symbols`, `strings`, `search`, `find_function_by_string`, `find_crypto_constants`, `entropy` |
| Read code | `function_summary`, `function_strings`, `decompile`, `decompile_batch`, `disassemble`, `disassemble_function`, `cfg`, `callgraph`, `reaches`, `xrefs`, `class_layout` |
| Read data | `read`, `hexdump` |
| Annotate | `set_name`, `set_comment`, `set_type`, `types`, `annotations`, `save_project`, `load_project` |
| Live process (Windows) | `live_attach`, `live_detach`, `live_modules`, `live_read`, `live_object`, `live_instances`, `live_watch`, `live_scan` |
| Anything else | `query`, `script` |

Deeper analyses are `borer://` resources: taint, struct layout, emulation,
input solving, unpacking, deobfuscation, binary diff, decoded strings, IOCs,
signatures and YARA rules. `borer://help` lists them all, and a client without
resource support passes the same URI to `query`.

From the command line:

```bash
borer triage    sample.exe
borer decompile sample.exe <address|name>
borer xrefs     sample.exe <address|name>
borer certify   sample.exe <address|name>   # how far the decompiled text was checked
borer help                                  # every command
```

`--pack <file>` loads target knowledge, `--no-pdb` skips the companion PDB, and
`--arch x86|x64|arm64` with `--base` and `--entry` maps raw code or a dump.

## More of what only borer does

### Ask again, get only what changed

An agent renames a function and asks for the same decompile again. Instead of
the whole text a second time, borer sends the lines that differ. These three
calls are one MCP session on borer 0.2.12:

```console
> query borer://decompile/0x1800ed890!limit=12
# showing 12 of 113 from 0
// sub_1800ed890 @ 0x1800ed890: 215 instructions, 36 blocks
...
extern uint64_t sub_1800eddf4(uint64_t, uint64_t, uint64_t);
...
# since=8704869d
# evidence: 5-c9635f3d

> set_name address=0x1800eddf4 name=crc32_fold_wide dry_run=false
renamed function 0x1800eddf4: sub_1800eddf4 -> crc32_fold_wide

> query borer://decompile/0x1800ed890!limit=12!since=8704869d
# changed since the answer tagged 8704869d: 15 lines, 1141 bytes now; the lines that differ from that answer follow, ...
@@ -13,1 +13,1 @@
-extern uint64_t sub_1800eddf4(uint64_t, uint64_t, uint64_t);
+extern uint64_t crc32_fold_wide(uint64_t, uint64_t, uint64_t);
# since=0a3dc285
```

Ask once more with the new tag and the answer is one line:

```console
> query borer://decompile/0x1800ed890!limit=12!since=0a3dc285
# unchanged since the answer tagged 0a3dc285: 15 lines, 1141 bytes; this note stands in for that text, ...
```

Names, comments and types live in a session overlay, never in the binary, and
every change is a dry run until `dry_run=false`.

### A struct from what the code does through a pointer

`RtlInitUnicodeString` fills a `UNICODE_STRING`. borer recovers the three
fields from the accesses alone, and the decompile carries the layout and the
function's effects in its header:

```console
$ borer layout ntdll.dll RtlInitUnicodeString
# structure through rcx of RtlInitUnicodeString, from 4 access site(s)
# 3 field(s) over 0x10 bytes
# offset  size  access        seen
0x0      2/8   written       2x
0x2      2     written       1x
0x8      8     written       1x

$ borer decompile ntdll.dll RtlInitUnicodeString
// RtlInitUnicodeString @ 0x1800993c0: 19 instructions, 4 blocks
// checked vs binary: statements as emitted, 4/4 blocks: 2 proved vs lift, 2 tested x3 states; unchecked: C spelling, later text passes
// signature: void RtlInitUnicodeString(PUNICODE_STRING a1, PCWSTR a2)
// effects: writes a1, a1+8, a1+2
// layout a1: 0x2 u16, 0x8 u64
uint64_t var1;

*(uint64_t*)a1 = 0;
a1->field_0x8 = a2;
if (a2 != 0) {
  var1 = wcslen(a2);
  *(uint16_t*)a1 = (var1 * 2 >= 0xfffe ? 0xfffc : var1 * 2);
  a1->field_0x2 = (var1 * 2 >= 0xfffe ? 0xfffc : var1 * 2) + 2;
}
```

Reading `borer://layout/<fn>,apply=<Name>` plans the struct and names the
`set_type` calls that state it; after them every decompile names the fields.

A class borer recovered takes one field at a time, and keeps the rest of its
layout as recovered: `set_type(name=<class>, fields=["0x270:m_pipeline"])`, or
`set_type(address=<method>, var="field_0x270", name="m_pipeline")` at a method
that reads the field through its own object. Every method that takes the class
as `this` then prints `this->m_pipeline`, and `class_layout` marks the row
`stated`. Fields merge by offset into a user struct too; `replace=true` drops
the ones not restated, and with no fields every field stated of a class. A
project's stated fields are held to the code again when it loads: those of a
class the code contradicts are skipped, with the reason.

### One function, any inputs, no process

```console
$ borer emulate ntdll.dll iswdigit,a1=0x35
# ran iswdigit from 0x18012aa90 with a1=0x35: returned after 11 steps, 0 bytes written
# registers: rax=0x4 rcx=0x4 rdx=0x4 r8=0x0 r9=0x0 ...
```

The same spec takes registers, memory writes, a stop address, a step budget,
a trace and a dump (`borer help` shows the grammar). `borer://diverge` runs a
function on two inputs and names the first instruction where the paths part.

What borer does not do yet is in [docs/limits.md](https://github.com/imjustprism/borer/blob/main/docs/limits.md). It has no
telemetry; the few things that reach the network are in
[docs/network.md](https://github.com/imjustprism/borer/blob/main/docs/network.md).

## Contributing

`python scripts/gate.py` runs what CI runs. The rules for people and coding
agents are in [`borer.rules`](borer.rules), and the terms for contributions in
[CONTRIBUTING.md](CONTRIBUTING.md).

| Crate | Role |
| --- | --- |
| [`borer-engine`](crates/borer-engine) | The analysis core: loading, lifting, emulation, decompilation. Pure and synchronous. |
| [`borer`](crates/borer) | The MCP server and CLI over the engine. |
| [`borer-live`](crates/borer-live) | Read-only access to a running process. |
| [`borer-fixtures`](crates/borer-fixtures) | Programs the tests decompile and check against their source. |

## License

borer is free software under the [GNU Affero General Public License
v3.0](LICENSE) (AGPL-3.0-only). If you distribute borer or a modified version,
or let users interact with a modified version over a network, you must offer
its complete source under the same license. Versions 0.1.0 and 0.1.1 were
released under MIT. A
commercial license is available for uses the AGPL does not fit: open an issue
or reach out on GitHub.

Third-party notices are in [NOTICE](NOTICE), and each release archive carries
the licenses of everything the binary links. Ghidra is a project of the NSA.
IDA and Hex-Rays are trademarks of Hex-Rays SA, Binary Ninja is a trademark of
Vector 35, and Claude is a trademark of Anthropic. borer is not affiliated with
or endorsed by any of them.

TDQS

A4.1/5.0

Scored across 34 tools

Disambiguation5/5

Descriptions explicitly scope each tool and cross-reference alternatives ('Not for X, use Y'), so close pairs such as decompile/disassemble, function/function_summary, and the string-focused tools are distinguishable. Minor overlap remains between query/script and binary_info/borer://triage, but not enough to cause misselection.

Naming Consistency4/5

All names are lower_snake_case and mostly follow a query-by-resource-noun or action-verb_noun pattern, with no casing inconsistencies. There is some mixing of noun-style query tools (`functions`, `strings`) and verb-style operations (`load_binary`, `set_name`), but the convention remains logical and readable.

Tool Count3/5

The server exposes 34 tools, which is heavy compared with the typical 3-15 range and feels overloaded. The broad reverse-engineering domain justifies many primitives, but several closely related variants could be parameterized or consolidated.

Completeness4/5

The static-analysis lifecycle is well covered: loading, inspecting, navigating, decompiling/disassembling, graphing, annotating, typing, saving/restoring and reloading. Gaps include no actual live-process tool despite descriptions referencing `live_read`, and no byte-patching or assembly operation.

Maintenance

ActivityMaintained
ResponsivenessNo recent community issues