The following post analyzes glibc-2.32+'s safe-linking feature introduced in December 2020 in the context of a CTF challenge I 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, and backward, bk, 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 linked list pointer.  For example, for a heap chunk pointer L with a linked list fw pointer P the value stored in L->fw is:

Source: Check Point Research

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 __malloc_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()".  My program 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 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, the often neglected sibling of __malloc_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.  The challenge allows exploitation without a single heap leak, and failure to protect the tcache_perthread_struct address in tcache bk pointers 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.

 

‹ Return to Blog