- Published on
Pwn Learning Path #7: Fixing a ROP Stack Alignment Crash
- Authors

- Name
- Muhammad Huzaifa
This lab gave me an important kind of failure: the overwritten return address was correct, execution reached win(), and the program still crashed.
The problem was stack alignment.
The failing instruction
The lab calls a small alignment check that uses movaps:
pxor xmm0, xmm0
movaps XMMWORD PTR [rsp], xmm0
movaps requires its memory operand to be aligned on a 16-byte boundary. A direct ROP return into win entered the function with the wrong stack alignment, so the aligned store faulted.
In GDB, the relevant check was:
p/x ((unsigned long)$rsp & 0xf)
Masking with 0xf shows the address modulo 16. A result of 0x0 means the address itself is divisible by 16.
Why ROP changed the alignment
The System V AMD64 ABI defines how the stack should be aligned around function calls. Normal compiled code enters a function through call, which pushes an eight-byte return address.
A ROP chain enters the target through ret. That is a different sequence of stack operations, so the callee can see the opposite eight-byte alignment from what its generated code expects.
The direct chain was:
72 bytes of padding -> win
It put the correct address in rip, but the stack state was wrong for the call made inside win.
What a plain RET gadget does
A plain ret gadget performs no register setup. It only:
- reads the address at
[rsp]; - jumps to that address;
- advances
rspby eight bytes.
Advancing rsp by eight flips its position between the two possible alignments modulo 16. The corrected chain became:
72 bytes of padding -> ret -> win
The corrected payload
from pwn import *
elf = context.binary = ELF('./stack_alignment', checksec=False)
rop = ROP(elf)
io = process(elf.path)
ret = rop.find_gadget(['ret'])[0]
payload = flat(
b'A' * 72,
ret,
elf.symbols['win'],
)
io.sendline(payload)
print(io.recvall().decode())
The fixed run printed:
local{stack_alignment_fixed}
How I now debug this symptom
When a chain reaches a libc function or target and then dies around movaps, I check:
- the exact crashing instruction;
- the effective memory address used by that instruction;
rspalignment at function entry and at the crash;- whether inserting one plain
retproduces the expected eight-byte shift.
The extra gadget is not a magic ROP charm. It is a deliberate stack-state correction.
Series navigation
← Part 6: cyclic offsets · Part 8: compiler-generated SSE alignment →