Skip to main content

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), and an MCP server (ropinator-mcp) so agents like Claude can drive gadget search programmatically (see 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 for exactly which).
  • Multi-format: ELF, PE, Mach-O, and raw/flat binaries.

Install

pip install ropinator

Requires Python 3.10+.

With uv

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 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, 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):

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

Or if running from the repo with uv:

{
  "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 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 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 - disassembly engine
  • Keystone - assembler engine (for gadget file loading)
  • z3-solver - symbolic execution backend
  • Textual - interactive TUI framework

License

GPL-3.0

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ropinator-0.4.0.tar.gz (117.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ropinator-0.4.0-py3-none-any.whl (139.9 kB view details)

Uploaded Python 3

File details

Details for the file ropinator-0.4.0.tar.gz.

File metadata

  • Download URL: ropinator-0.4.0.tar.gz
  • Upload date:
  • Size: 117.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ropinator-0.4.0.tar.gz
Algorithm Hash digest
SHA256 eb0c6f505006392251d587e39a2b7fe780dba078334bb6c013cf613b477caea3
MD5 69393f69862afe77c9e61e33dd3cf7e9
BLAKE2b-256 38ebf07794d71d9e15774808c8f9f916dc47788c233c317c7747ac31dbb0f436

See more details on using hashes here.

File details

Details for the file ropinator-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: ropinator-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 139.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.30 {"installer":{"name":"uv","version":"0.9.30","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"12","id":"bookworm","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for ropinator-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c0f778aa06babc982118df6fb344a71c671d526e604c796d40b7e4239b48bf9d
MD5 86c86bc623650cb2f151e4d55b1fefc1
BLAKE2b-256 13f8ee63eeaa2bd28aa34e45ce1371e27e61f02b8cf7100ed1de6f381c8106b8

See more details on using hashes here.

Release history Release notifications | RSS feed

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

This release

0.4.0 This release

2 files

0.3.0

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page