Welcome to the final challenge of HeapLAB Part 2, I call this one "Tcache Troll".
The formula is the same as the other challenges; drop a shell from the challenge binary using heap exploitation
knowledge you've gained throughout the course.
I'll begin by introducing the target binary,
then you can pause the video and attempt to write a working exploit on your own.
If you get stuck at any stage, you can unpause the video and we'll work through the next phase together,
rinse and repeat until we get our shell.
So without further ado, let's navigate to the "challenge-tcache_troll" directory and take a look
at the challenge binary.
Checksec reveals that this binary has full RELRO, stack canaries, NX and position-independence
all enabled.
It's linked against GLIBC version 2.28, which means that tcache will be present, but we won't
have to deal with the tcache
double-free mitigation.
Load the challenge binary into GDB and run it so we can see how it works.
Our challenge binaries aren't in the habit of giving up heap or libc leaks easily
and this one is no different.
You'll need to leak any addresses you feel are necessary yourself.
Other than that, it's very similar to the tcache binaries we've dealt with already, option 1 allows
us to request up to 8 chunks, limited to tcache sizes, and fill them with data.
Option 2 frees chunks by index, starting at zero.
For the first time, we're limited in the number of free() operations we can perform, a total of 5.
This will certainly make things interesting.
With option 3 we can read the first quadword of chunk user data.
This is also done by index, starting at zero.
Something that may not be immediately obvious is that this program zeroes chunk user data immediately
after allocation.
Some of the challenge binaries we've conquered before did this via calloc(), but as you know, calloc()
doesn't allocate from the tcache.
So instead, this program emulates its behavior by requesting chunks via malloc(), then zeroing their
data with memset().
The result is the same, and its purpose is to simulate a programmer correctly initializing their variables.
That should be all you need to get started.
The first thing you'll want to do is find the bug in the challenge binary.
After that, focus on leaking any addresses your strategy calls for.
As always, there is a pwntools template script in the challenge directory with helper functions that
wrap each of the program's menu options.
Example uses of those helper functions are available at the bottom of the template script.
Go ahead and pause the video now, if you need a hint then return here and we'll find the bug, then
discuss initial strategy.
Good luck.
While I have no doubt that you were able to find the bug, it can't hurt to check every avenue just
in case.
I'll load my copy of the challenge binary into GDB and run it.
Let's begin by checking for an overflow bug, I'll request a minimum-sized chunk, then send more data
than its user data region can hold.
Breaking in GDB and inspecting the heap shows no sign of an overflow, so let's keep searching.
I'll continue,
free that same chunk,
then attempt to read from it.
The error message means there's no read-after-free bug either.
However, attempting to free that chunk a second time works.
Breaking in GDB and inspecting the heap again reveals that I've just linked the same chunk into the
tcache twice, indicative of a double-free bug.
This is good news, since we not only know exactly how to exploit double-free bugs that affect that
tcache, we're also dealing with GLIBC version 2.28, which has no tcache double-free mitigation.
Our only major hurdle is that the entire process is subject to ASLR,
we have to forge a leak somehow.
Because our best bet on getting code execution is to hijack the malloc hooks, our first goal should
be to leak a libc address, something we've done successfully against previous challenge binaries.
The issue here is that tcache metadata only consists of heap addresses, and since the program stores
nothing but our own data on the heap,
the only way we can coerce a libc address onto the heap is to free a chunk into the unsortedbin.
The quickest way to do that would be to free a chunk outside of the tcache size range.
Unfortunately, this binary only allows allocations within that range.
We know that full tcachebins are ignored, so we could fill the 0x90 tcachebin, for example,
then free a 0x90 chunk into the unsortedbin.
However, under default conditions we need to free 7 chunks to fill that tcachebin, and this binary
only allows us to free 5.
If you watched the last part of the previous video, you'll know that both of these limitations can
be overcome by using GLIBC_TUNABLES environment variable.
Indeed, you could probably make your exploit much more efficient this way.
But since it won't help you against setuid binaries, consider GLIBC_TUNABLES out of scope
for this challenge.
By all means, come back and experiment with it though once you've beaten the challenge the intended
way.
So the question remains, how are we going to free a chunk into the unsortedbin to get that libc
address onto the heap so we can leak it?
If we were somehow able to tamper with the tcache count fields, for example by tcache duping into
the tcache itself, we could convince malloc that specific tcachebins are full when they really aren't.
For this we'll need a heap leak.
Take that as your next goal and figure out how to leverage the double-free bug to leak a heap address.
Pause the video while you do that and unpause if you need help figuring out what to do next.
Let's leak a heap address! On your screen now is my attempt at doing just that, you can grab a copy
of my script from this lecture's resources,
it's labeled as template number 2.
I've begun by requesting, then immediately double-freeing a chunk which I've labeled "dup".
Its size doesn't matter,
for now we're constrained to the tcache size range, so the outcome is the same for any chunk size the program
allows.
After that, I request the double-freed chunk back from the tcache, giving it a different name: "leaker".
This leaves us with a single chunk that is both linked into the tcache as the "dup" chunk and allocated
to the program as the "leaker" chunk.
You may recognize this as a simple tcache dup setup.
If we'd leaked a useful address by this point, we could use the "leaker" chunk to tamper with the "dup"
chunk's tcachebin forward pointer and link a fake chunk into the tcache.
However, we're yet to leak a useful address, so we're using this setup to leak a tcachebin forward pointer
rather than tampering with one.
Unfortunately, by allocating the "leaker" chunk, we will have clobbered the existing "dup" chunk's
tcachebin fd, so we'll need to repopulate it.
The next line frees the "dup" chunk yet again, I believe that's a triple-free at this stage.
Because it's already linked into the tcache, malloc populates the "dup" chunk's fd with its own address,
specifically its user data address, since we're dealing with the tcache.
The "dup" chunk is still free and the "leaker" chunk is still allocated, but we've written a heap address
into the allocated chunk.
We can now leak the heap by reading from the "leaker" chunk, which is what this next line does.
First, it uses the read() helper function to read the first quadword of the "leaker" chunk's user data,
which currently holds a heap address.
pwntools unpack() function is then used to convert this little-endian value into an integer we can manipulate
in our script.
The leaked address is that of the dup/leaker chunk's user data.
So if we subtract 16, we have the address of its metadata.
Subtracting a further 0x250 bytes to account for the tcache chunk preceding it yields the heap
start address, which I've recorded in a variable named "heap".
The last line simply prints this variable for debugging purposes.
I'll run my script with the GDB option to check if this heap leak is accurate, I'm omitting the
NOASLR option to ensure my calculation isn't a fluke.
Breaking with ctrl+c and printing the default heap mapping with the 'vmmap' command
shows me the heap start address.
Switching to the program window with alt+` and comparing this to the output of my script confirms
that we've successfully leaked and calculated the heap's location.
These addresses will most likely differ from your own output because ASLR is in effect, but as long
as both of your values match, everything is on track.
Now we're ready to leak a libc address.
We've burned through 2 of our 8 allowed mallocs, and 3 of our 5 frees, but if we get creative
we can still get a shell out of this challenge binary.
Our heap leak has given us the ability to tcache dup anywhere on the heap, including into the tcache itself.
If we were to do that, we would control the count fields and the entries.
Tampering with count fields is key to leaking a libc address, remember that freeing a chunk into
the unsortedbin is a great way to write libc addresses onto the heap,
but we can't do that to legitimately with just 2 more calls to free().
Pause the video again and plan your next phase, unpause if you're having a tough time leaking a libc
address.
On your screen now is my attempt at leaking a libc address from the challenge binary, you can download
a copy of this script from the lecture's resources.
It's labeled as template number 3.
The heap leak phase we just covered is here, and I've added a second phase titled "Leak Unsortedbin Address".
Before we explore that, let me point out 2 changes I've made to the heap leak phase.
Firstly, I've changed the dup/leaker chunk's size to 0x90. This allows us to reuse it in the
second phase, keeping our total mallocs & frees to a minimum.
I've also allocated a guard chunk after it to avoid consolidation with the top chunk,
you'll see why in a moment.
That brings us to the "leak libc" phase, we enter this phase with just 2 chunks on the heap, the
dup/leaker chunk and the guard chunk after it.
Let's get our bearings by copying the "io.interactive()" line from the very end of the script to a point just
after phase one.
Having done that, run the script with the GDB and NOASLR options.
Break in GDB and inspect the heap with 'vis'.
The 0x90-sized chunk is dup/leaker, we can see its user data address in the 0x90 tcachebin slot and
its fd also points to that same address, the result of our double-free.
We can confirm all this by using pwndbg's 'tcachebins', command.
From the perspective of our exploit script, this chunk has been freed thrice under the name "dup" and
is currently allocated under the name "leaker".
It was the "leaker" chunk that was able to read from this tcachebin fd, providing us with a heap address.
The 0x20 chunk is just the guard chunk, it's there to stop the 0x90 chunk getting consolidated with
the top chunk when we free it into the unsortedbin later on.
Speaking of which, how are we going to do that?
I mentioned tampering with tcache count fields before the pause and now that we've leaked the heap, we
have an opportunity to do exactly that.
This setup can easily be turned into a tcache dup, all we need to do is request a 0x90-sized chunk.
That chunk will be allocated from the 0x90 tcache, while simultaneously remaining linked
into it.
We can then use it to tamper with a 0x90 tcachebin fd, linking a fake chunk overlapping the
tcache itself into the 0x90 tcachebin.
And that's exactly what my script does.
So let's close GDB and get back to it.
Remove the "io.interactive()" line we pasted earlier
and take a look at the first line of phase 2.
It's making a request for a 0x90-sized chunk, which, as I mentioned, will allocate the duplicated
chunk in the tcache, allowing us to tamper with a 0x90 tcachebin fd.
The same line overwrites that fd with the address of the heap plus 16, effectively linking the tcache
chunk into the tcache.
The next 2 lines capitalize on this tcache dup by requesting the fake chunk overlapping the
tcache.
This line requests the "dup" chunk, moving the fake chunk to the head of the 0x90 tcachebin. Then
the subsequent line requests the fake chunk and uses it to tamper with the tcache count fields.
Specifically, it's setting the 0x90 tcache count to 7.
The 0x90 count field is eighth from the start, hence the 7 preceding null bytes.
The result of this is that from malloc's perspective, the 0x90 tcachebin is now full, and 0x90-sized
chunks freed hereafter will be linked into the unsortedbin, just what we wanted.
The next line simply frees the "dup" chunk once more, it's 0x90-sized and therefore gets linked into
the unsortedbin.
Since it's the last and only chunk in the unsortedbin, its fd will point to the unsortedbin chunk
in the main arena.
Now, all we need to do is leak that unsortedbin fd via the allocated "leaker" chunk.
That's what this line is doing.
Once again, it uses pwndbg's unpack() function to convert the leaked value to an integer.
The distance between the unsortedbin "chunk" and the start of the main arena in this GLIBC build is
0x60 bytes.
We then subtract the offset of the main arena from this value, which yields the libc load address.
We populate the "libc" object's "address" field with this value, which adjusts all future symbol lookups.
The last line prints the libc load address for debug purposes.
I'll run my template script with just the GDB option to ensure this new leak is giving us accurate
results.
Switching to the program window shows both our heap and libc leaks.
I'll copy the latter and switch back to GDB.
Breaking with ctrl+c and passing the 'vmmap' command an address will show it's containing mapping,
similar to the 'xinfo' command.
The output confirms that our calculations were correct.
The address printed by our script matches the start of the libc binary's executable segment.
We're making good progress.
We've leaked both the heap and libc, and combined with the double-free bug we have all the tools
we need to hijack the flow of execution.
We'll have to be efficient though, that libc leak cost us 3 more mallocs and another free, leaving
us with just 2 mallocs and 1 free to finish the job.
Once you're comfortable with how we forged our heap and libc leaks, consider what you're going to
target to get code execution.
And if you run out of mallocs or frees, think about how you could make later parts of your exploit
more efficient by modifying the earlier parts.
Pause the video again and see if you can wrap things up on your own. Return here if you've hit a brick
wall.
We're still looking at template 3 and I've added a title that explains what my next intention is.
I've decided to target the free hook to gain code execution, this binary won't let us make arbitrarily
large requests, making it impossible to pass a valid address to malloc as its size argument.
We could of course choose the one-gadget route, but as it's less portable, relying on a specific
GLIBC build rather than version, I prefer attempting the system() route first.
Before the pause, we had just 2 mallocs and 1 free remaining to beat this challenge.
We'll need that last free to trigger the hook,
so in reality we have just 2 mallocs to overwrite the free hook with the address of system().
This rules out simply performing another tcache dup, we have to work with what we've got.
The optimum path would be to tamper with tcache entries directly.
That way we spend one malloc on corrupting a tcache entry, then our second malloc requesting a fake
chunk and corrupting the free hook.
Unfortunately, this script leaves us with just 1 free chunk, the 0x90-sized one in the unsorted
bin, which isn't of much use to us.
However, if by this point we had seeded the tcache with a chunk overlapping the tcache, we could now
request that chunk and tamper with tcache entries.
Hopefully you remember what this line is doing,
it requests a chunk overlapping the tcache and uses it to modify the 0x90 count field.
Let's give this line a second task, seeding the 0x90 tcachebin with another fake chunk overlapping
the tcache.
This is what I meant about changing earlier parts of your script to make later parts more efficient.
We'll start by using pwntools p8() function to cover the remaining count fields, which total 64.
If we were dealing with GLIBC 2.30 or later, we'd need to use a p32() here to account for
the wider count fields.
We'd also need to set the 0x90 count field to 1 or above to allow allocations from the 0x90
tcache.
We can then use pack() to cover the 0x20 through 0x80 tcache entries, at which point we reach the
0x90 tcachebin slot.
Link a fake chunk overlapping the tcache into this slot, I'm using heap plus 0x50
so the first quadword of my fake chunk's user data overlaps the first tcache entry field.
This is purely to save on typing later since we're done manipulating the tcache count fields.
Running the script now with the GDB and NOASLR options,
breaking and inspecting the bins
reveals that despite not having made any further calls to malloc() or free(), we now have a chunk overlapping
the tcache linked into the 0x90 tcachebin.
We can burn 1 malloc() to request this chunk and use it to seed another tcachebin with a fake chunk
overlapping the free hook, something we couldn't do before because we hadn't leaked libc yet.
Returning to our exploit script, let's make that request.
This request will be serviced from the 0x90 tcachebin, the chunk it allocates will overlap the tcache
entry fields, the first of which is the 0x20 tcachebin.
This is our opportunity to link a fake chunk overlapping the free hook into the tcache.
We might as well use the 0x20 tcachebin since it's the most convenient.
Having done that, the next request for a 0x20-sized chunk will allocate the fake chunk we just linked
into the tcache, let's request it and overwrite the free hook with the address of GLIBC's system() function.
All that remains is to free a chunk that holds our command string to pass to system(), you can pick any
chunk for this.
I'll use the one that was preventing top chunk consolidation during our leak phase.
Label it so we can easily pass it to the free() helper function and write your command string into
its user data.
Lastly, add a line at the end of your exploit that frees your command string chunk.
We've exhausted all 8 mallocs and all 5 frees by this point, so hopefully this is enough to
work.
Run your exploit under production conditions
and you should get a shell.
Congratulations! You've beaten the "Tcache Troll" challenge binary.
In summary, we leveraged a double-free bug against a modern binary to leak its heap and libc addresses
then drop a shell.
We began by double-freeing a chunk into the tcache, then allocating one copy of that chunk back to ourselves.
From that moment on, the allocated copy of that chunk, designated "leaker", would allow us to read any in-
line malloc metadata we could write into it.
We leaked a heap address by freeing the already-free copy of our chunk, writing a heap address into
the "leaker" chunk's user data.
We used this heap leak in conjunction with our overlapping chunks to link a fake chunk into the tcache
that overlapped the tcache itself.
Allocating this chunk allowed us to tamper with tcache metadata.
We set the 0x90 count field to 7, this caused malloc to treat the 0x90 tcachebin
as full, and when we freed a 0x90 chunk, it was linked into the unsortedbin rather than
the tcache, writing a libc address into our "leaker" chunk in the process.
Having exfiltrated a libc address, we were poised to hijack the flow of execution. To do this, we
simply overwrote the free hook with the address of GLIBC's system() function,
then a freed a chunk containing our command string.
That command was passed to system(), resulting in a shell.
And that's it for the Tcache Troll challenge binary, if you were able to beat it on your own or even
just make good progress, then you have every right to be proud of yourself.
This challenge binary was compiled with full RELRO, NX, stack canaries and was position-independent,
it was also linked against GLIBC version 2.28 and had just one bug, a double-free.
You were able to leverage that bug to drop a shell, despite being limited to just 8 mallocs, 5
frees and zeroed user data on every allocation.
If you want a working exploit script with comments, you can download the solution from this lecture's
resources.
Don't forget that revisiting these videos to consolidate what you've learned (pun intended) is highly
recommended.
And the HeapLAB PDF, along with the malloc testbed, are here to help.
That's everything in terms of learning material and challenges, so join me in the final video where
we'll part ways.