In this video we're going to drop a shell from our second vulnerable binary using the fastbin dup
technique.
Once again, we'll be targeting one of the malloc hooks, which you'll recall can be used to intercept
calls to the core malloc functions.
This time we're able to call free(),
so why not hijack the free hook instead of the malloc hook?
One advantage of using the free hook is that it's first and only argument will always be a pointer to
chunk user data, meaning that if we overwrite the free hook with the address of GLIBC's system() function,
then freeing a chunk that contains our command string becomes the equivalent of passing that command
to system().
Make a copy of the exploit we wrote in the last video and open it in your editor,
I'll edit mine in place again.
Scroll to the lines we added,
we're going to start by changing the address of the fake chunk we linked into the 0x30 fastbin.
Instead of overlapping our fake chunk with those capital X's in the binary's data section,
we want to overlap it with the free hook.
So let's change the line where we link our fake chunk into the 0x30 fastbin. Replace the address
of the 'user' struct with the address of the free hook,
the symbol for which is __free_hook, and subtract 16 bytes,
remembering that malloc treats chunks as starting 16 bytes before their user data.
This way, the first quadword of our fake chunk's user data neatly overlaps the free hook.
Next, we want to change the line where we request our fake chunk and write to its user data.
This is what's going to get written into the free hook,
so instead of our string, we want to use p64() to write the address of the system() function.
Now we're set up to populate the free hook with the address of system().
Let's run our script with the GDB and NOASLR options.
Something's gone wrong, and we can see a call to abort() in the backtrace.
Switch to the _int_malloc() frame and use 'context code' to see the last call made from it.
This looks familiar,
it seems as though our fake chunk has failed the fastbins size check, and on using the 'dq' command
to inspect the memory surrounding the free hook
it's clear why. The free hook is here and our fake chunk size field is here, a null quadword that this
time we don't control.
Even worse, it looks like there are no naturally occurring, viable fake size fields anywhere nearby.
The furthest we could have used is here
if an 0x80 value were present.
So it looks like the free hook isn't a viable option after all.
But what about the malloc hook?
Let's inspect the surrounding memory, just like we did with the free hook.
The malloc hook is here, and this time there are some addresses nearby, but nothing that would make
a convincing size field,
all these values are far too large.
But let's double check
using pwndbg's 'find_fake_fast' command, which can help locate viable fake chunks that qualify for
the fastbins.
Pass it an address and it prints the locations of any fake chunks that overlap that address. Its output
is verbose
but it seems to have found a candidate fake chunk at this address
thirty five bytes before the malloc hook.
So let's see why pwndbg considers this a candidate by dumping memory at that address.
It appears as though pwndbg has coerced a 0x7f size field by misaligning one of the addresses in
memory before the malloc hook. This size field is comprised of the three most significant bytes of
this quadword and the five least significant bytes of the following quadword. If that seems
backwards
remember that GDB is displaying these little endian values to us in reverse order.
Using 'db', short for "dump bytes", to display the memory can help to clarify things as it shows how the
0x7f and following null bytes are contiguous.
And believe it or not, we can use this size field. When allocating from the fastbins, malloc does not
check for alignment, nor does it check that a chunk's flags are valid.
Some flag combinations can cause a crash, but 0x0f will work just fine.
But surely the address we're taking the 0x7f byte from will be subject to ASLR under production
conditions?
And it is.
But on most x86 Linux platforms, libraries are always mapped at addresses starting with
0x00007f.
If you're using a Mac right now, you can confirm this by typing 'vmmap $$' into
a terminal.
And the address that we're taking the 0x7f byte from is part of the standard I/O feature, which
we'll cover in a different technique,
suffice to say that it will always be present.
Furthermore, the following quadword that we're taking the zeros from is a padding quadword, which
will consistently be null.
So even under production conditions, we will always be able to coerce this 0x7f size field.
Armed with this knowledge, let's head back to our editor and modify the exploit.
First, we'll need to align our fake chunk with that 0x7f size field by replacing the free hook with
the malloc hook and subtracting 35 bytes from it,
that was the distance between the hook and the start of the fake chunk.
Then we have to change all our request sizes so that we're dealing with 0x70-sized chunks instead
of 0x30.
This way we link our fake chunk into the 0x70 fastbin and the 0x7f size field will pass the
size check.
Lastly, we need to add some garbage to the start of the data
we write into our fake chunk because the first quadword of its user data is no longer neatly aligned
with the malloc hook.
So add 19 bytes of garbage to the start of the second argument of this call to the malloc() helper function.
Once you've made those changes, run your script with the usual options.
This time, GDB doesn't stop
and if we break and dereference the malloc hook, printing a handful of instructions at the destination
with the 'u' command, we see that the malloc hook points to the system() function.
Now, all that's left to do is call malloc() with the address of a command string as the first argument,
just like we did in the House of Force binary.
However, recall that this program limits our request size to a maximum of 120 bytes,
that's not nearly high enough to be a mapped address.
But what if we didn't need to pass any arguments?
What if there was a way we could drop a shell simply by redirecting the flow of execution once? Enter
one-gadgets.
From the system() man page we can see that all system() does is use execl(), which is a wrap around execve(),
to run /bin/sh with the '-c' option followed by the command passed as an argument.
This means that there must exist in the GLIBC shared object
some code that does the equivalent of execve("/bin/sh") i.e. drop a shell.
And these code snippets are known as one-gadgets.
Instead of searching for these gadgets manually, we can use david942j's one-gadget finder that we
installed at the beginning of the course.
The syntax is 'one_gadget' followed by the path to the libc binary
your program is using. Normally this would be a constant path for all binaries on the system
but because our vulnerable binaries aren't linked to the system's GLIBC version, we need to use
this one-liner, which you can find in Appendix A of the PDF.
one_gadget prints the offset from the beginning of the libc shared object for each gadget it finds,
followed by the constraints of that gadget.
For example, the second gadget starts at 0xe1fa1 bytes after the start of the libc shared
object,
and its constraint is that when execution is redirected to this gadget, the value at stack pointer
plus 0x50 must be null.
That's because for this particular gadget, the argv array of the execve() call starts at stack pointer
plus 0x50.
On most systems, /bin/sh is a symlink to dash.
In fact, if we type 'man sh', it's the dash man page that opens.
Dash is very sensitive to malformed arguments, including unreadable addresses, nonexistent options
and non-existent paths,
and it will fail if it comes across any one of these.
And that's why ensuring the first entry in its argv array is null is the safest way to guarantee it doesn't
attempt to process any potentially broken arguments.
So let's find out if we're able to meet the constraints of any of these one-gadgets.
We'll need r12 & 13 to be or point to null, or stack pointer
plus 0x50
to be null,
or meet these rsi and rax requirements.
Run your script with the GDB and NOASLR options,
break, and set a breakpoint on the address in the malloc hook by dereferencing it with an asterisk.
Continue and switch to the program window
where we'll trigger malloc by requesting a chunk of any size between zero and one hundred and twenty.
Switch back to GDB, which has hit our breakpoint.
This is the state of the registers and stack
that will be passed to our one-gadget.
Let's inspect the registers with 'context
reg'.
It looks like r12 & 13 aren't viable,
neither are rsi and rax, that just leaves stack pointer
plus 0x50.
Dump the stack with the 'stack' command, followed by the number of quadwords you want to dump.
The offset from the stack pointer is shown on the left and unfortunately stack pointer plus 0x50 isn't
null.
But it is a valid address,
and the C11 standard states that the first entry in any argv array typically represents the name of the
program being invoked, which in most cases is ignored.
The next entry is null,
and that's good enough to satisfy the constraints of this one-gadget
even if the one-gadget finder doesn't think so.
We'll return to our editor and prepare to use this one-gadget by substituting the address of the system()
function with the address of our one-gadget.
The offset was 0xe1fa1 bytes from the libc load address.
An alternative to remembering the offset is to use the one_gadget tool's '--script' option, which runs your
exploit script with every gadget it finds.
We'll add one last call to the malloc() helper function to our script,
the request size must be between zero and 120 to be accepted by the program and trigger malloc()
but the data doesn't matter. After all,
just calling malloc() at this point should be enough to drop a shell,
the argument is irrelevant and the program won't even get as far as asking us for data.
Once you've made those final changes, go ahead and run your exploit under production conditions.
And you should
get a shell.
And that's one method of obtaining arbitrary code execution via a fastbin dup.
In summary, we built our fastbin dup primitive via a double-free bug, which we used to link a fake
chunk overlapping the malloc hook into the 0x70 fastbin.
We were able to allocate this fake chunk by using existing, consistent GLIBC metadata as a size field.
It didn't matter that the flags weren't valid or that the fake chunk was misaligned because malloc doesn't
check for those inconsistencies when allocating from the fastbins.
Then we wrote the address of a one-gadget over the malloc hook, a one-gadget being an existing set
of instructions which, when executed, drops a shell.
Finally, we triggered a call to malloc(), which was redirected via the malloc hook to our one-gadget
giving us our shell.
The fastbin dup can be a powerful primitive and works on GLIBC versions 2.31 and
below.
It does require knowledge of the address of your target and requires an attacker to forge or use an
existing size field to pass the fastbins size field
check.
As usual, there's a commented solution script available from this section's resources and an overview
of the fastbin dup technique, along with all the malloc internals we've covered so far,
plus one_gadget tool use in the PDF.
In the next video, we'll put to the test what you've learned so far
when we take on the first challenge binary.