When a chunk is removed from a bin, unlink()
is called on the chunk. The unlink macro looks like this:
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:
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
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.
More modern libc versions have a different version of the unlink macro, which looks like this:
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.