Metadata-Version: 2.3
Name: ropinator
Version: 0.4.0
Summary: A better way to build a ROP chain.
Author: Jeremy Dunn
Author-email: Jeremy Dunn <jeremy.dunn315@gmail.com>
Requires-Dist: capstone>=5.0.6
Requires-Dist: keystone-engine>=0.9.2
Requires-Dist: mcp>=1.0.0
Requires-Dist: textual>=0.80.0
Requires-Dist: textual-autocomplete>=4.0.6
Requires-Dist: z3-solver>=4.15.4.0
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# ropinator

A ROP gadget finder that searches by **behavior**, not just text patterns —
for humans (a full-screen TUI) and AI agents (an MCP server) alike.

## Features

- **Semantic, constraint-based search.** Ask for "a gadget that sets `rdi` to
  a controllable value" or "a stack pivot from `rcx`" instead of grepping
  disassembly. A Z3 symbolic-execution engine backs every search, with a fast
  affine ("linear") executor handling the common move/const/load/store/pivot
  cases without invoking the solver.
- **Two interfaces sharing one core.** A full-screen `textual` TUI for
  interactive use (see [Interactive TUI](#interactive-tui)), and an MCP
  server (`ropinator-mcp`) so agents like Claude can drive gadget search
  programmatically (see [MCP Server](#mcp-server)).
- **"Plan" mode gathers everything for a goal.** `plan_registers`/`plan_call`
  return every gadget candidate needed to hit a set of register values or
  fire a call/syscall, each annotated with its clobbers — you (or the agent)
  compose the final chain.
- **Structural search works everywhere; semantic search where the solver
  reaches.** Text/regex search and control-transfer search need no symbolic
  execution and work on every recognized architecture; semantic and plan
  searches need the constraint solver (see
  [Supported Architectures](#supported-architectures) for exactly which).
- **Multi-format**: ELF, PE, Mach-O, and raw/flat binaries.

## Install

```
pip install ropinator
```

Requires Python 3.10+.

### With uv

[uv](https://docs.astral.sh/uv/) is the recommended way to install and run
ropinator. To install it as a standalone tool (isolated, on your `PATH`):

```
uv tool install ropinator
ropinator                     # then run it directly
```

Or run it once without installing:

```
uvx ropinator                 # the TUI
uvx --from ropinator ropinator-mcp   # the MCP server
```

**Working from a checkout** (contributors): `uv` reads the pinned toolchain and
manages the virtualenv for you — no manual `python -m venv` needed.

```
git clone https://gitlab.com/stolenfootball-tools/ropinator.git
cd ropinator
uv sync --group dev           # create the venv and install everything
uv run ropinator              # run the TUI
uv run ropinator-mcp          # run the MCP server
uv run pytest                 # run the tests
```

## Usage

**ropinator is a TUI first.** Run it with no arguments and you get the
full-screen app — load a binary, search by behavior, inspect the symbolic
register state, and export, all from one screen with nothing to memorize:

```
ropinator
```

That's the whole workflow. See [Interactive TUI](#interactive-tui) for a tour of
the load screen, the workspace, and the hotkeys — everything below is just
optional ways to skip a step or two on startup.

### Shortcuts into the TUI

Every flag here still lands you in the same interactive app, just pre-populated:

```
ropinator -f binary.elf                    # preload a binary, skip the load screen
ropinator -f binary.elf -d 5               # ...and scan deeper (default depth 3)
ropinator -f binary.elf -b 0x400000        # ...with a custom base address
ropinator -f binary.raw --arch arm -b 0x10000   # override arch detection
ropinator -f binary.elf -g gadgets.txt     # preload gadgets exported earlier
ropinator --arch x86_64 -g gadgets.txt     # load gadgets with no original binary
```

Prefer arrow keys over flags? Launch bare and do all of the above from the load
screen instead — the file field has a path-completion dropdown.

### Headless export

The one mode that *doesn't* open the TUI: point `-o` at an output file to find
gadgets, write them, and exit — handy for scripting or piping into another tool.
Add `-a` to expand every address rather than collapsing duplicates.

```
ropinator -f binary.elf -o gadgets.txt
ropinator -f binary.elf -o gadgets.txt -a
```

## Interactive TUI

Launch the full-screen TUI and do everything from there — load a binary, search
by behavior, inspect, and export. No commands to memorize: you pick a search from
a sidebar, fill in a small form, and read the results in a table.

```
ropinator
```

With no binary loaded you land on a **load screen**: enter a file path, optionally
pick an architecture (defaults to auto-detect), depth, and base, then load. As you
type a path, a **dropdown lists the matching files and folders** (native Windows
paths included) — arrow keys or click to choose, and selecting a folder descends
into it. A second tab loads a previously exported gadgets file. (`ropinator -f
binary.elf` preloads and drops you straight into the workspace.)

The workspace has four parts:

- a **sidebar** of searches, grouped **Structural** / **Semantic** / **Plan**
  (semantic and plan searches are greyed out on architectures without a solver),
- a **form** that changes to match the selected search — register fields
  autocomplete from the loaded architecture,
- a **results table** (address · effect · instructions · notes), and
- a **detail panel** showing the full symbolic register state of the highlighted
  result.

| Key | Action |
|-----|--------|
| ↑/↓ or click | Pick a search in the sidebar |
| Enter / **Run search** | Run the current search |
| ↑/↓ in the table | Highlight a result → its register state fills the detail panel |
| ↑/↓, Enter/Tab, click (path fields) | Choose from the file dropdown; picking a folder descends into it |
| **a** | Toggle showing unchanged registers in the detail panel |
| **F2** | Command palette — jump straight to any search or action |
| **F3** | Load another binary |
| **F4** | Re-scan the current binary at a new depth (deeper = more, longer gadgets) |
| **F6** | Export the current results to a file |
| **F7** | Show the architecture's registers |
| **Ctrl-C** | Quit |

The hotkeys are function keys (plus Ctrl-C) chosen so nothing collides with VS
Code's default shortcuts when you run ropinator in its integrated terminal — and
because function keys reach the app even while a text field is focused.

While a search or a re-scan is running, a status box keeps you informed. Re-scan
(**F4**) re-runs gadget finding on the loaded binary with a new depth, so you can
start shallow (fast) and go deeper when you need longer gadgets.

### Searches

| Search | Fields | Finds |
|--------|--------|-------|
| Text search | pattern, regex | Raw instruction-text matches |
| Control transfer | kind (syscall/call/jmp/any) | Gadgets that fire the chain |
| Set constant | dst, value | `dst = value` |
| Pop / controllable set | dst | Controllable setters (`pop reg ; ret`) |
| Move register | dst, src | `dst = src` |
| Load from memory | dst, addr reg/literal, offset | `dst = [src + offset]` |
| Store to memory | addr reg, src, offset | Write-what-where (`mov [addr+off], src`) |
| Arithmetic | dst, op (add/sub), src1, src2 | `dst = src1 op src2` |
| Stack pivot | src, max offset | `rsp = src + constant` |
| Plan register goals | goals (`rdi=0x404000 rsi=0`) | Every gadget needed to set a group of registers |
| Plan a call | abi, target, args | Argument-register setters + a control transfer |
| Chain to controllable | target, max length, no-arith | Ordered gadget chains making a register controllable (shortest first) |

**Text search** and **Control transfer** are structural (no symbolic execution)
and work on **every** supported architecture. The semantic and plan searches
require the constraint solver — every architecture in the **Full support** tier
of [Supported Architectures](#supported-architectures), which is all of them
except 16-bit x86.

**Plan register goals** and **Plan a call** don't return a result table — they
render a requirements bundle: for each register, the direct constant setters, the
controllable (pop) setters, and one-level move fallbacks, plus the control
transfer for a call. Nothing is ordered or emitted — it's the raw material for
building a chain.

**Chain to controllable** goes one step further and actually *composes* a chain:
given a target register, it searches for an ordered sequence of gadgets that
leaves the register holding an attacker-controlled value — starting from the
fake stack and threading through pops, register moves, and (optionally) affine
arithmetic. Chains are reported **shortest first**, and only minimal ones (every
gadget feeds a later step) are shown.

**Plan a call** knows a calling convention for every architecture with a solver:
`sysv64`/`win64` (x86-64), `i386`, `aapcs` (ARM32), `aapcs64` (ARM64),
`o32`/`n64` (MIPS), `riscv`, and `ppc`. The ABI list is filtered to the loaded
binary, so you are only offered conventions that apply to it.

A syscall and a function call are **different conventions on the same
architecture** — on x86-64 argument 4 goes in `r10` for a syscall but `rcx` for a
call — so the **Target** field selects the register set, not just the gadget that
fires the chain. Give a **Syscall number** as well and it is planned into the
architecture's number register (`rax`, `x8`, `a7`, `v0`, `r0`), so the bundle
covers the whole call rather than only its arguments.

### Stack-pivot offsets

For pivots, the offset is the signed difference `rsp_final = src + offset`. For a
clean x64 pivot (`mov rsp, rcx ; ret`) the offset is `+0x8` because `ret` consumes
one return address from the newly-pivoted stack — place your fake ROP stack
starting at `[rcx]`. Memory pivots (`mov rsp, [rax+0x10]`) show up under **Load
from memory** with `dst = rsp`.

## MCP Server

Ropinator ships an MCP server so AI agents (Claude, Cursor, etc.) can search
gadgets programmatically without a human at the shell.

### Setup

Add to your MCP client config (e.g. Claude Code's `.claude/settings.json`):

```json
{
  "mcpServers": {
    "ropinator": {
      "command": "ropinator-mcp"
    }
  }
}
```

Or if running from the repo with `uv`:

```json
{
  "mcpServers": {
    "ropinator": {
      "command": "uv",
      "args": ["run", "--project", "/path/to/ropinator", "ropinator-mcp"]
    }
  }
}
```

### Tools

Search tools return **JSON** so agents can parse results directly. Each result
has `address`, `instructions`, `type`, `effect`, and (where relevant) `clobbers`
or control `kind`/`target` fields.

| Tool | Description |
|------|-------------|
| `load_binary` | Load a binary and scan for gadgets |
| `load_gadgets_file` | Load gadgets from a previously exported file |
| `search_gadgets` | Raw instruction-text search — the generic finder (all arches) |
| `find_pivots` | Find stack-pivot gadgets (`src_reg`, `max_offset`) |
| `find_moves` | Find register-to-register move gadgets |
| `find_loads` | Find memory-read gadgets (also covers memory pivots) |
| `find_const` | Find constant-loading gadgets |
| `find_stores` | Find write-what-where gadgets (`mov [addr+off], src`) |
| `find_setreg` | Find controllable setters (`pop reg ; ret`) |
| `find_arith` | Find arithmetic gadgets (add/sub) |
| `find_control` | Find control-transfer gadgets (`syscall` / `call reg` / `jmp reg`) |
| `plan_registers` | Bundle: all gadgets needed to set several registers to values |
| `plan_call` | Bundle: all gadgets needed to call a function/syscall with args |
| `find_chain` | Ordered gadget chains making a register controllable (shortest first) |
| `show_result` | Full register state for a result index (JSON) |
| `get_registers` | List GP registers for the loaded architecture |
| `session_status` | Summary of the loaded binary and cached results |

### Workflow

The server is stateful: call `load_binary` once, then run as many searches as
needed. Results from the most recent search are cached so `show_result(index)`
always works after any `find_*`/`search_gadgets` call — and after
`find_chain`, `plan_registers` and `plan_call` too, whose `index` fields number
every gadget in the bundle so each one can be inspected.

```
load_binary(file_path="target.exe")
find_pivots(src_reg="rcx", max_offset="0x40")
show_result(0)
find_moves(dst_reg="rdi", src_reg="rax")
```

### Errors

Invalid input is reported rather than returned as an empty result. A misspelled
register, an unknown arithmetic op, or an unrecognized `plan_call` target comes
back as `[error] ...`, where these tools used to return `[]` — indistinguishable
from "no such gadget exists in this binary", which is a very different answer to
act on. Affected: `find_moves`, `find_const`, `find_loads`, `find_arith`, and
`plan_call`.

### Getting all gadgets for a chain

`plan_registers` and `plan_call` return a **requirements bundle**: every gadget an
agent needs to reach a goal, annotated with clobbers. The bundle gathers
candidates but does not order them or emit a payload — the agent composes the
chain from the returned options.

```
# Set up a Linux execve/mprotect-style syscall (SysV ABI: rdi, rsi, rdx, ...)
plan_call(args="0x404000 0 7", abi="sysv64", target="syscall")

# Or drive individual register goals directly:
plan_registers(goals="rdi=0x404000 rsi=0 rdx=7")
```

Each register entry lists `controllable` (pop-style — supply the value on the
fake stack), `direct` (constant setters), and a one-level `via_move` fallback,
plus a `control` section with syscall/call/jmp gadgets to fire the chain.

`plan_call`'s `abi` argument covers every architecture with a solver — `sysv64`,
`win64`, `i386`, `aapcs`, `aapcs64`, `o32`, `n64`, `riscv`, `ppc` — and defaults
to the loaded binary's own convention, so it can usually be omitted. An ABI
belonging to a different architecture is rejected by name.

`target` selects the *register set*, not just the firing gadget: a syscall and a
function call differ on the same architecture (x86-64 passes argument 4 in `r10`
for a syscall but `rcx` for a call, and `rcx` is destroyed by `syscall` itself).
Pass `syscall_nr` to plan the number register too:

```
plan_call(args="0x404000 0 0", target="syscall", syscall_nr="59")
# goals: rdi=0x404000, rsi=0, rdx=0, rax=0x3b
```

## Supported Formats

| Format | Description |
|--------|-------------|
| **ELF** | Linux, BSD, embedded |
| **PE** | Windows executables and DLLs |
| **Mach-O** | macOS, iOS |
| **Raw** | Flat binaries (use with `-b` to set base address) |

## Supported Architectures

| Tier | Architectures | What works |
|------|----------------|------------|
| **Full support** | x86-64 (AMD64), x86 (i386), ARM32 / ARM32-BE (AArch32), ARM64 (AArch64), ARM Thumb / Thumb-BE, MIPS32/MIPS32LE and MIPS64/MIPS64LE, RISC-V 32/64, PowerPC 32/64 (big- and little-endian) | Parsing, gadget finding, and the full constraint solver — every search in the tables above |
| **Gadget finding only** | x86 16-bit (i8086) | Parsing and gadget finding; structural searches (text/regex, control transfer) work, semantic and plan searches don't (no solver) |

MIPS and PowerPC each exist in both byte orders in real-world use. `MIPS32`/
`MIPS64` (unmarked, matching the `mips`/`mipsel` Linux distro convention) are
big-endian, `MIPS32LE`/`MIPS64LE` are little-endian; PowerPC is big-endian by
tradition (`ppc`/`ppc64`) but `ppc64le` is the dominant modern Linux target.
Each pair shares the same solver/executor code; only the parsing config differs
(auto-detected from the ELF `EI_DATA` byte). MIPS also has
**branch delay slots**: the instruction after every `jr`/`jalr` always
executes before control transfers, so a gadget ending in one of those
includes its delay-slot instruction too — which can cost one extra unit of
search depth for such gadgets specifically (the delay slot has to fit inside
the same depth window as the rest of the gadget). PowerPC has no delay slots;
its returns are register-indirect through the link/count registers (`blr` via
`lr`, `bctr` via `ctr`), so a gadget stages its return target with a
`mflr`/`mtlr` or `mtctr` sequence.

See [Architecture names for `--arch`](#options) for the exact accepted names
and aliases for every entry above.

## Options

All options are optional. With none, `ropinator` launches the TUI; the flags
below just preload a binary/gadgets (or, with `-o`, run a one-shot export).

```
-f, --file FILE           Binary to preload into the TUI
-b, --base ADDR           Override base address
-d, --depth N             Max gadget depth (default: 3)
-a, --all                 Expand all gadget addresses (in the -o export)
-o, --output FILE         Non-interactive: find gadgets, export to file, and exit
-g, --gadgets-file FILE   Preload gadgets from an exported file
--arch ARCH               Override architecture detection (see below)
--no-cache                Don't read or write the on-disk gadget cache this run
--cache-dir DIR           Where to store cached scans (default: per-user cache dir)
```

Found gadgets are cached to disk automatically, keyed by the binary's contents,
architecture, base address, and depth. Re-loading the same binary (at that depth
or shallower — a deeper cache is filtered down) skips the scan entirely. Caches
for many binaries live side by side in one directory; use `--no-cache` to disable
or `--cache-dir` / the `ROPINATOR_CACHE_DIR` env var to relocate it.

Architecture names for `--arch` (case-insensitive; see
[Supported Architectures](#supported-architectures) for what each tier
actually does):

| Name | Aliases | Status |
|------|---------|--------|
| AMD64 | x86_64, x86-64, x64 | Full support |
| i386 | x86 | Full support |
| ARM32 | arm | Full support (little-endian) |
| ARM32BE | armeb, armbe, arm-be | Full support (big-endian) |
| ARM64 | aarch64 | Full support |
| Thumb | thumb2 | Full support |
| ThumbBE | thumb-be, thumbeb | Full support |
| MIPS32 | mips | Full support (big-endian) |
| MIPS32LE | mipsel | Full support (little-endian) |
| MIPS64 | mips64 | Full support (big-endian) |
| MIPS64LE | mips64el, mips64le | Full support (little-endian) |
| RISCV32 | rv32, riscv32 | Full support |
| RISCV64 | riscv, rv64 | Full support |
| PowerPC32 | ppc, ppc32 | Full support (big-endian) |
| PowerPC32LE | ppc32le, ppcle | Full support (little-endian) |
| PowerPC64 | ppc64 | Full support (big-endian) |
| PowerPC64LE | ppc64le, ppc64el | Full support (little-endian) |
| i8086 | | Gadget finding only |

## Dependencies

Installed automatically via pip:

- [Capstone](https://www.capstone-engine.org/) - disassembly engine
- [Keystone](https://www.keystone-engine.org/) - assembler engine (for gadget file loading)
- [z3-solver](https://github.com/Z3Prover/z3) - symbolic execution backend
- [Textual](https://textual.textualize.io/) - interactive TUI framework

## License

[GPL-3.0](LICENSE)
