Once you control a return address, the next question is what to point it at. If the binary doesn't hand you a convenient system() call with the arguments already set up, you build the behavior you need out of tiny fragments of the binary's own code — "gadgets," each ending in a ret — chained one after another. ROPgadget finds every one of those fragments for you instead of you scanning a disassembly by hand.
Listing every gadget in a binary
ROPgadget --binary ./vuln
This dumps every gadget ROPgadget can find — typically hundreds to thousands of lines for anything non-trivial, each one an address plus the instruction sequence at that address, like 0x00401234 : pop rdi ; ret.
Narrowing down to something useful
Scanning thousands of gadgets by eye doesn't scale, so filter for the specific pattern a ROP chain needs — commonly a way to load a value into a register before a call:
ROPgadget --binary ./vuln --only "pop|ret"
For a classic ret2libc chain you need a pop rdi ; ret (to set the argument register on x86-64) and the address of system and a "/bin/sh" string — ROPgadget's --only flag is how you go from "every gadget" to "the handful that actually matter."
Letting it build the chain for you
ROPgadget --binary ./vuln --ropchain
For simpler binaries, --ropchain attempts to assemble a working chain automatically (often targeting a mprotect + shellcode or a direct execve) and prints ready-to-use Python. It doesn't always succeed on harder targets, but it's worth trying before building one gadget-by-gadget by hand.
Wiring gadgets together with pwntools
In practice, ROPgadget finds the pieces and pwntools assembles them into the actual payload:
from pwn import *
pop_rdi = 0x00401234 # pop rdi ; ret — found via ROPgadget
bin_sh = 0x00402050 # "/bin/sh" string in the binary
system = 0x00401050 # system()'s address (from the GOT/PLT or a leak)
payload = b'A' * offset
payload += p64(pop_rdi) + p64(bin_sh)
payload += p64(system)
io.sendline(payload)
This is the same pattern underneath almost every ret2libc writeup: set up the argument register, then jump to the function that does the actual work — ROPgadget's job ends once it's told you where those gadgets live.
Why it's worth learning
Manually reading a disassembly looking for a usable pop reg ; ret gadget is possible but slow, and easy to miss one that's a few bytes offset from where you'd naturally look (an "unintended" gadget inside a longer instruction). ROPgadget automates exactly that search, turning "find a gadget that does X" from a manual disassembly review into a single filtered command.