原理
unlink是内存操作中的一个宏, 用来从双向链表中取出一个free chunk,其过程中的指针操作存在任意写的漏洞。
从双向链表中取出节点的过程,若是学过数据结构应该都比较清楚,主要代码是:
1 | P->fd->bk = P->bk |
但是在unlink宏中,为了安全性还增加了一些检查:
unlink源码:
1 | if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) |
要利用该漏洞,先要绕过这里的两处检查(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 | FD->bk = BK; // P = &P-0x10 |
在此之后,P就指向了比自己的地址低0x18个字节的位置,可以通过再次向P写入从而覆盖掉P本身,将其修改为任意地址,第三次向P写入则实现了任意地址写。
触发前提
- 堆指针是全局变量,或其地址是可泄露的
- 能够free一个smallchunk或largechunk(可以是伪造的)
- 能够控制下一chunk的prevsize和size
利用过程(64位为例)
法一
存在堆溢出时:
- 分配连续3个chunk a, b, c
- 在a中伪造chunk
p64(0) + p64(0x80) + p64(head_ptr-0x18) + p64(head_ptr-0x10) + padding
- 覆盖b的prevsize和size,修改inuse位,使之可以与a合并
p64(0x80) + p64(0x90)
- free掉b,触发unlink
- 写入a,覆盖指针为任意地址,
p64(0)*3 + p64(addr)
- 再次写入a,任意地址写入
法二
不能溢出,但有double free
- 分配连续3个fastchunk a, b, c
- 释放a,此时并不会修改后面的inuse位和prevsize
- 申请一个大内存,触发fastbin合并,这时会将fastbin中的a取出放入unsortedbin,可以实现对b的inuse位进行修改
- 触发double free,将a再次释放,放入fastbin
- 再申请与a相同大小的chunk,就会从fastbin中将a返回,同时不会改变b的inuse
- 布置a内存,释放b,触发unlink