top of page

Exploring Heap Exploitation Mechanisms: Understanding the House of Force Technique

Updated: 1 day ago


Introduction to Heap Exploitation


Pre-Requisites


Before diving into the intricacies of heap exploitation, it's recommended to have the following:


  • A basic understanding of computer memory architecture, including the concepts of stack and heap.

  • Familiarity with the C programming language.

  • Knowledge of basic data structures and pointers.

  • Basic familiarity with Linux commands and debugging tools like GDB.


Even if you’re not an expert in these areas, this blog includes explanations to help you grasp the concepts. However, having this foundational knowledge will significantly enhance your understanding of heap exploitation techniques.


What is the Heap?


The heap is a memory region in a program where memory is dynamically allocated during its execution. It typically grows from lower to higher memory addresses, contrasting with the stack, which grows from higher to lower addresses in most architectures. One key advantage of the heap is that it allows the size of the allocated memory to be adjusted during execution.


Memory can be allocated on the heap using functions such as malloc() or calloc(). These functions take the size of the requested memory as an argument and return a pointer to the allocated space. Once the memory is no longer needed, it should be released using the free() function to prevent memory leaks.


In the code (and figure) given below, we use malloc to allocate the heap.


C code to allocate memory using malloc

#include <stdio.h>
#include <stdlib.h> // For malloc and free

int main() {
    int *ptr; // Pointer to allocated memory
    // Allocate memory for 1 integer
    ptr = (int *)malloc(sizeof(int));

    // Check if malloc succeeded
    if (ptr == NULL) {
        printf("Memory allocation failed!\n"); // Exit with error
        return 1; 
    }

    // Assign and display a value
    *ptr = 42; 
    printf("Value: %d\n", *ptr);

    // Free the allocated memory
    free(ptr);
    return 0;
}

Heap Section


Memory sections of the heap and stack should only be readable and writable. They should not be executable because, if an attacker can hijack the program’s control flow and manipulate the instruction pointer (RIP for x64-bit or EIP for x32-bit), they could execute arbitrary code.


  • To view the memory regions and other segments of a running process in Linux, first obtain the process ID (PID) using:


ps aux | grep <process_name>

  • Then, view the memory mappings using:


cat /proc/<pid>/maps

In the image below, we can see where the heap starts and ends, as well as the stack.


Memory map for linux program

If we focus on the heap memory section, we can see in the second column, where permissions are mentioned, we observe that read and write are set. However, in the third entry in permission column, the value is a "-", indicating that it is not executable.


What is Not a Heap?


Now that we understand heap, let’s briefly discuss what sections are not considered as heap memory:


  • Stack Memory: The Stack memory stores local variables and manages function calls. Unlike the heap, the stack is not dynamic and has a fixed size.


  • Global/Static Memory: It stores global and static variables used during program execution. This memory persists throughout the program’s execution.


Dynamic Memory Allocator


Memory allocators are crucial for managing the operating system. They are responsible for the allocation, deallocation, and reallocation of memory during program execution. They allow programs to request memory dynamically, adapting to varying data sizes and usage patterns based on the program’s requirements.


Memory Allocation
Memory allocation

We dynamically allocate storage during program execution, but we cannot create a new variable “on the fly,” i.e., while the program is running.


glibc: The C Standard Library


The GNU C Library (glibc) is the standard C library for many Unix-like operating systems, including Linux. It provides implementations of dynamic memory allocation functions such as malloc(), realloc(), and free(). It provides essential functions for C programming and memory management.


glibc also provides higher-level functions that internally use system calls to interact with the kernel.


malloc() in glibc


The malloc() function in glibc is a component of its memory allocator, designed to manage dynamic memory efficiently. The allocator is responsible for allocating and freeing memory chunks during program execution, allowing programs to request memory as needed.


ptmalloc()

  • Most Linux-based operating systems use ptmalloc() as their malloc() implementation in libc.

  • ptmalloc() is an improved version of dlmalloc (Doug Lea’s malloc).

  • It is optimized for multi-threading, making it a better choice for modern operating systems.


Chunk

  • The memory allocator divides the heap into blocks called chunks.

  • Each chunk contains metadata that tracks its size and status (in-use or free).

  • Chunks are categorized into different bins based on sizes, which helps optimize the allocation and deallocation process.


