Post

Unlink Exploit

Understanding how unlinking happens in libc (old and modern) and how to exploit it

Unlink Exploit

Unlink Exploit

In this blog I will try to explain the unlink heap exploit technique and also show examples of how it works. I have had a hard time understanding how this technique works, so i thought let me try to explain it to someone else (via this blog post at least) to see if i really understood the technique.

Overview

These technique is triggered by the unlink macro hence the name. The unlink macro is used to remove a free chunks from the middle of a bin (double-linked list), the bins include unsorted bin, small bin, and large bin. The tcache/fastbin are singly-linked-lists they won’t apply the unlink mechanism . This exploit has a write-what-where primitive .

When a chunk is remove from a bin, the unlink() macro is called on the chunk. The unlink macro looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define unlink(AV, P, BK, FD) {                                            \
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  \
    FD = P->fd;                                    \
    BK = P->bk;                                    \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))           \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                                    \
        FD->bk = BK;                               \
        BK->fd = FD;                               \
        if (!in_smallbin_range (chunksize_nomask (P))   
    }              \
}

At first this macro was difficult for me to understand so here is another way to view it.

1
2
3
4
5
6
7
8
9
10
11
/*
Curr = current chunk (P)
Next = next chunk in doubly linked list (FD)
Prev = previous chunk in doubly linked list (BK) 
*/

Next = Curr->fd;
Prev = Curr->bk;

Next->bk = Prev;
Prev->fd = Next;

The macro is changing the pointers of the chunks before (BK/Prev) and after (FD/Next) the victim chunk (P/Curr). The below images show how the macro works in the unlink process.

The middle chunk is being unlinked.

Before Unlink

before_unlink

After Unlink

after_unlink

To clearly understand this technique we first have to understand heap consolidation.

Heap Consolidation.

When a chunk is freed, it is put into the unsorted bin. The chunk will first be merged with neighbouring free chunks, then added to the unsorted bin as a larger free chunk for future allocations. How it works:

  1. Consolidate backward If previous chunk in memory is not in use (prev_inuse bit == 0), unlink is called on the previous chunk to take it off the free list. The previous chunk’s size is then added to the current chunk’s size, and the current chunk pointer points to the previous chunk.
  2. Consolidate forward If next chunk in memory is not the top chunk and not in use (confirmed by next-to-next chunk's prev_inuse bit), unlink is called on the next chunk to take it off the free list. The next chunk’s size is added to the current chunk’s size.
  3. Add consolidated chunk to unsorted bin. The unlink macro is called.

In older glibc versions we can use this vulnerability to modify the fd and bk pointers of the P/Curr chunk to perform arbitrary read/write primitive after the macro executes.

Lets break down the unlink macro from a C language’s point of view.

  • 32-bit architecture
    1
    2
    3
    4
    
    BK = *(P + (0x4 * 3)); // Prev = *(Curr + (0x4 * 3));
    FD = *(P  + (0x4 * 2)); // Next = *(Curr + (0x4 *2));
    *(FD + (0x4 * 3)) = BK; // *(Next + (0x4 * 3)) = Prev
    *(BK + (0x4 * 2)) = FD; // *(Prev + (0x4 * 2)) = Next
    
  • 64-bit architecture
    1
    2
    3
    4
    
    BK = *(P + (0x8 * 3)); // Prev = *(Curr + (0x8 * 3));
    FD = *(P  + (0x8 * 2)); // Next = *(Curr + (0x8 *2));
    *(FD + (0x8 * 3)) = BK; // *(Next + (0x8 * 3)) = Prev
    *(BK + (0x8 * 2)) = FD; // *(Prev + (0x8 * 2)) = Next
    

If we had fully control of the chunk P/Curr (first chunk) , we can put any values to P->bk/Curr->bk and P->bk/Curr->bk (usually has to be global variable), and we can achieve arbitrary write.

If we can overflow the first chunk into the second chunk, then we are free to put any value to bk and fd of the second chunk.

We can achieve arbitrary write by:

  • change fd of fake chunk to point near global ptr so that P->fd->bk = P
  • change fd of fake chunk to point near to global pointer so that P->bk->fd = P
  • trigger ``unlink`
  • use first chunk to overwrite itself to point to an arbitrary location.

We need to overflow to the next chunk and unset the prev_inuse bit so that when we free the second chunk backward consolidation can happen and the unlink macro will be called.


Steps

  1. Allocate two adjacent chunk.
  2. Setup fd and bk of first chunk. (near global ptr)
  3. Overflow on first chunk to control the metadata of the second chunk.
    • Set the prev_size to 0
    • Unset the prev_inuse bit
    • Set the size. (larger the fastbins size)
  4. Trigger the unlink. (Free second chunk)
  5. Overwrite fd and bk of the fake chunk.
    • fd point to target address that we want to overwrite. (where)
    • bk points to address that we want to write. (what)
  6. Control flow will be redirected.
  7. Pwned.

Resources

This post is licensed under CC BY 4.0 by the author.