There were various heap challenges in many CTFs, exploiting the vulnerabilities in ptmalloc, the memory allocator of glibc. Each year the more advanced exploit techniques the players discovered, the more exploit mitigations were applied to the codebases.
In this blog post, I'll cover the new heap exploit mitigation introduced in glibc 2.32 called Safe Linking. Before digging into the main topic, I'll briefly explain the previous exploit techniques and its related mitigations applied, finally summarizing how effective I think the new mitigation is to defend attackers from exploitation.
Tcache in ptmalloc
Since glibc 2.26, the concept of tcache is added to the malloc implementation. Basically freed memories(also called chunks) are managed by singly linked list or double linked list, linked to the chunks that have the same size. Specifically for chunks that are smaller than the size of 0x80, each singly linked list is called fastbin. The tcache is almost the same concept as fastbin, but it covers bigger chunks than fastbin with no security checks such as the size of chunk or whether a chunk is double freed or not. So tcache improved the performance of ptmalloc, but it gave chances for attackers to more easily exploit the vulnerabilities in heap memory.
The code below is a part of tcache implementations from glibc 2.27. tcache_put is called when a chunk is freed to be put into tcache bin, and tcache_get is called to get freed chunk to be returned to malloc from tcache. There is no double free check logic, or any verification of the pointer returned to user. So attackers easilly get arbitrary memory by overwriting the pointer value in the cached chunk with arbitrary address that attackers wants.
Later in glibc 2.29, the logic that checks double free is added to source code by inserting a new pointer that points to tcache structure in library when a chunk is freed to tcache. This mitigation can be bypassed if an attacker can clear the pointer in freed chunk with memory write primitive. The logic is not that complicated, so it is recommended to read the source code for more details.
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
return (void *) e;
}
Safe Linking
In glibc 2.32 Safe Linking is added as security feature to the allocator. The basic idea is to do xor operation on the stored pointer with the address the pointer is to be stored in. The figure below describes the idea cleanly, where P denotes the pointer to the next entry in tcache(or fastbin) and L is the address that P will be written to. Please note that L is shift-righted by 12, which is to make the value page-aligned.
The result pointer is restored when the cached chunk is popped from tcache to be allocated, with checking if the restored pointer is aligned with minimal alignment value which is 0x10.
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
#define PROTECT_PTR(pos, ptr) \
((__typeof(ptr))((((size_t)pos) >> 12) ^ ((size_t)ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR(&ptr, ptr)
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void tcache_put(mchunkptr chunk, size_t tc_idx) {
tcache_entry *e = (tcache_entry *)chunk2mem(chunk);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;
e->next = PROTECT_PTR(&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *tcache_get(size_t tc_idx) {
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely(!aligned_OK(e)))
malloc_printerr("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR(e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *)e;
}
Bypass the mitigation
Safe Linking prevents attackers from tampering the linked list pointer that the freed chunks have. The point of Safe Linking is that it asks attackers for heap memory read primitive. The developer says that the effect of Safe Linking to the performance is negligible. So it is certain that Safe Link is effective for mitigating exploits with almost zero cost. However, the mitigation can still be bypassed if attacker can leak the address that is used to calculate protected pointer. Considering memory read primitive is common in CTFs, I don't think the Safe Linking will be the important topic in CTF scenes. Mitigation is just a mitigation, so it is necessary that developers always write code in secure ways so that they do not make any vulnerabilities in their project.