House of Force I
April 6th, 2023
Last updated
Was this helpful?
April 6th, 2023
Last updated
Was this helpful?
This technique specifically, is just going to be covering an arbitrary write via the House of Force. To see an example of the House of Force resulting in command execution, check the blog post below:
This article is not going to cover the heap, how it works, or the functions typically seen in it like malloc
, realloc
, free
, etc. I've already made a super in-depth blog post about that which you can find here if you'd like:
The techniques presented here are going to be pretty technical, so please do your due diligence by making sure you've got the prerequisite understanding down "to a T".
Remember in the previous blog, how we set up a function like the following:
Then we set a breakpoint after the call to malloc
and basically dissected the allocated chunk from the start of the heap, the chunk header, and user data, all the way to the top chunk.
Well it turns out that up until glibc v2.29
, the top chunk, i.e., this part:
Was never prone to integrity checks. This means that up until GLIBC < 2.29
, you could just supply any arbitrary value and overflow the heap. If we read the commit from the actual patch, which you can find here:
We can see the general steps that this type of exploit takes in order to compromise the heap. In essence, we're just overflowing the top chunk with a gigantic value such that our next allocation overwrites our target. Let's see this in action!
The binary I'm going to be using is called house_of_force
and it was given to us after purchasing the "Linux Heap Exploitation - Part 1" course by the amazing Max Kamper.
Before anything, let's just start off by talking about what we're actually trying to do with this binary. The binary will present us with a target during its runtime that we're meant to overwrite.
Let's start by examining the binary:
Cool, and if we interact with it, we can see the following:
The binary leaks the puts
function and the start of the heap. As mentioned in the hint above, we're supposed to overwrite a target which we can see if we select option two (2
):
As we can see, the target is a series of 7 "X
"s. So, let's move on and examine option one (1
):
After selecting option one (1
) in the menu, we're given an input to specify the size for an allocation, this section is presumably doing something simple like the following (although since I haven't reversed this section or seen the source code, I don't really know):
Next, we're asked to input some data which again, presumably just gets put into the user data section of the allocated heap chunk. That's all we can do for now, so let's attach this to gdb
to:
Verify if the malloc
function is allocated our specified size of bytes.
If the user data of the allocated heap chunk is being populated by our inputted data.
I'll open the binary inside of gdb
and just run it:
The first thing I'm going to do is quit the execution of the binary right now and verify if that heap address actually starts off at that address: 0x603000
:
Looks good! Okay, so let's restart with r
, and do what we did outside of gdb
, inside of gdb
this time:
Now, after pressing enter, we can ^C
and examine the heap:
So, like we've been doing for so long, let's inspect the heap chunks with vis_heap_chunks (vis)
:
Perfect. It's exactly as we were expecting. We requested 24 bytes
which is the minimum amount of user data that can be allocated to us before the chunk gets incremented by 16 bytes
from 0x20
to 0x30
. Let's actually try to allocate a size of 25 bytes
to see this mechanism at play once again (remember, if you have no idea what's going on right now or why it's 16 bytes
, read my previous blog post):
And now, if we inspect the heap, we can see it worked flawlessly:
Okay, so perfect. It worked as intended, now, let's try to see if we can find a bug or something in here. If you recall the no integrity checks section, GLIBC
up until version 2.29
didn't have any size integrity checks for the top chunk. This means that we could allocate an arbitrary size with malloc
and then write way more than that size is able to hold, effectively overwriting the top chunk as well. Well, first things first, how do we know that this is even a vulnerable version of glibc
? If we use the vmmap
command, we can see the following:
Indeed, It's using a vulnerable version of glibc
! We also knew this from the checksec
command we ran in the beginning:
Anyway, let's try to overflow the top chunk now. We'll start by allocating some bytes and then writing more than what's allocated for us, I'll just use a message (0xdeadbeefcafebabe1337c0ded00d15ea5edbac0ff33db404f0dcafebabefacedfood
):
Now, if we inspect the heap, we should see that our top chunk has been overwritten:
Hold on a second. Wait a minute! Hold your hor- okay, so where the hell is our target in memory exactly? Well, it's actually pretty easy to find out where our s are. If we run the program, the target's already there waiting to be overwritten so we can just query it using xinfo
:
The last line shows that the target is written in the binary's .data
section and furthermore, we can see that it's mapped to an address of 0x602010
. We're expecting to see an address full of Xs (0x58
in hex) when we inspect it using dq
or x/s
or something:
Perfect. It's all going so well. Now, how the f*ck are we meant to overwrite our target when it's lower in memory than our goddamn top chunk is? What do I mean by this? Well, let's see the following (professionally designed) table of address values:
0x602010 --> target
0x603000 --> heap
If our target was at an address of something like 0x603010
or something, hell even if it was at an address of 0x700000
, we could easily overwrite the target. However, it's not. The target is in a region of memory lower than the top chunk and it's not like we can go backwards, clawing at the walls of the regions of memory lower than the start of the heap at 0x603000
like a bunch of mud gophers or cave moles. ?
It turns out, if we make the top chunk a big enough value, it'll just start wrapping around until it reaches the target and even more past that point. We'll get this set up in a couple of moments. First, let's get what we've done so far set in an exploit script using pwntools
.
I came up with this script which is just a Frankenstein's monster recreation of the original exploit template given to us. I did this from the ground up to better understand what the script was doing exactly and this was immensely beneficial because now I can try my best to explain it to you! We'll explain the script as we progress through it. Right now, we're trying to prove the fact that if we input a big enough value, in this case, the maximum value (0xffffffffffffffff
), it'll wrap around to our target (and past it which we need to account for - which we'll see when the time comes). So, let's start the script with the GDB
argument so that a gdb
session starts simultaneously with our script, which is awesome for debugging:
Okay, enough, let's figure out what this script is doing.
Here we're just creating a simple malloc
wrapper function that'll enter the data for us whenever the program gives us this dialogue:
This is a function that will return the difference in bytes so that we can figure out our offset to the target. Lastly, we get to this segment of code:
So, in this segment of the code, we get our malloc
wrapper function to go through the regular procedure of requesting 24 bytes
except in the "data" section of it, it will populate the entire user data with those "O"s you see. Hold on... that means the next bytes it sets will just be overwriting the top chunk. Yes, precisely! This is why we've included that 0xffffffffffffffff
address right after the 24
O's, it's because that address will be overwriting the top chunk and it'll make the top chunk this impossibly large value. Furthermore, that address is the maximum value a hexadecimal address can actually be in 64-bit.
So, let's step through this program and see if it's set up in the appropriate manner before we exploit this program! If we ^C
in the gdb
instance, we get the all-to-familiar:
At this point of the script's execution, the overflow should've already been done so now it's just a matter of checking the damage:
Upon first glance, it just seems like "Oh, cool! The target is there" However, if you take a second to really understand where this target is, you'll see the elegance of this exploit. If you recall from the previous examples of allocating memory in the heap with malloc
, you'll remember that the first part of the blue memory chunk is the heap chunk header. It includes the size field and some flags for metadata on it. Right after that though, is our user data and would you look at where our target is... The start of the user data section! This means that now if we use malloc
to allocate any amount of bytes the first thing that'll get overwritten in the user data is going to be the target! It's set up perfectly to get overwritten. Let's do it.
With the heap perfectly aligned and the target perfectly set up to get overwritten, let's continue the program inside of gdb
with c
so we can interact with our Python script again:
Now, let's use option one (1
) and request any size we feel like, I'll stick with 24 bytes
:
Now, in this data section, whatever we write here will get allocated to the user data area that the target currently occupies. Let's think of something clever:
After we press enter, we can go back into the debugger and ^C
to inspect the chunk:
This looks extremely promising and for all intents and purposes, the target has been overwritten, let's do some sanity checks by proving this and printing out the value of the target before trying the same in the actual program to successfully call this a complete pwn.
Now, let's continue one last time to see if the target changes on the binary itself.
Look at that! We did it! The target's been overwritten. We've completely pwned this binary using the House of Force! Next, we'll cover a case of actual code execution using malloc
hooks in the House of Force!
Boom! We've overwritten the top chunk Now, let's figure out how we can turn this into something more dangerous like an arbitrary write.
Well... that's quite a lot of bytes. Let's continue to the start and see what we stumble across there:
__malloc_hook
malloc()
from the binarygdb
0x31
gets allocated, as expectedglibc
version in use: 2.28
glibc
version from checksec
100 bytes
to our 24 byte
user data chunkgdb
session malloc
wrapperSIGINT
signal interrupt reached, we can debug