The purpose of this video is twofold, we'll begin by learning about a tcache mechanism that I call
"tcache dumping", then we'll tackle our first tcache exploit mitigation.
Navigate to the tcache_dup_2.31 directory, where you'll see another demo binary.
Let's crack straight on and load it into GDB.
As usual, we'll set a breakpoint on the main() function, then run the program.
Once it's stopped, the 'vmmap libc' command shows us that this demo binary is using GLIBC version
2.31.
The last version we dealt with was 2.28.
The source panel shows that there's a little more going on in this demo binary than we're used to,
but it's nothing out of the ordinary.
These lines are simply requesting 14 0x20-sized chunks.
Rather than step through this loop, let's just set a breakpoint on the other side.
Continuing my streak of introducing useful GDB features that we don't need, you can set temporary break
points with 'tbreak' or just 'tb'.
Temporary breakpoints are deleted after the hit, and even though there's no real need here, I'll set
a temporary breakpoint on line 10, then continue.
Inspect only the first chunk of the heap with 'vis 1'.
This is, of course, the tcache and note its size, 0x290, larger than the 2.28 tcache
we saw in the previous video,
a side-effect of its double width count fields.
Now inspect 15 chunks with 'vis 15'.
The default is 10, and we've just allocated more than that.
Aside from the tcache, we now see the 14 chunks requested by the loop, nothing out of the ordinary so
far.
Use 'context code' to see what the program does next.
Another loop, this one simply frees the 14 chunks allocated in the previous loop in the same order
they were requested.
Execute this loop by setting a breakpoint, temporary or not, on line 13, then continue.
Once again, inspect the heap with 'vis 15'.
Here's where things get interesting, pwndbg's annotations show that half the chunks have been linked
into the tcache, whilst the other half have landed in the fastbins.
Since all these chunks are the same size, what is dictating which bin they land in?
Well, it turns out that the tcache has a limit on the number of chunks it can store in each of its bins.
The variable that controls this limit resides in the "mp_" struct and is labelled as "tcache_count".
Its default value is 7, and using the 'tcache' command
we can see that the 0x20 tcache count field has hit that limit.
Once a tcachebin is filled, freeing chunks of that size reverts to the pattern we're familiar with,
in this case all the chunks we're dealing with are 0x20-sized,
so once the 0x20 tcachebin was full, the remaining chunks were freed into the 0x20 fastbin.
If we'd been dealing with 0x90 chunks, once the 0x90 tcachebin was full, the remaining
chunks would have been consolidated and/or linked into the unsortedbin.
See the free flowchart in the PDF for a visual representation of this behavior.
If you're trying to exploit bins other than those of a tcache, you can either resume normal allocation
patterns by filling the target tcachebin or simply use chunk sizes that are beyond the tcache upper limit
of
0x410.
Interestingly, if you want to bypass the tcache altogether, the calloc() function doesn't allocate from
the tcache.
The malloc maintainers' reasoning behind this is that tcache chunks are "dirty", although what
makes them more "dirty" than any other chunk is never explained.
If you're operating locally, you can influence a handful of GLIBC's internal limits, including
the tcache_count using environment variables.
They're more of a curiosity since they're ignored in set user ID and set group ID binaries, but for
the sake of completeness, we'll take a quick look at them at the end of this video.
'context code' shows us that the last loop in this demo binary is going to request 7 0x20-sized
chunks.
We know that allocations from the tcache take precedence over all the other bins and that there are 7
chunks in the 0x20 tcachebin, so it shouldn't come as a surprise that executing this loop simply
empties the 0x20 tcachebin.
Confirm this by setting a breakpoint on line 16,
continuing, then issuing the 'bins' command.
The tcache is now empty and there are still 7 chunks in the 0x20 fastbin.
The last thing this demo binary will do is request one more 0x20-sized chunk.
This chunk will, of course, be allocated from the 0x20 fastbin since it's the only appropriate
occupied bin.
Make that request now with the 'next' command.
The state of affairs in which we're expecting this to leave the bins is that of 6 remaining 0x20-
sized chunks in the fastbins.
Issue the 'bins' command again to check if we're right.
It looks like there are indeed 6 remaining free chunks, but now they're all linked into the tcache.
This is the result of "tcache dumping", the term I mentioned at the start of this video.
It works like this:
When a chunk is allocated from the fastbins, malloc dumps any remaining chunks in that fastbin into
its corresponding tcachebin until full.
This behavior also applies to the smallbins. The unsortedbin does this too, although instead
of selecting a chunk to allocate, then dumping chunks into the tcache, it dumps exact-fitting chunks
right away, then either allocates the last exact fitting chunk it finds or grabs one from the recently
filled tcachebin.
The various malloc flowcharts in the PDF can help you understand this process, but for now we're only
concerned with tcache dumping from the fastbins.
So this demo binary has shown us that each tcachebin has a default limit of 7 chunks before normal
free() behavior resumes, and chunks can be dumped from the fastbins, smallbins and unsortedbin into
a tcache during malloc().
Now we're going to take a look at the tcache double-free mitigation introduced in GLIBC version
2.29
and one way of bypassing it.
Let's close GDB and familiarize ourselves with the tcache_dup_2.31 binary.
Checksec reveals that it's not dissimilar to the tcache_dup binary we recently tackled, with the
exception that it's linked against GLIBC version 2.31.
Load the binary into GDB and run it.
The menu is also similar to that of the tcache_dup binary, although we can request more chunks this
time.
The rules are similar too, tcache-sized chunks only, into which we can write up to 16 bytes,
and the double-free bug also makes an appearance.
So what's the big deal?
Surely we can just double-free a chunk like before?
Clearly not. GLIBC has aborted and the error message indicates that it has detected a tcache double-
free.
OK, perhaps the fastbins double-free mitigation was finally integrated into the tcache, let's run
the binary again and attempt to bypass this mitigation with the fastbin dup tactic.
Request 2 chunks, A & B.
Then free them in the order: A, B, A.
We're still triggering the double-free mitigation with the same error message.
It's time to find out how this check works.
Switch to the _int_free() frame and issue the 'context code' command.
There's the error message we've been seeing, but we're not getting the whole picture from this tiny
code snippet.
pwndbg does allow us to print more source lines with 'set context-source-code-lines',
but a more controlled, if less colorful way to view the pertinent lines of the check is to do the
following.
The check is initiated here.
The "e" variable is the chunk being freed and its "key" field is being compared to the address of the tcache.
So what is a chunk's "key" field?
Let's inspect the heap to find out.
Before the double-free mitigation aborted the process, we had freed chunk A, followed by chunk B.
This left chunk B at the head of the 0x20 tcachebin, evident here.
Chunk B's fd points to chunk A and chunk A's fd is null.
But what are these values?
You'd be forgiven for thinking they were backward pointers, but they both hold the address of the
tcache.
They are in fact these chunk's key fields.
In GLIBC 2.29 and later, when a chunk is freed into a tcache, the second quadword of its
user data is repurposed as a "key" field, into which the address of the tcache is written.
These "key" fields are used as a marker that the chunk is free.
Think of it as a fancy prev_inuse flag that applies to its own chunk rather than the previous one.
Every time a chunk is freed, its key field is checked. If it holds the address of the tcache, malloc
takes this as a hint that the chunk may already be free.
I use the term "hint" because a matching key field isn't a guarantee that the chunk is free.
It could be the unlucky result of random data.
Reissuing our 'list' command shows a comment beside the check that echoes this.
Vestigial key fields aren't an issue because they're zeroed on allocation.
If a chunk's key field does match the tcache address when it's freed, this triggers the main part of the
double-free mitigation.
A for loop iterates over every chunk in the target tcachebin, checking whether the chunk being freed
is already linked into that bin.
If it is malloc aborts and we see this familiar error message.
This check seems rather robust.
We can't just hide our duplicated chunk under another one like we do in our fastbin dup exploits,
because every chunk in the bin is checked, not just the first one.
If you take a moment to think about it, you can probably come up with a few ideas as to how we might
bypass this mitigation.
For example, using a write-after-free bug to corrupt a chunk's key field might work just fine.
In this case, we don't have a write-after-free bug however, meaning the key field will remain intact.
Our savior comes in the form of tcache
dumping. The double-free mitigation is only applied to chunks being freed, not those moving from other
bins into the tcache.
This can be taken advantage of in many different ways.
One example could be as follows:
Fill the 0x20 tcachebin, then double-free a chunk into the 0x20 fastbin using the fast
bin dup technique. Empty the 0x20 tcachebin, then request the duplicated chunk from the 0x20
fastbin.
This would dump the contents of the 0x20 fastbin into the tcache, including the duplicated chunk,
you would then be able to tamper with that chunk's tcache fd, linking a fake chunk into the tcache.
That approach seemed a bit simplistic, though, so I've tuned this binary to make it impossible.
If you try it, you'll find that you're 1 allocation short of being able to overwrite a malloc hook.
There is, however, a slightly more efficient way to bypass this mitigation, which we'll learn now.
Close GDB and make a copy of this technique's pwntools template.
Open the copy in your editor.
This pwntools script has the same helper functions as the last one we used.
Scroll to the bottom where you'll see some useful code already present, it requests 7 0x20-
sized chunks, then frees them into the tcache, filling the 0x20 tcachebin and resuming the normal
free pattern for 0x20-sized chunks.
Let's request another 0x20 chunk before the tcache is filled, I'll label mine "dup".
If we free this "dup" chunk after the 0x20 tcachebin is full, it will land in the fastbins.
We can see what this looks like by running our script with the GDB and NOASLR options,
breaking and inspecting the heap with 'vis'.
There are 8 free chunks, 7 of which are linked into the tcache with 1 in the fastbins.
Note that the fast chunk, the one labeled "dup" in my script, has no key field, unlike the tcache chunks.
Key fields are only added to chunks linked into the tcache, although the double-free check applies to
all freed chunks.
This means that if we were to double-free the "dup" chunk into the 0x20 tcachebin, having made
room for it, it would pass the tcache double-free check.
In fact, even if the main check were triggered due to random data in the key field, the "dup" chunk isn't
linked into the tcache yet, so it would still pass the check, making this a very safe approach.
Let's return to our editor to double-free that "dup" chunk.
First, we'll need to make room in the tcache for another 0x20-sized chunk.
We might as well empty the whole 0x20 tcachebin since we'll have to anyway before we can request the
fast chunk to trigger tcache dumping.
Use a for loop to request the 7 0x20-sized chunks in the tcache.
After that, we'll use the double-free bug to free our "dup" chunk for a second time.
This should leave it linked into both the tcache and the fastbins.
Run your script with the usual options so we can check if this worked.
Break and inspect the heap.
Our "dup" chunk does indeed reside in the tcache and in the fastbins simultaneously.
Our next request for a 0x20-sized chunk will allocate it from the tcache, allowing us to tamper with
fastbin metadata, but we wanted a tcache dup out of this.
A fastbin dup is subject to the size field integrity check,
and we want more freedom than that.
This is where tcache dumping comes in.
We're still going to request a 0x20 chunk and tamper with fastbin metadata, linking a fake chunk
overlapping the target data into the 0x20 fastbin.
We'll then request another 0x20 chunk, which will again be serviced by our "dup" chunk, although
this time from the fastbins instead of the tcache.
Before returning the "dup" chunk, malloc will dump any remaining chunks in the 0x20 fastbin into
the 0x20 tcachebin.
It does this simply by following 0x20 fastbin forward pointers until it comes across a null or
until the target tcachebin is full.
Because at this stage our fake chunk should be linked into the 0x20 fastbin, it will be moved
into the 0x20 tcachebin.
The only thing we have to be aware of is our fake chunk's forward pointer.
It's best if this is null to avoid the tcache dumping process dereferencing an unmapped address.
Fortunately, there are plenty of null quadwords near the target data.
Let's return to our editor and put the finishing touches on this exploit.
The next request for a 0x20-sized chunk will allocate the "dup" chunk from the tcache. We'll use this
to overwrite the "dup" chunk's fastbin fd with the address of a fake chunk overlapping the target data.
Because we're tampering with fastbin metadata, we have to subtract 16 from our target address to account
for the chunk header.
Remember that our fake chunk's fd must be null in this case, so we'll subtract a further 8 bytes,
ensuring that the fake fd doesn't overlap the target data.
This works because just like the fastbin, allocations from the tcache aren't subject to any alignment
checks.
Now that we've linked our fake chunk into the fastbins, we can request the "dup" chunk again.
During its allocation, malloc will dump the remaining chunks in the 0x20 fastbin into the
0x20 tcachebin. Since our fake chunk currently resides in that fastbin, it will be moved into the
tcache.
All that remains is to request our fake chunk from the tcache and use it to overwrite the target data.
Don't forget that we positioned the fake chunk such that its fd would be null, so we need to account
for that distance by writing 8 bytes of garbage data first.
Once you've added those final lines, run your script under production conditions.
You can hit return to print the menu, then pick option 3, which hopefully reveals that we
have overwritten the target data.
We've successfully bypassed the tcache double-free mitigation introduced in GLIBC 2.29 and forged
an arbitrary write primitive.
In summary, we leveraged tcache dumping to move a fake chunk from the fastbins into the tcache.
We began by filling the 0x20 tcachebin so that we could free a legitimate "dup" chunk into the
0x20 fastbin.
Once the "dup" chunk was in place, we emptied the 0x20 tcachebin, then used a double-free bug to
free the "dup" chunk a second time into the tcache.
Because the chunk was destined for the tcache, the fastbins double-free mitigation wasn't applied.
The tcache double-free mitigation was, but since the "dup" chunk had yet to be linked into the tcache,
its key field hadn't been populated, allowing it to pass the check.
This left our "dup" chunk linked into both the tcacheand the fastbins.
We requested it back from the tcache, using it to overwrite its own fastbin fd, linking a fake chunk
that overlapped the target data into the fastbins.
Then we requested the "dup" chunk again, this time it was allocated from the fastbins, triggering tcache
dumping.
The tcache dumping process moved our fake chunk from the fastbins, where it would have been subject to
the size field integrity check, into the tcache where no such check exists.
Finally, we were able to allocate our fake chunk from the tcache and use it to overwrite the target data.
If you want to refresh your memory on tcache internals, the HeapLAB PDF has you covered.
There's also a commented working exploit script available from this lecture's resources.
We've covered just one method of bypassing the tcache double-free mitigation, but as I mentioned earlier,
it can be done in different ways.
For example, by simply leveraging a fastbin dup first, which will cost you one more call to malloc()
but doesn't require your fake chunk's fd to be null.
Don't forget that tcache dumping also allows you to move chunks into the tcache from the smallbins and
unsortedbin, and that calloc() ignores the tcache entirely, opening up many more possibilities.
I've added a calloc() option to the malloc testbed for you to experiment with.
One last piece of malloc internals we didn't see in action is that GLIBC 2.30 introduced
a count field check that prevents allocation from a tcachebin with a count of zero, although we were
using GLIBC 2.31
and this still didn't affect our exploit.
I suppose we should wrap things up by dropping a shell from this binary.
There aren't any hidden snags, so I recommend pausing the video now and giving this one a try on your
own.
Once you've done that, return here and we'll drop a shell together, then I'll briefly explain the environment
variables that affect malloc, which I alluded to earlier.
No doubt you managed to get your shell without any serious trouble. Nonetheless,
let's quickly go through this together.
As usual
I'll edit my arbitrary write exploit in place,
you can do the same or make a copy.
The first thing we need to do is change the target of our arbitrary write.
Swap out the address in the binary for the address of the free hook, remembering to subtract 16 since
we're linking into the fastbins here, not the tcache.
We're going to need a chunk that holds our command string, so write your command into an appropriate
chunk, ensuring you label it so we can easily free it later.
On the line that originally overwrote the target data, switch out your win string for the address
of GLIBC's system() function.
Now, the free hook should hold the address of system(), so all that's left to do is free the chunk containing
our command string.
Having made those final changes, run your exploit under production conditions.
And you should get a shell.
Before we wrap things up, I promised I'd show you some environment variables that affect malloc.
A few of malloc's parameters have their own environment variables associated with them,
these can be found by opening the mallopt() man page and searching for the term "environment".
mallopt() is a function used to control some of malloc's parameters at runtime, and we'll cover it in
a later part of this course.
We can see from its manpage that, for example, setting the MALLOC_MMAP_THRESHOLD_ environment variable
would allow us to control the mmap threshold, perhaps useful in a House of Rabbit exploit.
I'll show you how it works; you can of course set your chosen environment variable in one of the
usual ways, per program or per session.
You could also set it from within GDB, which can help determine its effect.
Be aware that malloc parameters set via environment variables only take effect once malloc has been
initialized.
Another interesting environment variable is one called GLIBC_TUNABLES. These so-called "tunables" cover
various GLIBC parameters, including those of malloc.
Rather than each tunable having its own environment variable, they're passed to GLIBC as a colon-separated
list in the GLIBC_TUNABLES variable.
A quick way to look up which tunables are available is to dump the "tunable_list" array from within
GDB.
There is some overlap with the individual environment variables, the mmap threshold can also be set
via GLIBC_TUNABLES, but there are some unique tunables.
For example, entry 21 in the tunable_list is named "tcache_count", unsurprisingly, responsible for setting
the tcache_count value in malloc's mp_ struct.
You can use this tunable to set the tcache_count to any value you please, setting it to zero effectively
disables the tcache.
One interesting aspect of the GLIBC_TUNABLES environment variable is that its contents can be altered
by GLIBC. To allow this to work
the variable is copied into its own space.
This space is reserved by sbrk() at program initialization.
If I dump memory at the beginning of the heap mapping,
we can see the variable before the start of the heap.
This value here is the tcache chunk's size field.
GLIBC_TUNABLES get mapped here whether they're valid or not, so you can use this to influence the
default heap start address.
Unfortunately, while these environment variables appear useful from a local exploitation point of view,
they are ignored when there's a discrepancy between user or group ID and effective user or group ID,
for example, in a set user ID binary.
What's more, if a tunable has a security level of "ERASE", as to tcache_count, does it's considered too
unsafe to pass to child processes, and set user ID binaries will erase it from the environment altogether.
If you do find an exploitation vector that is foiled by this last mitigation, perhaps a set user ID
binary that sets its user ID to match its effective user ID before invoking a vulnerable child process,
then know that there is a bug in this implementation.
Including the same tunable twice in the GLIBC_TUNABLES environment variable is enough to get your chosen
tunable passed to a child process from a set user ID binary.
I'll probably have reported this bug by the time Part 2 is published, responsible disclosure and
all that.
So we've learned about the tcache double-free mitigation and how to bypass it.
We also had some fun with environment variables at the end there.
There's a commented working exploit that drops a shell from the tcache_dup_2.31 binary
available from this lecture's resources.
And don't forget that the HeapLAB PDF and the malloc testbed are here to help you experiment.
In the next video, we're taking on our final challenge binary. This one is, of course, tcache-themed.
Once you're comfortable with everything you've learned about the tcache.
I'll see you there.