The following post analyzes glibc-2.32+'s safe-linking feature introduced in December 2020 through the context of a CTF challenge developed for New York University's CSAW 2021 Quals competition. The challenge, titled "word_games," allows challengers to bypass safe-linking due to an out-of-order code sequence. This simple mistake results in a routine heap exploitation primitive that defeats safe-linking.
Safe-linking implements a new process for storing forward, fd
, pointers in fastbins and tcache linked lists within the heap. Specifically, the protection scheme masks these pointers, which contain heap addresses, by xor-ing their value with a shifted address. For example, for a heap chunk pointer L
with a linked list fw
pointer P
the value stored in L->fw
is:
Safe-linking protects against both partial and full pointer overwrites because the bit shift moves ASLR-randomized bytes into the least significant little-endian bytes. Furthermore, the security measure introduces additional alignment checks because the revealed linked list pointer must fall on a 0x0 boundary, and thus the last byte cannot be arbitrarily overwritten. A more detailed overview is available in Check Point Research's blog post from May 2020.
Safe-linking’s overarching goal is protection against common exploitation strategies, including corrupting fastbins and tcache pointers to overwrite addresses such as __free_hook
. While it does introduce an added layer of security (and consequently an added layer of frustration for hackers), the mitigation is not a silver bullet. The inspiration for my challenge is an excellent article about the proof of concept House of Io attack, written by Awarau and Eyal, used to bypass this security measure given certain program vulnerabilities. The challenge implements one of the identified vulnerabilities: "a badly ordered set of calls to free()
". The "word_games" CTF challenge includes a simple coding mistake which might be encountered in the wild due to developer oversight. This mistake is easily exploited to circumvent safe-linking.
Full challenge source code can be viewed here. Briefly, the vulnerability lies in an incorrect call to free when a linked list is deleted: the list node, mine
, is freed prior to a pointer in the list's second quadword, mine->fav
. Furthermore, a simple UAF vulnerability allows the user to read the mine->fav
value at any point in the program. The first vulnerability is below.
struct LL {
struct node* head;
char* fav;
};
struct LL* mine;
...
void delete_list(struct node* head) {
struct node* tail = get_tail(head);
struct node* tmp;
while (tail->prev != NULL) {
tmp = tail;
tail = tail->prev;
if (tmp->str) free(tmp->str);
if (tmp != head) free(tmp);
}
if (head == yours) {
yours->next = NULL;
}
else {
if (mine) free(mine);
if (mine->fav) free(mine->fav);
mine->head = NULL;
}
}
The structure is freed first, which places it in tcache and updates its second quadword (tcache_entry->key
) to the address of tcache_perthread_struct
. The following call to free(mine->fav)
successfully frees tcache_perthread_struct
because its pointer is not masked by safe-linking. Any subsequent request to read mine->fav
leaks tcache_perthread_struct
's fw
pointer, which is either a linked list entry (if in tcache) or a main arena address (if in the unsorted bin). The latter provides a more direct route to exploitation and is used to calculate libc addresses such as __malloc_hook
, __free_hook
, and system()
. This also means tcache_perthread_struct
is available in the unsorted bins. Any allocation size under 0x290 that tcache or fastbins cannot fulfill is allocated directly to tcache_perthread_struct
. This would not be particularly useful if safe-linking protected tcache entries within tcache_perthread_struct
. However, this is not the case and thus control of this structure allows arbitrary heap allocations. Furthermore, tcache does not validate the return chunk's size field and returns any 0x0 aligned allocation. Consequently, __free_hook
is now a viable candidate for an overwrite. This is the final step in the exploit, which can be viewed in more detail here.
This challenge demonstrates that simple programming mistakes still create exploitation opportunities even when safe-linking is enabled. These mistakes expose exploitation without a single heap leak, and failure to protect the tcache_perthread_struct
address in tcache_entry->key
renders safe-linking useless. Furthermore, the exploit does not require any new strategies; existing UAF and hook overwrite methods effectively circumvent the mitigation. In fact, the exploitation strategy is so familiar that some contestants solved the challenge without paying any attention to the masked pointers.
That said, this challenge presents a carefully designed scenario, as hypothesized by Awarau and Eyal, which bypasses safe-linking. In most circumstances, safe-linking is a useful mitigation that can complicate or inhibit exploitation by protecting against heap address leaks and/or tcache and fastbins linked list pointer overwrites. The door to heap exploitation remains open, but safe-linking brings us one step closer to shutting it for good.
Update: glibc introduced a change to the tcache_entry->key
value in version 2.34 which mitigates this attack strategy. Modern glibc generates a randomized eight-byte number during process initialization and stores that value, tcache_key
, in tcache_entry->key
.