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.
All sample code used in this blogpost is available for download
here
Memory chunks
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:
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.
Figure 2 – Extending memory chunk size
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.
Figure 3 – Shrinking memory chunk size
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 blackngel’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”).
Figure 4 – Shaping the heap
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.
Figure 5 – Overflowing and overlapping next chunk
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.
Figure 6 – Shaping the heap
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:
Figure 7 – Overflowing and overlapping next chunk
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:
Figure 8 – Getting the tls_dtor_list pointer address
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.