ret, which pops the next address off the stack and hands control to the next gadget. String enough gadgets together and you can perform arbitrary computation using only code the binary already contains.
What Is a Gadget?
A gadget is a short instruction sequence ending inret. The canonical examples look like:
ret pops into the instruction pointer. By placing gadget addresses and their argument values in sequence on the stack, you construct a program out of borrowed pieces. The technique is called Return-Oriented Programming because every “instruction” in your program is a return from some borrowed snippet.
When You Need ROP
You reach for ROP whenever
checksec shows NX enabled and you cannot simply jump to shellcode. On modern CTF binaries, NX is almost always on. Even on old 32-bit challenges, practicing ROP now pays dividends when you face heap exploits and kernel challenges later.system("/bin/sh"). To make that happen on x86-64 you need three things:
- The runtime address of
systemin libc (requires defeating ASLR first). - The address of the string
/bin/shin libc. - A
pop rdi; retgadget to load the/bin/shpointer into the first argument register before callingsystem.
Step 1 — Find Gadgets with ROPgadget
ROPgadget statically scans a binary for every usable gadget and prints its address and bytes. Run it against your challenge binary:
pop|ret filter:
Step 2 — Understand Stack Alignment
The x86-64 System V ABI requires the stack pointer to be 16-byte aligned at the point of a
call instruction. Many libc functions (including system) use SSE instructions internally that will segfault on a misaligned stack. If your exploit crashes inside system rather than giving you a shell, insert a bare ret gadget immediately before the system call to consume one 8-byte slot and restore alignment.Step 3 — Leak a libc Address (ret2plt)
ASLR randomizes libc’s base address on every run. Before you can callsystem, you need to know where libc actually landed. The standard technique is ret2plt: call a PLT stub (like puts@plt) with a GOT address as its argument. The GOT entry holds the resolved runtime address of that function in libc. Printing it gives you a fixed point from which you can compute libc base.
1
Identify a suitable leak function
You need a function that both prints data and is already in the PLT.
puts is ideal — it takes a single pointer in rdi and prints until a null byte. printf and write work too.2
Build the leak payload
Call
puts(puts@got) to print the runtime address of puts in libc, then return to main to get a second input window.3
Parse the leak and compute libc base
ljust(8, b'\x00') pads shorter outputs to 8 bytes before unpacking — puts stops at null bytes, so the printed address is often shorter than 8 bytes.Step 4 — Call system(“/bin/sh”)
Withlibc_base known, compute the absolute runtime addresses of system and /bin/sh, build the second payload, and send it.
1
Compute target addresses
2
Build and send the shell payload
Complete Exploit
Here is the full two-stage exploit assembled from all the pieces above:Using the pwntools ROP Class
The pwntoolsROP class can build chains automatically when the gadgets it needs are present in the binary or a loaded library. This is faster for standard call sequences:
libc.address tells pwntools the base, so all subsequent symbol lookups return absolute runtime addresses automatically.
ROPgadget Quick Reference
Troubleshooting Common Failures
Exploit crashes inside system() — not before it
Exploit crashes inside system() — not before it
Almost always a stack alignment issue. Add a bare
ret gadget immediately before the system address in your payload. On x86-64, rsp must be 16-byte aligned at the moment system is entered.Leaked address looks wrong (too short or null bytes cut it off)
Leaked address looks wrong (too short or null bytes cut it off)
puts stops printing at a null byte. If the leaked address contains a null byte in the middle (common when ASLR isn’t fully randomizing), use write(1, got_addr, 8) instead of puts — it prints exactly 8 bytes regardless of content.Gadget not found in binary — ROPgadget returns nothing
Gadget not found in binary — ROPgadget returns nothing
The binary may be very small. Search inside libc instead:
ROPgadget --binary libc.so.6 --only 'pop rdi|ret'. If you set libc.address before building your ROP([binary, libc]) object, pwntools will find gadgets in libc as well.Offset of 72 bytes doesn't work for my binary
Offset of 72 bytes doesn't work for my binary
The offset depends on the specific buffer size and stack layout of your binary. Re-run the cyclic pattern technique from the Buffer Overflow writeup to measure your exact offset. Never assume an offset from another writeup applies to your binary.