binary-ninja-reverse · git:20260925.e407d89 · 2026-09-25 · sha256 50b3efc9a99e984f

binary-ninja-reverse git:20260925.e407d89A

Immutable. This exact content never changes and is served at /api/v1/blob/50b3efc9a99e984f.

---
name: binary-ninja-reverse
description: Reverse engineering with Binary Ninja — HLIL/MLIL/LLIL intermediate languages, cross-references, type recovery, and headless batch analysis. Use for Binja-specific decompilation, IL-level analysis, and BN API scripting.
---

# Binary Ninja Reverse Engineering

Binary Ninja's strength is its **multi-level intermediate language (IL)** view.
Where IDA gives one pseudocode view, BN exposes LLIL, MLIL and HLIL separately —
which makes it the better tool for understanding optimizer behaviour, dataflow,
and what a compiler actually did to your code.

## Capability gate — read this first

Confirm what the local install can actually do before planning work:

```bash
binaryninja.exe --version          # prints the running build, e.g. "Binary Ninja 6.0"
```

| Capability | Availability |
|---|---|
| GUI analysis, HLIL / decompiler | yes — always |
| Debugger plugins | yes — always |
| **Python API / headless `-p` batch** | **depends on the licence on this machine** |
| **MCP bridge over loopback** | **yes — works with GUI-only installs** |

**The Python API and headless batch mode require a licence that is valid in the
running process.** Do not assume they are available, and do not report a scripted
result you did not actually obtain.

**When the API is not available, use the MCP bridge instead.** The
`binary_ninja_mcp` plugin runs *inside* the Binary Ninja GUI process, so it reaches
the API from the inside and does not need an externally licenced interpreter:

```
Binary Ninja GUI  ->  binary_ninja_mcp plugin  ->  HTTP on localhost:9009
                                                       ^
                                     MCP client  <-- bridge
```

Setup: install the plugin (Binary Ninja Plugin Manager, or copy the repo into the
Binary Ninja plugins folder), open a binary, click the button in the bottom-left
corner to start the HTTP server, then point your MCP client at it.

```json
{
  "mcpServers": {
    "binary-ninja-mcp": {
      "command": "npx",
      "args": ["-y", "binary-ninja-mcp@1.0.0", "--host", "localhost", "--port", "9009"]
    }
  }
}
```

The bridge is GPL-3.0 and is consumed as an external tool over loopback — keep it
out of this repository.

Probe the API before writing any script that imports it:

```bash
python -c "import binaryninja; print(binaryninja.core_version())"
# ModuleNotFoundError / RuntimeError -> the API is not usable from this interpreter
```

If that fails, take one of two paths:

- **MCP bridge over loopback** (preferred) — scripted access from inside the GUI
  process, see above.
- **GUI-only** — do the analysis by hand in the BN window and record the result,
  or route the automated part to IDA headless (`idat64.exe -A -S script.py`).

Never fabricate scripted output.

## IL levels — what each one answers

Pick the level that matches the question. Going too high loses the detail; going
too low drowns you in noise.

| Level | Form | Use it for |
|---|---|---|
| **HLIL** | near-C pseudocode | fastest read of intent; algorithm recovery |
| **MLIL** | typed SSA-ish IR | dataflow, where a value came from, optimiser output |
| **MLIL SSA** | explicit def-use | taint-style tracing, constant propagation |
| **LLIL** | register/flag level | what the machine actually executes; CET, stack layout |
| **Disassembly** | raw mnemonics | architecture quirks, alignment, AVX/NEON specifics |

### HLIL for intent
Read HLIL first. It inlines helpers and rebuilds loops, so a crypto routine or a
length check becomes readable C-like structure.

### MLIL for provenance
When you need to know *where* a value came from:

1. Select the variable in HLIL, press the IL-level hotkey to drop to MLIL.
2. Right-click the definition -> **Show all cross-references**.
3. Walk `MLIL_VAR_SSA` / `MLIL_SET_VAR_SSA` to trace the chain.

This is the fast path for "is this size field attacker-controlled?" — follow the
SSA chain back and see whether it bottoms out in a read from a network buffer.

### LLIL for the machine truth
Use LLIL when the decompiler is lying to you (it happens with hand-written asm,
obfuscated code, and packed regions). LLIL cannot hide a stack pivot or a
manual `jmp` into a gadget.

## Cross-references

`Ctrl+Shift+X`-equivalent — **Show cross-references** on any symbol or address.
For exploit work the useful views are:

- **Code xrefs** to a dangerous call (`memcpy`, `system`, `strcpy`) — shows every
  call site and the argument setup at each one.
- **Data xrefs** to a global — reveals which code writes a config value or a
  function pointer table. Writable function-pointer tables plus an overflow
  nearby is a classic control-flow hijack.
- **String xrefs** — fastest way into error paths and feature flags.

## Type recovery

BN's type propagation is its second strength. When a struct is wrong:

1. Open the **Types** view, define or import the struct.
2. Apply it to the variable (`Y` in HLIL).
3. Wrong field offsets usually mean the struct is mis-sized — check `size_t` vs
   `int` members on 64-bit, and check whether the compiler inserted padding.

Importing real headers beats guessing:

```
File -> Load Header File ->  <windows.h | your SDK header>
```

Then re-run analysis (**Actions -> Reanalyze**, or `Ctrl+P` -> reanalyze) so the
type library is applied to call sites.

## Common workflows

**"What does this function do?"**
1. HLIL, read the structure.
2. Rename variables as you understand them (`N`).
3. Set the function's type signature so call sites become readable.
4. Reanalyze. Callers now show typed arguments.

**"Where is this buffer written?"**
1. Find the allocation (`malloc` xrefs) or the stack slot in LLIL.
2. Data xrefs on the address -> every writer.
3. For each writer compare the copy length against the allocation size. That is
   the overflow, or the proof there isn't one.

**"Is this binary packed?"**
High entropy section, few cross-references, tiny import table, and an entry point
that does nothing but unpack = packed. Dump at the OEP and re-analyze the dump.

**"Patch it"**
BN writes patches back into the file: right-click an instruction -> **Patch**.
For a persistent change, patch bytes, then `File -> Save`. Prefer patching the
branch condition over NOP-ing a whole check — it keeps the diff readable.

## Comparisons to IDA

Use BN when: you need IL levels, SSA dataflow, or type propagation across a large
binary. Use IDA when: you need the widest decompiler coverage of exotic
architectures, mature idapython automation, or the largest plugin ecosystem.

For headless/automated reverse engineering prefer IDA (`idat64.exe -A -S script.py`).
If BN is on a paid licence, the equivalent is `binaryninja -p` with a script.

## Pitfalls

- **API may be unavailable.** Confirm with the import probe above before scripting.
  When it fails, use the MCP bridge — do not silently fabricate a scripted result.
- **Analysis cache.** Re-running analysis after a type change does not always
  refresh HLIL. Close and reopen the view.
- **`-p` still loads debugger plugins** on some builds. If startup hangs, use
  `-p` plus `--help` first to confirm the flag is honoured.
- **Do not trust the decompiler on obfuscated code.** Verify at LLIL, and verify
  again in a debugger before writing an exploit around it.
- **Probe first, always.** Reporting a scripted result you did not obtain is a
  fabricated result.