Welcome to your first challenge binary. In this video
I'll begin by explaining how challenges work, then I'll introduce the challenge binary and let you
get on with solving it.
Start by navigating to the challenge-
fastbin_dup directory.
This challenge will test what you've learned so far about fastbins and arenas.
The second challenge will involve writing a full exploit, including leaks, but we're starting easier
with this one.
All you have to do is drop a shell from the fastbin_dup_2 binary.
I'll walk with you through the initial investigation of the binary
then you'll pause the video to take on the challenge yourself.
If you get stuck or just want to verify your progress, you can unpause the video and I'll take you
through the next phase of the exploit.
So let's start with the usual drill of learning which protections this binary employs using
checksec.
Like everything so far, it's compiled with full RELRO, meaning that we'll have to rely on the malloc
hooks to get code execution rather than on the binary.
It has stack canaries and NX, and this time is also position-independent, meaning that we don't
know the location of the binary in memory.
Note that position-independent programs use different base addresses for the binary and heap, even
when running without ASLR enabled,
so expect to see different values than you're used to when using heap commands.
Lastly, the binary is linked against GLIBC version 2.30 without tcache support.
Run the binary under GDB and we'll see what we're dealing with.
We still have a libc leak, which will of course come in handy.
We can request a few more chunks this time,
up to 13,
and we can still only request fastbin-sized chunks.
But it looks like there's a catch,
we're unable to request 0x70-sized chunks.
That means any request size that would result in a 0x70-sized chunk being allocated won't be processed.
For example, I can request 0x58 bytes of user data, resulting in a 0x60-sized chunk being
allocated, which I can confirm by breaking in GDB and inspecting the heap,
but my request for 0x59 bytes is denied because it would result in a 0x70-sized chunk
being allocated.
This is the main obstacle you'll need to deal with.
Remember that requesting 0x70-sized chunks was critical to our success with the last fastbin dup
binary because it allowed us to use that existing 0x7f size field for our fake chunk overlapping the
malloc hook.
This isn't about coercing the program into letting you allocate 0x70 chunks,
it's about finding an alternative way to overwrite one of the malloc hooks via the fastbin dup technique.
The rest of the program works just like the fastbin_dup binary.
The 'free' option lets us free chunks by index
and the double-free bug is still present.
There's no 'target' option this time because our only objective is to drop a shell.
There's a pwntools template script to get you started.
Don't forget that the libc symbols are accessible via libc.sym.
symbol name, for example
libc.sym.main_arena for the main arena.
And if you need to refresh your memory on any pwndbg commands, pwntools functions or the one_gadget tool
take a look at Appendix A of the PDF. I would strongly recommend revisiting the 'Arena Layout' diagram
of the PDF, especially with regard to the proximity between the fastbins and the top chunk pointer.
All right,
now's the time to start planning your exploit.
We know that we can leverage this program's double-free bug for a fastbin dup.
The question is "how are we going to leverage that fastbin dup to overwrite one of malloc's hooks
if we can't request 0x70-sized chunks?" In a moment, pause the video and see how far you can get
on your own. If you need a steer
unpause
and I'll do my best to help you out.
Don't worry if you can't do everything on your own,
these challenges are designed to be difficult. Best of luck.
OK, I've added some lines to my copy of the template script.
Notice that whenever I'm able to write to a chunk, I use a different letter to mark that chunk in memory,
that makes identifying specific chunks in a memory dump a bit easier.
I've used the 'free' option to double-free chunk A into the 0x50 fastbin, there's nothing special
about that bin
it's just the size the template was already using.
Then I've requested the same size back and overwritten
its fastbin fd. Normally we'd put the address of a fake chunk here, but right now we don't have
a viable size field to anchor, as it were, our fake chunk.
I've also requested chunks B and A from the 0x50 fastbin so that my fake chunk, or in this case
just a garbage value, ends up at the head of the 0x50 fastbin.
Usually what we'd do at this point is allocate the fake chunk,
but let's consider what coming this far has achieved.
I'll run my script with the GDB and NOASLR options.
The 'fastbins' command shows that my garbage value is at the head of the 0x50 fastbin, inspecting
the main arena directly with the 'dq' command shows just that: my 0xdeadbeef value in the 0x50
fastbin slot.
So by its nature, the fastbin dup allows us to manipulate memory in an arena.
Consider how that might be useful when looking to create viable size fields for a fake chunk.
You can download a copy of my script so far from this section's resources,
it's labeled as template number 2. Pause the video again and see if you can make a little more progress.
If you got this far without help, then you're doing extremely well.
And if you didn't, don't worry,
these challenges aren't easy and it's normal for them to take hours, if not days to solve,
especially if you're new to exploit development.
They're here to encourage you to do things like browse the malloc source, read the man pages for things
like the system() function and the dash shell, and most importantly, think like a hacker.
All right.
I've added some more lines to my script, which, again, you can download a copy of from this section's
resources,
it's labeled as template number three.
The first part is the same as before
but I've swapped the garbage 0xdeadbeef value for a fake size field, 0x61, which will get written
into the main arena in its place.
If you hadn't already guessed, I'm going to use that size field to link a fake chunk into the main
arena itself.
That's what the second part of this script is doing.
I've performed another fastbin dup, this time against the 0x60 fastbin.
I've used that to link a fake chunk into it that overlaps the main arena.
main_arena plus 0x20 will line up my 061 size field
I wrote there in the previous dup with the fake chunk's size field.
I've also made two more requests for 0x60-sized chunks so that my fake chunk will be next to get allocated
from the 0x60 fastbin.
I'll run my script under GDB to show you what I mean.
The 'fastbins' command shows us that my 0x61 value is now at the head of the 0x50 fastbin
and a main arena address occupies the head of the 0x60 fastbin.
Both of these are the result of two different fastbin dups.
Inspecting the main arena directly shows where both of those value reside.
This is the 0x50 fastbin slot and this is the 0x60 slot.
The next chunk to be allocated from the 0x60 fastbin starts here and will use my 0x61 value as
a size field.
0x61 is a legitimate size value for a chunk in a 0x60 fastbin
so the fastbin size check will pass.
I'll demonstrate this by manually requesting a 0x60-sized chunk and writing some data into it.
When I return to GDB and dump the main arena, we see that my data, those 0x59 bytes representing
my capital Y's, have been written there.
So we can use one fastbin dup to set up a fake size field in the main arena,
then a second dup to allocate a chunk there, giving us the ability to overwrite malloc metadata
stored in an arena,
for example, the top chunk pointer.
Pause the video once again and see what else you can come up with.
Return here if you need some more advice.
I've added just one more line to my script.
It requests the fake chunk overlapping the main arena and writes some data into it, just like I did
manually before.
You can also grab a copy of my script so far from the resources section labeled as template number four.
This time I've written garbage data
up until the top chunk pointer, which I've then overwritten with the address of the malloc hook minus
16. We briefly touched upon the top chunk pointer when we were introduced to arenas, and it's what
we were indirectly manipulating during the House of Force technique.
I've chosen the malloc hook because our top chunk is going to need a size field that isn't zero,
and as we've seen, there aren't any existing viable size fields near the free hook.
I'll run my script and dump the main arena to show you what state it's in.
We can see the fake 0x60 chunk,
some of its user data has been filled with garbage,
and the top chunk pointer has been overwritten.
If we use 'dq' to view memory near the malloc hook
we see that malloc now considers the top chunk to start here, meaning this address, which incidentally
is another one of malloc's hooks, has become the new top chunk size field.
Now we're in an almost identical situation to the final stage of the House of Force,
all that's left to do is make a request that will be serviced from the top chunk so we can overwrite
the malloc hook. Any size that has an empty fastbin will do
so I'll request a 0x20-sized chunk manually.
But this time, it's not that simple, GLIBC has detected that there's something wrong with the top chunk
size field.
Now, the house_of_force binary was using GLIBC version 2.28, this binary
you'll recall from the checksec output, is using GLIBC version 2.30.
The 'Mitigations' section of the PDF reveals that GLIBC version 2.29 introduced a top
chunk size field integrity check.
If I return to GDB, switch to the _int_malloc() frame and issue the 'context code' command, we can
see that check.
malloc is comparing the size of the top chunk to something called av->system_mem. av is a variable
that represents the arena administrating this heap,
in this case, the main arena and system_mem is the current amount of memory
that arena has checked out from the kernel, a value which should be familiar
0x21000, the initial size of our heap.
If malloc notices that a top chunk is larger than the total memory its arena has checked out
then there's clearly something wrong and it will abort.
Currently, we're using a GLIBC address as our counterfeit top chunk size, which is much larger
than the system_mem value.
Pause the video again and consider how we might overcome this issue,
remembering that just like other chunks, the top chunk doesn't necessarily need to be 16 byte aligned.
Unpause
the video if you need a hand figuring this part out.
We were dealing with the top chunk size integrity check, which was comparing our fake top chunk size,
a GLIBC address, to the main arena's system_mem value and finding that it was much too large.
One way we can pass this check is to use the same misalignment trick we used against the first
fastbin dup binary.
Everything I've showed you so far is available for download from this section's
resources, labeled as template number five.
I've amended the line on which we overwrite the top chunk pointer in the main arena.
I've changed malloc hook minus sixteen to malloc hook minus 35,
the same value that we used in the last exploit to leave us with a reliable size field of
0x7f.
We could also get away with using 36
even though the least significant byte would be subject to ASLR, the total would still always be less
than system_mem.
I'll run my script and dump memory near the top chunk.
Now our top chunk has a size field which is much less than system_mem
we can safely request a chunk from it and overwrite the malloc hook.
Don't forget to account for the garbage you'll need to write to make up the distance between the new
top chunk location and the malloc hook,
last time we had to use 19 bytes.
We'll need to employ a one-gadget to drop our shell since just like the first fastbin dup binary,
our request sizes are too limited to pass in addresses.
Appendix A of the PDF has an example of the one_gadget tool use
if you need a reminder
There is one last wrinkle to this challenge,
it's OK if you don't get it on your own because it's something that we've only dealt with peripherally,
but take some time to think about it.
You'll find that you're unable to meet the constraints of any of the one-gadgets in this version of
GLIBC.
But we've learned that the constraints printed by the tool aren't always as strict as they seem.
Take a look at the man page for the dash shell,
specifically the arguments that can be passed on the command line and therefore end up in the argv
array.
Pause the video now and have a crack at this last phase.
Take your time
and if you find yourself really stuck, then unpause the video and we'll wrap this challenge up.
All right.
Let's get this one-gadget working.
I've chosen the same gadget that we used against the last fastbin dup binary with the constraint that
stack pointer plus 0x50 is null.
If you need to catch up to this point, you can download my script from this section's resources, labeled
template
number six. I've added a line to my script that overwrites the malloc hook with the address of the
one-gadget I selected.
For now that line is commented out in favor of one that overwrites the malloc hook with a garbage value
instead.
This is a quick and dirty way of ensuring GDB stops at the exact point we want it to
so we can inspect the program context.
Below that line is a further call to the malloc() helper function to trigger a call through the malloc
hook.
When I run my script with the usual debugging options
the program segfaults at the garbage address I supplied.
Now we can use 'stack 14' to see if our gadget's constraints will be met.
Stack pointer plus 0x50 isn't null,
but we know it's still safe since it's only argv[0]
and the address is readable.
Unfortunately argv[1] isn't null either,
which was how we got away with using this gadget last time.
But it does point to memory that we control.
Notice how the next three argv arrays are the same values I've been using to mark chunks, with these
capital L's corresponding to argv[1].
This situation is, of course, program dependent,
you might not be so lucky with different binaries.
In this case, we're fortunate because it means we can manipulate the first argument passed to the dash
shell and potentially avoid it failing.
If you've been experimenting with dash, you'll have noticed that it's sensitive to malformed arguments.
Passing a nonexistent option or file name to it will not result in a shell.
What's worse is that even if the first argument is valid, it will continue to parse arguments until
it hits a null, failing on any that are malformed or unreadable.
If you've been browsing the dash man page, however, you may have discovered a couple of solutions
to this problem.
The first is that if dash comes across a valid file name, it will attempt to parse it for further
commands, ignoring any succeeding arguments.
So if you're able to write your commands to disk, then this is a viable option.
A second, perhaps more practical solution is to pass the
'-s' argument which starts reading commands from stdin, ignoring any succeeding arguments.
So I'll return to my editor and swap in the line that writes the address of our one-gadget into the
malloc hook.
Running the script now
prints the error "M can't open L". Those strings come from the data I used to mark chunks in my script,
so if I replace the "L" marker with the '-s' option, not forgetting to null-terminate the string,
then run the script under production conditions without the GDB and NOASLR options,
I finally get a shell and hopefully so do you.
To summarize, our binary had a double-free bug and a libc leak, but wouldn't allow us to request
0x70-sized chunks.
So we leveraged the double-free to make two fastbin dup attacks.
We used the first to write a fake size field into the main arena,
then the second to allocate into the main arena, overwriting the top chunk pointer.
We aligned the top chunk pointer with a reliable libc address so that our fake top chunk had a size
field that pased the top chunk size
integrity check.
Then we made a request from that top chunk, overwriting the malloc hook with the address of a one-gadget.
The one-gadget constraints couldn't be met, but because we controlled the first argument passed to
the dash shell that is invoked, we were able to pass the stdin option, avoiding dash parsing
any malformed arguments and finally giving us our shell.
A working exploit for this challenge, complete with comments, is available from this section's resources.
Don't forget to review the PDF
if you need to refresh your memory on arena layouts, the fastbins, the fastbin dup technique
or any of the pwndbg commands and pwntools functions we've covered so far.
There is one big advantage to controlling two or more arguments passed to a one-gadget. Say we're attacking
a setuid binary, I'll turn the challenge binary into one now.
If I run my exploit, the shell I get still belongs to the user
I run the script as, that's because most modern shells will reset your effective UID
if it doesn't match your actual UID.
This is a safety feature to protect against exactly this sort of situation. When using the system() function
there isn't much we can do about this since by the time we've invoked a shell
the initial call to execve("/bin/sh") will already have reset our effective UID, but when using a
one-gadget, we're passing arguments straight to that initial invocation of dash.
This means we can pass the '-p' option, which disables this behavior.
I'll add the 'priv' option to my script in
what will become argv[1], keeping the stdin option which we still need, at what will become
argv[2].
Now when I run the script
I get a shell.
But this time it doesn't reset my effective UID.
That last part isn't necessary to solve the challenge, but it's a nice trick you can do with one-gadgets.
I hope you enjoyed your first challenge binary, if you solved it without help, then that's extremely
impressive.
If not, that's totally expected.
As I said earlier, they're designed to be very difficult.
Either way, I hope you learned something. In the next video
we'll get back to learning malloc internals again so we can understand our next heap exploitation technique
the Unsafe Unlink.