In this video, we're learning about the House of Corrosion, a collection of three primitives derived
from existing heap exploitation techniques. Primitive three gets a little outlandish, but you never
know what might come in handy during exploit development.
This will be our first leakless technique, and I'll be showing you one way to leverage it for a shell.
Navigate to the house_of_corrosion directory, where
we'll check out the vulnerable binary.
checksec reveals that it implements most Linux exploit mitigations and is linked against GLIBC
version 2.27
Specifically, the build that ships with Ubuntu 18.04. Using production GLIBC binaries has its drawbacks,
as we'll see shortly.
But I'd like to prove that this technique works against the real thing.
Let's run the house_of_corrosion binary under GDB to familiarize ourselves with it.
If you'd like to find the bug yourself, pause the video and do that now.
We can request a rather large number of chunks, at least by our standards, with option one, they're limited
to 0x3b00-sized and below.
Option two lets us free those chunks by index.
While we're here, let's check for a double-free bug.
No such luck.
Option three allows us to write data into our chunks.
We can write no more than 10 bytes.
Let's check for a write-after-free bug by attempting to edit the already free chunk zero.
The program doesn't complain, and inspecting its heap reveals that we've written 10 bytes into a free
chunk.
Our data has clobbered a 0x20 tcachbin forward pointer, plus two bytes of the succeeding quadword.
So our bug is a 10 byte write-after-free, starting at the first quadword of chunk user data.
This is, on its own, a pretty powerful bug.
We've leveraged smaller overflows for a shell before, and the involvement of the tcache makes it particularly
potent.
So what's stopping us from simply tcache duping over the free hook and being done with it?
Well, if I start the program over, we can see that it doesn't leak any addresses for us.
In the past, we've been able to forge our own leaks by reading data from a corrupted heap, but you've
no doubt noticed that this program doesn't feature a read option.
This, combined with the fact that the binary is position-independent and subject to ASLR, makes it
very challenging to forge an infoleak primitive.
It can still be done, and we'll learn how in a different technique.
But for now, we're going to continue without that knowledge.
We'll develop our exploit as if this program provides absolutely no output, just know that the
menu is only there to ease debugging and experimentation.
This will hopefully make a little more sense once we've learned more about the House of Corrosion,
so let's crack on.
Our inability to leak any addresses puts us in a tough spot.
What good is being able to tamper with tcache or fastbin linked lists if you don't know the address
of a fake chunk?
This is where the nature of our write-after-free bug becomes important.
If you've taken the time to experiment with this binary, you may have noticed that when it writes data
into its chunks, it only writes the number of bytes provided by the user.
This sounds obvious, but it could just as easily have been padding our input to the full 10 bytes if
we only provided, say, five bytes of input.
Because we control the length of the write-after-free
we can make partial overwrites of in-line malloc metadata.
I'll request, then free a couple of chunks to show you what I mean.
Note the value of this tcachebin forward pointer's least-significant bytes.
I'll leverage the write-after-free bug to write a single byte into the chunk holding that fd, using
ctrl+ d to terminate my input without sending the newline character.
Breaking and inspecting the heap afterwards shows that only the least-significant byte of the forward pointer was
overwritten.
These partial overrides can be very powerful because they mitigate much of the efficacy of ASLR. In this
case my ascii zero, represented by a 0x30 byte, has nudged that tachebin fd away from the chunk
it was pointing to and into the tcache itself.
I could take advantage of this to allocate a chunk into the tcache and tamper with an entry field.
Of course so far we're limited to the heap,
perhaps useful in a production binary, but our program doesn't store any sensitive data here, so we'd
like to venture further afield.
You know by now that a great way to write arena addresses onto the heap is to free a chunk into
one of the doubly-linked lists: the unsortedbin smallbins or largebins.
We can do that by filling a 0x90 or larger tcachebin or more easily by freeing a chunk outside of the
tcache range: 0x420 and above.
I'll start the program again and free a chunk into the unsortedbin.
Now we've written arena addresses onto the heap.
We still can't leak them, but we can use our write-after-free bug to tamper with them.
That could allow us to employ one of several heap exploitation techniques that we've already learned,
such as the House of Lore or unsortedbin attack.
If you want to put any ideas you've had so far to the test, now is a great opportunity to pause the
video and do so.
Once you're done experimenting, it's no doubt become clear that the prerequisites for many techniques
are difficult to satisfy without any leaks.
Furthermore, since we can only write up to 10 bytes into a chunk, we're limited to tampering with
an fd and just the two least-significant bytes of a bk.
In this situation, the unsortedbin attack stands out as the technique with the fewest prerequisites.
We simply modify an unsortedbin bk, which allows us to write the address of an arena's unsorted
bin to an arbitrary location.
In HeapLAB Part 1 we witnessed how powerful the unsortedbin attack can be when we used it to
get a shell via file stream exploitation in the House of Orange,
so we know it has potential.
What's stopping us from simply recreating that same exploit is that we needed both a libc leak and the
address of our fake vtable to make it work.
Here we have neither of those.
We can, however, use the partial overwrite technique I just demonstrated against the tcache to target
nearby data with our unsortedbin attack.
Let me show you what I mean.
Because our target process is subject to ASLR, it seems like we can't know the address of, well,
anything, let alone the unsortedbin.
But that isn't strictly true.
Under x64 Linux, the three most-significant bytes of library addresses are typically 0x00007f
the top end of the low canonical address range, something we learned whilst crafting our House of
Orange exploit in Part
1.
You'll sometimes see 0x7e instead of 0x7f
and memory pressure could influence this value further,
but most often we can reliably assume an 0x00007f address for libraries and runtime mappings.
x64 memory mapping has page-level granularity, meaning that the least-significant 12 bits of these
addresses will also be constant,
something we took advantage of in the previous lecture.
In our case, these bits of the unsortedbin address are 0xca0.
This will, of course, differ between GLIBC builds.
This leaves only 28 bits of any given address subject to ASLR entropy. Due to the little-endian nature
of x64 architecture, our partial overwrite allows us to tamper with the least-significant bytes of an
address without affecting the others.
Overwriting just the least-significant byte of an unsortedbin bk allows us to target an unsortedbin
attack anywhere from this address to this address without having to guess any ASLR bits.
Because our target program is single-threaded, we're guaranteed that the unsortedbin we're tampering
with belongs to the main arena, located within the libc binary's data section.
This means we can unsortedbin attack not just arena metadata, but libc data as well.
If this were a multithreaded program and our thread wasn't sharing the main arena, we could potentially
start new threads until it was.
It depends on how much control we had.
Unfortunately, there aren't any viable targets within range of this unsortedbin.
Even if we overwrote the top chunk pointer with the unsortedbin address and allocated into the
arena, we're back to square one because we can't write any useful addresses here, having yet to leak
any.
If we were willing to overwrite the two least-significant bytes of an unsortedbin bk, guessing
four bits or a nybble of ASLR entropy, we could vastly increase the range of our unsortedbin
attack, covering more GLIBC targets like my _IO_list_all for example.
Of course, we've already established that file stream exploitation via _IO_list_all isn't viable here
because we need two leaks that we don't have.
But that doesn't mean there aren't other equally good targets.
To understand which GLIBC variable we're going to subject to our unsortedbin attack, we're going
to step back in time to 2005, when the malloc maleficarum was published.
We've already learned three of the five techniques from the malloc maleficarum, the houses of Force,
Lore and Spirit.
Now we're going to take a look at the House of Prime.
It works like this:
Leverage a heap bug to set a chunk size field to a value below the minimum chunk size.
When that chunk is freed, it will be linked into the nonexistent fastbins minus one slot, writing
a heap address to that location.
The value at fastbins minus one is part of the same arena and holds a variable named max_fast.
max_fast dictates the maximum size of chunk that can be safely linked into the fastbins.
We know that under default conditions, there are 10 usable fastbin slots in an arena.
But overwriting the max_fast variable with a heap address via the House of Prime allows us to link
chunks into nonexistent fastbins with indices much higher than 10.
In this way, the House of Prime gives us the following primitive: write a heap address over variables
succeeding the fastbins array, including arena metadata.
And since we're using the main arena, this includes libc metadata, too, such as the stdio
file streams.
This is not dissimilar to what our theoretical unsortedbin attack from earlier could achieve, except
this time we're writing heap addresses instead of the unsortedbin address, and the range is potentially
much larger.
Unfortunately, the two methods for hijacking the flow of execution with this technique described in
the malloc maleficarum rely on the absence of two exploit mitigations that have since become commonplace
ASLR and NX.
What's more, in 2006 GLIBC 2.4 introduced two changes that mitigated the House of Prime:
a size sanity check in free that caught chunks below the minimum allowed size, and the max_fast variable
was moved out of arenas and into a global variable aptly named global_max_fast.
So what does a defunct technique from 2005 have to do with 2019's House of Corrosion?
Well, it turns out we can revive the House of Prime using an unsortedbin attack against that global_
max_fast variable.
We can also derive some new primitives from it and use them to drop a shell.
There's a lot to cover in the House of Corrosion, so we'll take it step by step, beginning by getting
our bearings within the pwntools template script.
The first thing you'll notice is that the libc object is initialized a little differently to how we've
done it in parts one and two.
Rather than parsing the actual libc shared object, we're reading symbol information from a separate
binary located within a .debug directory
in this program's runpath. This is a detached debugging symbols binary, and it holds information
on symbols like main_arena that are required by pwndbg's
heap introspection commands.
Debugging symbols like this aren't normally included in production libc binaries because they take
up so much space.
I include them in the libraries I build for this course to make debugging more convenient,
but the difference is stark
if you compare the size of the production libc this program is using to one of my debug builds a different
binary uses.
That's my libc shell function from Part 2 by the way.
It's in the PDF now if you didn't add it to your environment back then.
Some distributions provide these detached symbols via their package manager.
For example, Ubunto makes them available via the package named libc6-dbg.
Not all distros do this mind, Arch Linux, for example, does not provide an equivalent package.
The presence of these detached debugging symbols are what allowed us to inspect this program's heap
with pwndbg a moment ago.
Without them, debugging would be much more arduous.
GDB detects symbol files like this automatically, but at time of recording pwntools does not.
Which is why we have to feed them to it manually.
This is a bit of a hack and the checksec equals false argument is required to dodge an exception.
But the alternative; locating symbols manually, is far more time consuming.
There's also a rather long GDB directive, that's just so we can view the source code for this production
GLIBC build as we debug.
As usual, the script provides wrapper functions for each of the program's menu options, malloc, free
and edit.
There's also a malloc_chunksize function that simply performs some rounding so we can request chunks
of a specific size directly.
You don't have to use it, but I find it helps make exploit scripts more readable.
The last functions, gmf_chunksize and gmf_malloc we'll get to later.
At the bottom of the script is a ready-made example of how we might leverage our partial overwrite to
make a blind unsortedbin attack .The first three lines should be familiar;
request a chunk large enough to qualify for the unsortedbin when freed, followed by a top chunk
consolidation
guard, then free the large chunk into the unsortedbin.
The next line looks complicated, but all it's doing is using the write-after-free bug to clobber the
two least-significant bytes of that unsorted chunk's bk.
First, its fd is nulled, then pwntools's p16 function is used to write two bytes over the bk.
Remember, this program only lets us write up to 10 bytes total,
so this is our limit.
The target of our unsortedbin attack is the global_max_fast variable I mentioned a moment ago.
We prepare for this by masking off everything except the two least-significant bytes of that symbol's
address.
Of course, we can't know that symbol's address
and indeed, if you don't explicitly tell pwntools where a binary is mapped in memory, calls like
this simply return a symbol's offset from the beginning of its binary.
We can still make use of this information, however.
Recall what we know about the bk we're overwriting: it holds the address of the main arena's unsorted
bin, which has some fixed bits.
The address of our target, global_max_fast, also shares the same fixed bits.
Furthermore, the distance between these addresses is fixed.
global_max_fast will always be mapped only two pages after the main arena's unsortedbin, at least
in this GLIBC build.
That means by overwriting the two least-significant bytes of one address and successfully guessing one
nybble of ASLR entropy, we can convert it into the other address.
For example, if that entropy nybble were six in the unsortedbin address, it would be eight in the
max_fast address.
Rather than doing this addition ourselves, pwntools will take care of it if we tell it our guess for
that ASLR nybble in the libc load address.
There's nothing special about the number seven, but note that three and four are to be avoided. If the
libc binary is loaded at an address with a three or four in the least-significant nybble of ASLR
entropy,
our unsortedbin attack cannot succeed.
This is because the unsortedbin ends up being mapped at an address with an e or f in the nybble
we're trying to guess. Adding two to this value wraps around and increases the next most-significant
nybble of the address, which we're unable to tamper with without having to guess a further eight bits
of ASLR entropy.
We can see an example of this if we run our template script with the GDB and NOASLR options.
Wait for the process to stop.
Then check the libc load address.
It ends in 0x4000, one of our "unlucky" libc load addresses.
This results in the unsortedbin getting mapped at an address that ends in 0xfca0. Remember
the unsortedbin is treated as a chunk, so it technically starts at an arena's top chunk pointer.
We know global_max_fast is around two pages beyond this address, and we can see that its next most-
significant byte is a different value; 0xdd rather than 0xdc.
This has caused our unsortedbin attack to miss by a fair margin, hence the segfault. The source panel
is showing the moment our unsortedbin attack occurs, our corrupt bk was followed and the address
of the unsortedbin written over the fd at its destination.
Checking the mapping for our corrupt bk shows that it points into non-writable memory. The tail end
of the address should have read 0xdd1930
rather than 0xdc1930.
This wouldn't be an issue for us if it weren't for the fact that GDB picks this address every time when
ASLR is disabled, forcing us to debug our exploit with ASLR on.
This shouldn't slow us down too much.
It just means that whilst debugging our exploits, it will fail
roughly 2 out of 16 tries when ASLR chooses one of those "unlucky" libc load addresses.
There are some workarounds for this.
We can perform limited VA space Feng Shui with the ulimit command.
By shimming other libraries with LD_PRELOAD or limiting the available space with LD_PREFER_MAP_32_BIT_
EXEC.
Unfortunately, some of these solutions won't work on setuid binaries.
Some of them make radical changes to the libc load address, and all of them are only available locally.
Here at HeapLAB, we like to make things as difficult as possible for ourselves.
So we're going to continue as if we were attacking remotely.
Close GDB and return to your editor.
We're going to run this template script again, this time without the NOASLR option.
Normally that would mean we could only debug our exploits on the 1 in 16 chance, we correctly guessed
that least significant ASLR nybble, rather inefficient.
So this line helps us cheat whilst debugging by always guessing that value correctly.
We still can't do anything about the bad libc load addresses, though, so if the process segfaults,
just try again.
Run the script with just the GDB option.
Break and type vmmap libc- or alternatively, lm libc-.
Your libc load address will differ from mine, but if no segfault occurred, it should end in something
other than 0x3000 or 0x4000.
We can also see the byte that differed between the addresses of the unsortedbin and global_max_fast
when we hit a bad libc load address is now the same between those addresses.
Now, overwriting just the two least-significant bytes of the unsortedbin addressed can reliably
convert it into the global_max_fast address.
And that's exactly what our script has done.
It's leveraged the write-after-free bug to partially overwrite an unsortedbin bk, converting it
to the global_max_fast address minus 16, because that's how we target unsortedbin attacks.
The last line of our script simply allocates the corrupt unsorted chunk, triggering the attack.
Printing the global_max_fast variable reveals that we've replaced the default value of 128 or 0x80
with the unsortedbin address.
Remember, this value is interpreted as the largest chunk size that can be linked into any arena's fast
bins.
In this state, malloc will mistake large chunks as being eligible for fastbin insertion when freed.
This is the House of Prime primitive I described.
We just set it up differently to the original method.
It begins when a chunk is assessed for fastbin eligibility inside the _int_free function.
Just a note on debugging production libc binaries; if you set a breakpoint on _int_free, for example,
you may have noticed GDB sets multiple breakpoints.
That's because production libc binaries are highly optimized, so the compiler will do things like
inline functions into other functions to avoid the overhead of a call.
Only one of these breakpoints is the actual _int_free function.
The rest are the same code inlined into things like __libc_free and other locations where _int_free is
called.
Knowing this isn't crucial to exploit development, but it can be confusing if you're not aware of it.
Anyway, back to fastbin eligibility in _int_free.
The get_max_fast function simply returns global_max_fast.
At first glance, there appears to be a mitigation against what we're planning.
But this code isn't present in the final binary, builtin_unreachable is just an optimization hint
for the compiler.
After that, the fastbin into which the free to chunk will be linked is determined by the fastbin_
index macro, which we know shifts the chunk's size field four bits to the right then subtracts two.
For example, we've seen how a 0x20-sized chunk is expected to land in fastbin zero. 2 shifted right
by four bits is 2, minus two is indeed zero.
The gmf_chunksize function in our script, short for global_max_fast chunksize, essentially performs
the inverse of this operation. Having corrupted global_max_fast
we can pass it an address or more accurately an offset within the libc binary, and it will tell us
the size of chunk we need to free to overwrite that location with a heap address.
Let's see this in action.
Return to your editor where we'll request a large chunk to free.
When we request this chunk is important, by the end of this script, we've triggered an unsortedbin
attack, destabilizing the unsortedbin, making it likely an abort will occur on the next request.
But malloc won't be servicing requests from the unsortedbin because it now thinks the fastbins can
service almost any sized request
thanks to our global_max_fast corruption.
This will result in malloc attempting to allocate chunks from what it thinks are fastbin slots,
but in reality are just bits of GLIBC data, most likely resulting in a segfault or an abort.
This forces us to make our requests before triggering the unsortedbin attack.
Let's just reuse the top chunk consolidation guard.
I'm going to target the stderr vtable pointer.
I know I said file stream exploitation was off the cards because we can't leak any addresses,
but we'll see about that. For now
just play along.
We can use the scripts gmf_malloc helper function, which is just a wrapper around gmf_chunksize and
malloc_chunksize to request the appropriate sized chunk to corrupt a specific target,
just pass it an address.
This is GLIBC's stderr symbol, and it's vtable pointer resides at offset 0xd8 into a struct,
something we learned in the House of Orange.
Once you've modified that line, run your script with just the GDB option.
Remember, if you encounter a segfault in the unlink macro, check the libc load address and try again.
Break and let us get our bearings.
The heap looks like this, with only two chunks between the tcache and top trunk.
The first is our free, unsortedbin attack chunk with its corrupt bk.
Inspecting the global_max_fast variable confirms that attack was successful.
The second chunk is still allocated and its size was determined by the gmf_chunksize helper function
in our script.
We told it that we wanted to target the stderr vtable pointer. Each fastbin slot is one
machine word, in our case eight bytes wide.
So to determine which fastbin slot overlaps our target, we simply calculate the distance between
it and fastbin slot zero,
then divide by eight.
Of course, fastbin slot
0x161 doesn't exist, but thanks to our global_max_fast corruption, malloc thinks it does.
To calculate the size of chunk that, when freed, will be linked into fastbin slot 0x161.
We just invert the fastbin_index macro: add 2 then shift left by four.
This yields the size of our second chunk, 0x1630.
0x1630.
It's this whole process that the gmf_malloc helper function is automating for us.
Let's see if it works.
Right now our target, stderr's vtable pointer, points to where it should, into libc. Specifically
into the _IO_file_jumps table.
Now let's free that large chunk at index 1. We're hoping malloc will check its size against the
corrupt global_max_fast value, determine it's eligible for fastbin insertion and attempt to
link it into the imaginary 0x1630 fastbin, which happens to overlap stderr's vtable
pointer.
And it looks like we were successful.
We've just written a heap address to a more or less arbitrary libc location without leaking any addresses.
This is the first primitive of the House of Corrosion.
It's essentially the same outcome as the House of Prime, but via a blind unsortedbin attack against
global_max_fast.
Before we move on, if you've been inspecting the _int_free source code, you may have noticed a check that
should have mitigated this primitive.
After linking a chunk into a fastbin, the chunk that was previously at the head of that bin, labeled
as old here, is subject to the fastbin size integrity check.
It's the same one we have to bypass in our fastbin dup exploits, just implemented in free rather than
malloc.
The size of old is being compared to the size of chunks that should reside in its fastbin.
This check scuppers primitive 1 because old is not a pointer to a legitimate chunk, it's whatever
value our target variable held before we overwrote it with a heap address.
You can see that if the target is null, we get away with it.
But if it's not, there's a high likelihood we'll end up aborting or segfaulting.
Yet we haven't. Our attack was successful, and we know that our target, stderr's vtable pointer,
wasn't null beforehand.
It turns out the have_lock variable came to our rescue.
This check only fires when have_lock, which is an argument to _int_free, isn't zero.
However, __libc_free always calls _int_free with have_lock set to zero.
So this check is never performed unless _int_free is called from a different function that locks the
arena first, such as realloc.
So that's why primitive 1 works despite the existence of this check.
What separates this technique from the House of Prime are its second and third primitives.
Consider what we've just done in terms of malloc internals.
All that really happened was that a chunk got linked into a fastbin.
This familiar process has two parts:
The second we've just witnessed, malloc writes the freed chunk's address into the appropriate fastbin
slot in an arena.
The first part involves maintaining the fastbins linked list by copying the value currently occupying
the target fastbin slot into the freed chunk's fd.
We can confirm this by passing the --verbose flag to pwndbg's heap command.
Our large chunk's fd now holds the original value of stderr's vtable pointer.
This isn't a huge help to us directly, because this chunk is free and we don't have a read-after-free
bug, but we do have a write-after-free, allowing us to tamper with this value.
Since we've already guessed four bits of libc load address entropy, we can now safely overwrite the
two least-significant bytes of any libc address without having to guess again.
Of course, tampering with this vtable pointer and leaving it on the heap isn't much use to us.
We can't leak it and using it as a fastbin fd still requires passing the fastbin size integrity
check.
This is where House of Corrosion primitive number two comes in handy.
What would happen if we were to now allocate our 0x1630-sized chunk?
Well, malloc would consider our request within fastbin size range and attempt to allocate from the
same nonexistent fastbin slot we've just corrupted; stderr's vtable pointer.
That slot currently holds the address of our 0x1630 chunk,
the result of primitive one.
When we allocate this chunk, primitive one will be reversed.
The chunk's fd will be copied into the 0x1630 fastbin slot, restoring the vtable pointer,
including any modifications we've made to it, back to its original location.
In this way, primitive two builds on primitive one to allow us to tamper with arbitrary libc data.
Let's return to our script and try this for ourselves.
Begin by naming the 0x1630-sized chunk, I'll just call mine large_chunk.
We'll need to free this chunk, as I did manually a moment ago.
This triggers primitive one writing a heap address over stderr's vtable pointer and copying
its original value onto the heap.
Now we can tamper with that pointer.
I'm just going to point it somewhere nearby, a different table called _IO_str_jumps rather than
_IO_file_jumps.
We'll see the significance of this later.
As with our unsortedbin attack, we use an and mask to write just the two least-significant bytes
of this address.
Lastly, we'll need to allocate large_chunk a second time to restore our modified vtable pointer
to its original location.
Having made those changes, run your script with the GDB option until it's stable.
Break then issue the lm command to see where stderr's vtable pointer is pointing now.
As expected to the closest symbol is _IO_str_jumps, which we can confirm should be _IO_file_
jumps by issuing the same command against the stdout
and stdin file streams.
So that's House of Corrosion primitive 2: modifying libc variables in place.
There's still much more to learn
so let's briefly review what we've covered so far.
Primitive 1 is simply a modern House of Prime. Having corrupted global_max_fast, freeing large
chunks results in heap addresses being written over sensitive GLIBC data, provided our arena
is the main arena.
A side effect of primitive one is that the target GLIBC variable is written to the heap. Primitive
2 takes advantage of this, leveraging the same bug that we used to corrupt global_max_fast to partially
or completely overwrite the target variable whilst it resides on the heap.
Having done this, allocating the large chunk used in Primitive 1 copies the modified GLIBC
variable back into its original location.
These two primitives on their own are very powerful and only require a handful of mallocs and frees
to trigger. They can even be used to leak a libc address from a program like this one that won't
allow us to read from the heap.
We'll learn how to do that later, but for now we'll keep treating this program as if we can't leak
anything from it.
This brings us to Primitive 3, arguably the most powerful, but also complex
so I've reserved a separate video for it. Before moving on
make sure you're comfortable with primitives 1 & 2.
There are example pwntools scripts available from this lecture's resources if you need them.
When you're ready, I'll see you in the next video.