In this section we're learning about a GLIBC malloc feature known as the tcache, introduced in
GLIBC 2.26 in 2017. Once we've covered the basics
I'll explain my reasons for introducing this concept so late in the course.
But for now, let's dive into how the tcache works and what it is.
Tcache is shorthand for thread-cache.
It's an idea borrowed from Jason Evans malloc, "jemalloc", and used to be known as "magazines", a term
dating back to the original Solaris operating system.
GLIBC thread caches exist to help alleviate resource contention between different threads.
GLIBC malloc does its best to avoid attaching multiple threads to the same arena by creating a new
arena and heap for each thread that calls malloc().
There is, however, a limit on the number of concurrent arenas dictated by how many cores are available
to a process.
When this limit is reached, new threads must share their arena with other threads.
This can cause problems, the malloc() function is non-re-entrant and therefore uses locks to regulate
arena access.
When a thread needs to request memory, it may have to wait for one or more other threads sharing the
same arena to complete their own requests.
The tcache was introduced to combat the slowdown caused by this scenario.
In GLIBC 2.26 and later, each thread gets its own tcache, which can be thought of as a miniature
arena, to which that thread has exclusive access.
When a thread frees a chunk, rather than linking it into that thread's arena, malloc links it into that
thread's tcache.
Now when a thread makes a request to malloc, it may be served a chunk from its own tcache without
having to wait for any other threads to complete their allocations from their shared arena.
As usual when learning new malloc internals, we have a demo binary that's going to show us this
concept in action.
Navigate to the tcache_dup directory and load the demo binary into GDB.
Set a breakpoint on the main() function and issue the 'run' command.
Before we continue, type 'vmmap libc'
and note the GLIBC version, it's 2.28 which means that the tcache should be enabled by default.
So far, when we've dealt with GLIBC versions later than 2.25, we've been using libc binaries compiled
without the tcache and you may recall seeing a "no_tcache" extension in some of the library paths.
I drew attention to this early on in Part 1 of the course, but it probably didn't mean anything if
you weren't already aware of what the tcache was.
I understand how this may seem misleading, these later versions of GLIBC haven't been behaving exactly
how they would in production, and you should know that I did not take this decision lightly.
My primary reason for omitting the tcache so far is that the techniques we've learned originally targeted
GLIBC versions prior to the introduction of the tcache and therefore didn't factor it in.
But they can still be leveraged against later versions with it present.
We'll soon learn that tcache behavior can be manipulated and allocation patterns forced to return
to those we're familiar with.
What's more, we'll see that that the tcache undoes much of the efficacy of malloc's exploit mitigations, often
making exploitation of targets that do use the tcache considerably easier than those that don't.
Rest assured that all the techniques we've learned so far can be leveraged against the same versions
of GLIBC we wrote our exploits for with the tcache enabled.
In some cases, we may have had to jump through a couple more hoops, but often it would have actually
made things easier.
I think that introducing the tcache late like this allowed us to learn those techniques without undue
complexity whilst also learning how to deal with exploit mitigations present in later versions of GLIBC.
I hope you're satisfied with my reasoning, in the meantime on with the show.
The source panel shows us that the first thing this program does is request 2 minimum-sized chunks,
A & B. We know what that looks like
so execute those lines with 'next 2'.
Now inspect the default heap with 'vis'.
Something odd has happened, chunks A & B are present, but they're preceded by a 0x250-sized
chunk.
This is the main thread's tcache.
Tcaches are indeed allocated on the heap, inside a regular chunk like any other that can be freed,
consolidated, etc..
And yes, inline arenas are as dangerous as they sound, we'll be sure to take advantage of this in
our next challenge binary.
When debugging single-threaded programs, we typically see the main thread's tcache at the start of
the heap like this, but if we ended up sharing an arena with other threads, their tcaches could end
up being allocated anywhere on the heap.
The 'context', shortened to 'ctx', 'code' command reminds us that the next thing this program will do is free chunks
A & B.
From what we've learned so far, we'd expect both these chunks to end up in the fastbins, so let's
see if that holds true now that the tcache is in play.
Use 'next' to free chunk A then 'vis' to inspect the heap again.
Chunk A is now labeled as being linked into the tcachebins and 2 changes have occurred in the tcache
itself.
This value has incremented and this quadword now holds the address of Chunk A's user data.
I'll explain what we're seeing.
A thread cache is a simple struct, consisting of 2 regions; "counts" and "entries", of which there are
64 of each.
Entries can be thought of as the bins in an arena.
Indeed, they behave much like a regular arena's fastbins.
Each of these "entries", or bins, is the head of a singlly-linked, non-circular, last-in, first-out list of
free chunks.
Those lists hold chunks of a specific size, from 0x20 all the way through 0x410.
In fact, the only difference between these so-called tcachebins and the fastbins, aside from the
maximum size they hold, is that tcachebins reference chunks by the address of their user data rather
than their metadata.
The "counts" region consists of a running total of free chunks in each tcachebin.
The first count field indicates how many chunks are in the 0x20 tcachebin, the second count field
corresponds to the 0x30 tcachebin, etc., with the last count field corresponding to the
0x410 bin.
Count fields are a single byte each in GLIBC versions 2.26 through 2.29
and 2 bytes long in versions 2.30
and later.
If you see a tcache inside a 0x290 chunk instead of a 0x250, it has the wider count
fields and therefore GLIBC 2.30 or above is in use.
The 'context code' command reminds us that chunk B will be freed next, so let's make that happen,
then inspect the heap again.
pwndbg has annotated chunk B as being linked into the 0x20 tcachebin, and we can see some more
changes on the heap.
The 0x20 tcachebin slot now holds the address of chunk B's user data, meaning chunk B is now at the
head of the 0x20 tcachebin.
The 0x20 count field now reads 2, since there are 2 chunks in the 0x20 tcachebin.
The first quadword of chunk B's user data, has been repurposed as a forward pointer which points to
chunk A's user data.
Chunk A's fd is null, indicating the end of the list.
We can see an overview of the current thread's tcache with pwndbg's 'tcache' command.
And we can more thoroughly inspect the contents of its bins with the 'tcachebins' command, which shows
the bin size and number of chunks in any occupied bins, followed by the addresses of chunks contained
within.
As I mentioned, tcachebins are last-in, first-out.
'context code' shows us that 2 requests will now be made for 0x20-sized chunks, C & D, so go
ahead and make them with 'next 2'.
Inspect the heap, then print their return values with 'print c' and 'print d'.
These addresses show the program was allocated chunks A & B in the reverse order they were freed,
confirming the tcachebins last-in, first-out behavior.
Finally we'll confirm the maximum chunk size eligible for tcache insertion, 'context code' shows this program
will now request, then free, a 0x410-sized chunk, use 'next 2' to do this.
When we inspect the heap this time, use 'vis 3' to avoid printing the large chunk we just allocated.
We can see the last tcache entry here, which now holds the address of that 0x410-sized chunk,
confirmed with the 'tcachebins' command.
The 0x410 count field has also been incremented.
One last invocation of 'context code' shows the last thing this demo binary does is request, then free
a 0x420-sized chunk, allocating a guard chunk in between to avoid top chunk consolidation.
Use 'next 3' to execute these last 3 lines.
The 'tcachebins' command shows that the tcache remains undisturbed, whereas the 'unsortedbin' command reveals
that the 0x420-sized chunk wound up in the unsortedbin, which is expected behavior for chunks
that size.
We still have a little more to learn about the tcache, but for now, all we need to remember are the
following points:
Tcaches are basically miniature arenas that are allocated on the heap itself. They hold chunks
of sizes between 0x20 and 0x410 inclusive and behave almost exactly like the fastbins, with
the exception that they use pointers to chunk user data rather than metadata.
One last thing that may not have been clear from this demo binary is that a thread's tcache takes precedence
over its regular arena.
For example, freeing a 0x50-sized chunk will land it in the tcache rather than the 0x50 fastbin.
And by the same token, requesting an 0x80-sized chunk when there are 0x80-sized chunks in the tcache,
fastbins, smallbins and unsortedbin will result in the chunk from the tcache being allocated.
This makes sense because the tcache was implemented for the purpose of speed.
In fact, the tcache allocation code resides in the __libc_malloc() function and can return a chunk without
even entering the _int_malloc() function, which does all of the malloc heavy lifting.
Of course, as we know, speed comes at a price and we're about to learn how steep that price was.
It's time we moved on from our demo binary to this technique's pwnable.
The technique in question is known as the "Tcache Dup" due to its similarity to the fastbin dup.
Its origin is unclear, although even if I could, I'd be loathe to assign credit for this to any one
person, as it's such a simple technique that it's likely many hackers familiar with GLIBC malloc
developed it independently.
We'll start, as we usually do, by running checksec on our target binary.
The output reveals that we're dealing with the usual combination of full RELRO, stack canaries, NX
and a non-position-independent binary.
It's linked against a GLIBC version 2.28.
The lack of "no_tcache" extension signifies that I've compiled it normally and therefore we should expect
the tcache to be available.
Load the tcache_dup binary into GDB and run it.
We're greeted by a libc leark and the familiar numbered menu, option 1 lets us request up to 4
chunks.
They must be in tcache range, which we know is anything up to and including 0x410-sized chunks.
On successful allocation, we're prompted for some data to write into that chunk,
the program accepts up to 8 bytes.
Option 2 allows us to free our allocated chunks by index, starting at zero.
Lastly, option 3 prints the target data, which will become the focus of the arbitrary write
we'll craft once we've learned the tcache dup technique.
Break in GDB and inspect the heap with 'vis' to check that everything is as we expect.
There's the chunk we requested, then freed, linked into the main thread's 0x20 tcachebin, confirmed
with the 'tcachebins' command.
The target data resides at the "target" symbol in this binary's data section.
In a moment, you'll see the pause screen, take this opportunity to find the bug in this binary and perhaps
gain some familiarity with freeing and requesting chunks from the tcache as well.
If you're feeling particularly confident, you could take a crack at exploiting this binary before I've
even explained anything about the technique! I know that's unprecedented
but as I've already alluded to, the introduction of thread caches into GLIBC malloc involved some
pretty big security concessions.
So find the bug
and if you want, try crafting an arbitrary write primitive as well.
Whatever you choose, return here once you're done and we'll take a look at the tcache dup technique
together.
Hopefully you found the bug and perhaps even leveraged it to craft an arbitrary write primitive.
If not, don't worry, we're going to start from scratch and locate the bug now.
I'll load the tcache_dup binary into GDB and run it.
At the menu, I'll request a minimum-sized chunk and check for an overflow bug by attempting to write
more than the allotted 8 bytes into its user data region.
Breaking with ctrl+c and inspecting the heap with 'vis' shows just 8 bytes of my input made it into
the requested chunk's user data, so no overflow bug.
I'll continue in GDB, hit return to print the menu and this time attempt a double-free by freeing
the chunk at Index zero twice in a row.
Usually we'd see either a program error message along the lines of "this chunk was already freed" or a
GLIBC abort message as our double-free triggered an exploit mitigation.
I promise I didn't just forget to program an error message into this binary,
so let's break and inspect the heap to see if anything out of the ordinary has happened.
pwndbg has annotated the 0x20 chunk I freed as being linked into the tcachebins twice.
Indeed, we can see the 0x20 tcachebin count field reads 2, and the 0x20 tcachebin itself
holds the address of the freed chunk.
The first quadword of that freed chunk has been repurposed as a forward pointer and points to its own user
data.
We've seen this, or something similar, when we were learning the fastbin dup technique in Part 1 of
the course.
The fastbin dup turned a singly-linked, non-circular list into a circular one, exactly what we've achieved
here.
So there is a double-free bug in this binary, and we've just used it to free the same chunk into the
tcache twice.
But why didn't GLIBC abort? Surely if the tcache works like the fastbins, it would have inherited
its double-free mitigation code.
This is the price paid for speed. In GLIBC versions with the cash enabled prior to 2.29,
there are no tcache double-free checks.
That means we can even continue to free the same chunk repeatedly, like this,
without consequence.
This results in a sort of short-circuited fastbin dup where we can ignore the steps in which we use
a second chunk to help us bypass the double-free mitigation and skip straight to overwriting inline
malloc metadata.
Since we're already familiar with this concept, I'll close GDB, then we'll make a copy of this technique's
pwntools template script.
Open your template copy in your editor of choice.
This script has just 2 helper functions; malloc(), which takes "size" and "data" arguments and wraps
option 1 from the program menu and free(), which simply takes an "index" argument and wraps option
2.
Scrolling to the bottom of the script shows the usual example uses of those helper functions.
And right now all it does is request a minimum-sized chunk, then free it.
I'm going to remove the example header and comments to tidy things up.
Let's begin by replicating what I just did in GDB, leveraging the double-free bug to free chunk A a second
time.
This leaves chunk A linked into the 0x20 tcachebin twice.
The next request for a 0x20-sized chunk will be serviced from the 0x20 tcachebin, yielding
chunk A.
Chunk A will then be both allocated to the program and linked into the 0x20 tcachebin.
We can take advantage of this situation in exactly the same way we took advantage of it in the fast
bin dup technique by overwriting inline malloc metadata, specifically chunk A's forward pointer,
linking a fake chunk into the tcache.
Remember that the tcache references chunks by their user data instead of their metadata,
this means when we overwrite chunk A's fd, we can point it directly at our target data rather than
16 bytes before it.
Once you've added those 2 lines to your script, run it with GDB and NOASLR.
Break in GDB and inspect the heap with 'vis'.
We can see chunk A, still annotated as being linked into the tcache, with its user data address in the
0x20 tcachebin slot.
Note that the 0x20 tcachebin count field reads 1, indicating just a single chunk is present
in that bin.
Chunk A is also allocated to us and our metadata overwrite appears to have been successful, the 'tcachebins'
command confirms that chunk A's forward pointer now holds the address of our arbitrary write target
in the program's data section,
those X's.
We're just a couple of steps away from overwriting that target data, all we need to do is request 2
minimum-sized chunk, both of which will be allocated from the 0x20 tcachebin.
Chunk A will be first, followed by our fake chunk overlapping the target. The data we send here should
overwrite the target.
Having done so, we can check whether we were successful with option 3.
Clearly we were. We just leveraged the tcache dup technique to craft an arbitrary write primitive, and
yes, it really is that simple.
Let's switch back to the GDB window to address some unanswered questions.
I imagine you're wondering what happened to the size field check. In our fastbin dup exploits, we've
had to craft or find a viable fake size field to bypass the fastbins size field integrity check.
The tcache however, possesses no such mitigation.
Dumping memory near the target
reveals the size of our fake chunk is zero, about as invalid as you can get in terms of chunk size
fields.
Hopefully it's becoming clear what I meant when I told you that the introduction of the tcache had a
negative security impact on the GLIBC heap implementation.
How about the tcache count fields? Before we took over manually, the 0x20 tcachebin count was 1,
signaling that there was only a single chunk in that bin.
But we requested 2 chunks from that bin, chunk A and our fake chunk. In GLIBC versions prior to
2.30, malloc doesn't use count fields to determine whether a tcachebin is occupied, and if we inspect
the heap with 'vis',
we can see that the 0x20 tcachebin count field has simply wrapped around to its maximum value.
Let's return to our editor and put the finishing touches on this exploit.
All that remains is to make the same 2 requests we made manually a moment ago, a request for a minimum-
sized chunk, which returns chunk A, and another for the same sized chunk which returns our fake chunk,
if it can be called that, since we didn't even prepare any fake malloc metadata.
Once you've added those requests, run your exploit under production conditions.
You can hit return to show the menu, then select option 3 to print the target data, which should
now be under our control.
And that's all there is to the tcache dup technique!
In summary, our target binary is linked against GLIBC version 2.28, which has the tcache enabled
by default.
It has a double-free bug, which we leveraged to free a tcache-eligible chunk into the same tcachebin
twice.
Prior to GLIBC 2.29, the tcache has no double-free mitigations, so we just freed the same chunk
twice in a row to achieve this.
We then requested that same chunk back from the tcache, leaving it both free and allocated simultaneously.
In similar fashion to how we've capitalized upon fastbin dup scenarios, we overwrote that duplicated
chunk's tcachebin fd with the address of a fake chunk.
Next, we requested the duplicate chunk again, leaving only our fake chunk linked into the tcache.
Finally, we were able to request our fake chunk from the tcache and use it to overwrite the target data.
As you can see, the tcache dup technique is very simple and extremely powerful; no double-free mitigation
and no size field integrity check make it easy to leverage and provide no restrictions on potential
targets.
If you want to recap anything we've learned about the tcache so far, don't hesitate to replay parts
of this video or check out the "tcache" section of the HeapLAB PDF.
There's also a commented solution script available from this lecture's resources.
Before we dive even deeper into tcache internals, what would a new technique be without dropping a shell
with it?
I get the feeling you're not going to struggle too much with this one, so I'm just going to leave you
to it.
Pause the video now and give it a try.
I doubt that took you very long, but just in case you hit any snags, let's drop a shell with the tcache
dup technique together.
As usual, we'll build on our arbitrary write primitive, which you can make a copy of or edit in place
like I'm doing.
First, we'll need to select a new arbitrary write target. This binary limits us to 4 allocations, which
we've already reached by this point,
so the free hook seems like a sensible choice.
Substitute the "target" symbol for the free hook on the line where we overwrite the 0x20 tcachebin
forward pointer.
Remember that the tcache uses pointers to user data so there's no need to adjust this value.
This should have the effect of linking a fake chunk overlapping the free hook into the tcache.
Next, swap out the win message on the line where we overwrite the target data with the address of GLIBC's
system() function.
Because our fake chunk overlaps the free hook, this should overwrite the hook with system()'s address.
As we know, using the free hook in our exploit requires us to write a command string into a chunk,
we can use any spare chunk for this.
Lastly we'll add a line that frees the chunk containing our command string.
Because we've overwritten the free hook with the address of system(), this becomes the equivalent of
passing a pointer to our command, in this case "/bin/sh", to the system() function.
Once everything is in order, we can run our exploit under production conditions and we should be presented
with a shell.
Not a bad result for just 4 allocations.
The summary of what we just did is brief;
building on the arbitrary write primitive we forged, we simply targeted the free hook and replaced it
with the address of system(). There was no need to find or create fake chunk metadata because there was
no size field integrity check to deal with.
I mentioned earlier that presence of the tcache can make exploitation easier and it's worth considering
how some of the binaries we've exploited so far could be affected if they were linked against GLIBC
versions with the tcache enabled.
Take the House of Rabbit binary, for example. If it were linked against GLIBC 2.28, as
this binary was, that limit of 4 fast-sized chunks would be enough to perform a tcache dup
drop a shell, as we've done here.
In fact, even the House of Rabbit binary that didn't allow any fast-sized requests would be vulnerable
to the same trivial attack because the tcache size range extends far beyond that of the fastbins.
This is what, in my opinion at least, makes heap exploitation so interesting, a bug in a program
may be vulnerable in different ways, depending on which GLIBC version its host operating system is
using.
In the next video, we're going to learn some more tcache internals and take on a tcache mitigation that
was introduced in later versions of GLIBC.
Before we do, there's a commented solution script for this binary available from this lecture's resources.
And don't forget about the malloc testbed if you want to experiment some more with the tcache.
I'll see you in the next video.