Bins

  • Smallbins: Smallbins handle chunks of less than 1024 bytes on a 64-bit system. They are managed as doubly linked lists using both forward (fd) and backward (bk) pointers. There are 62 smallbins, each corresponding to a specific size class.


  • Fastbins: Fastbins are designed to manage chunks less than 128 bytes, allowing rapid allocation and deallocation without the overhead of merging free chunks. When a chunk is freed, it is placed in the fastbin, making it quickly accessible for future requests. There are a total of 10 fastbins. On a 64-bit system, a fastbin consists of 10 linked lists, and the maximum chunk size is 160 bytes.


  • Largebins: Largebins are designed to allocate chunks larger than 1024 bytes on a 64-bit system. These bins manage larger memory requests separately and typically use a more complex data structure, such as a linked list, to allow efficient traversal and management of free chunks.


  • Unsorted bins: Unsorted bins serve as a holding area for free chunks that do not fit into the other categories. They contain chunks of various sizes that have been freed but not yet categorized. When the allocator needs to find a chunk for allocation, it can check the unsorted bin, allowing for flexible management of free memory.


Summary of Bins

Total Bins Per Thread

Bin Name

Number of Bins

Small Bins

62

Large Bins

63

Unsorted Bins

1

FastBins

10

Tcache Bins

64

Size Ranges

Bin Type

Size Range

FastBins

< 128 bytes

Small Bins

< 1024 bytes

Large Bins

> 1024 bytes

Unsorted Bins

Various sizes

Tcache Bins

24 – 1032 bytes


Arena


In glibc, arenas are designed to allow multiple threads to access separate memory regions without interfering with each other. These regions, known as “arenas,” help manage memory allocation more efficiently in multi-threaded applications. An arena is a contiguous memory region.


Each application can have a primary arena, often referred to as the “main arena.” The malloc() function includes a static variable that points to this main arena, and additional arenas can be created as necessary. Each thread can utilize one or more arenas, with the main arena being initialized at the program’s start. If the existing heap becomes exhausted, a new arena can be allocated, allowing for the dynamic expansion of the memory pool.


Number of Arenas for System


For 32-bit systems:


Number of Arenas = 2 * number of cores

For 64-bit systems:


Number of Arenas = 2 * number of cores

How to view the Arena?


For the purpose of this demo, we will be using the binaries and source code hosted in our github repository named Heap Overflow Labs.


To setup, run the following commands:



Viewing the Arena
Viewing the arena

To view the aforementioned details, we perform the following steps (after setting up the Git repo as mentioned previously):


  1. Load the Binary in GDB: 

gdb ./main
  1. Run the Binary

  2. Use the run (or r) command to start the binary execution:

r
  1. Interrupt Execution

  2. While the binary is running, press Ctrl + C to pause execution. This will stop the program at its current state, allowing us to inspect memory structures.

  3. View the Arena- Use the `arenas` command provided by pwndbg to inspect the arena:

pwndbg> arenas

Here,


  • Arena Type: This indicates the type of arena. In this case, main_arena signifies the default arena used by glibc for memory allocation. This arena is shared among all threads unless additional arenas are explicitly created via pthread_create or similar methods. Additional arenas are typically used to improve performance in multi-threaded applications by reducing contention on the main_arena.


  • Arena Address: This is the memory address of the arena structure, such as 0x7ffff7bafc20. It points to the malloc_state structure in memory, which contains metadata about the arena, including information about bins, locks, and other state data used to manage dynamic memory allocations within the arena.


  • Heap Address: This shows the starting address of the heap region managed by this arena, e.g., 0x602000. The heap is where dynamic memory allocations occur, and this address represents the base of the heap associated with the main_arena or any other arena. For main_arena, this heap is shared among all threads unless separate heaps are explicitly allocated for secondary arenas.


  • Map Start and Map End: These fields specify the range of memory addresses allocated for the arena’s heap region. For example, if the range is from 0x602000 to 0x623000, it indicates that the operating system has mapped memory pages within this address range for the heap. These mappings are managed by the OS using system calls like mmap or sbrk, and additional regions may be mapped dynamically if more memory is required.


  • Permissions (perm): This indicates the permissions of the memory region. For example, rw-p means the region is readable (r), writable (w), and privately mapped (p). Private mapping implies that changes to this memory region are not shared with other processes and are unique to the current process. Permissions may vary depending on the memory segment’s purpose (e.g., executable code may have r-x).


  • Size: The total size of the memory allocated for this heap or arena (e.g., 21,000 bytes). This value represents the amount of memory currently mapped by the operating system for allocations within this arena. Note that this size can grow dynamically as more memory is allocated, depending on the allocation strategy (brk or mmap) used by the glibc allocator.


Heap Exploitation via House of Force


Now that we have covered the basics of the heap and its internals, we can move on to exploring heap exploitation techniques. There are over 30 documented techniques for heap exploitation; some have been patched in modern memory allocators, while others remain viable in specific contexts.


In this blog, we will focus on a simple yet illustrative technique known as the “House of Force.” While it is not possible to cover all heap exploitation techniques in a single blog, I will strive to make this technique as easy to understand as possible.


What is House of Force


Background


The House of Force is a heap exploitation technique derived from the broader “House of XXX” framework, which encompasses various methods for exploiting glibc memory allocation. The foundational concepts of this technique can be traced back to the 2004 publication “The Malloc Maleficarum,” which introduced several strategies for exploiting vulnerabilities in glibc’s memory management.


The Concept Behind House of Force


In versions of glibc prior to 2.29, the top chunk’s size field lacks integrity checks. By overwriting this field, an attacker can request a size that manipulates the memory layout.


For example: Assume the top chunk starts at 0x405000 and the attacker’s target is located at 0x404000.


By overflowing and modifying the top chunk’s size to 0xfffffffffffffff1, the next memory allocation request will cause the allocated chunk to overlap with the target data.


This manipulation enables an attacker to control memory allocations, potentially leading to arbitrary code execution or memory corruption.


Why It’s Still Relevant?


Although this is a relatively older technique, it is essential to understand its underlying mechanics. Grasping the House of Force lays a foundation for tackling more advanced heap exploitation techniques, as many modern methods build upon similar principles.


A Simplified Outlook


Simply put, in House of Force, the attacker overflows the heap and hijacks the allocation size to control program flow and potentially execute arbitrary code.


Conditions for Executing House of Force


For the House of Force technique to be successful, the following conditions must be met:


  • Control the Top Chunk’s Size Field: The attacker must be able to overwrite the top chunk’s size field to manipulate its value.


  • Control Over Heap Allocation Size: The attacker must influence the size of memory allocations to exploit the overwritten top chunk size and manipulate memory layout. This control is achieved through overwriting the top chunk size and subsequently influencing the behavior of malloc() calls


Run the following command to view the checksec output for the binary "main":


checksec --file=main

Checksec output for main file
Checksec output for main file

In our case, the checksec output indicates “No PIE,” meaning the binary will load at a fixed memory address every time it is executed.


Executing House Of Force


Now, let’s run the binary inside pwndbg, a powerful GDB plugin for binary exploitation, which also allows us to disable ASLR directly within its environment.


After loading the binary, we have presented with three options:


  • AllocHeap Option: This option calls the malloc function, allowing us to allocate memory up to four times. It takes the size and data as input, allocating the specified memory size to the heap.

  • SavedString: This option displays the value of the target variable, which is initially set to WRITEME.

  • Exit Option: This option exits the program.


These options are presented in the program menu, as illustrated in the figure below:


program menu loaded via gdb
Program menu for "main" binary loaded in gdb

Our goal is to overwrite the target value using the House of Force technique by manipulating the top chunk and writing data to its region.


To proceed, press Ctrl+C to interrupt the program’s execution and enter the pwndbg prompt. From here, we can visualize the heap section and analyze its current state.


heap section and its current state.
Heap section and its current state

We successfully allocated a 24-byte chunk and confirmed that it contains our data, "iamhere." However, you may notice that the actual allocated size is 32 bytes instead of 24. This discrepancy arises due to alignment and metadata overhead:


  • Alignment: The requested size (24 bytes) is rounded up to the nearest multiple of 8 bytes for memory alignment, resulting in a 32-byte allocation.


  • Metadata Overhead: Each heap chunk includes an 8-byte metadata header, which stores the size of the chunk and flags. In this case, the value 0x21 indicates:


    • The total size of the chunk is 32 bytes (0x20 in hexadecimal for size + 1 flag byte).

    • The previous chunk is marked as “in use.”


In the above output, at 0x603020, we observe the start of the top chunk, and its size is 0x020fe1. This value is dynamic and changes as more chunks are allocated. The top chunk acts as a free memory pool, allowing the heap to expand when additional memory is requested. As memory is allocated or freed, the size of the top chunk adjusts accordingly, providing flexibility for future allocations.


In our next iteration, we have allocated another chunk, but this time we wrote data exceeding 24 bytes, causing a buffer overflow. As a result, we successfully overwrote the size of the top chunk.


Overflowing the buffer by exceeding 24 bytes
Overflowing the buffer

Note: We are using glibc versions earlier than 2.29, which do not perform sanity checks on the size of the top chunk. This lack of validation constitutes a security vulnerability. In later versions, this issue was patched, and additional protections such as the tcache mechanism were introduced in glibc to mitigate such exploits.


Now, let’s inspect the heap again to observe the changes.


Visualizing the overflow
Visualizing the overflow

When we wrote data exceeding 24 bytes, it resulted in overwriting the size of the top chunk. By changing the size of the top chunk to its largest possible value, 0xffffffffffffffff, we can cause memory overlap. At this point, malloc will assume that the entire memory region is part of the top chunk, allowing us to request significantly more memory than was originally intended.


In our case, the target variable resides in the data section of the program. Since the heap now assumes the top chunk size is 0xffffffffffffffff, it will treat the data section as part of the heap. To exploit this, we need to calculate the “wraparound” distance between the target variable and the start of the heap. This distance will determine how much memory we need to request to overwrite the target variable.


Furthering The Exploitation Phase


After successfully crafting the conditions for a heap overflow, we proceed to the next steps in our exploitation strategy using the pwn library. A template was created with the pwn library to facilitate sending crafted data to the heap. The exploitation process focuses on calculating the wraparound distance and manipulating heap memory to achieve our objectives.


Calculating the Wraparound Distance


We define a function called delta to compute the wraparound distance between two addresses. This is crucial for determining how far we need to write data to reach our target variable in memory.


The formula used here is:


def delta(x, y):
    return (0xffffffffffffffff - x) + y  # Calculate the wraparound distance between x and y

Formula to calulate wraparound
Formula to calulate wraparound

Where:


  • x is the starting address (e.g., the heap address offset).

  • y is the target address we aim to manipulate.


Memory Leak and Initialization


Before manipulating memory, we need to gather critical information about the program’s current state. This includes leaking memory addresses and initializing the required variables.


Memory leak and initialization formula
Memory manipulation

Steps:


  • Start the Process: We begin by interacting with the program and retrieving the address of the puts() function. Then, we subtract its known offset from the leaked address, revealing the base address of libc, enabling us to locate essential structures.


  • Leak Heap Address: Next, we capture the starting address of the heap. This is crucial for precise calculations later when we are manipulating memory and targeting specific locations.


This initialization ensures we have the required information to proceed with the exploitation reliably.


Manipulating the Heap


With both the heap address and the target variable’s address obtained, we can proceed to allocate memory and perform the overflow.


Manipulating the heap with information obtained
Manipulating the heap with information harvested

  • First Allocation: Allocate 24 bytes and overwrite the top chunk size with 0xffffffffffffffff. This tricks the allocator into treating the entire memory region as part of the heap, effectively bypassing size checks.


  • Calculating the Wraparound Distance: Use the delta function to compute the exact number of bytes required to reach the target variable. This ensures precise control over the memory layout and avoids unintended side effects.


  • Second Allocation: Allocate memory using the calculated distance. This step allows us to directly write to the target variable’s memory location through an overflow.


  • Final Allocation: Allocate another 24 bytes and write a payload (e.g., "0xrottenrabbit") into the target variable’s location. This value now overwrites the original target variable, enabling control over program flow or achieving other exploitation objectives.


Executing heap manipulation

The Road to Shell Access


After laying the groundwork in our previous sections, we now dive into one of the most exciting phases of heap exploitation: popping a shell! This is where our carefully crafted heap manipulations finally pay off, showcasing the power and potential impact of our exploit.


Address Information


The "main" binary is printing the following addresses:


  • puts func address leak() ---> 0x7d95dc26df10

  • heap address ---> 0x3dd6000


Since PIE (Position Independent Executable) is disabled, the binary and its associated libraries load at the same address every time. This allows us to reliably leverage the libc address leak to execute a system command.


Exploiting malloc Hooks


In glibc, core memory management functions like malloc() and free() use hooks implemented as writable function pointers in the glibc data section. Attackers can exploit these hooks to redirect function calls and execute arbitrary code during memory allocation and deallocation.


By overwriting these hooks, our goal is to hijack the program’s execution flow and execute arbitrary commands.


Bridging the Gap to __malloc_hook


To manipulate the __malloc_hook, we must cover the gap between the top chunk and the __malloc_hook pointer. This involves:


  • Using malloc() to Fill the Gap: We allocate memory with the payload below. This overwrites the size of the top chunk.


Using malloc to fill the gaps
Using malloc() to fill the gap

  • Adjusting for the Distance: Calculate the distance to __malloc_hook and adjust by subtracting 0x20 from the __malloc_hook address to account for alignment and metadata.


By carefully executing these steps, we redirect the program’s flow and execute our intended command through the overwritten __malloc_hook.


In the aforementioned, we subtract 0x20 to account for the chunk metadata, which precedes the user data in the heap. This adjustment ensures that our calculations align with the usable portion of the allocated chunk, allowing us to target the correct location in memory for manipulation.


The distance calculation provides the precise offset needed to bridge the gap between the top chunk and the malloc hook. Using the formula libc.sym.__malloc_hook - 0x20, we locate the start of the usable area of __malloc_hook. Subtracting (heap + 0x20) from this value positions us correctly at the beginning of the payload area in the top chunk.


Once we calculate the distance, we pass this value to malloc() to cover the gap between the top chunk and __malloc_hook.


After filling the gap, we overwrite the malloc hook with a payload. For example, we write another 24 bytes followed by a random address (e.g., 0xdeadbeef) to demonstrate control over execution. This will disrupt the program’s flow and break execution. To ensure predictable memory addresses, run the program with NOASLR enabled using GDB. In Vim, execute the following in Command-Line Mode:


`:!./% GDB NOASLR

running_no_aslr_1

If we try to allocate malloc, then the program will stop executing and show the error as below because of "an invalid address".


Executing the final exploit

Now, instead of using the address 0xdeadbeef, we replace it with the address of /bin/sh from libc. This allows us to execute the system("/bin/sh") command, effectively popping a system shell.


searching bin/sh inside libc

malloc(24, b"Y"*24 + p64(0xffffffffffffffff))  # Overwrite top chunk size to maximum

distance = (libc.sym.__malloc_hook - 0x20) - (heap + 0x20)  # Calculate distance from current top chunk to __malloc_hook

malloc(distance, "/bin/sh\0")  # Allocate memory to overlap __malloc_hook and write "/bin/sh" string

malloc(24, p64(libc.sym.system))  # Overwrite __malloc_hook with system() function address

cmd = next(libc.search(b"/bin/sh"))  # Find "/bin/sh" string in libc

malloc(cmd, "")  # Trigger system("/bin/sh") to pop a shell

Here:


  1. Writing /bin/sh:

    • At line 3, malloc(distance, "/bin/sh\0") writes the /bin/sh string at the calculated memory location (aligned with __malloc_hook). This sets up the payload for executing a shell.


  2. Overwriting __malloc_hook:

    • At line 4, malloc(24, p64(libc.sym.system)) overwrites the __malloc_hook function pointer with the address of system(). This ensures that the next invocation of malloc() calls system().


  3. Invoking the Shell:

    • At line 5, cmd = next(libc.search(b"/bin/sh")) searches libc for the exact address of the /bin/sh string.

    • At line 6, malloc(cmd, "") triggers the system("/bin/sh") command, launching a system shell.


By executing this final step, we achieved shell access. This demonstrates how precise heap manipulation, combined with knowledge of libc internals, enables control over program execution. The shell grants us full access to the system, showcasing the power of this exploitation technique.


Final exploit execution

Conclusion


Heap exploitation techniques like the House of Force demonstrate the complexities and risks associated with memory management systems such as glibc. While many of these vulnerabilities have been mitigated in modern systems, understanding their mechanics remains critical for defending against and identifying potential attack vectors.


By exploring concepts like chunk metadata manipulation, wraparound calculations, and malloc hook exploitation, we highlighted the foundational principles of heap exploitation. Although techniques like the House of Force are no longer effective in many modern systems, they serve as a stepping stone for understanding more advanced and creative exploitation methods.


Mitigations


To protect against such vulnerabilities, organizations must:


  • Enable modern mitigations like PIE, stack canaries, and ASLR.

  • Validate all inputs thoroughly and sanitize them as needed.

  • Avoid manual memory management wherever possible and use safer higher-level abstractions like smart pointers in C++ or memory-safe languages like Rust.

  • Use hardened memory allocators.

  • Ensure libraries like glibc are updated to the latest versions with tcache and sanity checks.


References


 

Register for instructor-led online courses today!


Check out our self-paced courses!


Check out our bundled Pricing & Plans for cost effective plans!


Contact us with your custom pen testing needs at: info@darkrelay.com  or WhatsApp.

123 views

Recent Posts

See All

コメント


bottom of page