unsafe unlink
历史悠久的攻击手段。
原理核心
从 unsorted bin 等双向链表取出的时候,会进行 unlink 操作,攻击点就是如下的代码:
FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD
如果我们可以控制 P 的 fd 和 bk 指针,就可以做点攻击。
古老的攻击
我们构造:
P->fd = target_addr-24
P->bk = expect_value
那么:
FD->bk = BK <==> *(target_addr-24+24) = expect_value
BK->fd = FD <==> *(expect_value+16) = target_addr-24
实现任意地址读写的目的,但是我们还是需要确保 expect value + 16 地址具有可写的权限,并且被破坏之后不影响程序运行。
现在的攻击
现在 unlink 有了如下的检查,所以之前的方式就不行了:
FD->bk != P || BK->fd != P,
题外话
多说一句,其实这个检查也给很多其他一些攻击带来了困扰。有些攻击重点不是在于 unlink,他有其他的攻击方式,但是避免不了要把 chunk 从双向链表种取出来。需要能过掉这个检查就行,所以这种攻击通常就直接伪造这样的字段,可以带入上面的式子,是能够 PASS 掉的:
chunk->fd == chunk
chunk->bk == chunk
那么现在的 unlink,我们进行这样的构造:
P->fd = &P-24
P->bk = &P-16
那么
FD->bk != P <==> *(&P-24+24) != P
BK->fd != P <==> *(&P-16+16) != P
FD->bk = BK <==> *(&P-24+24) = &P-16
BK->fd = FD <==> *(&P-16+16) = &P-24
相当于修改了 P 的指针指向比自己低 24 字节的地址,即下图中灰色框的内容变成 0x7ffff0840-24。

如果我们可以对 P 的指针有写入多个字节的功能,就能够改变 P 的指针了,具体可以看下一章节。
用途
关键是这有啥用... 从上面就能看出,现在的 unlink 已经今非昔比了,攻击力量断崖式下降。
下面是一个例子,必须要先了解 house of einherjar 才可以。
house of einherjar 回顾
回顾 house of einherjar,如下图所示,我们是修改了下图中 chunkC 的 prev_in_use 字段,这样 free(C) 的时候会合并我们伪造的红色框。

由于合并红色框的时候,会默认红色框在双向链表中(想一想为什么,因为如果是 fastbin 大小,释放块的时候,不会修改 nextchunk 的 prev_in_use 字段),所以会执行 unlink 操作。为了避免检查出错,我们将 fd 和 bk 都改成自身,这样可以 PASS 检查。
unsafe unlink 的构造
而 unsafe unlink 的区别就是这里的 fd 和 bk 不同,不再是自身,而是伪造的地址。 如下图所示:

除了大小不一样,唯一的区别就是 fd 和 bk,这里是因为:
gdb> p &a
$1 = (uint64_t **) 0x555555558020 <a>
fd = &a-24 = 0x555555558020-24 = 0x555555558008
bk = &a-16 = 0x555555558020-16 = 0x555555558010
unsafe unlink 后的利用
所以在 free(c) 之后,两个块合并了,然后进行了 unlink 操作,最终的结果:

即最终指向了低自己 24 字节部分,如果我们这个指针有写入多个字节的功能(基本上肯定有,因为 a 原来是堆的地址):
// 一般都会有写入多个字节的功能,因为 a 一开始是堆的地址,即一般都是下面的语句
// uint64_t *a = malloc(0x20);
a[3] = target_addr
最终变成下图所示,我们成功可以在 target_addr 上任意写入!

a[0] = 'hi~'