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.
As always, please find below the references to my previous posts relative to heap exploitation:
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:
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:
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:
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].
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:
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: