In this video we're having a crack at our first HeapLAB Part 2 challenge binary, it's going to test
everything we've learned about single null byte overflows.
Begin by navigating to the challenge-poison_null_byte directory. We've already taken a brief
look at the security stance of this binary, but once more can't hurt
so run checksec against the poison_null_byte binary.
It looks like we've got the full suite of protections with full RELRO, stack canaries, NX and position
independence and as we noted in the previous video, it's linked against GLIBC version 2.25.
2.25 is pre-tcache and pre size vs. prev_size checks.
If you ever want to know which exploit mitigations are compiled into the GLIBC version you're targeting
check the "Mitigations" section of the HeapLAB PDF.
Let's find out how this binary works by running it on the command line.
We're immediately presented with the numbered menu, and it looks like we're not getting any leaks for
free.
Option 1 allows us to request up to 14 chunks, although their size is limited to small chunks with
1000 bytes of user data or less.
With option 2, we can edit chunks by index, meaning we can write into their user data.
Option 3 lets us free chunks by index and option 4 allows us to read data back from them.
Option 5 simply quits the program.
Do you remember ltrace from HeapLAB Part 1?
Well, let's use it to see what's going on under the hood when this binary makes memory requests. Use the
-e option to isolate function names ending in "alloc".
When I make a request for memory with option 1
ltrace reveals that the program is using the calloc() function to make these requests.
In Part 1 we learned that calloc() is just a wrapper around malloc that ensures heap memory is zeroed
before allocation.
It's used here to simulate a developer initializing their variables and can make heap exploitation a
little tougher.
If it's been some time since you completed HeapLAB Part 1, let me remind you of how these challenges
work.
Your goal is to find the bug, then leverage what you've learned about heap exploitation so far to drop
a shell from this binary.
Typically, the first thing you'll want to do is leak libc followed by the heap if you need to.
Don't worry, if you're finding it difficult, these challenges are designed to be tough and may take
you hours, days or even weeks to solve if you're new to exploit development.
I often add a twist at the end to catch out more experienced exploit developers, so watch out for
that too.
The rest of this video will incrementally offer hints and guidance until we eventually have a working
exploit.
At this juncture, I invite you to pause the video when the pause screen shows and use the opportunity
to get familiar with the challenge binary and of course, find the bug.
If you're feeling confident, you can continue and try to build a working exploit on your own, and
I encourage you to make as much progress as you can without unpausing the video.
However, if you're really stuck or feel like you need a hint, return to the video and I'll reveal
a little more about this challenge.
I'm sure you've already guessed what the bug is in this challenge binary, after all, we are in the
poison null byte section of the course.
Nevertheless, let's make sure we're not missing anything.
I'll load my copy of the binary into GDB,
run it,
and begin by requesting some different sized chunks.
Breaking in GDB and inspecting the heap
shows us the chunks I've just requested with sizes 0x20, 0x110, 0x90 and 0x20
respectively.
Let's go ahead and check this binary for each bug category we've covered in this course. I'll continue,
hit return to print the menu and start by checking for a double-free bug.
No double-free.
Incidentally, in this scenario I'm taking these error messages at their word because I wrote the
code, but just because a production binary claims that it's caught an error, that doesn't necessarily
mean that it's handled it correctly.
Always check.
Moving on, let's see if there's a read- or write-after-free bug present by attempting to use the "read"
and "edit" options on the chunk I've just freed.
No use-after-free bugs. That leaves overflows, so let's attempt to write more than 24 bytes of
garbage into the first 0x20-sized chunk's user data.
Break in GDB and inspect the heap with 'vis'.
Unsurprisingly, this program incorrectly null terminates its input, leading to a single null byte
overflow.
So that's our bug.
Now it's up to you to exploit it and you can leverage either of the single null byte overflow techniques
we've covered.
For example, writing a counterfeit prev_size field here, then freeing the 0x100 chunk could lead
to a House of Einherjar attack.
Remember that the House of Einherjar can be leveraged to consolidate a chunk with a legitimate free chunk
on the heap as well as a fake chunk.
If I dump default heap memory using the sbrk_base trick we learned in Part 1, it's clear that
it's also possible to leverage the Google Poison Null Byte technique against this binary.
If the 0x110 chunk were free before we clobbered its least-significant byte, then we could
request a couple of chunks in the space it left and the 0x90 chunk's prev_size and prev_inuse metadata
wouldn't be updated.
Either technique can get you a shell from this binary, the choice is yours.
If you need to refresh your memory on either of them, go ahead and revisit the earlier videos from
this section.
Don't forget that you've got the malloc testbed to hand if you want to prototype parts of your exploit
and that's your first priority should be leaking a libc address that will give you access to code
like the system() function or one-gadgets, along with the address of execution hijack points like the
malloc hooks.
Of course, there's also a pwntools template script in this directory with all the usual helper functions
to help get you started.
Pause the video again and see what sort of damage you can do with that single null byte overflow,
return here if you get stuck or need a hint.
I've added some code to this challenge's pwntools template script, which you can see on your screen now.
If you want to follow along, you can grab a copy from this lecture's resources,
it's labeled as template number 2.
This time, I've used a naming convention that you may have come across in my other solution scripts.
Normally we label chunks by the order in which they're allocated; chunk A, chunk B, chunk C, etc.
But here I've labeled to them by their purpose;
"overflow", "victim", "consolidate" and "guard".
There's no reason to pick one naming scheme over another,
after all it doesn't affect the exploit,
I just thought I'd use this one today.
You may have noticed that I'm setting up a Google Poison Null Byte attack in this script, where I'll
be using the single null byte overflow to decrease the size value of the "victim" chunk after freeing it.
If you've chosen to use the House of Einherjar technique, rest assured there is a solution script available
for that route as well, and I'll be sure to indicate the point at which the two techniques converge.
Before freeing the "victim" chunk, I've added a commented-out line that prepares fake chunk metadata used
to pass the size vs. prev_size check.
Of course, the version of GLIBC this challenge binary is linked against, 2.25, doesn't have that
check, but I've kept it in
just to clarify that this technique could still be leveraged against later versions of GLIBC.
The next line frees the "victim" chunk, after which the "overflow" chunk is filled with garbage data,
at which point the single null byte overflow will occur, clearing the least-significant byte of the victim
chunk's size field.
In my script, the "victim" chunk is 0x210-sized, so after the overflow it will be left as a
0x200 chunk.
As we discussed in the Google Poison Null Byte video, you could safely pick a range of different sizes
for your "victim" chuck.
You may be wondering why I picked 0x90 for the size of the overflow chunk, when it could just as
easily have been a 0x20 chunk, reason will become clear later.
The remaining lines in this template simply complete the Google Poison Null Byte technique, just like
we did against the malloc testbed earlier.
It requests 2 chunks in the space left by the freed "victim" chunk, labeled "victim_A" and "victim_B", respectively.
It frees victim_A, then frees the consolidated chunk, which will be consolidated backwards with victim_A
over victim_B, creating overlapping chunks.
This leaves us with a layout similar to what you see here.
The chunk I've labeled victim_B is 0x100-sized and still allocated, allowing us to read from and write
to its user data.
Overlapping victim_B is a free 0x2a0-sized chunk, resulting from the consolidation of victim_A
with the "consolidate" chunk.
If I run my script with the GDB option,
break and inspect the heap.
We can see that 0x2a0-sized chunk here.
The victim_B chunk's user data starts here, which as I mentioned, we can still read from and
write to.
I should point out that it's at this stage where our 2 poison null byte techniques converge. If
you've used the House of Einherjar technique to create overlapping chunks, the rest of your exploit will
look very similar to mine.
If you've been struggling to get this far, then you can use this template as a foundation for the rest
of your exploit.
I've not dug too deeply into what's going on at the malloc level because this challenge isn't here
to teach you the Google Poison Null Byte technique,
that was the previous video.
Rather, it's here to give you a chance to leverage it in a complete exploit.
If you still need to refresh your memory on either poison null byte technique, I encourage you to do so
before moving on.
Now that we've coerced overlapping chunks, it's time to forge a libc leak and possibly a heap leak
too, if you feel your exploit might need it.
If you need some inspiration, check out the exploit you wrote for the One-Byte challenge in HeapLAB
Part 1.
Pause the video again and consider how you could go about this.
Take your time and see how much progress you can make on your own. If you're having trouble leaking libc
come back here, unpause the video, and I'll talk you through the next phase.
I've added just a handful more lines to my script to leak a libc address, you can download this copy
from this lecture's resources, its template number 3.
Template 2 left us with overlapping chunks, having leveraged the single null byte overflow for a Google
Poison Null Byte attack.
To leak a libc address
all I've done is request a 0x100-sized chunk.
That request was serviced by remaindering the0x2a0 chunk resulting in main arena unsortedbin
metadata being written into the victim_B chunk's user data.
This is the same trick we used to leak libc when we were tackling the One-Byte challenge.
Notice that before leaking malloc metadata via the victim_B chunk, I've also freed the "overflow" chunk.
This is why I set the "overflow" chunk's size to 0x90, rather than something smaller.
When I free it, it's linked into the unsortedbin, after which the remainder overlapping victim_B
has both an fd that points to the main arena's unsortedbin and a bk that points to the "overflow"
chunk on the heap.
This means we can leak both libc and the heap in one go.
The last lines of this script simply leak those libc and heap addresses by reading the victim_B chunk's
user data with the read() helper function.
Note that the read() helper takes 2 arguments, the index of the chunk to read from and the amount of
data to return in bytes.
By default, the read option from the challenge binary's menu dumps the entirety of chunk user data
so you can use that second argument to isolate the information you need.
This line just uses the read() helper function to capture the first 16 bytes of the victim_B chunk's
user data, which at that moment holds unsortedbin fd & bk pointers.
The next line uses the first 8 bytes of that data, the address of the main arena's unsortedbin,
to calculate the libc load address.
pwntools' unpack() function is used to convert the little-endian data into an integer.
The line after that captures the heap start address from the last 8 bytes of the leaked data, then
the last two lines simply log that information to the terminal.
I'll run the script with the GDB and NOASLR options so we can check if it works and so we can see the
heap layout after it's done.
Breaking and inspecting the heap with 'vis' shows us the "overflow" chunk at the start of the heap, now
linked into the unsortedbin.
It's followed by an allocated 0x100 chunk, this is the second incarnation of victim_A.
Then there's another free unsorted chunk.
That second unsorted chunk has an fd that points to the main arena's unsortedbin and a bk that
points to the "overflow" chunk.
It also overlaps the still-allocated victim_B chunk, which is what we're using to leak those fd and
bk pointers.
If we alt+` to the program window, we can see that the script, having done its calculations, has
logged both the libc load address and the heap start address to the terminal.
Switching back to the GDB window, we can confirm that the heap start address matches,
and by issuing the 'vmmap libc' command, also confirm that the libc start address matches.
So using the Google Poison Null Byte technique or the House of Einherjar if that's what you chose earlier,
we've leveraged a single null byte overflow to leak both libc and the default heap.
All that's left is to somehow hijack the flow of execution.
I'll leave you to come up with some ideas for this last phase
and if you think of something that might work, go ahead and try it out.
Remember that you can download the template scripts I've been using from this lecture's resources and
you've got the malloc testbed and PDF to help you along the way. Pause the video again and do your
best to hijack the flow of execution.
Return here and unpause if you want another hint.
But don't forget that the last part of these challenges usually involve an extra obstacle,
so don't be afraid to spend some time on it and get creative.
OK, let's look at how we might hijack the flow of execution using our primitive of overlapping chunks.
When we dropped a shell from the One-Byte challenge binary in Part 1, we employed the House of Orange
technique.
However, we did so because we couldn't take an easier route.
If you recall, the One-Byte program didn't allow us to request 0x70-sized chunks, meaning
we were unable to fastbin dup over the malloc hook using the misalignment trick we'd learned earlier.
This challenge binary has no restriction on requesting 0x70-sized chunks,
so I've added some lines to my exploit that perform a fastbin dup over the malloc hook.
If you want to grab a copy of my exploit so far, you can get one from this lecture's resources, it's
labeled as template number 4.
Be aware that if you wanted to take a different path to code execution, such as the House of Orange,
the version of GLIBC we're dealing with here has the libio vtable hardening patch, which makes
file stream exploitation slightly tougher.
The first line I've added just allocates the "overflow" chunk again, if you skip to the part where we
freed the "overflow" chunk so that we could leak the heap at the same time as we leaked libc, then
you don't need this part.
In fact, we're not going to need a heap leak at all for this exploit,
so you're welcome to remove both.
The next line makes a request for a 0x70-sized chunk, which is allocated by once again remaindering
the free chunk overlapping victim_B, the one from which we just leaked a libc address.
It labels this 0x70 chunk "fast", then immediately frees it.
Now we're in a situation where we have a free 0x70 chunk overlapping the still-allocated
victim_B chunk, giving us the opportunity to tamper with fastbin metadata.
And I've just borrowed from our fastbin dup exploit in Part 1, by overwriting this 0x70
chunk's fd with the address of the malloc hook minus 0x23.
If 0x23 seems like a magic number to you, be sure to revisit the fastbin dup material.
If I run my script with the GDB and NOASLR options,
then break and inspect the heap,
we can see our free 0x70 chunk, which overlaps victim_B.
And taking a look at the fastbins
reveals that the 0x70 fastbin metadata has been corrupted.
Dumping a few quadwords at the fake chunk we injected into this bin
shows its 0x7f size field
and that it overlaps the malloc hook.
At this point, we could make two 0x70-sized requests, the second of which would allocate this
fake chunk.
Then we could overwrite the hook with the address of a one-gadget, for example.
Check out the last section of Appendix A in the HeapLAB PDF if you need a reminder on how to search
for one-gadgets.
I'm going to leave you to wrap this exploit up on your own.
Bear in mind that, as I mentioned earlier, there is one last hurdle.
The last portion of this video will show you how to complete the exploit, dropping a shell from the
challenge binary,
so make sure you've exhausted all possibilities before continuing.
Pause the video now, and if you manage to drop a shell or really are out of ideas, then head back
here and we'll work through the last part together.
If you figured out the last piece of the puzzle on your own and you're just here to see my solution,
congratulations, you should really be proud of yourself. If you're here because no matter what you
did, those one-gadgets just wouldn't behave,
don't worry
I'm here to help.
Before the pause, we had leveraged the single null byte overflow bug to create overlapping chunks. We'd
used that overlap to leak libc and were preparing to use it again to fastbin dup over the malloc
hook.
Hopefully you went away and tried this, and since the challenge binary won't let us make arbitrarily
large requests, overwriting the malloc hook with the address of a one-gadget seemed like a sensible
approach.
However, satisfying any of the one-gadget constraints in this scenario proves difficult.
At this point, you may have considered file stream exploitation instead and props to you if you tried.
Unfortunately, the version of GLIBC we're working with, 2.25, implements a mitigation against
file stream exploitation.
If you did your homework and read up about this mitigation, you'll have found that it can be bypassed
somewhat trivially by writing any nonzero value into the dlopen hook, perhaps via an unsortedbin
attack.
If you tried this, you probably came to the conclusion that chaining unsortedbin attacks together
isn't easy.
One to disable libio vtable protection and one to trigger a House of Orange attack
is one unsortedbin attack
too many.
So the fastbin dup failed us, as did the unsortedbin attack, but what if we combined them?
We know that the fastbin dup technique relies upon the presence of a fake chunk size field.
In the case of the fake chunk overlapping the malloc hook, this is supplied by a pointer to a library
address which consistently starts with the value 0x7f and is followed by a null padding quadword.
We've so far been unable to target things like the free hook with a fastbin dup because there aren't
any pre-existing fake chunk size fields close enough to it.
But an unsortedbin attack could be used to provide one. Remember that the unsortedbin attack writes the
address of an arena's unsortedbin to a location of our choosing.
Since arenas, both main and otherwise, will typically be mapped at 0x00007f addresses, if we point
an unsortedbin attack at the memory before the free hook, it will write a viable fake size
field there.
On your screen now is the tail end of my pwntools template, script number 5, which you can download
from this lecture's resources.
I've only modified this last call to the edit() helper function.
Before the pause, that edit() helper was being used to overwrite a 0x70 chunk's fd with the address
of the fake chunk overlapping the malloc hook.
This time, I've overwritten that same fd with the address of a fake chunk I'm going to prepare overlapping
the free hook.
Recall that the 0x70 chunk we're tampering with was created by a remainder operation, that means its
succeeding chunk is linked into the unsortedbin.
The allocated victim_B chunk we're using to overwrite the 0x70 chunk's fd is 0x100-sized, which
means it can also be used to tamper with that succeeding chunk's metadata.
I've written some garbage data up until that next chunk's size field, which I've replaced with a
0x20 value.
There's nothing special about 0x20 in this case, it's just that we need to allocate this chunk to
trigger our unsortedbin attack rather than sort it, because we still have work to do after the unsortedbin
attack.
You could use a different value here,
for example, 0x130 if you want everything to line up nicely on the heap.
We know that partial unlinking ignores a chunk's fd,
so I've just written a null there and finally I've replaced my now 0x20-sized chunk's fd with
the address of memory before the free hook.
We'll see why I picked 0x23 here and 0x16 here shortly.
To help us visualize what's going on, I'll run my script with the GDB and NOASLR options.
Break and inspect the heap with 'vis'.
Here's the free 0x70-sized chunk
we've been tampering with since the last pause.
It's also where our victim_B chunk starts.
I've used victim_B to clobber the 0x70 chunk's fastbin fd with the address of the free hook minus
0x16.
There's nothing there right now, but that's where I'm going to prepare a fake chunk.
After the 0x70 chunk is the 0x20 chunk I've forged the size field of. Remember, you can change
this if you want to.
That 0x20 chunk is linked into the unsortedbin, and I've overwritten its bk, again using victim_B,
with the address of the free hook minus 0x23.
That's my unsortedbin attacked set up, which I can now trigger by continuing,
switching to the program window and making a small request to allocate that unsorted 0x20 chunk.
Switching back to GDB,
breaking and once again inspecting the memory 0x16 bytes before the free hook now reveals a
0x7f size field, the result of our unsortedbin attack.
The free hook minus 0x16 is about as close as I could make this fake chunk to the free hook, which
is why I also chose free hook minus 0x23 for the unsortedbin attack location.
If you've been playing around with unsortedbin attacks against different addresses near the free hook,
you may have noticed that some of them won't work because they get clobbered by nulls.
I'll leave it as homework for you to figure out exactly why this happens.
The important thing is that now we can allocate a fake chunk overlapping the free hook, and that means
we don't need to use a one-gadget and can instead simply overwrite the free hook with the address of
system(), then free a chunk that holds the string
"/bin/sh".
So let's close GDB and return to our editor to make those changes.
The first thing we need to add is a request for a 0x20-sized chunk which triggers the unsortedbin attack.
That gives our fake chunk overlapping the free hook a viable size field.
So next, we can request two 0x70-sized chunks, the second of which will be serviced by our
fake chunk, which I'll label "overwrite".
Next, we can use that "overwrite" chunk to write the address of the system() function over the free hook.
We'll need to use just six paddoeg byte to make up the distance between the start of the fake chunk's
user data and the free hook.
All that's left is to free a chunk with our command string in its user data, for this I'm just going
to use the guard chunk that was protecting the chunks we used at the start of our exploit from being consolidated
with the top chunk.
Having made those changes, I'll run my exploit under production conditions.
And it looks like we've successfully dropped a shell from the poison_null_byte challenge binary.
In summary, this program had a single null byte overflow bug, against which we leveraged one of the
two poison null byte techniques that we've learned.
Those techniques create an overlapping chunks primitive, which we initially used to like the libc
load address. We went on to use those overlapping chunks again to both prepare an unsortedbin attack
against an address just before the free hook and tamper with 0x70 fastbin metadata.
We then triggered our unsortedbin attack, which provided us with a viable fake size field in front of
the free hook.
Remember that we'd chosen to free hook as our target in this case, because we were unable to satisfy
any one-gadget constraints, rendering the malloc hook unusable.
We were then able to allocate a fake chunk overlapping the free hook, thanks to the fake size field
provided by our unsortedbin attack.
We used that fake chunk to overwrite the free hook with the address of GLIBC's system() function.
Finally, we freed a chunk with the string "/bin/sh" in its user data, which was interpreted as a call
to system("/bin/sh"), dropping a shell and completing the challenge.
If you were able to make any progress on your own or even complete the poison_null_byte challenge without
help, that's very impressive.
This was a difficult challenge, and we dropped a shell with nothing but a single null byte overflow.
The program had full RELRO, stack canaries, NX, was position-independent and used calloc() to zero all its memory
allocations.
There is a copy of my final exploit for this challenge available from this section's resources labelled
pwntools_solution.py.
There's also a second exploit that uses the House of Einherjar to create overlapping chunks rather than
the Google Poison Null Byte technique, that one's labelled pwntools_solution_2.py, and they're both commented
for clarity.
As always, if you want to refresh your memory on either poison null byte technique, there's a summary
of each in the PDF along with the pertinent malloc internals.
If you're curious to see how this exploit might work against later versions of GLIBC, then the
malloc testbed is a great place to find out.
That's it for the Poison Null Byte tehnique,
join me in the next video where we'll be requesting very large chunks as we learn the House of Rabbit.