In this video we'll take on another vulnerable binary to
learn our second heap exploitation technique, the fastbin dup.
I like to give credit where credit's due, but its origins are unclear.
It's a more recent technique than the House of Force, and I've seen it documented as a "fastbin attack"
as early as 2014
but it's probably been around for longer.
Either way, let's explore the vulnerable binary for this technique.
From the fastbin_dup directory, load the fastbin_dup binary into GDB.
It's another CTF-style binary written in C, this time with a different class of heap bug.
We can run checksec from within GDB, which shows us that full RELRO, stack canaries and NX are
all enabled.
The binary isn't position-independent and is linked against GLIBC
version 2.30
compiled without the tcache.
Run the program without setting any breakpoints, a libc address is leaked, but no heap address
this time. We're being prompted for a username,
for now I'll just enter some garbage data.
Now we're presented with a familiar numbered menu.
Option 1 allows us to request up to seven chunks, just like before we're prompted for a request size
but we're limited to fastbin sizes only; 0x20 through 0x80.
An 0x80-sized chunk holds 120 bytes of user data,
so that's the largest request we can make.
I'm requesting a 0x30-sized chunk.
One way to request a chunk of specific size is to take the desired chunk size,
in this case 0x30,
and subtract 8 for 0x28, which is the amount of user data a chunk of that size holds.
Then we're asked for any data we want to write into that chunk,
I'll send some more garbage data.
Let's break in GDB and inspect the heap with 'vis' to check everything is as expected.
There's our 0x30-sized chunk with whatever garbage data we decided to write into its user data
followed by the top chunk.
Continue and hit return to print the menu again.
Option 2 allows us to free chunks by index starting at zero,
so let's free our 0x30 chunk at index zero.
Break and inspect both the heap and fastbins.
The 0x30-sized chunk has been linked into the 0x30 fastbin and the first quadword
of its user data has been repurposed as a forward pointer, which is null because it's the first chunk
to be linked into this bin.
If we continue and hit return, we can see that option 3 prints the target. Once again
our first objective is to overwrite the target data, proving that we can leverage the fastbin dup technique
for an arbitrary write.
This time, the target is part of a struct named "user", which also holds the username we gave earlier.
Let's confirm this by breaking again and printing the 'user' struct.
If we want more information on a struct, we can use GDB's 'ptype' command to inspect it, which shows
us that the 'user' struct holds sixteen bytes of our user name, followed by the target.
If you want to visualize this layout in memory use 'dq', not forgetting the ampersand.
Here's our username and here's the target data.
In a moment, I want you to pause this video, familiarize yourself with the fastbin_dup binary
and see if you can find the bug.
Once again, there's no need to go digging through assembly,
just have a play with the program's menu and see if you can get it to crash or exhibit any strange behavior.
Go ahead and pause now.
If you found the bug,
great work,
if not, I can tell you that this binary has a so-called double-free bug.
A double-free is when a program can free a chunk that's already free, which sounds benign, but let's
think about what effect that might have. If we freed our 0x30 chunk twice
it would get linked into the 0x30 fastbin twice, meaning that it could be allocated for two
different things simultaneously.
For example, it might be allocated to hold some sensitive data like an admin password hash, then allocated
again from the same fastbin to hold some user-controlled data which could lead to the user being able
to read or overwrite that sensitive data.
So let's try to double-free our 0x30 chunk.
Get your program back to the same state as mine by running it under GDB, setting any username, requesting
a 0x30-sized chunk and freeing it.
I'll return to the program with 'continue',
then we'll both select the 'free' option.
Our 0x30 chunk is at index zero and we've already freed it once,
so let's free it a second time.
Something's wrong,
GDB has stopped,
and if we scroll up to the last output from the program, we see that it received the abort signal. Proceses
don't often abort,
and if they do, it's a sure sign
we've triggered one of GLIBC's heap exploit mitigations.
To find out more about which mitigation we've triggered
use the 'frame' command, which can be shortened to just 'f', to switch to frame four within the _int_free()
function.
_int_free() is just the name for part of the free procedure that does most of the heavy lifting. Issue the
'context code' command to print the source lines surrounding the last call made from _int_free().
Here's the mitigation and it's explained by this comment: "check that the top of the bin is not the
record
we are going to add, i.e. a double free".
This is one of many GLIBC heap exploit mitigations, most Linux memory allocators have few, if any,
mitigations like this,
but GLIBC has been around for a long time and has a well protected heap implementation.
This particular mitigation is here to stop the same chunk ending up in the fastbins twice in a double-
free scenario.
And if it's triggered, this error message is printed and the process receives the abort signal.
But we're exploit developers,
when we come across a mitigation, we first consider how to bypass it.
So what is the fastbins double-free mitigation checking for?
The comment says that when the top of the bin, meaning the value in that fastbin's slot in the arena,
is the same as the record being added, meaning the chunk being freed, then that's a double-free
and the process should abort. The 'vis' and 'fastbins' commands
show us that our 0x30 chunk was indeed at the top of the 0x30 fastbin
when we tried to free it for the second time.
But what if a different chunk was at the top, or head, of the 0x30 fastbin
when we tried to double-free?
There doesn't seem to be any accounting for that scenario in the mitigation code.
So let's try it! Close GDB,
make a copy of the pwntools_template Python script and open it in your editor.
Scroll to the bottom and once again, we're only interested in what's between the dashed lines.
The first thing this script does is deal with the username prompt, setting a username of "George", which
we can change later.
It then uses the malloc() helper function, which works the same way as in the House of Force scripts to
request two 0x30-sized chunks and fill them with garbage data.
The malloc() helper function returns the index of the chunk it requested,
this way we can use variables like 'chunk_A' and 'chunk_B' to keep track of chunks without having to remember
their index.
This time we've also got a free() helper function, which takes a single argument;
the index of the chunk you want to free. Like the malloc() helper function
it's just a wrapper around the 'free' option of the binary
and the script uses it here to free both chunks A and B.
Let's run this script with the GDB and NOASLR options,
break with ctrl+c, and inspect the heap with 'vis' and 'fastbins'.
We see chunks A and B filled with capital A's and B's respectively
and note that both have been freed into the 0x30 fastbin.
Importantly, chunk B is at the top of this bin since it was the last chunk to be freed.
Chunk B has a forward pointer that points to chunk A and chunk A's fd is null.
So what happens if we free chunk A now?
It's already linked into the 0x30 fastbin
so it would be a double-free, but it's not at the top of the bin,
that's chunk B.
Well, let's try it.
Continue in GDB,
switch to the program window, hit return and free chunk A at index zero for the second time.
The first thing you'll notice is that the program doesn't abort
like before.
Switch back to GDB,
break,
and inspect the heap again with 'vis' and 'fastbins'.
Chunk A is now at the top of the 0x30 fastbin and has a forward pointer that points to chunk B,
but chunk B's forward pointer still points back to chunk A.
We've turned a non-circular list into a circular one and we can see chunk A is annotated as being linked
into the 0x30 fastbin twice.
We've successfully bypassed GLIBC's fastbins double-free mitigation and linked a chunk into the
same bin twice,
also known as a fastbin dup.
Now, unfortunately, the example I gave earlier of requesting the duplicated chunk for two different
purposes doesn't apply here.
There are no admin password hashes on this heap,
and besides, we want an arbitrary write.
So consider this:
what if, having performed our fastbin dup, we request another 0x30-sized chunk?
We know the fastbins take precedence over the top chunk, so we'd be allocated chunk A.
Then we'd be in a position of controlling chunk A's user data, because it was legitimately allocated
to us,
whilst the first quadword of that user data was still being used as a forward pointer in the 0x30
fastbin. That could allow us to tamper with said fd, linking a fake chunk that overlaps our target
into that fastbin.
Then we could request that fake chunk and overwrite our target data.
Let's give that a go,
taking it step by step. Close GDB and return to your editor.
We can remove the "example" title,
then the first thing we want to do is use the free() helper function to double-free chunk A, just like
we did manually a moment ago.
That sets up our fastbin dup.
Next, we want to request another 0x30-sized chunk.
Label it what you want, I'll label mine "dup".
malloc will service this request with chunk A because it's at the top of the 0x30 fastbin.
Whatever we write into the first quadword of this 'dup' chunk will be treated as a fastbin forward
pointer because chunk A is now both allocated to us and free simultaneously.
If we write the address of the 'user' struct here, we're effectively linking a fake chunk that overlaps
our target data into the 0x30 fastbin.
Let's run our script with the usual options,
break in GDB and see what state we've left the heap in with 'vis' and 'fastbins'.
Chunk B is now the first chunk in the 0x30 fastbin, followed by chunk A, but chunk A's fd points
to the 'user' struct now
and if we start requesting 0x30-sized chunks, we'll get chunk B, then chunk A,
then the third chunk we request will overlap our target data.
So let's add those requests to our script, close GDB and return again to your editor, add two more
requests for 0x30-sized chunks,
then add a third.
This one will overlap the target data.
Overwrite the target with whatever you like,
then run the script with the usual options.
We've hit a snag,
GDB has immediately stopped
and from the backtrace panel, we can see that the abort() function was again called.
Use the 'frame' command to switch to frame four within the _int_malloc() function. Much like _int_free(), the
_int_malloc() function actually does most of the memory allocation heavy lifting.
Issue the 'context code' command to print the source lines surrounding the last call made from _int_malloc().
And there's our error message, which is much more cryptic this time,
it simply reads "memory corruption (fast)".
This one isn't commented, but it's checking whether the size field of the chunk malloc is about to allocate
is the same as the size of fastbin
it's being allocated from.
After all, it wouldn't make sense for a 0x90-sized chunk to find its way into, say, the
0x30 fastbin because the 0x30 fastbin only holds 0x30-sized chunks.
If we use 'dq' to inspect the 'user' struct, we see that our fake chunk's size field is zero, which
is why it's failing the size field check.
But 'ptype' reminds us that we control this memory via our username.
So what if we set up a fake size field?
Let's give it a go!
Return to your editor and change the username to eight bytes of garbage,
I'll use a null quadword, plus a fake 0x31 size field using the p64()
function.
Run your exploit script with the usual options,
and this time all seems well. Switch to the program window, pick the 'target' option from the menu
and if everything went to plan, you'll see your data printed instead of the target data. Switch back
to GDB, break and use 'dq' to inspect the memory around the target.
You'll see your fake size field that we set up to fool the fastbin size field
check.
Awesome!
We've leveraged the fastbin dup technique to make an arbitrary write.
To summarize, this binary has a double-free bug that we used to link a chunk into the same fastbin
twice.
We were able to bypass the fastbins double-free mitigation by ensuring that our victim chunk
wasn't at the top of its fastbin when we freed it for the second time.
Then we requested that duplicated chunk and wrote a pointer to a fake chunk overlapping
our target memory into the first quadword of its user data. Because the duplicated chunk was still
linked into the fastbin when we did this,
the first quadword of its user data was being used as a forward pointer.
This had the effect of linking our fake chunk into that fastbin.
We then requested our fake chunk from the fastbin and used it to overwrite the target data.
We were able to bypass the fastbins size field check by using data we controlled near the target to
set up a fake size field.
As usual, there's a commented solution script available for download from this section's resources.
There's an overview of the fastbin dup technique, along with all the malloc internals we've covered
so far in the PDF.
There's also a "Mitigations" section that has information on all the GLIBC heap exploit mitigations to
date.
In the next video, just like we did for the House of Force, we'll turn this arbitrary write into code
execution.