堆利用学习之unlink

原理

unlink是内存操作中的一个宏, 用来从双向链表中取出一个free chunk,其过程中的指针操作存在任意写的漏洞。
从双向链表中取出节点的过程,若是学过数据结构应该都比较清楚,主要代码是:

1
2
P->fd->bk = P->bk
P->bk->fd = P->fd

但是在unlink宏中,为了安全性还增加了一些检查:
unlink源码:

1
2
3
4
5
6
7
8
9
10
11
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
malloc_printerr ("corrupted size vs. prev_size");
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);
else {
FD->bk = BK;
BK->fd = FD;
...
}

要利用该漏洞,先要绕过这里的两处检查(64位为例)

  • 第一处检查chunksize(P) != prev_size (next_chunk(P)),检查下一个chunk的prevsize是否等于当前chunk的size,所以需要通过溢出等手段设置下一chunk的prevsize
  • 第二处检查__builtin_expect (FD->bk != P || BK->fd != P, 0),检查当前chunk是否是前一个chunk的后继,同时也是后一个chunk的前驱,也就是检查链表是否真的是链接好的。但是这个检查有个致命的缺点:

    因为FD->bk == *(FD+0x18)BK->fd == *(BK+0x10)
    若在FD中存入&P-0x18,那么表达式将变为FD->bk == *((&P-0x18)+0x18) == *&P == P
    若在BK中存入&P-0x10,那么表达式将变为BK->fd == *((&P-0x10)+0x10) == *&P == P
    从而就绕过了检查
    注意:&P是表示指向目标chunk的指针的地址

绕过检查后,满足FD->bk == P && BK->fd == P,所以有:

1
2
FD->bk = BK; // P = &P-0x10
BK->fd = FD; // P = &P-0x18

在此之后,P就指向了比自己的地址低0x18个字节的位置,可以通过再次向P写入从而覆盖掉P本身,将其修改为任意地址,第三次向P写入则实现了任意地址写。

触发前提

  1. 堆指针是全局变量,或其地址是可泄露的
  2. 能够free一个smallchunk或largechunk(可以是伪造的)
  3. 能够控制下一chunk的prevsize和size

利用过程(64位为例)

法一

存在堆溢出时:

  1. 分配连续3个chunk a, b, c
  2. 在a中伪造chunkp64(0) + p64(0x80) + p64(head_ptr-0x18) + p64(head_ptr-0x10) + padding
  3. 覆盖b的prevsize和size,修改inuse位,使之可以与a合并p64(0x80) + p64(0x90)
  4. free掉b,触发unlink
  5. 写入a,覆盖指针为任意地址,p64(0)*3 + p64(addr)
  6. 再次写入a,任意地址写入

法二

不能溢出,但有double free

  1. 分配连续3个fastchunk a, b, c
  2. 释放a,此时并不会修改后面的inuse位和prevsize
  3. 申请一个大内存,触发fastbin合并,这时会将fastbin中的a取出放入unsortedbin,可以实现对b的inuse位进行修改
  4. 触发double free,将a再次释放,放入fastbin
  5. 再申请与a相同大小的chunk,就会从fastbin中将a返回,同时不会改变b的inuse
  6. 布置a内存,释放b,触发unlink

题目

文章作者: Hpasserby
文章链接: https://hpasserby.me/post/dbc7b210.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Hpasserby
支付宝赞赏
微信赞赏