In this section we're going to learn what sort of damage we can do with a single null byte overflow,
sometimes called a poison or poisoned null byte.
The term was coined in 1998 by Olaf Kirch, who used it to describe a single null byte overflow on the
stack. In 2014
Chris Evans of Google Project Zero published a blog post titled "The Poisoned Null Byte 2014 Edition"
in which they describe a single null byte overflow on the GLIBC heap, which they leveraged for a root shell.
I've linked to that blog post in this lecture's resources and I recommend checking it out.
We're going to learn two different ways of exploiting poison
null bytes on the heap,
then you can decide which one you want to use against our next challenge binary.
We'll begin our poison null byte adventure by learning a technique known as the House of Einherjar. The waters
are a little muddy as to the origin of this technique,
but in my opinion credit goes to Chris Evans because it was this technique that was used in their
pkexec exploit.
Nonetheless, two years later in 2016, Hiroki Matsukuma, a pentester and member of Tokyo Westerns
CTF team, published slides describing their House of Einherjar technique, which takes advantage of a single
null byte overflow into a chunk's size field in much the same way as in Chris Evans exploit.
If I were feeling pedantic, I'd say credit is really due to Solar Designer, since, as you'll see,
this technique is really just a different take on the Safe Unlink.
Whatever your opinion on the matter, navigate to the house_of_einherjar directory, where you'll find the
binary that's going to help us learn this technique.
Begin, as we usually do, by seeing what protections our binary has enabled.
Checksec tells us that the house_of_einherjar binary has the protections we've come to expect; full
RELRO, stack of canaries and NX.
It's non-position-independent for the usual reasons, and is linked against GLIBC version 2.28
compiled without the tcache.
Let's check out how this binary works by running it under GDB.
Straight away we're asked for a username, I'm sure you can guess what the "username" field is going
to be used for
so let's just enter some garbage for now.
Before the menu is printed, we're given a heap leak. The reason this leak occurs after we're asked to
input a username will become apparent later.
From the program menu, it looks like we can request up to 3 chunks with option 1.
Their sizes are limited to small chunks, excluding the fastbin range, so I'll request a couple of
0x90-sized chunks.
With option 2 we can free these chunks by index.
And with option 3 we can write data into them.
Of course, option 4 prints the target, which is once again part of a struct named "user", which
resides in this program's data section, holding both our username and the target data.
Breaking in GDB with ctrl+c and inspecting the "user" struct with 'ptype' and dump quadword reveals
the same layout we've become familiar with when forging fake chunk metadata.
Some data that we control, in this case our username followed by some sensitive data,
in this case the target, those X's.
Inspecting the default heap with the 'vis' command shows us what we expect; two 0x90-sized
chunks, the first of which has been freed into the unsortedbin, the second containing the data I
wrote into it.
As is now customary, I'll give you a moment to familiarize yourself with the house_of_einherjar binary
and of course, find the bug. Pause the video while you do that, then resume once you're ready.
Hopefully, my introduction to this lecture prompted you to check for a single null byte overflow,
which is indeed the bug we're dealing with here.
I'll load the house_of_einherjar binary into GDB and run it.
I'll request a chunk and fill it with garbage data.
Then I'll break and inspect the heap.
We can see that my Y's have filled this chunks user data, leading to a single null byte overflow
into, in this case, the top chunk's size field.
If you're curious, the overflow occurs because after this program reads the correct number of bytes
into a chunk's user data, it then appends a null byte in an attempt to terminate the string.
As I mentioned earlier, this is one of the more common ways in which single null byte overflows can
occur.
So let's consider what we can do with this bug, clearly overflowing into the top chunk's size field
like this isn't going to have much impact.
It only changes the size by a small amount, making it neither a particularly small nor large value.
We'll do a lot more damage if we overflow the single null byte into a normal chunk's size field, and
there are two things we need to take into account before we do.
The first is whether the victim chunk is allocated or free.
We're actually going to target both in this section,
but for now the House of Einherjar requires us to pick an allocated chunk.
The second thing to consider is the size of our victim chunk, if we target a chunk with a single byte
size field, the overflow will the zero it and chunks with zero size aren't very useful.
But what if our victim had a size that ended in zero, for example 0x100.
I'll start the program again and set us up to overflow into a 0x100-sized chunk.
As you can see, we've got a 0x90-sized chunk to overflow from, followed by a 0x100 chunk.
Now, the least significant byte of the 0x100 chunk isn't zero, it has a set prev_inuse bit.
So while overflowing a single nail byte into its size field won't affect its size value, it will clear
that prev_inuse flag.
Cast your mind back to HeapLAB Part 1, when we leveraged the Unsafe Unlink technique. There we
also used an overflow to clear a chunk's prev_inuse flag, just with a different chunk size and a more controlled
overflow.
The House of Einherjar and the Unlink techniques share this starting point, clearing the prev_inuse flag on
an allocated chunk.
And yes, both Unlink techniques can be leveraged using a single null byte overflow, provided you
can target a chunk size field that already ends in a zero byte.
You'll recall that when we freed the victim chunk in the subsequent stage of the Unlink techniques,
malloc erroneously thought that the previous chunk was free and was tricked into consolidating the
victim chunk with a fake chunk that we had forged.
This is still the case in the House of Einherjar.
The only difference is that the end goal is not to take advantage of the unlinking process itself,
rather to simply pass the safe unlinking checks so we can allocate our fake chunk, which hopefully
overlaps other sensitive data, or at least some inline malloc metadata.
To achieve this, we will of course need the help of pwntools, so close GDB and make a copy of the
pwntools_template.py Python script.
Opened the copy in your editor of choice and scroll to the bottom.
As we've come to expect, there are some example uses of the helper functions I've written and the script
also takes care of the username input for us.
Normally, the code that captures heap or libc leaks is placed outside of the dashed lines, but in this case
it resides after the username code,
since the house_of_einherjar binary does things a little out of the order we've become used to.
We'll keep the username code, the heap leak capture and the 2 calls to the malloc() helper function, but
remove everything else.
This leaves us in the same state I showed you in GDB a moment ago with a 0x90-sized chunk A
followed by a 0x100-sized chunk B.
Let's add the next part of the exploit where we trigger the single null byte overflow into chunk
B's size field, clearing its prev_inuse flag.
Don't forget that in consolidation attacks we'll need to provide a fake prev_size field to help malloc
locate our fake chunk during the backward consolidation attempt
so we'll set up a placeholder value for now.
Use the edit() helper function to fill chunk A with garbage.
Followed by our counterfeit prev_size field in the last quadword of its user data.
Because we've filled chunk A's user data, the house_of_einherjar program should incorrectly null
terminate our input, clearing chunk B's prev_inuse flag.
Having added those lines, run your script with the GDB and NOASLR options.
Break with ctrl+c and issue the 'vis' command.
It shows us that chunk B's prev_inuse flag has been cleared by the overflow, marking chunk A as free
even though it's still allocated.
If we were to free Chunk B now, malloc would attempt to consolidate it with its previous chunk, in
doing so it would use Chunk B's prev_size field to locate that previous chunk.
We can see from the 'vis' command output that the last quadword of chunk A's user data has been repurposed
as chunk B's prev_size field, which of course we control because chunk A isn't really free.
In the Safe Unlink technique, we set our counterfeit prev_size field to a value that allowed malloc
to find a fake chunk we'd prepared in chunk A's user data, then unlink it in preparation for consolidation.
We're going to do the same thing here for the House of Einherjar, with two small differences.
The first difference is that our fake chunk won't reside on the heap.
Instead, we'll craft it in this program's data section, overlapping the target data.
The second is that we're uninterested in leveraging the unlinking process, we don't know the address
of any pointers to chunks because unlike the safe_unlink binary, this program keeps its chunk pointers
on the stack, which we haven't leaked.
All we care about is that the unlinking process is successful and that our fake chunk gets consolidated
with chunk B.
Let's return to our editor to forge a fake chunk and set our counterfeit prev_size field to an appropriate
value.
Hopefully by now you're familiar enough with forging malloc metadata that this part should come naturally
to you.
We're going to edit the "username" variable, the contents of which end up in that "user" struct in the
house_of_einherjar binary's data section, just before the target data.
The first quadword can take any value as it forms an unused prev_size field,
and we'll ensure that the fake chunk's size is at least large enough to overlap the target.
Of course, we also need this chunk to pass the safe unlinking checks, but we know how to do that
by now, set both forward and backward pointers to the address of the fake chunk itself.
With our fake chunk metadata ready, we just need to set chunk B's size field to the distance between
chunk B, which starts after chunk A and our fake chunk.
We're able to do this because we have a heap leak, but we'll see later that there are other ways to
leverage the House of Einherjar that don't require this.
We'll run our script with the usual options to make sure everything is in place before we go ahead
and free chunk B.
The 'vis' command shows our counterfeit prev_size field now has a value of 0x1080.
This seems very small and it is, we've disabled ASLR whilst debugging, which means that the default
heap gets mapped right after the program's writable segment.
Rest assured that when we enable ASLR to test our exploit under production conditions, that prev_size
field will take a much larger value.
If we use the 'dq' command to dump memory at the "user" struct, we can see our fake chunk.
It has a size field of 0x31, giving it more than enough user data to overlap the target
and both its fd and bk point to itself, allowing it to pass the safe unlinking checks.
When we free chunk B, malloc will check the surrounding chunks for consolidation eligibility. Because
we've cleared chunk B's prev_inuse flag with the single null byte overflow, malloc will attempt to consolidate
it backwards with its previous chunk. To locate the previous chunk, malloc subtracts the value of chunk
B's prev_size size field from chunk B's address.
In this case, 0x603090 minus 0x1080 equals 0x602010,
the address of our fake chunk in the program's data section.
malloc will unlink our fake chunk from what it perceives to be a linked list, then consolidate it with
chunk B.
If there were a chunk between chunk B and the top chunk, the newly consolidated fake chunk would be
linked into the unsortedbin.
But in this scenario, chunk B borders the top chunk
so both chunk B and our fake chunk will subsequently be consolidated into the top chunk.
Either way, this allows us to then allocate a new chunk from memory overlapping our target data.
So let's go ahead and free chunk B manually. Continue in GDB, alt+` to the program window, hit return
to print the menu and use option 2 to free the chunk at index
1.
Ah, it seems we've triggered one of malloc's exploit mitigations, the error message reads "corrupted
size versus prev_size".
Incidentally, if you ever forget which exploit mitigation an error message is tied to, you can check
the "Mitigation error messages" heading under the "Mitigations" section of the HeapLAB PDF.
This mitigation is known, unsurprisingly, as the size vs. prev_size check and was introduced
in GLIBC version 2.26. Remember that the house_of_einherjar binary is linked against version
2.28.
The check is implemented in the unlink() macro, so we'll have to go digging through the source code to
find it.
alt+` to the GDB window and use the following command to view the source code surrounding the size
vs. prev_size check.
There's the error message and this is the check itself.
It's attempting to compare the size field of the chunk being unlinked with the prev_size field that led
it to that chunk.
Of course, those two values should match, and this check was introduced specifically as an attempt
to combat exploitation of single null byte overflows.
Fortunately for us, there are a couple of ways in which we could pass this check.
The first is simply to set our fake chunk's size field to match that of our bogus prev_size field.
However, the house of einherjar binary only leaks its heap start address after we've already input our
username, which contains our fake chunk metadata, and we're unable to go back and edit it.
This means that as soon as we enable ASLR, making the distance between the heap and our fake chunk unpredictable
until after we've already crafted our fake chunk, the exploit will fail.
There is a second way in which we could pass the size vs. prev_size check, and I want you to
pause the video and see if you can figure it out. In this context
'p' is the address of the chunk being unlinked.
The chunksize() macro is simply finding p's size.
The next_chunk() macro is using p's size field to locate its succeeding chunk.
And the prev_size() macro is just finding that chunk's prev_size field.
If you hadn't already figured this out, the flaw in this implementation of the size vs.
prev_size check is that it operates entirely from within the context of the chunk being unlinked, known
as the "victim" chunk.
It has no idea where the prev_size field that led it to that chunk resides.
This means that it has to rely on the size field of the victim chunk in order to locate the prev_size
field with which to compare it.
This can lead to a disconnect between the prev_size field the macro intends to find and the prev_size field
it actually finds.
Let's consider what's happening when our fake chunk undergoes this check.
The size of the chunk is determined by the chunksize() macro, which simply checks the second quadword at
the address it's passed. The result is accurate and in this case, yields 0x31.
This value is compared against a prev_size field, which is found as follows:
The next_chunk() macro attempts to find the succeeding chunk by adding the victim chunk's size to its
address, here 0x602010 one plus 0x30 yields 0x602040, which is not the address
of chunk B, rather this random patch of memory in the program's data section.
Next, the prev_size() macro simply returns the value at that location.
The result is that instead of comparing our fake chunk's size filled with chunk B's prev_size field.
It's compared against memory at an offset from our fake chunk that we control.
Sadly, in this case, we can't influence any memory after our fake chunk in which to provide another
bogus size field, but we don't actually need to.
There is one value we can choose for our fake chunk's size that will always pass the size vs. prev_size
check.
That value is 8.
If our fake chunk size were 8, then the size vs. prev_size check would proceed like this; find the
succeeding chunk by adding the victim chunk's size to its address.
0x602010 plus 8 is 0x602018. Take that chunk's prev_size field: 8, and compare it against
the victim chunk's size: also 8.
The two values match and the check is passed.
So a size field of eight will always pass the size vs. prev_size check, despite not being a legitimate
chunk size.
Let's close GDB and return to our editor to modify our fake chunk's size field.
Having done that, add a call to the free() helper function to free chunk B, which hopefully now will
consolidate backwards with our fake chunk successfully.
Before we wrap things up, let's run our exploit script with the GDB and NOASLR options one
more time to see what state we've left malloc in.
Break and inspect memory at the "user" struct.
Not much has changed, except that our fake chunk's size now looks suspiciously like a top chunk size
and issuing the 'top_chunk' command confirms that the top chunk does indeed now reside in the program's
data section.
So we've ended up with a similar result to the House of Force.
Remember that chunk B was not only consolidated with our fake chunk, but it also bodered the top chunk,
meaning that the entire space between our fake chunk and where the top chunk previously was has now
been consolidated into the top chunk.
If chunk B didn't border the top, our fake chunk would have instead been linked into the unsortedbin.
This is a less desirable outcome due to the unsortedbin chunk size integrity check during backward
consolidation, a chunk's new size is calculated not by adding chunk size fields together, but rather
by adding the freed chunk's prev_size value to the freed chunk's size.
Once we enable ASLR and the distance between the heap and program's data section greatly increases, this
can result in a very large fake chunk getting linked into the unsortedbin, which will subsequently
fail the size integrity check when we try to allocate it.
As we know from the House of Force, GLIBC versions, 2.28 and below don't have any top chunk
size integrity checks,
so it's preferable to do things the way we have here by consolidating with the top chunk, unless you're
able to modify your fake chunk's
size field between consolidation and allocation.
So now that we've successfully completed the consolidation phase of the House of Einherjar, let's return
to our editor to request our fake chunk and make the arbitrary write.
The next request we make will be serviced from the top chunk, which now sits just before our target
data.
And we'll need to send a couple of filler quadwords to make up the distance between the start of our fake
chunk's user data that held our counterfeit fd and bk and the target.
Finally, let's run our exploit under production conditions without the GDB and NOASLR options.
Selecting option 4 from the menu prints our data, verifying that we were able to leverage the House
of Einherjar technique to make an arbitrary write.
To summarize, this binary has a single null byte overflow bug on the heap, which we used to clear
a chunk's prev_inuse flag.
The size of our victim chunk ended in a null byte, meaning that the overflow didn't affect its size value,
just its flags.
We provided a bogus prev_size field for the victim chunk, the value of which we set to the distance
between the victim chunk and a fake chunk we'd prepared elsewhere in memory.
When we freed the victim, its clear prev_inuse flag indicated to malloc that it should attempt backward
consolidation.
malloc used our bogus prev_size field to locate our fake chunk and consolidated it together with
the victim chunk.
For this to be successful, our fake chunk had to survive the safe unlinking check, which we passed
by providing counterfeit fd and bk pointers that pointed back to our fake chunk.
We also passed the size vs. prev_size check during the unlink procedure by setting our fake chunk's size
field to a value that will always pass the check; 8.
Once the victim chunk was consolidated with our fake chunk, it was also consolidated with the top chunk.
This resulted in moving the start of the top chunk to the address of our fake chunk.
We made one last memory request, which was serviced from the new top chunk location, overlapping our
target data, which we subsequently overwrote.
Before we move on, there are a few things worth noting about the House of Einherjar; the size vs. prev_size
check was introduced in GLIBC version 2.26, meaning that you don't have to deal with it if you're
developing exploits against earlier versions.
However, a second more robust size vs. prev_size check was introduced in GLIBC version 2.29
which requires the fake chunk's size field to actually match the freed chunk's prev_size field.
Lastly, we've used a fake chunk in this example of the House of Einherjar, but there's no reason we couldn't
use it to consolidate with existing free chunks on the heap instead.
In fact, it's generally more advantageous to do so.
Not only does this method not require a heap leak, since the distance between chunks on the same heap
is both relative and deterministic, but legitimate free chunks will inherently pass this safe unlinking
checks.
Don't worry, you'll get an opportunity to leverage the House of Einherjar in this way against the next
challenge binary. For now, if you want to refresh your memory on this technique, you can consult
the HeapLAB PDF as usual, and download a commented solution script from this lecture's resources.
In the next video, we'll explore a second poison null byte technique from the same stable as the House
of Einherjar.