Exploiting Heap Overflows: Anatomy of a Modern Use-After-Free
"Heap corruption vulnerabilities continue to offer rich attack surfaces. This writeup examines the mechanics of a Use-After-Free (UAF) vulnerability, specifically targeting the tcache (Thread Local Cache) heap management structure introduced in modern glibc allocators."
Exploiting Heap Overflows: Anatomy of a Modern Use-After-Free
Memory safety vulnerabilities remain one of the most prominent attack vectors in native application exploitation. While stack-based exploits have become increasingly difficult due to compilers introducing compiler-enforced protections (like Stack Canaries and SafeStack), Heap corruption vulnerabilities continue to offer rich attack surfaces.
This writeup examines the mechanics of a Use-After-Free (UAF) vulnerability, specifically targeting the tcache (Thread Local Cache) heap management structure introduced in modern glibc allocators.
1. The Vulnerability Pattern
A Use-After-Free (UAF) occurs when a pointer is not cleared (set to NULL) after the memory it points to has been deallocated with free(). If the application subsequently references this "dangling pointer," it can lead to undefined behavior, system crashes, or arbitrary code execution.
Here is a simplified C vulnerability snippet demonstrating the flaw:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct User {
char name[16];
void (*print_privileges)();
};
void guest_privileges() {
printf("[*] Role: Guest User\n");
}
void admin_privileges() {
printf("[!] WARNING: Root Administrative Shell Spawned!\n");
system("/bin/sh");
}
int main() {
struct User *user = malloc(sizeof(struct User));
strcpy(user->name, "Operator");
user->print_privileges = guest_privileges;
printf("[*] Allocating operator... Address: %p\n", user);
user->print_privileges();
// Deallocate the memory block
free(user);
printf("[*] Freed operator block!\n");
// Danger: Pointer is NOT set to NULL (Dangling Pointer)
// Allocating a new string block which will re-use the same chunk
char *malicious_payload = malloc(sizeof(struct User));
printf("[*] New allocation re-using same chunk space: %p\n", malicious_payload);
// Overwriting print_privileges function pointer with admin_privileges address
long *overwrite_ptr = (long *)(malicious_payload + 16);
*overwrite_ptr = (long)admin_privileges;
// Use-After-Free: Accessing dangling pointer
printf("[!] Calling user->print_privileges() after reallocation...\n");
user->print_privileges();
return 0;
}2. Heap Bin Poisoning (tcache Exploitation)
In glibc (version 2.26 and later), the Thread Local Cache (tcache) manages singly-linked bins for small memory chunks. Because speed is favored, tcache chunks are stored in a singly-linked list (LIFO stack) without sanity validation in earlier versions.
When a chunk is freed, it is placed in the tcache list:
tcache_entry -> [Chunk A] -> [Chunk B] -> [NULL]The Attack Flow
- Freeing Chunks: Freeing two chunks of the same size.
- Double Free / Poisoning: If we corrupt the forward pointer (
next) of a freed chunk in memory to point to an arbitrary target address (e.g., the global offset table, or a stack address), the next allocator call tomalloc()returns our poisoned target address.
3. Debugging Heap State under GDB
Using GDB with the pwndbg or peda extension, we can observe the allocation bins:
4. Modern Mitigations
Modern heap implementations introduce protections to make direct exploitation significantly more difficult:
- Safe Linking: Introduced in glibc 2.32, pointers in the tcache and fastbins are obfuscated (XORed with a randomized base page address), preventing direct overwrite poisoning without a heap memory leak.
- tcache Double Free Protection: A validation check uses a
keyfield inside the chunk structure to verify whether a chunk is already present in the tcache array before pushing it.