The toddler’s introduction to Heap Exploitation, Unsafe Unlink(Part 4.3)

Exploiting a heap overflow vulnerability is not always straightforward. Between else, the allocator imposes various checks during the chunk assignment/freeing process which require extra steps in order to achieve an exploitable result. In this post we assume that we have discovered such a vulnerability and we are going to explore the “next” steps in order to successfully exploit it. More specifically, we are going to manipulate the unlink MACRO in order to allow us to take control of an arbitrary pointer and modify the data in which it points to. Under specific conditions (such as a pointer that points to a function), we may redirect the code execution and be able to run arbitrary commands.

The unlink macro

We saw from the previous post that during a chunk freeing process and when specific conditions occur, the allocator will consolidate adjacent chunks into bigger ones for more efficient memory assignment. Simply said, assume that you have the chunks A, B, C and B gets freed, then according to the current implementation free will check if A or C are in use, and in case they aren’t it will try to create a bigger chunk by consolidating them to B . The implementation of this logic is depicted in the code snippet below:

Consolidating in free

Following the unlink macro at lines 3977 and 3986 we end up to the following definition:

This code will modify (see lines 1350–1351) the fd and bk pointers of the chunk header (see figure below) in the context of a double list re-arrangement:

The process is similar to deleting a node from a double list:

But, before anything happens, a validity check is performed (at Line 1347):

First notice that FD = P→fd and BK = P→bk , so FD should point to next adjacent chunk and BK to previous adjacent chunk:

So, for the double linked list to be valid BK→fd and FD→bk must point to P :

After checking this condition we have the following assignments FD->bk = BK and BK->fd = FD, so our chunks now will look as below:

Exploitation plan

As we mentioned in the beginning of the article we fully control the contents of a chunk and due to an overflow bug we can modify the metadata of an adjacent chunk. So on our way to successful exploitation we have to pass the unlink check:

In order to do the we will do the following:

Create a fake chunk inside the controlled chunk

We are going to insert particular values to particular memory addresses in order to form a valid chunk struct, inside the data sector of the controlled chunk:

As we want to pass the unlink check, the values that we are going to insert as fd and bk in the fake chunk have to point to structs which their respective fd and bk pointers will point back to our fake chunk! To visualise the concept, lets see a couple of figures:

Imagine that the Global_Var table forms a chunk struct, then at the memory address 0x6020b0 we would have the previous chunk’s size, at 0x6020b8 the current chunk’s size, at 0x6020c0 the fd pointer and at 0x6020c8 the bk pointer. So we have fake_chunk.fd→bk = 0x1967030 and fake_chunk.bk →fd=0x1967030 , which will pass the unlink validation check.

Next step:

Modify the header of the next chunk in order to show the FAKE CHUNK as free

Remember: due to the heap overflow, we can write beyond the CONTROLLED CHUNK’s boundaries, thus we can modify the header of the NEXT CHUNK:

Regarding the type of modification, recall that the header of an allocated chunk consists of the current size and the previous chunk’s size if and only if the previous chunk is free. I am posting the chunk struct once again so you don’t have to scroll up:

Remember also that the mchunk_size embeds 3 flags indicated by the last 3 bits of the value. So, if the size is0x10 and the previous chunk is in use the mchunk_size will look as bellow:

We don’t care about the rest of the flags right now as we only need to flip the last bit in order to indicate that the previous chunk is not in use. This will trigger the backward consolidation process, which in its turn will trigger the unlink macro.

Last but not least, the mchunk_prev_size must also correspond to the size of the fake chunk in order to bypass the rest of the security checks. If everything is at is should be, when the NEXT CHUNK, the FAKE CHUNK will be consolidated and the fd, bk pointers of the FAKE CHUNK are going to be overwritten in two subsequent steps:

1st step: After Line 1350 is executed
2nd step: After Line 1351 is executed

And here comes the next tricky part, the controlled chunk points to its data part, so by modifying chunk[0] it is like modifying the contents where fd points to, and since we control were fd will point (via the chunk[3]) we can control the contents of the address where which is contained at chunk[3].

Show Case

Assume the following C program:

Let’s take it line by line to get to the bottom of this. At Line 6 we define a pointer to a function that returns void and doesn’t take any parameter. At Line 8 we define a pointer to an unsigned integer and at Lines 10 to 17 we define two functions, the doNothing which does absolutely nothing and the shell which pops up a shell. In Line 26 we have the first malloc of 0x420 bytes, so after this statement we will have the following chunks:

So chunk0_ptr points to 0x5555555592a0 (the data part of the chunk) while the header of the same chunk starts 0x10 bytes before, at 0x555555559290 . Finally, the address of the chunk0_ptr is at0x555555558018 , so, to resume, after the first malloc we have the following:

In Line 27 we have the second call to malloc and after this our chunks will look as below:

Creating a fake chunk

Now we come to this part which corresponds to the fake chunk creation:

Line 29 will set the size of the fake chunk to 0x421 since chunk0_ptr[-1] is 0x431

And Lines 30–31 we set the fd/bk pointers of the fake chunk:

chunk0_ptr[2] = 0x555555558018 — 3 * 8 = 0x555555558000
chunk0_ptr[2] = 0x555555558018 — 3 * 8 = 0x555555558008

Thus our controlled chunk set up will be as follows:

And this will pass the unlink check as if you recall from the unlink check, we will have the following:

FD = P->fd => FD = 0x0000555555558000
BK = P->bk => BK = 0x0000555555558008

Subsequently, the FD→bk will move the FD pointer 3 positions forward (as this is bk’s position in the chunk header => 0x0000555555558000 + 0x18 = 0x0000555555558018) and FD→fd will move the BK pointer 2 positions forward (as this is fd’s position in the chunk header => 0x0000555555558008 + 0x10 = 0x0000555555558018)

To summarise, our set up so far, is as follows:

“Fixing” the next chunk’s header

This is the easies part to grasp: Since we can write beyond the controlled chunk it would be trivial to overwrite the adjacent one. This is what Lines 33–35 demonstrate:

chunk1_hdr[0] will point to the mchunk_prev_size and chunk1_hdr[1] to the current size. Line 35 will flip the last bit so the fake chunk will show up as not in use:

We are now ready to call free:

After free

Let’s now see the effect of the free function. Notice that, before calling it we have the following:*0x555555558018 = 0x00005555555592a0

During the free execution though, the following statements will be executed:

And immediately:

Indeed:

Finally notice that &chunk0_ptr = chunk0_ptr[3]

Write anything anywhere

Recall from our C program the following lines right after the free call:

The d variable points to the doNothing function but since we control the contents of chunk0_ptr we can modify the value of chunk0_ptr[3] , thus after Line 41 we will have the following:

So it will be &chunk0_ptr = 0x00007fffffffe338 which contains the address of the doNothing. So, chunk0_ptr[0] points here:

Thus Line 42 will overwrite the contents of this memory address with the address of the shell function:

Which completes the last exploitation step:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store