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
rdito a controllable value" or "a stack pivot fromrcx" 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
textualTUI 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_callreturn 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
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
eb0c6f505006392251d587e39a2b7fe780dba078334bb6c013cf613b477caea3
|
|
| MD5 |
69393f69862afe77c9e61e33dd3cf7e9
|
|
| BLAKE2b-256 |
38ebf07794d71d9e15774808c8f9f916dc47788c233c317c7747ac31dbb0f436
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0f778aa06babc982118df6fb344a71c671d526e604c796d40b7e4239b48bf9d
|
|
| MD5 |
86c86bc623650cb2f151e4d55b1fefc1
|
|
| BLAKE2b-256 |
13f8ee63eeaa2bd28aa34e45ce1371e27e61f02b8cf7100ed1de6f381c8106b8
|