Skip to content

Repository files navigation

ROPForge

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.


Table of contents


What is this, in plain words?

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 2-minute primer on ROP

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.

Who is this for?

  • 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).

Install

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/ropforge

To confirm everything works:

cargo test          # runs the full test suite; should be all green

Tip: the examples below use cargo run -- for convenience, which builds and runs in one step. Once built, you can call ./target/release/ropforge directly instead — it's the same program, faster to start.

Your first chain (quick 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.elf

You'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 raw

That's the whole loop: point at a binary → pick a goal → get a payload.

Reading the output

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 raw gives you the same thing as one hex string (no comments) — for piping into other tools.
  • --format pwntools gives you a Python script using pwntools, with each line commented, ready to adapt.

Command reference

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, stage1 and stage2.)
  • pwntools — a paste-ready Python exploit script. For --goal pivot it's a complete interactive exploit that reads the leak, sends stage 2, and triggers the pivot.

Worked examples: the ROP Emporium challenges

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 build

A couple of these are worth understanding, because they show what the tool actually automates:

  • pivot is 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 pwntools gives you a complete script that reads the leak, plants stage 2 in the leaked buffer, then pivots the stack onto it.
  • fluff has no normal way to write memory and no pop 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 sources rdi through __libc_csu_init.

You can reproduce all of this — see verify/README.md.

Using ROPForge inside your own project

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.

How it works under the hood

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
  1. Load — parse the ELF and pull out the executable code (via the goblin crate).
  2. Find — scan for gadgets by walking backward from every ret (via the iced-x86 disassembler).
  3. 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.
  4. 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).
  5. 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.

What it can't do (scope & honest limits)

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 rdx on 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.

How it compares to other tools

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.)

Troubleshooting & FAQ

"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.

Contributing & development

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          # formatting

CI 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.

Legal & ethical use

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.

License & credits

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.

Releases

Packages

Contributors

Languages