In this post, we want to share some notes on how to exploit heap-based overflow vulnerabilities by corrupting the size of memory chunks. Please note that we do not present here original content but only want to share with the community two detailed write-up. The first one exploits a basic heap-based overflow by enlarging the size of memory chunks. The second one shrinks their sizes in order to turn a NULL byte off-by-one error – present in a hardened binary (all memory corruption mitigations are enabled) – into remote code execution.
Before going further, we strongly encourage the reader to go through glibc malloc internals. The post made by sploitfun is probably the best documentation on glibc allocator (ptmalloc2). Here we just recap the structure of an allocated/free memory chunks:
Corrupting chunk sizes
There is several techniques to exploit a heap-based overflow. In the following, we will focus on the techniques presented in Goichon’s paper that consist in overflowing the chunk size field. Either enlarging or reducing the size of memory chunks could lead to interesting scenarios where one can overlap a memory chunk into another chunk. If the overlapped chunk contains memory pointers, then an attacker can overwrite them to leak sensitive data and/or to execute code.
The figure below illustrates the first scenario where the size of a chunk is extended. If we manage to (i) allocate three contiguous chunks, namely A, B and C, (ii) free the second one B and extend its size, (iii) allocate a larger chunk than previously requested for B, then chunk C will be overlapped.
Shrinking the size of chunks to produce overlapping chunks is more complex. Figure 3 illustrates the different steps leading to overlapping chunks. First, we allocate three large contiguous chunks A, B and C. Then, we free chunk B and shrink its size by overwriting the chunk size field. In the third step, we allocate two chunks B_1 and B_2 that feet on that freed chunk. As the size of chunk B has been corrupted, the prev_size of chunk C will not be updated and thus the freed B’s space is unused from C’s perspective. Now, if we free the chunk B_1 and chunk C, then chunks B_1, B_2 and C will be merged. A subsequent allocation larger than B_1 initial size will overlap chunk B_2. For further infomration about this technique, please refer to Tavis Ormandi’s code.
The vulnerable code
Since the solutions of the challenges we did are meant not to published, we decided to create a new one by reworking slightly the war game from the excellent blackangel’s Phrack paper.
The code below manages a set of agents with basic operations (creation, deletion, profile edition and modification).
When a new agent is created, two memory chunks are allocated. The first one holds a pointer to the agent name along with the length of the agent name, the agent id, and some reserved data. The second chunk holds the agent name pointed to by field name of the first chunk. Those two chunks are freed when agents are deleted.
The vulnerability stems from insufficient reserved space to hold the agent name. More precisely, the allocated chunk at line 89 does not account for the 2-chars (“A_”) that prefix agent names. Therefore, we can overflow a chunk with two bytes and hence corrupt the size of the next chunk.
Write-up – The easy way
Notice that we cannot apply directly the scenario depicted in figure 2. Indeed, if we create three agents and delete the second one, then we cannot enlarge the size of the freed and merged chunks enough to reach interesting data of the third agent. As stated earlier we can overwrite a chunk with only two bytes (one byte + NULL byte). So, we need to shape the heap beforehand so that we can get two adjacent chunks holding agent’s names.
Figure 4 sums up the steps to shape the heap:
- We create two agents Ag_0 and Ag_1.
- We delete agent Ag_0.
- We create a new agent Ag_2 by requesting a large size to store its name than for Ag_0. The chunk holding the name of agent Ag_2 is allocated next to the chunk holding the name og agent Ag_1.
- We create a new agent Ag_3 that represents the target to overlap.
- We create an additional agent Ag_4 that holds the strings “/bin/sh”. Our goal is to execute a shell by calling system(“/bin/sh”).
The next step is delete agent Ag_2 and corrupt the size of the chunk holding its name (see Figure 5). Now, if we create a new agent Ag_5 on the space left free by agent Ag_2, then the chunk holding the main structure of agent Ag_3 will be overlapped. In our case, we point the name’s pointer to free‘s GOT entry.
If we dump, the data (agent_show) of agent Ag_3, we can leak the address of a libc function and deduce where system address is mapped to.
The final attack stage is to edit the data (agent_edit) of agent Ag_3 to redirect free() calls to system() calls. Now, if we delete Ag_4, we got a shell.
The exploit is available here:
Write-up – The hard way
Assume now that we apply the following patch to the vulnerable code. Ok, that is better but the code is still vulnerable to an off-by-one heap-based overflow. We cannot enlarge the size of chunks but if we allocate large ones, we can shrink them by overwritting the LSB of the chunk size with a NULL byte.
The scenario depicted in Figure 3 cannot be applied in one go. We need first to shape the heap in order to produce overlapping chunks.
A closer look at the patch shows that we cannot overflow chunks while editing agents. The chunk size can be corrupted only during agent creation. So the only way to corrupt the chunk size is to allocate a fastbin chunk followed by a large chunk, then free both of them and finally reallocate the fastbin chunk by overflowing this time the size of the next chunk. We rely on a fastbin chunk since it is not merged with adjacent chunks when freed.
Figure 6 sums up the steps to shape the heap:
- We create two agents Ag_0 and Ag_1. A fast bin chunk is allocated to hold the name of agent Ag_1.
- We delete Agent Ag_0.
- We create two agents Ag_2 and Ag_3 by requesting large sizes to hold their names.
- We delete Ag_2 and Ag_1.
Now, we are ready to overwrite the size of the previously freed chunk (structure holding Ag_2’s name) by requesting small size (we need a fast bin allocation) for the newly created agent Ag_4.
Then, we create three additional agents Ag_5, Ag_6 and Ag_7 that fit in memory on the free space left by Ag_2. Once deleting Ag_5, Ag_7 and Ag_3, the chunks holding Ag_6’s data will be overlapped if we create a new agent as shown below:
Note that we created agent Ag_7 and deleted it in the sole purpose to get fd and bk pointers adjacent to the structure holding the name of agent Ag_6. These addresses will be leaked later in order to resolve some libc addresses.
In our exploit, we create a new agent Ag_8 such that its data overlaps the len field of agent Ag_6. Dumping the data of this agent will leak the address the fd pointer from which we can deduce the address of system in libc address space.
Note that in our example, we assume that the binary has been hardened by enabling all gcc’s memory corruption mitigation flags. This means that we cannot rely on the previously technique to overwrite a GOT entry.
Our goal to achieve code execution is to create a fake tls_dtor_list which is a single linked list of functions that run at program exit:
The address of the tls_dtor_list pointer could be derived by setting a pointer on function __call_tls_dtors which iterates over the tls_dtor_list:
This pointer will be used to overwrite the name pointer of agent Ag_6 when editing the data of agent Ag_8. Finally, we edit the data of agent_6 that points now to tls_dotr_list and copy there our fake tls_dtor_entry.
The exploit is available here: