rhme3 exploitation writeup
The following is my writeup of how I took on the RHME3 exploitation challenge. I’m fairly new to CTF, so this post is a fairly verbose tale of fails and successes as I worked my way through.
We’re given two binaries and a port on a host:
Ok, what do we have here:
Running strings across libc showed it was version 2.23; fairly recent. There aren’t going to be any walk-in-the-park exploits against this. Given that I’m entirely new to glibc exploitation, I pulled a copy of the source to have nearby.
A quick run over with checksec showed that there was no ASLR and no full RELRO. nice.
Connecting to the host provided gives us a basic menu system:
Straight away it looked like someone had put together a program with the ability to add, remove, modify bits of data. It felt like it was going to be heap related. I had a bit of a tinker with the program, and a pretty obvious thing stood out to me: the Select player function seemed independent of two others: you could Select a player, and when you asked to Remove, it asked you independently which player you’d like to remove. Perhaps we have some kind of use-after-free?
Ok, time to look at the binary locally. It wouldn’t fire up, so I threw a copy in IDA and started walking through the startup code. With a bit of trial and error I realized it needed to have a user ‘pwn’ on the system to setuid() to, and a directory /opt/riscure/pwn. I created these and the binary happily fired up. The binary forks for every connection, so I setup a script to attach gdb to the forked process as soon as I connect. Of course, I also wanted to make sure I had LD_LIBRARY_PATH set to the libc version I’d been provided, to make sure any addresses or offsets are valid.
While in IDA, I had a quick look at the layout of the add_player function, and took some basic notes on what the main struct looked like… this will be very relevant shortly:
Jumping between IDA and gdb, I managed to get a crash pretty quick. Sure enough, if you Select a player, then Remove that player, you have a stale pointer to memory that has been free’d.
The following demonstrates this. I’ve redacted the printing of the entire menu each time:
We’ve managed to retain a reference to a player which has been removed and when viewed, a pointer has been shown as part of displaying the players stats.
So at this point I did a load of reading up on the glibc malloc implementation and classic attacks against it. I suspected maybe I could overwrite a players attack/defense/etc points to corrupt heap metadata and overwrite the forward & backward freelist pointers. After a load of getting it wrong, I looked for an alternative idea.
In the glibc heap, there are two major freelists which store chunks of data that have been malloc()’d and subsequently free()’d: bins and fastbins. Bins is a doubly-linked list of larger memory sizes with all kinds of nice features such as coalescing: two adjacent blocks of free memory will be merged together and shifted to the appropriately sized bins freelist. Fastbins on the other hand is for small allocations, and as the name suggests is designed for small and quick allocations.
I took a look at how memory was laid out when a player is created. There are two malloc’s for adding a player, and two free’s for removing one. This is what a player looks like in memory:
So when a player is created, 0x18 bytes is allocated for the main player struct, and then however many bytes is needed for the name (based on strlen) is also allocated. Because the length of the name determines how many bytes are allocated, we can choose to have this allocation in bins or fastbins.
So our basic bug primitive is that we can allocate some data, get a pointer to that data, free that data, and continue to access the free’d chunk. Exactly what can we read/write to the chunk after it’s been freed? Heap metadata. A regular malloc_chunk looks roughly like this:
This is what a free chunk looks like: two size fields, and forward and back pointers to have the chunk on the linked list. Of course, on the fastbins freelist, the previous size and the Back Pointer aren’t used, so can be completely ignored. The Prev Size and Size fields are inaccessible to a normal user of malloc(), the pointer you receive when you call malloc would be pointing to location B; the pointer to location A is what the glibc heap manager uses internally. When this memory is in use (IE, you’ve called malloc), your data starts from B; given that your chunk is no longer on the freelist, the FD and BK pointers are not required, so you can read/write there.
So a quick recap; we have a pointer like B to a chunk of data on a singly-linked list, so by editing a player that has been selected and free’d, we end up overwriting heap freelist pointers. What can we do with this? we can overwrite the freelist data to point somewhere interesting, and after a couple of malloc’s, our interesting location will be handed to us via malloc. If you remember that the player name calls malloc and then lets us write arbitrary data in as a name, if malloc returns an address of our choosing we have a write-what-where primitive.
There’s a catch though. People have been messing with heap metadata for a long time, so some basic checks are in place to make sure the freelist hasn’t been screwed up too bad. Fortunately for us, the fastbins checks don’t have any of the fanciness that the regular bins list does (I guess it’s harder to check the integrity of a singly-linked list?). The key check we need to bypass is in the above malloc_chunk diagram, there’s a size field. When the heap code walks the freelist it needs to see that the chunk is indeed fastbins size; small.
If you don’t get this right, you see this:
So we can overwrite a given piece of memory, assuming it is preceded by a small number. Given there is no RELRO, we can straight-up overwrite the GOT, but none of those addresses are preceded by a small value.
I spent a fair amount of time tinkering with this piece of code from the fantastic how2heap collection.
There’s a very obvious candidate for this: we can create a player whose ‘defense’ value is very small, and it’ll allow us to overwrite the name pointer to point to anything of our choosing. If we then edit the players name, we’re writing data of our choosing to a location of our choosing; win!
So here’s the workflow:
- Create a player, lets call him ‘fake’, with defense points of 0x21
- Add, Select, and Remove a player to get a stale pointer to the freelist
- Edit the player (overwrite the fastbins freelist pointer) to point to our fake player – where we’ve configured it to look like a fake heap chunk – and overwrite the fake players name pointer to point into the GOT
- Edit the fake players name (which is a pointer to the GOT), overwriting the address of a GOT function pointer
- Call that function, get EIP
- After a load of trial and error, this resulted in getting control of the instruction pointer. I now started doing a bunch of looking into how to turn this into a shell, before realizing there is such a thing as a “one-gadget shell”, thanks to Gynvaels slide deck. Using the one_gadget tool, I found a nice address that only required clearing of r12 and RCX registers.
I ended up overwriting the strlen() function, as it was called when I added a new user and gave me access to my own data on the stack. I called a stack pivot gadget which just slightly altered RSP to point into my data and was able to call a couple of basic ROP gadgets to clear the right registers and grab a shell.
I threw together a terribly kludgey python exploit using manually fetched addresses from leaking the a heap pointer (seen above when viewing a player) as well as leaking the libc base by forcing a malloc corruption error on the remote host, which resulted in a full error printout including addresses of all loaded libraries.
My final exploit with it’s hardcoded leaked addresses is available on this github gist. Having now seen what other people did using pwntools, I really need to up my game with CTF tooling.
I learnt a shit-ton, and thank the Riscure+Argus teams for putting this together. I spent a lot of time banging my head against the desk, but am happy to have persisted and learn a load.
Resources I leant on heavily: