This video is a little out of place.
It doesn't teach a new technique, nor does it cover any new malloc internals.
But I still think what we're going to learn is important.
We're taking a look at so-called heap Feng Shui, a term used by Alexander Sotirov at Blackhat Europe
2007 to describe the purposeful arrangement of heap chunks during exploit development.
Now, Wikipedia tells me that historically Feng Shui was used to orient buildings in a favorable manner,
but nonetheless heap Feng Shui has become somewhat of an established term within the heap exploitation
community.
We've already engaged in limited heap Feng Shui when we exploited the one_byte and poison_null_byte
challenge binaries, tactically allocating and freeing chunks to place them in the path of overflows
and overlaps.
Now, we're going to take that a step further and craft chunks that wouldn't otherwise exist on the
heap.
To give our craftsmanship some context, we're going to exploit a modified version of the house_of_rabbit
binary while it's still fresh in our memory.
Navigate to the house_of_rabbit_nofast directory, where you'll see the modified binary and a pwntools
template script.
Checksec output is the same as before
so let's run the binary under GDB to see what has changed.
The menu is similar, with the only visible difference being that we can now request up to 13 chunks,
4 more than earlier.
However, on making a request with option 1
it's apparent the rules have changed; we can no longer request fast-sized chunks, that means any request
for 0x78 bytes or less will be ignored by the program.
We're limited to normal chunks this time, 0x90-sized and above.
From a House of Rabbit perspective, this isn't a huge problem.
Most of the technique uses large requests to manipulate malloc metadata.
But the restriction on fast chunks will make the first phase more difficult, in which we link our fake
junk into the fastbins.
Originally, we simply performed a fastbin dup to achieve this, requesting 2 chunks of identical
size, freeing them one after the other, to bypass the fastbins, double-free mitigation, then requesting
the duplicated chunk and using it to tamper with fastbin metadata.
We're still going to begin by linking our fake chunk into a fastbin, after all, as we've discussed,
they're the easiest bins to do this to.
However, we need to do so without ever actually requesting a fast-sized chunk
and that's where heap Feng Shui comes in.
All we need to make this work is a chunk linked into a fastbin and the ability to control its forward
pointer.
Of course, we'll have to take advantage of a bug to do this, and we'll rely on the double-free we
leveraged against the original house_of_rabbit binary.
First, we're going to understand what a double-free bug is in a slightly different light.
Mitre's CWE, or common weakness enumeration, labels double-free as a peer of use-after-free.
And indeed, they both fall into the "expired pointer dereference" category, also known as a "dangling
pointer".
Treating a double-free as a dangling pointer can broaden its application.
So far, we've thought of it in this way: a double-free allows us to call free() on a heap address even
after the chunk at that address has already been freed.
But since free chunks can be consolidated and the entire heap may be reorganized because of this, we
can instead think of it thus:
a double-free allows us to call free() on an arbitrary heap address.
Now, we know that free() only needs a size field to operate on and fast chunks aren't consolidated on
free(), they're simply linked directly into their appropriate fastbin.
So if we want to link a chunk into a fastbin, all we need to do is free a chunk to set up a dangling
pointer, rearrange the heap so that dangling pointer points to a fast chunk size field, 0x20,
0x30, etc..
then free the dangling pointer.
If that sounds complicated, don't worry, I'll walk us through it step by step.
We'll start by closing GDB and making a copy of the pwntools_template.py script.
Open the copy in your editor of choice and scroll to the bottom.
There are no differences between this template script and the one we used in the original House of Rabbit
exploit, other than that our fake chunk is already set up with a size field of 1.
Don't worry about the other House of Rabbit stages quite yet, we're starting with this minimal template
so when we inspect the heap, it's clear what's going on.
We can copy/paste a lot of code back in from our original exploit later.
In its current state, this script simply requests two 0x90-sized chunks, A & B, then frees
them.
Run the script with GDB and NOASLR, and I'll show you what I mean when I say a double-free can be
thought of as a dangling pointer.
Break in GDB with ctrl+c, then inspect the heap with pwndbg's 'vis' command.
As you can see, both chunks A & B have been consolidated with the top chunk, leaving no allocated
chunks on the heap and no free chunks in the bins.
Let's look inside of the top chunk by dumping memory with 'dq', we can use the main arena's "top"
field to do this. Dump 38 quadwords.
We know that free space, other than sometimes being repurposed as in-line malloc metadata, isn't
cleared.
Even if memory is getting zeroed on allocation with calloc(), it's still never zeroed on free().
Within the top chunk, we can still see the remnants of chunks A & B.
Chunk B's user data started here, and that's where the dangling pointer still points.
At any time in the future, we could call free() on this address.
I'll give you an example of how this might be useful.
Remember that address, 0x6030a0.
I'll continue in GDB, switch to the program window, then request 2 chunks. The first will be
0x90-sized again
and the second, I'll make 0x100-sized this time.
I'll return to GDB, break and inspect the heap again with 'vis'.
Our two latest chunks are present, and remember the address of the dangling pointer;
0x6030a0.
Now the chunk holding that user data is a different size, 0x100.
More importantly, in a production program this chunk might have been allocated for something completely
different to the chunk that created the dangling pointer.
Think about the impact of this; that 0x100 cunk could belong to a totally different part of the
program, even a different thread sharing the same arena.
Freeing it via the buggy code could cause a double-free or use-after-free in code that doesn't have
any bugs.
It's things like this that allow you to get really creative with heap exploits and is why I enjoy them
so much.
Fortunately, our situation is a little simpler than cross-thread, double-frees.
All we need to do is change the size of the chunk we're freeing to a fast chunk size.
This isn't without its own difficulties, we can't request fast chunks, which means we'll have to coerce
a fast chunk size field onto the heap somehow.
Perhaps we could just write a size field there ourselves in chunk user data.
I felt that would be a bit too simple however, and you may have noticed that I've restricted the
number of bytes we can write into chunk user data from 16 to 8.
Since chunks and therefore user data always start at a 16-byte alignment, we can't make those 8
bytes align with a size field, so we'll need to take a more complex route.
We could employ our old friend remaindering,
so let's close GDB and return to our editor to think things through.
Right now, all this script is doing is setting up that dangling pointer, so let's label it as such.
Swap out the "chunk_B" variable name for something more descriptive.
We want to coerce a fast chunk size field into the space previously occupied by the dangling pointer's
chunk's size field.
Let's use 0x20 as our target size to keep things compact.
The minimum chunk size we can request is 0x90, 0x90 plus 0x20 is 0xb0.
Therefore, remaindering a 0xb0 chunk by requesting a 0x90 chunk should yield a 0x20-sized remainder.
So we'll request a 0xb0 sized chunk,
followed by a top chunk consolidation guard.
Then we'll free the 0xb0 chunk.
After that, we'll request a 0x90-sized chunk, which should remainder the 0xb0 chunk.
Run your script with the GDB and NOASLR options and see what state we've left heap in by breaking,
then issuing the 'vis' command.
It worked! The heap is in a stable state, remember that we haven't actually caused any corruption
or triggered any bugs yet, and there's a 0x20 size field in the spot where the dangling pointer
chunk size field used to be.
Using the double-free bug to free the dangling pointer again will link that 0x20 chunk into the fast
bins.
Doing this now is premature, but we're going to do it anyway to learn why.
Beforehand, issue the 'record' command.
'record' prompts GDB to log every executed instruction, allowing us to undo our mistake afterwards.
If 'record' doesn't work on your distro, don't worry, it's not integral to learning heap exploitation
and we won't make extensive use of it during the course.
GDB seems to have problems breaking during recorded read operations when the target is remote, to counter
this set a breakpoint on the puts() function, which the program uses to print parts of its menu.
OK, let's free this 0x20 chunk, continue in GDB and switch to the program window.
Don't hit return to print the menu, it'll trigger the puts() breakpoint. We know option 2 is the
free() option, so we'll just use it blind.
Free the chunk at index 1, originally chunk_B.
The program should hit our puts() breakpoint and stop, so switch back to GDB and inspect the heap again.
Now we can see that the 0x20-sized chunk is labeled as being linked into both the unsortedbin and
the 0x20 fastbin.
This is interesting on its own, since it's essentially a double-free mitigation bypass.
Just as important is the fact that using heap Feng Shui, we've successfully linked a chunk into the
fastbins without ever allocating a fast-sized chunk.
All that's left to do is tamper with that fast chunk's fd, pointing it at our fake chunk in the binary's
data section, then the rest of the House of Rabbit should fall into place.
We need to request a chunk that overlaps the 0x20 chunk's fd. Since we can't request 0x20
chunks,
we'll have to consolidate it first.
This is tougher than it sounds, the 0x20 chunk's unsortedbin fd has been clobbered by its fast
bin fd, which is null.
This means that freeing either of the 0x90-sized chunks will result in a segfault as the unlink() macro
attempts to dereference a null pointer during consolidation.
It looks like we'll need to delay our double-free until we've cleaned up the heap a bit, this is why
we used the 'record' command. With record mode active
we can step not just forward, but backwards as well by adding the "reverse-" prefix to some of GDB's commands.
What makes this so powerful is that breakpoints also work when executing in reverse and GDB will record
user input, which is replayed when execution continues in the forward direction.
Record mode is really useful for finding bugs, here it's just going to save us from starting the program
over again, having made a mistake.
But I thought it was worth pointing out in case you hadn't come across it before.
Rather than executing in reverse, we can jump straight to the beginning of our recording with 'record
goto begin'.
Inspecting the heap again reveals that we have indeed reversed our mistake of freeing the 0x20
chunk too early, allowing us to try an alternative path without having to start the whole program over
again.
Before we continue, I mentioned that GDB will replay our user input if we continue execution in the
forward direction.
To avoid this, we can delete the execution log with 'record delete'.
Let's see what effect freeing the succeeding 0x90 chunk would have.
Continue and switch to the program window where we're already being prompted for which chunk we want
to free.
Pick chunk 3 and then switch back to the GDB window.
Inspecting the heap shows that this won't work, the 0x90 chunk has consolidated with both the
0x20 chunk and the top chunk, destroying the 0x20 size field we worked so hard to create.
The only other option is to free the preceding 0x90 chunk first, that way it'll consolidate with
the 0x20 chunk without affecting its size field.
After that, it's safe to free the other 0x90 chunk, which should consolidate the top chunk over everything,
returning the heap to its initial state.
Rewind once more and delete the recording log.
Since this is the last time we'll run this test, we can just stop recording altogether with 'record
stop'.
Before continuing, tell GDB to ignore breakpoint 1, our puts() breakpoint, the next 4 times
it's hit.
This will allow us to use the menu to free a second chunk without stopping.
I know this part is a faff, but we only needed these breakpoints because of the GDB issue I mentioned
earlier.
Continue, then switch to the program window so we can test this last theory.
Free the chunk at index 4, followed by the chunk at index 3.
I don't expect you to remember these chunk indices, that's what our script is for, this is just an
experiment.
Once you've freed those chunks, switch back to GDB and inspect the heap with 'vis'.
Once again, we're left with just the top chunk and nothing in the bins, so issued the same 'dq'
command we used earlier to see what's inside the top chunk.
The output reveals that the vestigial 0x20 size field is still intact, meaning when we free it
via the dangling pointer, it will still have the desired effect.
This time, the 0x20 fastbin fd won't clobber any active unsortedbin metadata, maintaining
stability.
We're also left with enough free space to allocate a chunk overlapping the 0x20 chunk, allowing
us to tamper with its fd.
This is a much more favorable situation than before when we freed the 0x20 chunk too early.
We're nearly done, and now that we've determined the best path we can add our actions to the exploit
script, so close GDB and return to your editor.
We're back to before we freed anything, so add a line that frees chunk E,
that's the one currently sitting just before the 0x20 chunk that we forged.
After that, add another line that frees chunk D, the 0x90 chunk after the 0x20 chunk.
This should leave the heap in the same state that we last saw it in GDB, so at the end of our script
we'll add a line that frees the dangling pointer, finally linking the forged 0x20 chunk
into the fastbins.
Now, I understand that things have gotten a touch complicated by this stage, so don't hesitate to
re-watch the video so far if you want to clarify anything.
Next, we'll request a 0x90-sized chunk, the smallest we can in this scenario, which will
be allocated from the top chunk.
This should leave the top chunk neatly overlapping the 0x20 chunk, so we'll allocate another
0x90 chunk from it and use that to tamper with the 0x20 fastbin fd, linking our fake chunk
into that fastbin and completing our objective.
Having added those lines, run your exploit script with the usual options.
Break and inspect the heap.
The final layout is rather simple, consisting of two 0x90-sized chunks. The second chunk
is linked into the 0x20 fastbin and its fd points to our fake chunk in the program's data section,
confirmed by the fastbins command.
Because we had to perfectly overlap the second 0x90 chunk with the 0x20 chunk, we've ended up
changing its size field.
This would cause problems if we were capitalizing on this fastbin dup in the conventional way,
but since we're attempting the House of Rabbit, it won't be an issue.
Remember, in GLIBC 2.25 there isn't a fast chunk size integrity
check in malloc_consolidate().
There are ways in which we could fix the size field if we wanted a conventional fastbin dup,
but I'll leave that as an exercise to pursue in your own time.
Of course, if we controlled more than just the first quadword of chunk user data, we could perhaps
provide our own counterfeit size field.
There are many possibilities.
We're simply dealing with a more restrictive scenario here for learning purposes.
Our exploit so far has achieved roughly the same outcome as the fastbin dup in our original House
of Rabbit exploit.
Granted, it took us 4 more requests this time,
but what's important is that through the magic of heap Feng Shui, we were able to essentially perform
a fastbin dup and link a fake chunk into the fastbins without ever requesting a single fast-sized chunk.
In summary, our target binary has a double-free bug, which we've come to understand is really just
a form of dangling pointer.
We used this bug to set up a dangling pointer on the heap, then allocated, freed and remaindered chunks
until we'd manipulated malloc into writing a fast chunk size field into the spot our dangling pointer
pointed to.
We then used the double-free bug to free this never-allocated chunk, before requesting a chunk overlapping
it, tampering with its fastbin fd and linking a fake chunk into the fastbins.
This is just one example of how manipulating GLIBC heap layouts, or "heap Feng Shui", can turn an
impossible situation into a working exploit.
To prove what we've done is actually usable, we're going to wrap up this House of Rabbit exploit by
dropping a shell.
Most of the code from your original exploit can just be copy/pasted into this one.
You'll need to increase the main arena's system_mem value, trigger malloc_consolidate() to move your fake
chunk into the unsortedbin, then sort it into bin
126.
Be aware that you'll have to make 2 small adjustments to your exploit to get it working. If you want
to treat this as a mini challenge binary then pause the video in a moment and see if you can get
that shell.
If you get stuck, you might do, one of the obstacles is a little devious, then come back and unpause.
I'll give you a hint and one more chance to finish this on your own before revealing the solution.
Welcome back.
On your screen now is the first part of my exploit for this modified house_of_rabbit binary.
If you want to follow along, you can download a copy from this lecture's resources, it's labeled
as template 2.
As you can probably tell, I've simply copy/pasted the relevant lines from our previous House of Rabbit
exploit.
The initial fake chunk size field was already taken care of in this template, and I've copied the 3
lines we used to increase the main arena's system_mem value between that and our heap Feng Shui.
Our heap Feng Shui replaced the fastbin dup code, so I've omitted that.
Hopefully, you recall that this should be followed by an attempt to trigger malloc_consolidate(), starting
our fake chunk on its journey from the fastbins to the largest largebin by moving it into the unsorted
bin.
Originally, we achieved this by allocating, then immediately freeing a chunk that bordered the top
chunk.
The aggregate size of those chunks was beyond the fastbin consolidation threshold, triggering a call
to malloc_consolidate().
Although that approach still works here, the limit on total allocations imposed by the program means
we have to be slightly more efficient.
I briefly mentioned in the House of Rabbit video that to do this, we could just free the
0x60000-sized chunk we used to increase system_mem, and that's what I've done here.
After that, the exploit runs more or less identically to our previous attempt.
We change the fake chunk's size field to one that will land it in bin 126 when it's sorted,
then request a larger chunk to trigger the sorting.
Our fake chunk size field is changed again, this time to the distance between the fake chunk and our
target, which in this case is still the free hook, plus enough to make a final allocation overlapping
said target.
The gap between the fake chunk and the free hook is bridged by a large request.
Then a chunk overlapping the free hook is allocated with the purpose of overwriting it.
Remember that the free hook doesn't sit on a 16-byte boundary.
This fact is, of course, compiler dependent, but seems to hold true for most current versions of
GLIBC, including this one.
Because our fake chunk is 16-byte aligned however, we can't line up the first quadword of user data
belonging to the chunk overlapping the free hook with the free hook itself.
We could, of course, misalign our fake chunk, but that's not an option in this scenario.
With the previous house_of_rabbit binary, this wasn't an issue because we could write up to 16 bytes
into chunk user data, which is what this last line is attempting; writing 1 quadword of garbage
followed by the address of GLIBC's system() function.
Unfortunately, for this binary I decided to add an extra challenge by cutting the number of bytes we
can write into chunk user data to just 8.
We can see the effect of this change if I run my template script with the GDB and NOASLR options.
Break and dump memory near the free hook.
This is the size field of a chunk overlapping the free hook, which means everything so far has been
successful.
This is the null quadword I used to make up the small gap between the start of this chunk's user data
and the free hook.
And this is the free hook itself.
At first glance, it looks like we've succeeded, that definitely looks like a libc address.
But disassembling the instructions at the memory pointed to by the free hook reveal that it's pointing
into the main arena.
The free hook still holds an unsortedbin backward pointer from the remaindering process.
The reason for this is obvious,
our 8 bytes of user data just can't reach the free hook.
Because of this alignment issue, we have to pick an alternate target to the free hook.
Now, I imagine you didn't get this far in the video without at least trying the malloc hook, which
is 16-byte aligned.
And you probably noticed it wasn't possible for essentially the same reason;
we can only write into the first quadword of chunk user data, so we must align the remainder of our
fake chunk such that its first quadword of user data overlaps the target.
Since that quadword is repurposed as a forward pointer when the remainder is linked into the unsorted
bin, it replaces the hook before we can overwrite it with our own data.
Execution gets redirected into the main arena, which is marked non-executable, causing a segfault.
If you haven't tried using the malloc hook in this exploit yet, don't take my word for it, give it
a go.
Once you have and you've confirmed for yourself that it's a dead end, I want you to start thinking
about alternatives.
In a moment, I'll prompt you to pause the video again and take a crack at finishing this exploit yourself.
The basic premise remains the same; we want to overwrite one of the malloc hooks so we can redirect
execution and drop a shell.
We've already tried the free hook and the malloc hook, but there are other hooks available to us.
We can search for them using 'nm', a tool for listing symbols that's probably already installed on your
system.
Pass it the path to the GLIBC binary we're interested in, we can do this using the same a method we
use to pass it to the 'one_gadget' tool.
If you ever forget that one-liner, we can find it in Appendix A of the HeapLAB PDF.
And if you haven't done so already, you can make life easier for yourself by binding that one-liner
to a shell function like this.
Either way, pass the libc binary's path to 'nm',
followed by the -D option, because we're dealing with a dynamic object.
This on its own will print a list of all dynamic symbols belonging to the target GLIBC binary,
of which there are a lot.
We'll narrow this down to just the symbols we're interested in using 'grep'.
The output reveals a total of 6 malloc hooks, only 2 of which we've tried.
There's also a 7th that doesn't follow the naming convention.
Any one of these with an offset ending in zero are fair game in this scenario.
Notice how the free hook offset ends in an 8, putting it just out of reach of our 16-byte-aligned
arbitrary write.
"__morecore" and "__after_morecore_hook" are called before and after, respectively, malloc requests more
memory via the sbrk() syscall.
This means they can be reliably triggered by exhausting the main arena's top chunk, although it's
difficult to pass precise arguments to them.
We're already familiar with the free hook and the malloc hook. The "__realloc_hook" is of course used
when realloc() is invoked.
The "__memalign_hook" is used when requesting memory at a specific alignment, and we've probably missed
our opportunity to put the "__malloc_initialize_hook" to good use.
If you want to know exactly when a hook is being used, let me show you an easy way to find out, using
the malloc hook as an example.
I'll run the template script with the GDB and NOASLR options.
Then break with ctrl+c.
As we know, the malloc hooks take the form of function pointers.
Function pointers can be awkward in GDB because we can't set breakpoints on them.
Normally, one would simply dereference the function pointer to set a breakpoint at the address it points
to, but most of the malloc hooks are null at runtime, rendering this approach useless.
One way we can overcome this is to use hardware watchpoints.
The 'rwatch' command can be used to set a read watchpoint on the malloc hook, which will break when GLIBC
attempts to read from it.
If I continue, switch to the program window and request a chunk,
the program stops.
When I switch back to GDB, the source panel is showing the point at which malloc checks the malloc
hook to see if it's been populated.
In this case, it's still null, so malloc would continue as normal.
You can use watchpoints to find out when each of the malloc hooks we've identified gets checked and
therefore potentially dereferenced and called.
Armed with this information, see if you can use it to hijack the flow of execution.
Pause the video now and do your best to drop a shell from this modified house_of_rabbit binary.
Did you get a shell?
If so, great work! If not, let's figure out how we can together.
Before the pause, we were deciding which malloc hook to hijack,
so let's narrow it down.
The free hook's virtual address ended in an 8, putting it beyond the reach of our 16-byte-aligned
quadword
arbitrary write.
The realloc hook has the same problem, so scratch that too.
The malloc hook seemed promising, but gets overwritten prematurely with unsortedbin metadata, causing
a segfault as we try to overwrite it with controlled data.
The memalign hook is difficult to trigger, and the malloc_initialize hook is, unsurprisingly, only used
during malloc initialization.
That just leaves morecore and the after_morecore hook, which exist to allow developers to patch in their own
system memory request routines.
If you've been experimenting with watchpoints or browsing the malloc source code, you'll have noticed
that both of these hooks are used when the default heap is extended.
Let's see this in action.
Fire up the template script we were using earlier, passing it the GDB and NOASLR options.
Break and set read watchpoints on both the morecore and after_morecore hooks.
Continue in GDB, then switch to the program window.
To extend the default heap, we need to make a request that can't be serviced from any of the bins and
is larger than the top chunk.
It must also remain below the mmap threshold to avoid being allocated via mmap().
There's currently a 0x60000-sized chunk in the largebins, the top chunk holds approximately
0x21000 bytes and the mmap threshold stands at 0x61000.
That leaves a window of chunk sizes between 0x60010 and 0x60ff0 to trigger top
chunk extension.
Request a chunk somewhere in that range
then switch back to GDB.
The backtrace panel shows that sysmalloc() was called, which we know is responsible for requesting
memory from the kernel, indicating that we've successfully triggered top chunk extension.
When a watchpoint is hit, execution stops on the next instruction.
So right now we find ourselves in the function
the morecore hook was pointing to: __default_morecore(), which is clearly just a thin wrapper around sbrk().
Now that we've confirmed we can reliably trigger calls through the morecore hook, let's see if it's viable
for dropping a shell.
Issue the 'regs' command to dump the CPU register contents.
We know the 64 bit Linux calling convention dictates a function's first argument is stored in the rdi
register, which in this case holds the value 0x60000.
This isn't our request size, nor is it the normalized request size, it's the amount malloc wants
to extend the heap by.
This value will only be incremented in page sizes, meaning we can't pass an accurate value, for example
the address of a "/bin/sh" string to the morecore hook function.
We'll have to rely on a one-gadget, so let's see what's available for this GLIBC build.
Of course, if you're not using zsh, you'll need to replace "zsh" with "bash" and if you want
to use the libc() shell function we created earlier in a new shell like this, you'll need to add it
to your .bashrc or equivalent.
The 'one_gadget' tool reveals we have 3 options with relatively simple constraints; either rax,
stack pointer plus 0x30 or 0x50 must be null.
Clearly rax isn't null, and the 'stack' command shows us that, sadly neither are the required stack
offsets.
We know that these constraints aren't as strict as they appear, but remember, the value at the first
offset must at least be a readable address for the one-gadget to work.
It looks like the morecore hook might not be viable, at least not for this GLIBC build.
So let's continue in GDB and wait for the after_morecore hook watchpoint to fire.
Once it does, let's check the registers and stack.
rax isn't null but stack pointer plus 0x30 is.
Hold on a second, though, you could be forgiven for thinking this might work, but we haven't actually
reached the code that calls into the hook yet.
Watchpoints are great for finding out when a hook is being used, but to determine whether they're
viable for use with a one-gadget, it's best to corrupt the hook, then wait until execution stops at
the exact point the hook gets called.
This scenario demonstrates that perfectly, the source panel shows us that the after_morecore hook has
just been read, but there's a check between that and the line on which the hook is actually called.
Of course, here, the hook won't be called because it's null.
But if we populate the rsi register, which at this point should hold the after_morecore hook value, we
can continue as if the hook was in fact populated.
Cast your eyes over the instructions in the disassembly panel.
The next instruction xors the eax register with itself, in effect, zeroing it.
Interestingly, in x86_64 operations on 32 bit registers clear the most-significant bits of the corresponding
64 bit register.
If we execute this instruction with the 'stepi' command, shortened to 'si',
it results in the entire rax register getting zeroed.
This means our one-gadget with the rax == null constraint just became viable.
Returning to the disassembly panel, we can see the call into the after_morecore hook here. Remember
rsi holds the hooks contents at this point.
But there's still more code to be executed, first step 2 instructions with 'si 2'.
And notice that now the source panel is showing different code entirely.
This is a side effect of optimization, because efficiency is important in something as ubiquitous as
a standard library, GLIBC binaries are compiled with a lot of optimizations enabled.
This can result in operations being conducted in a different order to what's laid out in the source
code.
That's great for runtime efficiency, but makes debugging rather challenging.
Unfortunately, part of this mostly unrelated code clobbers the null quadword sitting at stack pointer
plus 0x30.
If we step 2 more instructions and inspect the stack,
we can see the damage.
Fortunately rax is now null,
so we still have a viable one-gadget. Of course, all of this could have been avoided if we'd just
corrupted the target hook, let the program crash, then inspected the register and stack state in the
aftermath.
Nonetheless, as I dragged you through this process, perhaps you learned something. Maybe that was
never trust a watchpoint, maybe it's that attention to detail really matters in exploit development.
Now that our latest malloc hook adventure is over, having chosen a viable candidate to get our shell
from, let's wrap up this exploit.
Close GDB and return to your editor.
We only need to change 3 existing lines, then add a fourth to make this work.
Let's start by replacing the free hook with the after_morecore hook on the line that calculates the distance
between our fake chunk and the arbitrary write target.
To account for this change, a small modification to the size we add to our fake chunk is also required.
Remember, we do this to ensure there's enough memory left over after the big gap-spanning allocation
to accommodate a 0x90-sized chunk that will overlap the target hook.
On the line that was previously used to overwrite the free hook, remove the second pack() call and replace
the null in the first pack() with the address of the one-gadget we picked, the constraint of which is
rax == null.
If you've forgotten the offset, you can just copy this one.
Lastly, we need to trigger the after_morecore hook by making a request that falls into this size window
for top chunk extension that we calculated earlier.
Once you've made those final changes, run your exploit under production conditions.
And you should get a shell.
Let's summarize what we've covered in this video.
Our binary was a modified house_of_rabbit pwnable, with the added restrictions of not being able to
request fast chunks and 1 quadword less control over user data.
The bug, a double-free, was the same as in the previous house_of_rabbit binary as was much of our
exploit.
Our primary objective was overcoming the inability to request fast chunks by employing so-called heap
Feng Shui. We already summarized this part of our attack, which involved crafting a fast-sized chunk
on the heap purely by using calls to malloc() and free(), not too dissimilar to when we generated a free
chunk using only calls to malloc() in our House of Orange exploit in Part 1.
Our secondary objective was dealing with less chunk user data control, which caused us to reevaluate
our choice of malloc hook to hijack.
We learned that we could trigger the morecore and after_morecore hook by forcing malloc to extend the default
heap's top chunk.
Along the way, we played with some useful GDB features like watchpoints and record mode. Watchpoints
are great and you've probably come across them before.
Record mode is less well known and has its limitations,
it doesn't like certain instructions,
it ignores hardware watchpoints, and it seems to have some trouble with remote debugging.
Nonetheless, it can make life a bit easier by reversing actions you've taken to avoid restarting an
entire process or by helping you track down elusive bugs.
As always, there's a commented exploit script available for download from this section's resources.
In the next video, we're back to learning new malloc internals and exploitation techniques, specifically
the tcache and Tcache Dup technique.
I'll see you there.