Did you find the bug? If you did, great, if not don't worry
this isn't a bug hunting course either.
I can tell you that the house_of_force binary has an overflow bug
when receiving the data it writes onto the heap.
Let's confirm this.
Load the house_of_force binary into GDB just like we did before and start it with the 'run' command.
Use option 1 to request some heap memory,
I'm going to request a minimum sized chunk to keep things compact, but you can request any size you
like.
Then, when prompted for data, send more than the amount of user data your chunk has available.
For example, my request for 24 bytes will yield a 0x20-sized chunk with 24 bytes of user data,
so I'll send more than 24 bytes by spamming a load of capital Y's, because why let the capital A's
have all the fun.
Now let's break in GDB with ctrl+c and inspect the heap with 'vis'.
We can see that malloc has allocated a 0x20-sized chunk as expected, and the program wrote our
capital Y's into that chunk's
user data.
But we can see that eight bytes of our input has overflowed the chunk.
Not only that, our data has overwritten the top chunk size field.
So this binary has an eight byte overflow when reading data into chunks.
Is that a realistic bug?
Perhaps, it could just as easily have been an off-by-one error in an array of long integers, for example.
The point is that even a single byte overflow can corrupt the size field of the succeeding chunk,
including that of the top chunk.
And it's the top chunk
that's our focus in the House of Force.
We can use our overflow to control what size malloc perceives the top chunk to be
because, as I mentioned earlier, many versions of GLIBC have no top chunk size integrity checks and
malloc will take this value as gospel.
So let's check which version of GLIBC we're working with.
When you issue the 'vmmap' command pwndg lets you grep for any terms you specify afterwards.
'vmmap libc' should narrow things down. From the output
we can see that this binary is linked against GLIBC version 2.28, which doesn't have any
top chunk size integrity checks. For a list of which heap exploit mitigations were introduced in which
versions of GLIBC, check the 'Mitigations' section of the PDF.
The eagle-eyed may have noticed this
no-tcache extension. The tcache is a malloc feature that was introduced in GLIBC version
2.26.
We won't cover the tcache in part 1 of the HeapLAB course, but be aware that some of our vulnerable
binaries use malloc versions compiled without the tcache.
Rest assured that this is just to prove these techniques work on later versions of GLIBC without having
to learn about the tcache first.
If anything, the presence of the tcache makes malloc easier to exploit.
It's worth noting that regardless of which version of GLIBC a binary was built against, at runtime
it will use whichever GLIBC version is available on the OS it's installed on.
This means that just because a binary isn't vulnerable to exploitation on one platform, that it won't
be vulnerable to exploitation when installed on a different platform.
These course binaries are the exception to that rule,
forcibly run against specific versions of GLIBC for learning purposes.
Back to our vulnerable binary, let's consider what we can do with our overflow.
We control the top chunk size field, meaning we can convince malloc that this heap's top chunk is
any size we want.
This is a simplified Linux process memory map,
much like the output of the 'vmmap' command.
Typically, the binary itself resides at the lowest address, followed by the default heap in a region
known as the program break.
Then there's often a large gap followed by the libraries, including GLIBC,
then another gap,
and finally the stack at the highest address.
If we overwrote this heap's top chunk size field with a large value, then from malloc's perspective
that top chunk could extend across this gap,
overlapping sensitive data in the library or stack regions.
But don't forget that our target data, those capital X's, resides in the data section of the binary
at a lower address than the heap.
But what if we wrote a really large value into the top chunk size field?
Then it would appear as though it extended past the end of the virtual address space, wrapping around
back to the start.
Then we could make another large request from malloc, which would be serviced by this heap's top chunk,
wrap around the V.A. space and overlap the binary's data section.
And that's exactly what we're going to do.
We'll leverage this binary's overflow to overwrite the top chunk size field with a very large value, then
we'll make a large request from the top chunk
that wraps around the VA space, spanning the gap between the heap and our target data,
then make one final request that overlaps the target so that we can overwrite it.
To achieve this we're going to need to send some non-printing characters to the program, do some
math and make the whole process repeatable.
This is where pwntools comes in. Close GDB and make a copy of the pwntools_template.py
script.
Call it what you want with the exception of pwn.py,
the first thing pwntools scripts do is import a library named pwn.py and your script will try to
import itself.
I'm calling my copy xpl.py which is short for exploit and makes tab completion easy.
Open the copy in an editor of your choice,
I'll be using vim just to show that we don't need anything special like an IDE to do this stuff.
This is our first pwntools script.
All it's going to do is start the house_of_force binary and send it
any input that we tell it to.
Don't be alarmed by the quantity of text here,
most of this is pwntools boilerplate and helper functions that I've added to avoid this becoming an
exercise in Python.
We're only going to write four lines of code to exploit this binary.
Scroll to the bottom of the script.
All we're concerned with is this region here between the dashed lines, which currently holds some example
uses of those helper functions
I mentioned. Lines starting with log.info are just printing debug information.
Recall that this binary leaks the address of its heap, that's captured in a variable named 'heap'.
The 'elf' variable represents the binary and we have access to its symbols via elf.sym.symbol_name,
for example
elf.sym.target
is the address of those capital X's
we want to overwrite.
The malloc() function is a wrapper around
option 1 from the binary's menu.
It takes two arguments; the size to request and the data to send.
This example does the equivalent of selecting option 1 from the program's menu, sending the value
24 when prompted for the size,
then sending some arbitrary data, 24 capital Y's, when asked for data.
This last line uses the delta() function, which we'll cover shortly.
When we run this script, pwntools will start the house_of_force binary,
send it
the input we've told it to,
then return control to us via an interactive prompt.
So let's try it.
You can run the script from the command line or from within vim using the !./% command,
where ! indicates what follows should be interpreted as a shell command and % is replaced
by the name of this buffer.
pwntools starts by printing the checksec output for each binary it loads,
in this case, the house_of_force binary and its GLIBC shared object.
These three lines are the result of those log.info() calls.
And lastly, we're given an interactive prompt so we can talk to the program ourselves.
Hit return to print the menu and we can see that one of our four chunks has been allocated.
But wouldn't it be great if we could inspect the heap at this point?
We could attach GDB manually each time, but pwntools gives us a much simpler way to do so.
Quit the program with option 3,
hit return once to ignore the pwntools message about receiving an end of file,
and again if you launched from vim.
This time, run the script with the GDB option in all caps.
pwntools will do exactly as it did before, start the binary, send its input and give us an interactive
prompt.
But this time, it will attach GDB to the process between the first and second steps and open the debugging
session in a separate terminal.
Depending on your OS, you can use alt or option backtick to switch between GDB and the program prompt.
If you're not using Ubuntu, that key combination might be different.
I'm going to use this two window layout throughout the course because it's the default setting
on Ubuntu
and it doesn't require students to make a lot of changes to their environment to behave like mine.
It is worth noting that you can get both pwntools and pwndbg to use different layouts that are much
more efficient to work in than this, like tabs or split windows,
Just check their respective documentation.
We can break in GDB with ctrl+c, then use 'vis' to inspect the heap as usual.
And it looks like our script worked.
A 0x20-sized chunk has been allocated and filled with capital Y's.
Notice that the heap addresses are different from what we've seen so far,
this is because running your script with the GDB option attaches GDB to the already running process
rather than starting it under GDB.
This means that ASLR is in effect, and although we want our exploits to work under ASLR, debugging
them with it active can be cumbersome
so next time we run our script, I'll show you how to temporarily disable it.
Return to your editor by closing GDB, then hitting return
and let's de-clutter a bit by removing the log.info() lines from our script.
Now, we know that the next eight bytes we send after these capital Y's will overwrite the top chunk
size field.
So let's append that value to the second argument of this malloc() function.
This is the part where we have to send non-printing characters,
so to avoid typing a lot of backslash x's and accounting for endianness, we can instead use pwntools
"pack 64" or p64() function, which is just a wrapper around struct.pack().
We want to set the top chunk to a large value,
so why not use the largest 64 bit value we can using 16 f's?
Once you've added that to your script
run it again with the GDB option
and this time append the NOASLR option, again in all caps.
Break in GDB and use the 'vis' command, which shows us that we've successfully overwritten this heap's top
chunk size field with a very large value.
This is our House of Force primitive. malloc
now considers this top chunk to be huge and will fulfill much larger requests from it than it otherwise
would.
So let's move on to the second phase of our attack.
We're going to request enough memory from malloc to span the gap between this top chunk and our target
data. Close GDB and head back to your editor.
First, we'll need to calculate the distance between the top chunk and our target.
We can use the delta() function for this, which compensates for the fact that our allocation is wrapping
around the VA space.
delta() takes two arguments, a start and end address.
The start address is the top chunk.
We know its location because this binary leaks its heap address, which you'll recall our pwntools script
captured in the 'heap' variable. Add 0x20 to that value to account for the 0x20-sized chunk
that we already allocated, resulting in the address of the top chunk.
The end address is the target,
those capital X's in the binary's data section.
Take 0x20 away from this value to stop just short of the actual target data.
We could overlap the large chunk we'll be requesting with the target, but we'd have to write a lot
of data into it before we reached the target,
most likely causing a segfault
as the program attempted to write to read-only or unmapped memory. It's easier to stop just short
then request another small chunk that overlaps our target data.
Now that we've calculated the chunk size we want, let's request it and write a single byte of garbage
into it.
Once you've added those lines, run your script with the GDB and NOASLR options.
Break in GDB.
And this time issue the 'dq target-16' command, which gives us a view of the memory surrounding
the target data.
There's the target,
and this is the top chunk size field sat right in the middle of the program's data section, which we
can confirm using the 'top_chunk'
command.
Notice how we've scrubbed a lot of its size away,
moments ago this was the largest possible 64 bit value.
Now, the next request we make from malloc will be serviced from this rogue top chunk and the first
quadword of its user data will neatly overlap the target.
And we can do this part manually. Continue in GDB,
alt or option backtick to switch to the interactive prompt, use option 1 to request a small chunk
and anything we write into it will overwrite the target data and subsequently be printed
when we select option 2.
Great.
Let's add that last request to our script.
Quit the binary and return your editor
where we'll add one more call to the malloc() helper function.
I'll request a minimum sized chunk and write the same string I used just now into it.
This time, run your script without the GDB and NOASLR options to ensure that it works under production
conditions.
Select option 2 to print the target, which confirms that we've successfully overwritten the target
data with our own.
And that's all there is to it.
We've leveraged the House of Force technique to make an arbitrary write.
To summarize, this binary has a heap overflow bug that we were able to leverage to overwrite that heap's
top chunk size field with a large value.
This was enough to convince malloc that the top chunk was much larger than it actually was, allowing
us to make a large allocation from it that otherwise would have failed.
The large chunk we requested spanned the gap between the heap and our target data.
Then we made one last request for a chunk that overlapped our target, allowing us to overwrite it.
If you're unsure of any of the steps we took, there's a solution script available for download from
this section's resources,
it does exactly what I just showed you and is commented for clarity.
Don't forget you can also find an overview of the House of Force technique, along with all the malloc
internals we've covered so far in the PDF.
So we've learned the House of Force technique and leveraged it for an arbitrary write.
In the next video, we're going to learn how to turn that arbitrary write into code execution.