In this video, we're going to learn a heap exploitation technique based on another of Chris Evans proof
of concept works.
As I mentioned in the previous video, the naming of single null byte overflow techniques is a bit
muddled.
The technique we're about to learn is generally known as the Google Poison Null Byte technique.
And whilst it's true that the original was uploaded to the Google Project Zero blog by Chris Evans,
this isn't the technique they used in their pkexec root
shell exploit.
If you're browsing the PoC files via the Project Zero blog I linked in the last video, the file name associated
with this technique is a bit of a mouthful.
It's "shrink_free_hole_alloc_overlap_consolidate_backward.c
It's the result of Project Zero members ruminating on different approaches to exploiting single null
byte overflows on the GLIBC heap.
If you read the aforementioned blog post, you'll know that they decided to use what we've just learned
as the House of Einherjar technique in their final exploit.
Nonetheless, the following year a company named Context IS published a short paper in which they
detailed leveraging what we're about to learn as the Google Poison Null Byte technique against an undisclosed
piece of software.
Of course, the Google Poison Null Byte or just Poison Null Byte technique
again takes advantage of a single null byte overflow on the heap.
It is slightly more complex than the House of Einherjar, but especially against 64 bit architectures
it could be considered more "realistic".
Personally I enjoy hacking for the creativity, and I'm happy to leave realism to nation states.
Whatever your opinion, the reason I call the Poison Null Byte technique more realistic than the House
of Einherjar, despite both having been used to exploit real world programs, is due to the nature of single
null byte overflows.
As we've discussed, they're typically the result of incorrect string termination.
This means that the bytes used in the payload that triggers the overflow are likely to be part of a
string, which may limit the range of values those bytes can take and make it unlikely that null bytes
can be used.
In our House of Einherjar exploit we had to forge a prev_size field in the last quadword of chunk
user data before the overflow, meaning that our prev_size field was part of the payload that triggered
the overflow, and yet it had to contain null bytes and non-printing characters for our exploit to be
successful.
If you've read the Project Zero blog I linked, you'll know that they had to overcome this exact issue.
In fact, they didn't even control the prev_size field in their exploit,
it was a fixed string provided by the program.
If you haven't already, you can check out the blog to learn how they solved that problem.
What you'll notice about the technique we're going to learn now is that it doesn't require us to forge
a prev_size field inside what's likely to be treated as a null-terminated string, which is why I say
it's a more "realistic" method of exploiting a single null byte overflow than the House of Einherjar.
The goal of the Poison Null Byte technique is simply to create overlapping chunks on the heap.
We've seen what sort of damage that can lead to in previous challenge binaries.
Here's how it works; rather than targeting an allocated chunk's size field with a single null byte overflow
like we did in the House of Einherjar, the Poison Null Byte technique targets a free chunk's size field.
Imagine a heap layout like this with three allocated chunks, A, B and C.
The size of chunk A doesn't matter, it's there because we need a chunk to overflow from. The size
of chunk C is anything in the consolidation range, so-called normal chunks of 0x90 and
above.
Chunk B in between chunks A and C has a size value of 0x110 or above, with a least significant
byte that is not null,
the inverse requirement of the House of Einherjar.
In this example, chunk B has a size value of 0x210.
Now, let's say we free chunk B.
It will get linked into the unsortedbin, adopting fd and bk pointers, but more importantly for
this technique, malloc will use its size field to locate the succeeding chunk, where it will clear
the prev_inuse flag and set a prev_size field.
So far, this is normal behavior and this is where our single null byte overflow comes in.
We want to overflow from chunk A's user data into the least significant bite of chunk B's size field.
This has the effect of decreasing the size of chunk B, at least from malloc's perspective, something
that we actively avoided doing in the House of Einherjar.
In this case, we've scrubbed 16 bytes off chunk B's size value.
It was a 0x210-sized chunk,
now it's 0x200-sized.
This has no immediate effect on the state of the heap, but it will have implications when we allocate
this chunk.
So next, we're going to allocate 2 chunks into the space previously occupied by chunk B, I'll label
them chunks B1 & B2.
Chunk B1 must be normal sized, whereas chunk B2 can take any size.
In HeapLAB Part 1 we learned about remaindering, and that's what happens here, chunk B is remaindered
once when we request chunk B1, and perhaps again when we request chunk B2 although in this example,
we've simply exhausted chunk B to fill the space.
Normally, what should have happened during the allocation of B1 was that after chunk B2 was created
and linked into the unsortedbin, chunk C's prev_size field would get updated with the size of hunk
B2.
But thanks to our poison null byte, malloc considers chunk B slightly smaller than it really is and
therefore updates the succeeding chunk's prev_size field at an address 16 bytes before it should do.
This means that chunk C's prev_size field remains the same.
What's more, once we've allocated chunk B2, the same mistake leads malloc to fail to update chunk C's
prev_inuse flag, instead writing it 16 bytes before it should.
This leaves us in a situation whereby if we were to free chunk C, because its prev_inuse flag is clear
and its prev_size field still reads 0x210, malloc will attempt to consolidate it with chunk B1.
This would lead to a large free chunk overlapping the still-allocated chunk B2, at which point we
can potentially make all sorts of mischief, changing size fields, tampering with fd and bk pointers,
etc.
Of course, we know that this will fail.
There may not be any size versus prev_size checks in GLIBC 2.25, but even if there were vestigial
fd & bk pointers in chunk B1, they would still fail
the safe unlinking checks.
We could provide a counterfeit fd and bk, but that would require a heap leak, so instead we'll just
free chunk B1.
That way, it gets legitimately linked into the unsortedbin and will pass the safe unlinking checks.
If we were targeting a version of GLIBC with size vs. prev_size checks, we'd still need to forge
a prev_size field somewhere inside chunk B to satisfy the check during the remaindering process.
Now that chunk B1 is free, we can safely free chunk C. Because its metadata was never updated
chunk C still has a clear prev_inuse flag and prev_size field from when we originally freed chunk
B. This means that malloc will consolidate it backwards with chunk B1.
This leaves us with a large free chunk that we can allocate overlapping chunk B2, which is still
allocated.
As we know, creating overlapping chunks is a great primitive to base and exploit on, and we'll be
capitalizing on this in the next challenge binary.
For now, let's head over to the malloc testbed, where we'll implement the Poison Null Byte technique
in practice before moving on.
Hopefully you remember the malloc testbed from HeapLAB Part 1. It's a tool that I use to help
me learn about malloc internals, especially when new GLIBC versions are released, and to test heap
exploits against those different GLIBC builds.
The first thing we want to do is match the GLIBC version the malloc testbed is linked against
to the version we'll be targeting with our exploit.
Without getting too far ahead of ourselves, let's run checksec against the poison_null_byte challenge binary
in the challenge_
poison_null_byte directory.
Checksec shows that it's linked against GLIBC version 2.25 so we'll use the change_glibc_version.py
Python script to link the malloc testbed against the same version.
We already investigated how the malloc testbed works in HeapLAB Part 1
so let's jump straight to making a copy of the pwntools_template.py script and I'll call mine google_poison_
null_byte.py
Open the copy in your editor and scroll to the bottom.
Remove all the example lines except for the two that request chunks with the malloc() helper function.
We're going to begin by creating the chunk layout we saw in the slides.
3 chunks, A, B and C, and to help keep things simple, a guard chunk to avoid top chunk consolidation.
Recall that chunk A can be any size, it just serves as a chunk from which to overflow into chunk B.
I'll change mine to a 0x20-sized chunk to keep things compact.
In keeping with the example, we'll will make chunk B 0x210-sized, but as long as it's larger than
0x100 and doesn't have a null least-significant byte, the Poison Null Byte technique should
be viable.
Chunk C must be a normal chunk, we'll cover ways in which fast chunks can be consolidated in a later technique
but for now, let's keep things simple and make chunk C 0x90-sized.
Whilst the House of Einherjar involved overflowing a null byte into an allocated chunk's size field, in the Poison
Null Byte technique we're overflowing into a free chunk.
So add a line that frees chunk B.
Once you've added those helper function calls, run your script with the GDB and NOASLR options.
Break with ctrl+c and inspect the heap by issuing the 'vis' command.
The output just fits on my screen and we can see chunks A, B and C followed by a guard chunk.
Chunk B is free and has been linked into the unsortedbin, as expected, it has fd & bk pointers.
Chunk C's prev_inuse flag has been cleared and now chunk C has a prev_size field that reads
0x210; the size of chunk B.
This is where our exploit begins, we're going to leverage a single null byte overflow from chunk A into
the least-significant byte of chunk B's size field, changing that 0x211 value to
0x200.
So close GDB with ctrl+d and return to your editor.
Add a call to the edit() helper function and fill chunk A's user data with garbage.
The malloc testbed has a lot of bugs, but it doesn't null-terminate its input, so we'll have to
simulate a single null byte overflow by appending a null byte to our input using pwntools pack 8 or
p8() function.
To check that this has had the desired effect, run your script with the usual options.
Break, then inspect the heap.
We can see that our poison null byte did the trick, chunk B's size field now reads 0x200 rather
than 0x210, which has confused pwndbg into not printing its succeeding chunks.
Return once more to your editor, where we'll implement the next phase of the exploit.
Now that we've tampered with chunk B's size field, we're going to make a request that will remainder it.
To keep things neat I'll split chunk B down the middle by requesting a 0x100-sized chunk that I'll label
chunk_B1.
Once you've added that line we'll bounce back into GDB to see what effect it's had.
Break and inspect the heap again.
Chunk B has been remaindered. When we made that request for a 0x100 chunk, chunk B was sorted into
the 0x200 smallbin, after which it was remaindered during a binmap search, leaving chunk B1 to be
allocated and linking the remainder back into the unsortedbin.
This is expected behavior and what we're interested in is chunk C.
Since 'vis' didn't print it, we'll have to inspect it manually with the 'dq' command.
These are chunk C's prev_size and size fields, its prev_inuse flag is clear, which is correct
since its preceding chunk is free.
But it's prev_size field still reads 0x210, that is incorrect.
Its previous chunk is now 0x100-sized, the value that we see here.
This was malloc's attempt at updating chunk C's prev_size field during the remaindering process.
It's written the updated prev_size value 16 bytes short of where it should have because the overflow
scrubbed 16 bytes off chunk B's size, making malloc think that it ends earlier than it really
does.
Let's take this a step further and request that remainder chunk.
Continue in GDB, alt+` to the program window, hit return to print to the menu and use the
"malloc" option to request a 0x100-sized chunk.
Once you've done that, alt+` to the GDB window, break again and inspect the heap with 'vis'.
Not much has visibly changed, although that remainder chunk has now been allocated from the unsorted
bin.
Use the 'dq' command again to dump memory after what 'vis' is showing us.
Here's where things get interesting, note that now not only is chunk C's prev_size field unmolested, but malloc
has tried and failed to update its prev_inuse flag, which has instead been written here.
This leaves us in a similar situation to the House of Einherjar.
If we were to free chunk C, malloc would attempt backward consolidation.
In this scenario, rather than providing a fake chunk, we're going to free chunk B1 and let malloc
consolidate it with chunk C, creating a free chunk that overlaps the still-allocated remainder chunk.
Let's return to our editor and make those final changes.
Start by requesting chunk B2, that's the one we requested manually a moment ago that gets allocated
from chunk B's remainder.
Then use the free() helper function to free chunk B1, followed by chunk C.
This should trigger the backward consolidation, yielding overlapping chunks.
One last time, run your script with the GDB and NOASLR options.
Break and inspect the heap where we can see the fruits of our labor.
Between chunk A and our guard chunk is a free 0x2a0-sized chunk linked into the unsortedbin.
This is the result of consolidating chunks C and B1.
But underneath that chunk, as it were, chunk B2 is still allocated.
It's at this point that we would go about capitalizing upon this overlap.
Perhaps we could tamper with program data, leak heap metadata, etc.
And that concludes the Poison Null Byte technique.
To summarize, we leveraged a single null byte overflow to slightly decrease the size of a free chunk,
which we've been calling chunk B. We then allocated 2 chunks in its place, B1 and B2. Because of
the size field corruption, malloc failed to update the succeeding chunk,
Chunk C's, metadata leaving its prev_inuse flag clear when it should be set and an out-of-date value
in its prev_size field.
We then freed chunk B1, followed by chunk C. Because chunk C's prev_inuse flag was still clear
malloc attempted backward consolidation.
It used chunk C's out-of-date, prev_size field to locate the previous chunk, which led it to chunk
B1.
This had the effect of consolidating those chunks together to form a single free chunk that overlapped
the still-allocated chunk B2.
We'll leave the Poison Null Byte technique there because we know by now how to leverage overlapping
chunks, we've use them to leak heap metadata in previous challenge binaries
and we know that if we used it to tamper with fastbin metadata, for example, we could build a
fastbin dup primitive.
There are a lot of possibilities.
If you want to refresh your memory on the Poison Null Byte technique, there's a brief summary in the
PDF and a commented solution script available from this lecture's resources.
In the next video, we're going to use what we've learned about single null byte overflows to drop
a shell from our first challenge binary of HeapLAB Part 2.
I'll see you there.