Unlink Exploit

Overview

When a chunk is removed from a bin, unlink() is called on the chunk. The unlink macro looks like this:

FD = P->fd;    /* forward chunk */
BK = P->bk;    /* backward chunk */

FD->bk = BK;    /* update forward chunk's bk pointer */
BK->fd = FD;    /* updated backward chunk's fd pointer */

Note how fd and bk are written to location depending on fd and bk- if we control both fd and bk, we can get an arbitrary write.

Consider the following example:

We want to write the value 0x1000000c to 0x5655578c. If we had the ability to create a fake free chunk, we could choose the values for fd and bk. In this example, we would set fd to 0x56555780 (bear in mind the first 0x8 bytes in 32-bit would be for the metadata, so P->fd is actually 8 bytes off P and P->bk is 12 bytes off) and bk to 0x10000000. Then when we unlink() this fake chunk, the process is as follows:

FD = P->fd         (= 0x56555780)
BK = P->bk         (= 0x10000000)

FD->bk = BK        (0x56555780 + 0xc = 0x10000000)
BK->fd = FD        (0x10000000 + 0x8 = 0x56555780)

This may seem like a lot to take in. It's a lot of seemingly random numbers. What you need to understand is P->fd just means 8 bytes off P and P->bk just means 12 bytes off P.

If you imagine the chunk looking like

Then the fd and bk pointers point at the start of the chunk - prev_size. So when overwriting the fd pointer here:

FD->bk = BK        (0x56555780 + 0xc = 0x10000000)

FD points to 0x56555780, and then 0xc gets added on for bk, making the write actually occur at 0x5655578c, which is what we wanted. That is why we fake fd and bk values lower than the actual intended write location.

In 64-bit, all the chunk data takes up 0x8 bytes each, so the offsets for fd and bk will be 0x10 and 0x18 respectively.

The slight issue with the unlink exploit is not only does fd get written to where you want, bk gets written as well - and if the location you are writing either of these to is protected memory, the binary will crash.

Protections

More modern libc versions have a different version of the unlink macro, which looks like this:

FD = P->fd;
BK = P->bk;

if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
    malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
    FD->bk = BK;
    BK->fd = FD;
}

Here unlink() check the bk pointer of the forward chunk and the fd pointer of the backward chunk and makes sure they point to P, which is unlikely if you fake a chunk. This quite significantly restricts where we can write using unlink.

Last updated