This article is intended for the people who already have some knowledge about heap exploitation.
If you already know some heap attacks on glibc<2.26 it’ll be fully understandable to you.
But if you don’t, don’t worry - I’ve tried to make this post approachable for everyone with just basic knowledge.
If you really know nothing about the topic, I recommend heap-exploitation.
Tcache is an internal mechanism responsible for heap management.
It was introduced in glibc 2.26 in the year 2017.
It’s objective is to speed up the heap management.
Older algorithms are not removed, but they are still used sometimes - for example for bigger chunks, or when an appropriate tcache bin is full.
But heap exploitation with this mechanism is a lot easier due to a lack of heap integrity checks.
The convention used in this post is that we call the pointer to the next chunk fd, and to the previous - bk as it is called originally in normal heap chunk.
Tcache overview
You can grab glibc 2.26 from here.
The all source code that is interesting for us is located in a file malloc/malloc.c.
In this version of glibc two new functions were created:
Both of these functions can be called at the beginning of functions _int_free and __libc_malloc.
tcache_put is called when the requested size of the allocated region is not greater than 0x408 and tcache bin that is appropriate for a given size is not full.
A maximum number of chunks in one tcache bin is mp_.tcache_count and this variable is set to 7 by default.
This variable is set here
and the root is at the following piece of code:
/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#endif
tcache_get is called when we request a chunk of the size of tcache bin and the appropriate bin contains some chunks.
Every tcache bin contains chunks of only one size.
From the code above we can see that it is a single linked list, similar to fastbin - it contains only a pointer to a next chunk.
Also, the list is LIFO, like in fastbins.
But there is a difference - each tcache bin remebers how many chunks belong to this bin in a variable tcache->counts[tc_idx].
What’s strange calloc doesn’t allocate from tcache bin.
a@x:~/Desktop/how2heap_mycp$ gdb -q ./mp
pwndbg: loaded 170 commands. Type pwndbg [filter] for a list.
pwndbg: created $rebase, $ida gdb functions (can be used with print/break)
Reading symbols from ./mp...(no debugging symbols found)...done.
pwndbg> r
Starting program: /home/a/Desktop/how2heap_mycp/mp
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
> malloc 0x50
==> 0x555555559670
> malloc 0x50
==> 0x5555555596d0
> malloc 0x61
==> 0x555555559730
> free 0x555555559670
==> ok
> free 0x5555555596d0
==> ok
> free 0x555555559730
==> ok
> ^C
Program received signal SIGINT, Interrupt.
[...]
pwndbg> bins
tcachebins
0x60 [ 2]: 0x5555555596d0 —▸ 0x555555559670 ◂— 0x0
0x70 [ 1]: 0x555555559730 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x0
smallbins
empty
largebins
empty
pwndbg>
Tcache attacks
Due to a lack of integrity checks in tcache, many attacks are easier.
double free
Let’s consider a double free vulnerability as a first example:
As a result, we got the same pointer 2 times.
On older glibc (<2.26) to get the same result this attack is a bit more complicated:
output:
hello
0x602420
0x602460
0x602420
We additionally need to free another chunk between due to this integrity check - we cannot add a new chunk to a fastbin list when there is already the same chunk on top.
printf is called at the beginning because program crashes otherwise.
Probably this is because when printf is called for the first time it initializes his buffer by mallocing some area.
House of Spirit
House of Spirit is also super easy:
output:
0x7fff899700c0 0x7fff899700c0
By freeing never allocated region we put it in the tcache bin list.
And we can obtain this region when malloc is called with appropriate size as an argument.
This is useful when we have the ability to overwrite some pointer by buffer overflow.
In older glibc we needed to put more effort due to this healthcheck.
We need to create another fake chunk after the fried one.
Like here.
tcache/fastbin poisoning
If we want to exploit malloc to return a pointer to a controlled location we can simply overwrite a pointer to a next chunk.
We can forget about this integrity check in older mechanism:
output:
aaaaaaaaaaaaaaa
We cannot do this by freeing only one chunk because each tcache bin remebers how many chunks belong to this bin.
libc leak
If we want to leak the libc address on glibc 2.26 we can do this:
This program prints fd of the chunk inside an unsorted bin.
fd of the last chunk and bk in the first chunk in an unsorted bin are set to a pointer in libc.
If we can request malloc of at most 0x100 size this won’t work because the fried chunk won’t go to an unsorted bin list but to a tcache bin.
It works only with older glibc:
Hopefully if we make tcache bin full (max capacity is 7 chunks), deallocated chunk will be put in unsorted bin:
tcache attacks summary
More attacks exist for glibc with tcache.
For example House of Force works in the same way as previously.
Also, it’s easy to make overlapping chunks by overwriting size to a bigger value.
After tcache was introduced heap exploitation is much easier.
The exception is a buffer overflow by a single NULL byte, like in children tcache CTF task.
I used an old attack with chunks of the smallbin size.
I prevented them from going into the tcache, by making the tcache bin full.
The version of libc is 2.27 but there is no difference between 2.26 and 2.27 for us:
a@x:~/Desktop/children_tcache$ strings libc.so.6 | grep LIBC
[...]
GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Decompiled binary looks like below:
TL;DR:
We can
create chunk on the heap and read data into it
delete a chunk
print data in a chunk
Everything is fine, except new_heap function which is vulnerable to buffer overflow by single NULL byte.
Before free, the area is filled with 0xDA byte.
We can have max 10 chunks allocated at the same time and maximum requested size of a chunk is 0x2000.
Normally, when we free chunk of the size of smallbin, there is a check whether its neighbour is freed.
If so, it will consolidate with it.
When we free c chunk it consolidates with a and b because of 2 reasons:
We have cleared the PREV_INUSE bit of chunk c so it thinks that its previous neighbour is freed.
We have set prev_size of chunk c to value 0x210 which is a total size of chunks a and b.
This attack can be shorter:
a: 0x602010
b: 0x602120
A: 0x602010
B content: 0x7ffff7dd1b78
In the end, we skipped allocation and deletion of B chunk because it is not needed.
After c is freed, we have one unsorted bin that contains the area that is a summary of a, b and c areas.
After we allocated chunk A, the unsorted bin split to 2 parts.
One part was returned by malloc, the other part remained at the unsorted bin and the chunk begins at the same place when b.
In our examples, the first allocated chunk has a different size than others which is 0x108.
The example would work with 0xf8 but in this challenge, strcpy is used so it breaks on NULL byte so
we couldn’t overwrite prev_size by 0x200 value.
With size equal to 0x108 we can overwrite prev_size to 0x210.
We can accomplish the same attack on a newer libc, by using the same algorithm.
But there is one difference - before freeing chunks we need to make tcache bin full.
So the attack below does the same leak as the attack previously but also it goes further.
After the leak, it causes double free because B and b point to the same chunk of size 0x1f8.
Later, this attack is performed.
And the last step is to implement an exploit in python.
It does the same thing as previous code, except that malloc returns to us a region at &__free_hook.
Then we overwrite __free_hook to one-gadget RCE. Later it calls free.