RHME3 Quals - Exploitation

September 02 2017

This was a fun challenge, a lot of mistakes were made and a lot of things were learnt! Shout out to 0x4a47 my team mate for the RHME3 CTF aswell.

As a good exploit challenge starts, we begin by running file on the binary to see what we learn about it.

main.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=ec9db5ec0b8ad99b3b9b1b3b57e5536d1c615c8e, not stripped

So it is a 64 bit executable that is dynamically linked, cool. Then what we do is run checksec to get an idea of the protections enabled on the binary.

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

So we have NX but no PIE, and partial RELRO, looks to me like ROP - but we'll see. Upon running the binary I see that it terminates instantly - which is weird because I netcat to the provided server and I am presented with some form of menu. Running the binary through ltrace I get the following output.

__libc_start_main(0x4021a1, 1, 0x7fff8c920288, 0x4022c0 <unfinished ...>
getpwnam("pwn")                                        = 0
exit(1 <no return ...>
+++ exited (status 1) +++

It is looking for a user named pwn in the /etc/passwd file, which must be for the challenge running on the server. I am not interested in setting up the environment for this challenge to run as intended so I simply patch the binary so that it runs.



Could have just nop'd it all but whatever it runs. So running the binary for real, we are presented with a menu where we are able to add/edit/remove/select/show players and the whole team. So we are dealing with a struct of a player being stored in a team. This screams heap exploitation.

Welcome to your TeamManager (TM)!
0.- Exit
1.- Add player
2.- Remove player
3.- Select player
4.- Edit player
5.- Show player
6.- Show team
Your choice:

So after playing with the binary and reading through IDA I see that there is a concept of selecting a player. And how this is done, is that there is a global array of size 10 called players which stores pointers to player objects on the heap. A player struct is always sized 0x18 bytes and is as follows:

struct {
uint32_t atk; /* attack value */
uint32_t def; /* defence value */
uint32_t spd; /* speed value */
uint32_t pre; /* precision value */
char * name;  /* pointer to name */
} player;

So when a player is selected, using their index at the menu option, a global variable called selected is set to the desired player's address as per what is stored in the players global array.

Now where a bug lies is in the removal of a player. Here is the block which is responsible for the actual removal of the player - see if you can spot the bug.


The bug lies in the fact that if the player was the currently selected player, it is never removed from being the currently selected player. And thus we have a UAF vulnerability, because the Edit player and Show player menu options use only the currently selected player!

Okay cool so we have a UAF, how can we use this? Well we want to start leaking memory because we can assume that ASLR is enabled on the server. How can we do this? Simple! We can do this by creating a player with a really large name (upwards of the coded limit of 255 bytes) which will create a player chunk of size 0x20 bytes and a corresponding name chunk directly after. We then select player 0 using the menu, remove player 0 such that the name chunk now contains the smallbin array bin address in its FD and BK entries of the free chunk struct - which is exactly where the name of the player used to appear! Using Show player from the menu we are able to leak this, as the player name address isn't overwritten by free chunk meta data when the player was deleted.

# select player 0 so we can read the ptrs out of it when we free it (UAF)
# now free it so we can leak the pointers to the small bin array
# now lets read the 'name'! ;)
# ----------------
r.recvuntil("Name: ")
lkd_fd = r.recvuntil('\n', drop=True) 
lkd_fd = u64(lkd_fd.ljust(8,'\0')) # unpack it and adjust it
log.success("Leaked main arena ptr: 0x%x" % lkd_fd)
r.recvuntil('choice: ') # get to clean state
# ----------------
# now that we have this, we can find the offset from libc base!
libc_base = lkd_fd - 0x3c4b78 # (hard coded offset found from gdb for this libc)
log.info("Hence libc_base: 0x%x" % libc_base)

Now that we have leaked libc base, we want to get an arbitrary write primitive such that we can overwrite __malloc_hook or the GOT or something and get code exec.

After reading other writeups as soon as the competition ended I realized I really made this hard for myself with what I continue to do here - but I will continue as it is a completely legit solution although fairly long :)

How I thought of getting the write primitive is that when we rename a player, the program gets the address of the player name and does some size checks of the existing name and the proposed new name to ensure we don't need to realloc the name chunk, and if not then it simply writes the new name into the place of the old one. So if I can overwrite the name address within a player chunk, then I can rename that player with any value I want - that I want to be stored at the address I just replaced the player name address with in the chunk. Hence I would replace the player name address with the malloc GOT entry, and then rename the player with the address of a one shot gadget now that we have a libc leak.

The way in which I did this made the challenge a bit more difficult! I had the ability to overlap chunks by creating two players (with short names)-> selecting the first one -> then removing them in reverse order. They would then exist in the following order in the fastbin freelist:

P1 -> P1n -> P2 -> P2n -> NULL

So what I could do is rename the P1 player with an address I would want to appear on the freelist - with the caveat that the memory address that it pointed to appear like a chunk with a freelist-sized size value. For example I would have to rename the player with an address that pointed to a region of memory that looked like (where X's are *don't-cares):

XXXXXXXXXXXXXXXX 0000000000000021

Because then malloc would be happy making the allocation for a player chunk with this, otherwise malloc would throw errors like the following:

*** Error in `./main.elf': malloc(): memory corruption (fast): 0x00007f589e667b20 *** 

Which would be the case if I tried to create two players with short names (so 4 chunks in total) when the freelist looked corrupted like in the following example:

fastbin 1 @ 0x7f549ccedb28 { 
    0x9800c0->fd = 0x9800e0->fd = 0x5858585858585858 Linked list corrupted 

To prevent this and a lot of checking, the default values for my add_player() function in my script simply made players with attribute values which appeared as though they were the start of a player chunk with a NULL QWORD and then the value 0x21 in the next QWORD.

So now that I could overlap chunks, the idea was to first leak a heap address so that I could add arbitrary memory to the fastbin freelist which I could then malloc to get overlapping chunks, which I would soon be able to use to change the player name address of a player chunk, and then rename the chunk I just altered for my arbitrary write.

So seeing as how I could create a fastbin freelist as illustrated above, I simply had the P1 selected and chose Show player from the menu which leaked the address of the P2 chunk on the heap. Success!

# get a UAF on player 2 by getting it to the front of the fastbin freelist
# P2 -> P2n -> P3 -> P3n -> NULL

# ----------------
# Read the FD ptr of Player 2's name chunk (so the addr of P3 chunk)
r.recvuntil("Name: ")
lkd_loc = r.recvuntil('\n', drop=True) 
lkd_loc = u64(lkd_loc.ljust(8,'\0')) 
r.recvuntil('choice: ') 
log.success("Leaked heap ptr: 0x%x" % lkd_loc)

After some hard lessons learned, I found that it was easy to overlap a player chunk on top of a name chunk, because during a player creation the player chunk was always malloced before the name chunk, so would use the front of the freelist to create the chunk, however I really only had control over the FD pointer attribute of a name chunk, because I could place whatever name I wanted in the chunk thus altering the fastbin freelist. However when I tried to do this for a player chunk I ran into issues. Reasoning being was that since the binary uses atoi() to read in the player attributes via a 4 byte read I could only provide a maximum of 999 to the attributes - which wasn't enough for me to construct a heap address out of the atk and def attributes of the player.

So what I ended up doing instead was overlapping a player chunk (which is all I could do at the time) over a name chunk. I did this in such a way that I could select the player whose name chunk was being overlapped - delete the player which is overlapping the name chunk - and then rename the player which is selected (the one whose name chunk is being overlapped). What this enabled me to do was rename the overlapped chunk such that I could overwrite the FD pointer of the player chunk itself. Thus finally giving me the ability to corrupt the freelist with a chunk - which when alloced - would actually be a name chunk and not a player chunk.

# We want to corrupt the FD ptr of Player 2 name chunk to point to Player 3's
# actual player chunk
# Restore player 2, but having '4141414141414141 0000000000000021' as the name
# for the purpose of the name chunk allocation at the very last stage
# Malloc a forced chunk where the player overlays the Player 3 name chunk and
# the name chunk for this allocation goes to the bottom of the heap because
# of it's size because we don't care about it

# Remove the player chunk we just created

# Select the player 4 chunk so we can rename it, changing the FD ptr of the 
# player chunk we just created - allowing us to malloc our own NAME chunk
# wherever we want

All I had to do now was make a new player, which would create a player chunk somewhere we didn't care, but then malloc the name chunk on top of an existing player chunk I had set up. What this enabled me to do was rename that player chunk such that the player name of the chunk I was overlapping was altered to a value of my choosing (malloc GOT entry). And then all I had to do was rename the chunk I had just overlapped and altered such that when renamed, would write to the malloc GOT entry with the magic one shot gadget I found earlier.

# Create a chunk - which will be overlapping the original Player 2 player chunk
# the name that will overlay the name Ptr of player 2 is the addr of got_malloc
# and now we write the one shot gadget to the malloc GOT entry

# and initiate a malloc call via creating a player

# shellz

And the flag!

$ ls
$ cat flag 
$ whoami 

This exploit actually ended up using the House of Spirit heap exploit technique outlined in a phrack paper. I haven't had time to read the paper - but now that I have used the technique in practice I will now!

Some issues I ran into which I learnt from included that I tried to make the location I wanted to write to __malloc_hook. This value is NULL to begin with, so when I tried to write the magic one shot gadget to it, the binary did a realloc at this address, failing, because when it did strlen(NULL) at __malloc_hook the length was zero.. So the GOT entry for malloc which contained a full address turned out to be a much better candidate!

Another one which stumped me was that the exploit was working fine local, using the same libc file that was provided - however would fail on remote on what I have good reason was a malloc_consolidate issue which I ran into earlier when removing a player. If the freelist was corrupted with a non-chunk or non-NULL value, malloc_consolidate is called in free() under some circumstances and goes through and consolidates chunks in the fastbin. And if the fastbin is corrupted then the program fails - so what I ended up doing to fix this local was make the third chunk have a NULL name - because the start of the name ended up being on the fastbin freelist due to how I was manipulating the heap - and this worked. I ended up just making all players have null names because I was convinced the same issue was happening somewhere else on the remote server when running the exploit - because the exploit would fail when removing a player right towards the end of the exploit (like it was doing local before I fixed it). However this didn't fix it. What I ended up doing (and you can see this in the full exploit) was just create a few players before I actually start the normal exploit itself. I am not 100% sure of the reason why this fixed the issue but I can put it down the just offsetting some heap addresses which may have had newlines/nulls in the address such that it didn't break my exploit any longer, as it turned out ASLR was off (which I learned from running the exploit multiple times and reading the leaks).

Here is the final exploit: