Hi everyone.

In this post, I'm going to show you how radare2 can be used to perform heap analisys in the glibc. My purpose is to create a reference with examples, that shows what can be done in radare2. I do this cause I haven't found too much info about this on internet, only the heap module presentation made by n4x0r in the r2con 2016.

However, I prefer text, so I'll write here the commands with examples, ready to be consulted and copypasted.

In case you are only searching for the commands, you can check the cheatsheet I have prepared based on the content of this post.

## Glibc heap intro

Before diving into radare2, I'm going to introduce a series of concepts related to heap that will be necessary to understand to proceed with the post. Anyway, I will explain them further in their respective sections.

Allocator

An allocator is a module that manages the heap of a program. Thus, it is easier for a programmer to use it, through the malloc and free functions. In this post, we will review the glibc allocator, which is a fork of the ptmalloc2 allocator. However, there are more allocators, like jemalloc or tcmalloc.

Chunks

Chunks, as the name suggests, are chunks of memory that can be used to store any type of data. This are allocated for the program by using the malloc, calloc or realloc functions, and released with free. Chunks are found in a memory region known as heap.

Heap

A heap is a contigous region of memory which can be splitted to create chunks. Each heap belongs to an arena, and several can exists in the same program. The main heap is allocated in the memory region named [heap].

Arena

A arena is composed by a malloc_state structure, where bins are stored, and one or several heaps. A program can hold many arenas, that are created when new threads are spawned. The initial arena is the main_arena.

Bins

Bins are lists of chunks that are available to be used. There are different types of bins, that are used based on the situation. These are:

• Unsorted Bin -> Bin to insert chunks quickly, before insert them in other bins.

• Small bins -> For small chunks.

• Large bins -> For large chunks.

• Fast bins -> Cache bins for very small chunks.

• Tcaches -> Cache bins that allows to several threads to access to small chunks without blocking the arena.

Also, I will let explanations for some interesting phenomenons that happens in the memory management:

Consolidation

It happens when a process asks for a chunk bigger than those that a small bin can contain (and also the fast bins). In the consolidation the chunks of the fast bins are moved to the unsorted bin, thus allowing these to merge between them to avoid fragmentation, since it is expected that the program will require big chunks.

Tcaches Recharging

When the allocator is searching for chunks in a fast bin or an small bin, it takes the opportunity to moved as many chunks to the tcaches as it can. This also happens when it is discarding chunks from the unsorted bin. This way the tcaches are full of chunks and the locks of the arena are decreased.

In this post, I will describe these entities with more detail. However, in case you need more help to understand the topic, you can check the following resources:

Moreover, in order to practice, you should check the how2heap repository, which teach different heap exploiting techniques.

Furthermore, I would like to clarify that the data shown in this article was get from my personal experiments and revisions of code of different glibc versions. To perform my tests, I have used the glibc versions 2.32, 2.28 and 2.19, in a x86 computer, with programs of 32 and 64 bits.

Notwithstanding, glibc allows to adjust several parameters that can change its behaviour, so use this as an orientation, but not as an absolute truth. If you are not sure about a certain aspect, the best approach is to do your own experiments. ;)

Now, let's go to the topic.

Note: You shouldn't have too much problem with the radare2 commands shown in this post, even if you are a radare2 begginer. Otherwise, you can check any tutorial like this by Megabeets.

The first thing to do is open our program with radare2. We need to start radare2 with the debugger in order to analyze the heap, so we have to use the flag -d in the command line:

r2 -d ./heapshow


After radare2 is running with a program (and a code analysis was done if it is needed), we can set a breakpoint with db to stop the program execution and exploring the memory. You can also use Ctrl-c in the middle of the execution to let radare2 to take the control.

Once the execution was stopped, to explore the heap you can use the commands of the dmh group.

• d -> Debug commands.

• dm -> Memory maps commands.

• dmh -> Heap commands.

Moreover, you can view the heap commands help with dmh?:

[0x5624802e1193]> dmh?
Usage:  dmh   # Memory map heap
| dmh                                          List chunks in heap segment
| dmh @[malloc_state]                          List heap chunks of a particular arena
| dmha                                         List all malloc_state instances in application
| dmhb @[malloc_state]                         Display all parsed Double linked list of main_arena's or a particular arena bins instance
| dmhb [bin_num|bin_num:malloc_state]          Display parsed double linked list of bins instance from a particular arena
| dmhbg [bin_num]                              Display double linked list graph of main_arena's bin [Under developemnt]
| dmhc @[chunk_addr]                           Display malloc_chunk struct for a given malloc chunk
| dmhf @[malloc_state]                         Display all parsed fastbins of main_arena's or a particular arena fastbinY instance
| dmhf [fastbin_num|fastbin_num:malloc_state]  Display parsed single linked list in fastbinY instance from a particular arena
| dmhg                                         Display heap graph of heap segment
| dmhg [malloc_state]                          Display heap graph of a particular arena
| dmhi @[malloc_state]                         Display heap_info structure/structures for a given arena
| dmhm                                         List all elements of struct malloc_state of main thread (main_arena)
| dmhm @[malloc_state]                         List all malloc_state instance of a particular arena
| dmht                                         Display all parsed thread cache bins of all arena's tcache instance
| dmh?                                         Show map heap help


The description gives you an idea of what can be done with each one, but I will show you some examples to a better understanding.

Tip: You can use ? after a command to show it help.

## Arenas

An arena is composed by a malloc_state and one or various heaps. Also, a process can hold one or many arenas, based on the threads number. The threads can share an arena if it is necessary. The initial arena is called main_arena.

To identify the arenas of a process, the malloc_state structures must be located in memory. You can do this with the dmha command:

[0x55e517c4f1da]> dmha
main_arena @ 0x7f393d58fc40


The output shows the addresses of the arenas malloc_states. In this process, apart from the main_arena, there is another arena that is probably being used by a second thread.

Once arenas are located, radare2 can be used to display more details of each one. In case of examining the main_arena, it is not necessary to specify its memory address, since radare2 uses it automatically.

## malloc_state

Each arena has a malloc_state structure. Its definition is the following:

It stores plenty of information about the arena. The most interesting members are the following:

• fastbinsY -> The fast bins, which are 10. For each one, there is a pointer to the first chunk in the fast bin. In there are no chunks, the pointer is set to 0 (NULL).

• top -> The address of the heap top chunk.

• last_remainder -> The address of the last_remainder chunk. When a chunk is splitted into 2, in order to create an smaller chunk to return to the program, the chunk that remains free is the last_remainder. It is referenced for efficiency.

• bins -> The bins, which are 127, although the first isn't used. Inside this attribute the unsorted bin, the small bins and the large bins are contained. For each bin, 2 pointers are used, fd e bk, that points to the first and last chunk of the bin. In case of bin is empty, then this pointers points to the entry itself.

• next -> The address of the next malloc_state.

• system_mem -> Current heap size.

In order to show an arena malloc_state, you can use the dmhm command. If not address is specified, then it shows the info about the main_arena:

[0x55e517c4f1da]> dmhm
malloc_state @ 0x7f3938000020

struct malloc_state main_arena {
mutex = 0x00000000
flags = 0x00000002
fastbinsY = {
Fastbin 01
chunksize: == 0032 0x0,
Fastbin 02
chunksize: == 0048 0x0,
Fastbin 03
chunksize: == 0064 0x0,
Fastbin 04
chunksize: == 0080 0x0,
Fastbin 05
chunksize: == 0096 0x0,
Fastbin 06
chunksize: == 0112 0x0,
Fastbin 07
chunksize: == 0128 0x0,
Fastbin 08
chunksize: == 0144 0x0,
Fastbin 09
chunksize: == 0160 0x0,
Fastbin 10
chunksize: == 0176 0x0,
}
top = 0x7f3938000f00,
last_remainder = 0x0,
bins {
Bin 001: Unsorted Bin [ chunksize: undefined 0x7f3938000020->fd = 0x7f3938000080, 0x7f3938000020->bk = 0x7f3938000080,
Bin 002:              ┌ chunksize: == 000032  0x7f3938000030->fd = 0x7f3938000090, 0x7f3938000030->bk = 0x7f3938000090,
Bin 003:              │ chunksize: == 000048  0x7f3938000040->fd = 0x7f39380000a0, 0x7f3938000040->bk = 0x7f39380000a0,
......................│....
Bin 031:              │ chunksize: == 000496  0x7f3938000200->fd = 0x7f3938000260, 0x7f3938000200->bk = 0x7f3938000260,
Bin 032:   Small Bins │ chunksize: == 000512  0x7f3938000210->fd = 0x7f3938000270, 0x7f3938000210->bk = 0x7f3938000270,
Bin 033:              │ chunksize: == 000528  0x7f3938000220->fd = 0x7f3938000280, 0x7f3938000220->bk = 0x7f3938000280,
......................│....
Bin 063:              │ chunksize: == 001008  0x7f3938000400->fd = 0x7f3938000460, 0x7f3938000400->bk = 0x7f3938000460,
Bin 064:              └ chunksize: == 001024  0x7f3938000410->fd = 0x7f3938000470, 0x7f3938000410->bk = 0x7f3938000470,
Bin 065:              ┌ chunksize: >= 001088  0x7f3938000420->fd = 0x7f3938000480, 0x7f3938000420->bk = 0x7f3938000480,
Bin 066:              │ chunksize: >= 001152  0x7f3938000430->fd = 0x7f3938000490, 0x7f3938000430->bk = 0x7f3938000490,
......................│....
Bin 095:              │ chunksize: >= 003008  0x7f3938000600->fd = 0x7f3938000660, 0x7f3938000600->bk = 0x7f3938000660,
Bin 096:   Large Bins │ chunksize: >= 003072  0x7f3938000610->fd = 0x7f3938000670, 0x7f3938000610->bk = 0x7f3938000670,
Bin 097:              │ chunksize: >= 003136  0x7f3938000620->fd = 0x7f3938000680, 0x7f3938000620->bk = 0x7f3938000680,
......................│....
Bin 126:              │ chunksize: >= 524288  0x7f39380007f0->fd = 0x7f3938000850, 0x7f39380007f0->bk = 0x7f3938000850,
Bin 127:              └ chunksize: remaining 0x7f3938000800->fd = 0x7f3938000860, 0x7f3938000800->bk = 0x7f3938000860,
}
binmap = {0x0,0x0,0x0,0x0}
next = 0x7f393d58fc40,
next_free = 0x0,
system_mem = 0x21000,
max_system_mem = 0x21000,
}


(The output is stripped for the sake of space)

In this output we can see different information. For example, there are no chunks in the fast bins, the heap size is 0x21000 bytes and there isn't last remainder.

Wait, where are the tcaches? Well, they are in other place since they were created to avoid threads from blocking the malloc_state. The tcaches are stored in another place, in the tcache_perthread_struct, which can be found in the heap (since the glibc version 2.26).

## Chunks

Chunks are chunks of memory from heap that are created through calls to the malloc family and released by using free.

At the beginning, the heap only has one chunk, the top chunk, and when new chunks are requested, the top chunk is splitted to create new chunks of the required size. In case of the requested chunk size is too big, then the chunk is allocated by using mmap.

Moreover, for efficiency reasons, the chunks are aligned to 8 bytes addreses in 32 bits and to 16 bytes in 64 bits. This means that the chunk size is always a multiple of 8 in 32 bits and a multiple of 16 in 64 bits. However, there is an exception. Since glibc 2.26, in x86 or i386 architecture, used in most of the personal computers and servers, the chunks are always aligned to 16, regarless of the program bits.

Furthermore, cause chunks are aligned to certain memory addresses, it is not possible to create a chunk of any size, so several malloc invocations with different sizes in a near range creates a chunk of the same size.

For instance, both malloc(100) and malloc(101) would reserve a chunk of the same size, that would be 112 bytes in 64 bits. In case you want to calculate the chunk size based on a malloc call size, you can use the following snippet or the gmcalc tool.

You can check the program architecture and bits in radare2 with i:

[0x7f8491e94090]>i~machine[1-]
AMD x86-64 architecture
[0x7f8491e94090]> i~bits[1]
64


This time we have a x86 program of 64 bits.

In order to get the glibc version, you can look the memory maps with dm and search for libc:

[0x557602f15189]> dm~libc:0[9]
/usr/lib/x86_64-linux-gnu/libc-2.32.so


You can spot the version 2.32 in the glibc filename.

Back to the topic, in every chunk we can find the malloc_chunk structure:

Note: The INTERNAL_SIZE_T type is a alias for size_t, which is 8 bytes long in 64 bits and 4 bytes in 32 bits.

Note: The member mchunk_prev_size is known as prev_size in older versions, so I will use this last name since is more compact. Same to mchunk_size, which was usually called size.

The prev_size (mchunk_prev_size) field indicates the previous chunk size when it is free. In the other case, the previous chunk can use this prev_size field to store arbitrary data.

       .-----------.
| prev_size |
| size      | free chunk
chunk | fd        |
| bk        |
---|-----------|---
| prev_size | <- previous chunk size
| size      | allocated chunk
chunk | prevdata  |
| prevdata  |
---|-----------|---
| prevdata  | <- previous chunk data
chunk | size      |
| ....      |
'-----------'


When the previous chunk is released, then the glibc writes in the prev_size field it size. Also, the flag PREV_INUSE of the size field is set to 0, as we will see.

The size (mchunk_size) field indicates the chunk size, it includes the header (prev_size + size).

In 32 bits, the minimum chunk size is 16 bytes, and 32 bytes in 64 bits. Therefore, even the calls to malloc(0) will return a chunk of these sizes:

• 32 bits: malloc(0) -> 16 bytes. 8 bytes of data and 8 bytes of header.

• 64 bits: malloc(0) -> 32 bytes. 16 bytes of data and 16 bytes of header.

Moreover, since every chunk is a multiple of 8, then the 3 least significant bits aren't used to indicate the size. These bits are used as flags with special meanings:

• P (PREV_INUSE) -> First bit is set (0x1) if the previous chunk is free.

• M (IS_MMAPPED) -> Second bit is set (0x2) if the chunk was created with mmap.

• N ou A (NON_MAIN_ARENA) -> Third bit is set (0x4) if the chunk is not in the main_arena.

      chunk
.--------------.
| prev_size     |
| size  |N|M|P| | <-- special flags
| fd            |
| bk            |
| fd_nextsize   |
| bk_nextsize   |
'---------------'


The next fields, fd and bk, are pointers used by the free chunks in the bins to create links by pointing to the next an previous chunk in the same bin, respectively.

Otherwise, when the chunk is being used, these pointers are useless and their space can store arbitrary data of the program. Actually, when a chunk is allocated by using malloc, the returned pointer doesn't point to the beginning of the chunk, but fd address.

                  chunk
.-----------.
| prev_size |
| size      |
malloc(x) --> | fd        |
| bk        |
'-----------'


Lastly, the pointers fd_nextsize and bk_nextsize are used by large bins, that contains chunks with different sizes. Thus, these pointers are used to point to the beggining of the chunks with the next (larger) or previous (smaller) size, respectively.

Ultimately, a chunk can hold store different information based on its state:


allocated           free               free
(large bin)
.-----------.     .-----------.     .-------------.
| prev_size |     | prev_size |     | prev_size   |
| size      |     | size      |     | size        |
| userdata  |     | fd        |     | fd          |
| userdata  |     | bk        |     | bk          |
| userdata  |     | userdata  |     | fd_nextsize |
| userdata  |     | userdata  |     | bk_nextsize |
| userdata  |     | userdata  |     | userdata    |
| userdata  |     | userdata  |     | userdata    |
'-----------'     '-----------'     '-------------'


It should be noted that in the moment a chunk is released by free, the data remains unaltered, except for those fields overwritten with the pointers used by the bins. It is responsibility of the programmer to erase that data and/or assume that chunks will contain "random" data.

To show a chunk in radare2, you can use the dmhc command with its address preceded by @. For example dmhc @0x5583f1f1f270:

[0x5583f0e61282]> dmhc @0x5583f1f1f270
struct malloc_chunk @ 0x5583f1f1f270 {
prev_size = 0x0,
size = 0x20,
flags: |N:0 |M:0 |P:1,
fd = 0x5583f1f1f2a0,
bk = 0x5583f1f1f010,
}
chunk data =
0x5583f1f1f280  0x00005583f1f1f2a0  0x00005583f1f1f010   .....U.......U..


In this case the chunk is in a bin (it is not indicated in the chunk, but I'm telling you). Thus fd and bk point to other bin chunks (or the bin entry in the malloc_state).

You can note that chunk data shows the same bytes as fd and bk, this is because when chunk is being used, data starts in fd.

The following example shows a used chunk:

[0x5624802e1193]> dmhc @0x562482199250
struct malloc_chunk @ 0x562482199250 {
prev_size = 0x0,
size = 0x410,
flags: |N:0 |M:0 |P:1,
fd = 0x6f77206f6c6c6568,
bk = 0xa646c72,
fd-nextsize = 0x0,
bk-nextsize = 0x0,
}
chunk data =
0x562482199260  0x6f77206f6c6c6568  0x000000000a646c72   hello world.....
0x562482199270  0x0000000000000000  0x0000000000000000   ................
...
...


Here, you can appreciate that fd and bk values are too weird to be in a bin, and chunk data shows that data is the string hello world, so you can imagine that chunk is being used, even if it is not explicitly indicated in the chunk metadata (we should check the next chunk P flag).

If you don't specify an address, dmhc will try to parse the current address as a chunk. This can lead to parse weird data like the following:

[0x55cb6f22e1cc]> dmhc
struct malloc_chunk @ 0x55cb6f22e1cc {
prev_size = 0xbf00000e7f358d48,
size = 0xfffe73e800000000,
flags: |N:0 |M:0 |P:1,
fd = 0xc3c900000000b8ff,
bk = 0x80c48348e5894855,
fd-nextsize = 0x358d480000001dba,
bk-nextsize = 0x1bf00000e72,
}
chunk too big to be displayed


We can see that chunk size is huge, so we can imagine that there is no chunk in that address.

## Heap

Heaps are large contigous memory regions, where chunks are created. The main heap is created with the sbrk syscall and its memory region is called [heap]. The rest of the heaps are created through mmap.

As we said before, when it is created, a heap only contains a big chunk called top chunk, that is splitted to create new chunks when their are required.

Moreover, when it is necessary, the heap size can be increased, by using sbrk or mmap (depends on how the heap was created).


initial heap                 heap                       heap
.-----------.             .-----------.             .-----------.
|           |   malloc    |   chunk   |             |   chunk   |
|           | ----------> |           |             |           |
|           |             |-----------|             |-----------|
|           |   malloc    |   chunk   |             |   chunk   |
|           | ----------> |           |             |           |
| top_chunk |             |-----------|             |-----------|
|           |             |           |   malloc    |   chunk   |
|           |             | top_chunk | ----------> |           |
|           |             |           |             |-----------|
|           |             |           |   malloc    |   chunk   |
'-----------'             '-----------' ----------> |           |
|        |-----------|
| sbrk   |           |
'------> | top_chunk |
|           |
'-----------'


This scheme shows how the top_chunk is splitted after several malloc calls. Also sbrk is used when more heap is required.

We also have to know that huge chunks are created by calling mmap directly, without using any heap.

You can check if the main heap exists by examinating the memory maps with dm:

[0x5614183471c2]> dm~heap]
0x0000561418c3f000 - 0x0000561418c60000 - usr   132K s rw- [heap] [heap]


In order to list the heap chunks, you can use the dmh command:

[0x5564c637d1dd]> dmh

Malloc chunk @ 0x5564c76f9250 [size: 0x3f0][free]
Malloc chunk @ 0x5564c76f9640 [size: 0x120][allocated]
Top chunk @ 0x5564c76f9760 - [brk_start: 0x5564c76f9000, brk_end: 0x5564c771a000]


You should be able to examinate the heap of any arena, however, when I tried to show the heap of a thread arena, radare2 indicates that it is corrupted:

[0x56159e0e21d8]> dmha
main_arena @ 0x7f6066b3fc40
[0x56159e0e21d8]> dmh 0x7f6060000020

Malloc chunk @ 0x7f6060000b50 [corrupted]
size: 0x0
fd: 0x0, bk: 0x0

Top chunk @ 0x7f6060000f00 - [brk_start: 0x7f6060000000, brk_end: 0x7f6060021000]


I reported this issue so I hope it will be resolved. Moreover, when I run the program in 32 bits, radare2 shows perfectly the thread arena, however the main_arena is corrupted. ¯\_(ツ)_/¯

Additionally, the heaps created by mmap hold a heap_info structure at the beginning:

typedef struct _heap_info
{
mstate ar_ptr; /* Arena for this heap. */
struct _heap_info *prev; /* Previous heap. */
size_t size;   /* Current size in bytes. */
size_t mprotect_size; /* Size in bytes that has been mprotected
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
} heap_info;

This structure indicates, in the ar_ptr member, the address of the arena to which the heap belongs.

We can inspect the heap_info of the arena heaps with dmhi:

[0x559dfb80022e]> dmhi @0x7f3bd8000020
malloc_info @ 0x7f3bd8000000 {
ar_ptr = 0x7f3bd8000020
prev = 0x0
size = 0x21000
mprotect_size = 0x21000
}


However, due to the main arena is in a predefined position, a heap_info structure is not necessary. Therefore radare2 will return the following error if you try to inspect the heap_info of the main_arena:

[0x559dfb80022e]> dmhi @0x7f3bdd14cba0
main_arena does not have an instance of malloc_info


Why does it say malloc_info instead of heap_info? I'm don't know…

## Bins

Bins are lists with chunks that are not being used. A chunk must be in one bin at the same time. The bin choosen to insert a chunk is a complex topic, which depends heavily on the chunk size, but also in other optimization factors.

Actually, the algorithms used to insert and remove chunks from bins are designed to be efficient. As a consequence, they can behave in a unpredictable way at first sight. The best approach to understand them is to play with a program to become familiar with the bins behaviour.

There are 5 types of bin, that can be divided in 2 groups. The "regular" bins or double linked, and the cache bins or singled linked.

The doubled linked bins use the fd and bk pointers. They are FIFO (First In First Out) queues, where the chunks are inserted at the beginning and searched starting by the end. These bins are:

• Unsorted bin -> Bin where chunks are inserted without any order, before being inserted in large bins or small bins.

• Small bins -> To small chunks. The size is the same for all chunks in the same small bin.

• Large bins -> To large chunks. They can contain chunk with different sizes that are sorted based on it.

On the other hand, the single linked bins only use the fd pointer. They are LIFO (Last In First Out) queues, since the chunks are inserted and searched in the beginning of the bin. These bins are:

• Fast bins -> Cache bins for the smallest chunks.

In the case chunks that fits in several bins, usually in tcaches, fast bins and small bins, the order is the following:

1. Tcaches -> Chunks are inserted in tcaches always that it is possible, both through free calls and tcache recharging.

2. Fast bins -> Chunks are inserted in fast bins when tcaches are full.

3. Small bins -> Chunks are inserted in small bins if the tcaches are full and the chunk does not fit in a fast bin.

The double linked (fd and bk) bins are:

• Unsorted bin

• Small bins

• Large bins

These bins can be found in the malloc_state bins member. This attribute contains a pair of pointers fd and bk for each bin, that point to the first and last chunk, respectively. If there are no chunks, these pointers point to the bin entry itself.

We can show the doubled linked bins with the dmhb command:

[0x7f8491e94090]> dmhb
Bin 001:
double linked list unsorted bin {
0x7f8491df1ca0->fd = 0x7f8491df1ca0
0x7f8491df1ca0->bk = 0x7f8491df1ca0
}
Bin 002:
double linked list small bin {
0x7f8491df1cb0->fd = 0x7f8491df1cb0
0x7f8491df1cb0->bk = 0x7f8491df1cb0
}
Bin 003:
double linked list small bin {
0x7f8491df1cc0->fd = 0x7f8491df1cc0
0x7f8491df1cc0->bk = 0x7f8491df1cc0
}
..............| Stripped Output |...................
Bin 064:
double linked list small bin {
0x7f8491df2090->fd = 0x5637a11c0030->fd = 0x7f8491df2090
0x7f8491df2090->bk = 0x5637a11c0030->bk = 0x7f8491df2090
}
Bin 065:
double linked list large bin {
0x7f8491df20a0->fd = 0x7f8491df20a0
0x7f8491df20a0->bk = 0x7f8491df20a0
}
..............| Output stripped |...................
Bin 126:
double linked list large bin {
0x7f8491df2470->fd = 0x7f8491df2470
0x7f8491df2470->bk = 0x7f8491df2470
}
Bin 127:
double linked list large bin {
0x7f8491df2480->fd = 0x7f8491df2480
0x7f8491df2480->bk = 0x7f8491df2480
}

}


(The output is stripped for the sake of space)

In order to show only the bins that contains some chunk, we can filter the dmhb output with grep:

[0x7f8491e94090]> dmhb | grep -E 'fd =.+=' -C 2
Bin 064:
double linked list small bin {
0x7f8491df2090->fd = 0x5637a11c0030->fd = 0x7f8491df2090
0x7f8491df2090->bk = 0x5637a11c0030->bk = 0x7f8491df2090
}


To show an specific bin, you can specify the bins desired index in dmhb:

[0x7f8491e94090]> dmhb 64
Bin 064:
double linked list small bin {
0x7f8491df2090->fd = 0x5637a11c0030->fd = 0x7f8491df2090
0x7f8491df2090->bk = 0x5637a11c0030->bk = 0x7f8491df2090
}


#### Unsorted bin

The unsorted bin is a doubled linked bin, which can be travelled forwards and backwards by using the fd and bk pointers of the chunks. Is a FIFO (First In First Out) queue, where chunks are inserted at the beginning, in the fd pointer of the bin entry, and are searched from the end, starting in the bk pointer.

It is the first bin in the malloc_state bins member and its chunks are unsorted, allowing to do fast insertions. Therefore, the glibc inserts the chunks in the unsorted bin firstly. Afterwards, when it is travelled by searching a chunk, all the discarted chunks are inserted in their respective bins, small or large.

However, usually the chunks that fits in the small bins are inserted directly in those. This happens because it is easy to determine the correct bin for a chunk and due all the chunks in a small bin are the same size, they don't need to be sorted. In my experiments, chunks destined to an small bins where only inserted into the unsorted bin when a consolidation was happening.

We can check the unsorted bin with dmhb 1:

[0x55b3cb19b27d]> dmhb 1
Bin 001:
double linked list unsorted bin {
0x7ff8f1a9eca0->fd = 0x55b3cce31370->fd = 0x55b3cce31f90->fd = 0x7ff8f1a9eca0
0x7ff8f1a9eca0->bk = 0x55b3cce31f90->bk = 0x55b3cce31370->bk = 0x7ff8f1a9eca0
}


In this example the unsorted bin have 2 chunks (0x55b3cce31370 and 0x55b3cce31f90).

It is possible to show the unsorted bin of other arenas by specifying the address of the malloc_state, following the format dmhb 1:malloc_state:

[0x55f3489b8250]> dmhb 1:0x7fbd44000020
Bin 001:
double linked list unsorted bin {
0x7fbd44000080->fd = 0x7fbd44000080
0x7fbd44000080->bk = 0x7fbd44000080
}


In this case the unsorted bin is empty.

#### Small bins

The small bins are double linked (fd and bk) FIFO (First In First Out) queues. They are used for small chunks. The chunks are inserted at the beginning of the bin, and taken from the end.

There are 62 small bins in 64 bits, with chunk sizes from 32 (0x20) until 1008 (0x3f0) bytes.

On the other hand, in 32 bits the number of small bins varies based on the chunk alignment, with the minimum size 16 (0x10) bytes. In the case of chunks being aligned to 8, there are 62 small bins that can hold chunks of sizes until 504 (0x1f8) bytes. On the contrary, if chunks are aligned to 16, then sizes reach 1008 bytes (0x3f0) bytes, the same as 64 bits, but in this occasion there are 63 small bins.

We can also calculate the required chunk size for an small bin with gmcalc.

You can check the small bins in radare2 by using dmhb and grep:

[0x7f8491e94090]> dmhb | grep 'small bin' -B 1 -A 3
Bin 002:
double linked list small bin {
0x7f87dc2d77c8->fd = 0x202f000->fd = 0x202f040->fd = 0x7f87dc2d77c8
0x7f87dc2d77c8->bk = 0x202f040->bk = 0x202f000->bk = 0x7f87dc2d77c8
}
Bin 003:
double linked list small bin {
0x7f87dc2d77d8->fd = 0x7f87dc2d77d8
0x7f87dc2d77d8->bk = 0x7f87dc2d77d8
}
..............| Output stripped |...................
Bin 062:
double linked list small bin {
0x7f87dc2d7b88->fd = 0x7f87dc2d7b88
0x7f87dc2d7b88->bk = 0x7f87dc2d7b88
}
Bin 063:
double linked list small bin {
0x7f87dc2d7b98->fd = 0x7f87dc2d7b98
0x7f87dc2d7b98->bk = 0x7f87dc2d7b98
}
Bin 064:
double linked list small bin {
0x7f87dc2d7ba8->fd = 0x7f87dc2d7ba8
0x7f87dc2d7ba8->bk = 0x7f87dc2d7ba8
}


(The output is stripped for the sake of space)

We can see that the small bin number 2 has 2 chunks.

We can also apply a filter to show only the small bins that contains chunks:

[0x7f87dbf4bc37]> dmhb | grep 'small bin' -B 1 -A 3 | grep -E 'fd =.+=' -C 2
Bin 002:
double linked list small bin {
0x7f87dc2d77c8->fd = 0x202f000->fd = 0x202f040->fd = 0x7f87dc2d77c8
0x7f87dc2d77c8->bk = 0x202f040->bk = 0x202f000->bk = 0x7f87dc2d77c8
}


#### Large bins

The large bins are bins that contains big chunks. They are FIFO (First In First Out) queues with a double link (fd and bk).

Moreover, the large bins can contain chunks of different sizes, sorted from largest to smallest. Additionally, to increase the travelling speed, the chunks of the large bins use the fd_nextsize and bk_nextsize pointers, which point to the chunks with the next (larger) and previous (smaller) sizes. Only the first chunk of each size use these pointers.

The size range of the large bins starts where small bins ends until the chunks allocated with mmap.

Each large bin has a range of sizes that can hold, by starting the firsts with ranges of 64 bytes, and increasing the range in last ones. In the following table the sizes of the chunks that are hold by each large bin in different environments. The size ranges are also indicated.

The bins between 95 and 98 are most curious, since they vary in each environment. It seems that some bins with no standard ranges are introduced in order to homogenize the ranges of the later bins. You can use gmcalc to calculate the sizes of a large bin if you need it.

Even if there are 126 bins, from number 123 it becomes more difficult to take a measure. This is due to many chunks are allocated by mmap and not in the heap. However, the minimum size that was allocated by mmap changed in different test, but it seems that from 0x20000 bytes, a chunk can be allocated by mmap if it is neccesary.

To show the large bins in radare2, you can filter with grep:

[0x5583f0e61282]> dmhb | grep 'large' -B 1 -A 3
Bin 065:
double linked list large bin {
0x7f981ba440a0->fd = 0x7f981ba440a0
0x7f981ba440a0->bk = 0x7f981ba440a0
}
Bin 066:
double linked list large bin {
0x7f981ba440b0->fd = 0x7f981ba440b0
0x7f981ba440b0->bk = 0x7f981ba440b0
}
..........................................
Bin 110:
double linked list large bin {
0x7f981ba44370->fd = 0x5583f1f1ff90->fd = 0x7f981ba44370
0x7f981ba44370->bk = 0x5583f1f1ff90->bk = 0x7f981ba44370
}
..........................................
Bin 126:
double linked list large bin {
0x7f981ba44470->fd = 0x7f981ba44470
0x7f981ba44470->bk = 0x7f981ba44470
}
Bin 127:
double linked list large bin {
0x7f981ba44480->fd = 0x7f981ba44480
0x7f981ba44480->bk = 0x7f981ba44480
}


To get the large bins with chunks, dmhb with grep also can be used:

[0x5583f0e61282]> dmhb | grep 'large' -B 1 -A 3 | grep -E 'fd =.+=' -C 2
Bin 110:
double linked list large bin {
0x7f981ba44370->fd = 0x5583f1f1ff90->fd = 0x7f981ba44370
0x7f981ba44370->bk = 0x5583f1f1ff90->bk = 0x7f981ba44370
}


### Fast bins

The fast bins are cache bins for the smallest chunks. It is common for a process to allocate and free little chunks continuously. And there are the fast bins to help.

There are 10 fast bins per arena, even if only the seven firsts (the smaller ones) are used in practice. Each fast bin only has chunks of an specific size.

In 64 bits, the fast bins can hold chunks from 32 bytes until 128 bytes. This size will be increased until 168 if all fast bins were used.

On the other side, in 32 bits, the fast bins can hold chunks with a minimum size of 16 bytes. The maximum size 112 bytes when chunks are align to 16, and 64 bytes in case they are aligned to 8.

You can also calculate the size of a fast bin with gmcalc.

The fast bins are single linked LIFO (Last In First Out) queues, that only uses the fd pointer to point to the next chunk. Both chunk insertions and removals are done in the fast bin header, following the corresponding pointer of the fastbinsY member of the malloc_state.

Furthermore, the chunks inserted in a fast bin are not marked as free (flag P of the following chunk). Thus, the chunks that are inserted in a fast bin stay there without being merged until they are reused again or a consolidation happens. This way, the merge of a chunk that is probable to be reused in a short period of time is avoided.

You can use the command dmhf to show the fast bins:

[0x5627a3a97306]> dmhf
fastbinY {
Fastbin 01
fastbin 1 @ 0x7f6e9df65c50 {
0x5627a47d9760->fd = 0x5627a47d9740->fd = 0x5627a47d9720
}
Fastbin 02
Empty bin  0x0
Fastbin 03
Empty bin  0x0
Fastbin 04
Empty bin  0x0
Fastbin 05
Empty bin  0x0
Fastbin 06
Empty bin  0x0
Fastbin 07
Empty bin  0x0
Fastbin 08
Empty bin  0x0
Fastbin 09
Empty bin  0x0
Fastbin 10
Empty bin  0x0
}


In case you want to want to see only the fast bins with chunks, you can use dmhf | grep -w 'fastbin' -A 2:

[0x56173d3ee355]> dmhf | grep -w 'fastbin' -A 2
fastbin 1 @ 0x7fcf8b64ec50 {
0x56173f36d760->fd = 0x56173f36d740->fd = 0x56173f36d720
}
--
fastbin 4 @ 0x7fcf8b64ec68 {
0x56173f36d9b0
}


### Tcaches

Tcaches (Thread Caches) are a kind of special cache bins, implemented in glibc 2.26. They can be accesed by a thread without blocking the arena malloc_state, which increases the performance. To achieve that, the tcaches entries are stored in a structure apart from malloc_state. In the tcache_perthread_struct:

The entries member is an array of tcache_entry, and each entry contains a next pointer, that points to the first chunk of the tcache, and a key member that is used to protect against double frees. Additionally, there is the count member, that is used to count the chunks of each tcache in an efficient way.

The tcaches, like fast bins, are single linked and they only use the chunk fd pointer , which is interpreted as next. They are LIFO (Last In First Out) queues, where chunk insertions and removals are done in the header.

In contrast with the rest of the bins, the fd (next) pointer points to the fd (next) pointer of the next chunk, instead of its beggining. The last chunk in the tcache points to NULL (fd = 0x0). The bk pointer holds the key value, by pointing to the tcache_perthread_struct.

The following diagram shows a tcache with 2 chunks:

                                            .-----------.        .-----------.
| prev_size |        | prev_size |
| size      |        | size      |
tcache_perthread_struct.entries[i].next --> | fd (next) |------->| fd (next) |---> 0x0
^     ^             .----------<| bk (key)  |  .----<| bk (key)  |
|     '-------------'           '-----------'  |     '-----------'
'----------------------------------------------'


There are 64 tcaches per thread, and each one can hold until 7 chunks of the same size.

In 64 bits, they can hold chunks from 32 (0x20) bytes until 1040 (0x410) bytes. On the other hand, in 32 bits, the minimum size is 16 bytes. The maximum depends on the alignment, if the alignment is 16 then the maximum is 1024 (0x400) bytes, whereas with an alignment to 8 the maximum is 520 (0x208) bytes.

You can calculate the size of a tcache chunk with gmcalc.

The chunks of a tcache, as well as those in the fast bins, are not marked as free, so they are not merged with other chunks.

You can use dmht to check the tcaches with chunks:

[0x7f8491e94090]> dmht
Tcache main arena @ 0x7f8491df1c40
bin : 1, items : 3, fd :0x5637a11c0000->0x5637a11be830->0x5637a11bffd0
bin : 2, items : 2, fd :0x5637a11bd910->0x5637a11bfd30
bin : 3, items : 2, fd :0x5637a11c0430->0x5637a11bfd70
bin : 5, items : 1, fd :0x5637a11bfb20
bin :33, items : 1, fd :0x5637a11bd620
bin :59, items : 1, fd :0x5637a11bf6c0


In this example there are tcaches with 1, 2 or 3 chunks.

If there is no chunks in the tcaches, the output would be similar to the following:

[0x563d885dc2e4]> dmht
Tcache main arena @ 0x7f4222dfbba0


## Errors

I will let you here some common errors that can arise when your are examinating the heap.

It is possible to receive the following error while your are executing some of the previous commands:

[0x7f94f6c29090]> dmha?
Warning: Can't find glibc mapped in memory (see dm)


This could happens due to several reasons:

• The process is not running. Try with dc to execute the program.

• The program is running, but glibc was still not mapped. Try with breakpoint in db main and then dc.

• The program does not use glibc. Aren't you in Windows, are you?

Anyway, you can check that glibc is mapped into the process with dm (as radare2 tell you):

[0x557602f15189]> dm~libc:0[9]
/usr/lib/x86_64-linux-gnu/libc-2.32.so


You can get this error:

[0x000010c0]> dmha
dbg.glibc.tcache = 1
Warning: Can't find arena mapped in memory (see om)


radare2 return this error to me when I forget to specify the -d (debug) flag in the r2 command.

Another possible error is the following:

[0x55b07c433207]> dmha
No Heap section


This error happens when there is no arena nor heap, which are created in the first call to malloc (or realloc, calloc).

You can verify if the heap is there looking for [heap] with dm:

[0x55b07c433221]> dm~heap]
0x000055b07da3b000 - 0x000055b07da5c000 - usr   132K s rw- [heap] [heap]


If the heap was not created, the previous command will not display any output.

### Safe-Linking protection in tcaches and fastbins

When you check the tcaches you can get something similar to the following output:

[0x7f9694a1b8cb]> dmht
Tcache main arena @ 0x7f9694bbdba0
bin : 3, items : 7, fd :0x55d1c5dfc530->0x55d498c3991c->0xffffffffffffffef


In the case of the fast bins, something like this:

[0x7f9694a1b8cb]> dmhf | grep -w 'fastbin' -A 2
fastbin 4 @ 0x7f9694bbdbc8 {
0x55d1c5dfc8b0->fd = 0x55d498c395bc Linked list corrupted


In both cases the bins are shown as corrupted (in the tcache you can deduce it from that weird pointer value 0xffffffffffffffef). This is due to the Safe-Linking protection implemented in glibc 2.32, that protects the fast bins and tcaches pointers.

#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

In order to hide and reveal the real pointer value, PROTECT_PTR and REVEAL_PTR are used to perform a XOR operation between the real pointer value and the address of the pointer itself. For more information, you can check the CheckPoint post, authors of the technique.

To indicate to radare2 to calculate the real value of the pointers, you must set to true the dbg.glibc.demangle variable:

[0x7f9694a1b8cb]> e dbg.glibc.demangle = true


After, the bins are displayed correctly:

[0x7f9694a1b8cb]> dmht
Tcache main arena @ 0x7f9694bbdba0
bin : 3, items : 7, fd :0x55d1c5dfc530->0x55d1c5dfc4c0->0x55d1c5dfc450->0x55d1c5dfc3e0->0x55d1c5dfc370->0x55d1c5dfc300->0x55d1c5dfc290

[0x7f9694a1b8cb]> dmhf | grep -w 'fastbin' -A 2
fastbin 4 @ 0x7f9694bbdbc8 {
0x55d1c5dfc8b0->fd = 0x55d1c5dfc840->fd = 0x55d1c5dfc7d0->fd = 0x55d1c5dfc760->fd = 0x55d1c5dfc6f0->fd = 0x55d1c5dfc680->fd = 0x55d1c5dfc610->fd = 0x55d1c5dfc5a0
}


I would like to thank @meowmeowxw for showing me the solution to this problem.

## Conclusion

Well, I hope that this walk for the heap with radare helps you to perform heap analysis in the future.

Happy hacking ;)