Yet another explanation of glibc’s
unlink mechanism and abusing them. I know there are many well documented articles and blog posts about the subject but I think until you take a moment and go through the source yourself, you will always feel like there’s something missing. It also helped me greatly stepping through the code in gdb with the source listing. You can do that by downloading the sources of your version of glibc. Specify the source lookup directory in gdb with
directory <source dir>/malloc/ and breaking on
Before thinking about exploitation, think about the implementation. Let’s get familiar with how free works. The structure of a heap chunk is defined in malloc.c. Datatype
INTERNAL_SIZE_T is 4 bytes on 32bit system and 8 bytes on 64bit system. The
prev_size field contains the size in bytes of the previous chunk bordering the current chunk, ONLY if the previous chunk is free and not part of a fastbin list, since fastbins are sorted by size in the first place. The
size field contains the current chunk’s size with the 3 Least Significant Bits meaning:
- bit at position 1 -
PREV_INUSE- Turned on if previous chunk is in USE.
- bit at position 2 -
IS_MMAPPED- Turned on if chunk is allocated via mmap.
- bit at position 3 -
NON_MAIN_ARENA- Turned on if the chunk belongs to thread arena.
BK are the forward and backward pointers pointing to the previous and next chunks in a doubly linked list of free chunks.
The pointer returned by
malloc to the user is pointing to
chunk->FD which is the same address we supply to
free. However, free internally calls to
_int_free which takes as one of its arguments the actual beginning of a chunk at
chunk->prev_size. So throughout the rest of this blog post we will reference a chunk with
P, not the address that’s passed to the user by
Finally we are at the source of free. I’m going to remove a bunch of code that doesn’t concern us. The comments prefixed with
! are my own, everything else is from malloc.c
So, we simplified it to
For consolidate we have to pass the following checks:
- P->size > 0x80
- P->IS_MMAPPED == 0
- P can not be the top chunk (wilderness). Top chunk’s address is taken from the main_arena structure in libc’s address space
- Nextchunk is calculated by adding P->size to P
- Nextchunk needs to have lower address than top_chunk+top_chunk->size
- Nextchunk->PREV_INUSE needs to be 1 otherwise it will throw a double free error
- Nextchunk->size needs to be valid heap chunk size
Ok, now that we have the required understanding of
free, let’s go through what happens when we free a chunk that’s not eligible for fastbins. Basically we take that second branch and consolidate the chunk.
The heap is a dynamic data structure that contains contiguous memory chunks. Some chunks are in use, some are not and the wilderness as the last chunk for future allocations. From the figure below, say we want to free
chunk 4. Because
p->prev_inuse == 0 malloc is making 2 assumptions.
- The previous chunk is
- Because the previous chunk is
FREEit will be part of a free list.
Next, the size variable holds the total length of the previous chunk and the current chunk.
p is changed to
p - prevsize which means that we have succesfully consolidated both chunks, the current and the previous and now the current
p is this combined new chunk.
p has changed to
chunk 3, the unlink macro will unlink
chunk 3 out of the free list and later add it to the unsorted bin.
p chunk to unlink, BK and FD a tempts for the corresponding pointers. The exploit mitigation check basically says, if
chunk 6->BK != chunk 3 or
chunk 1->FD != chunk 3 error out. But if the check is passed, overwrite
chunk 6->BK with chunk 1 and
chunk 1->FD with chunk 6.
This results in
chunk 3 unlinked from the free list, consolidated with
chunk 4 and inserted in the
Let’s think of what happened. We freed
chunk 4 and this caused
chunk 3 to get unlinked ? This influenced the pointers of
chunk 1 and
chunk 6 !? And remember how the distance to
chunk 3 was calculated ?
chunk 4 - chunk 4->prev_size, because it’s looking for the previous bordering chunk, right? So now, what if we control the area around
chunk 4 that we are going to free and we set
chunk 4->prev_size to
0, this will cause
free to think
chunk 3’s address is right ON TOP of
(chunk 4 == chunk 3).
Since we control the buffer of
chunk 4 and
free thinks it will run
chunk 3, we actually control
chunk 3's FD and BK. These values will be written to
BK->FD. Again, we get to write
fake chunk 3->BK to fake chunk 3->FD->BK and
fake chunk 3->FD to fake chunk 3->BK->FD ONLY if the
target's address->FD or BK == fake chunk 3 (which is chunk 4, which is ptr we pass to free - INTERNAL_SIZE_T * 2)
Since I’m sure this is super confusing (especially if you are reading it from a half-assed explanation like this) I’m going to do a general workflow for the required steps for the unlink to work.
- Find target value to overwrite (obviously :)), let’s call this value
- We need to know the addresses on the heap
&TARGET - INTERNAL_SIZE_T * 2or
&TARGET - INTERNAL_SIZE_T * 3needs to be stored at known location
- If it’s
&TARGET - INTERNAL_SIZE_T * 2this will be like
fake chunk 1in our examples
- If it’s
&TARGET - INTERNAL_SIZE_T * 3this will be like
fake chunk 6in our examples
- If it’s
fake chunk 1->FDneeds to point to the chunk we are unlinking
fake chunk 6->BKneeds to point to the chunk we are unlinking
You understand why everything explain until now was about consolidating backwards ? Because the previous chunk bordering the chunk we are freeing is currently FREE (or at least we are making free to think that), so it just combined the two bordering chunks and unlinks the previous chunk. To consolidate forward we just have to make the
nextchunk be free, free checks this by doing
(P + P->size (to find nextchunk) + nextchunk->size)->PREV_INUSE, so it’s like
nextchunk's nextchunk -> PREV_INUSE. And remember if either the previous or the next chunks are free it needs to run unlink on them so they are not part of the free list anymore. Also, they both can be free ! In that case we will consolidate backwards AND forwards and run unlink on both.
For forward consolidate to take place in our examples,
chunk 5 had to be green (free :P).
- Thanks for reading, I hope I didn’t confuse you more !