Unlink



unlink是大名鼎鼎的heap based漏洞,虽然glibc不断对unlink宏的安全性进行加固,但是Unlink依旧能够实现较好的漏洞利用效果,在新版本下使用unsafe-unlink最终能够达到arbitrary 4 bytes mirrored overwrite的效果。

unlink简介

malloc.c的实现中使用unlink宏完成对双向循环链表的删除操作,最简单版本unlink宏如下

在ptmalloc中,malloc与free实现中都利用到了unlink宏
下面所说的前与后是和heap的增长方向一致的,箭头指向的方向就是前,箭头反方向就是后,如下所示:
frist chunk的 prev chunk是second chunk
second chunk的next chunk 是second chunk

我们着重分析一下完整的unlink宏,下面是glibc-2.23中的unlink,当我们free的chunk
get_max_fast ()>size且该chunk不是mmaped分配的,都会 trigger unlink。
无论是unsorted bin,small bin还是large bin,都是双向循环链表组织的。

BK,FD是两个chunk的静态指针,指向p->fd,p->bk,之后这两步操作是unlink的核心

熟悉双向链表很快能够理解,Unlink实质就是在做双向链表的删除,基本操作如下图:

在32bit操作系统下:

在64bit操作系统下:

如果我们能够控制fdbk这两个指针的值,就可以将向任意4个字节或者8个字节写入内容
同样对于large bin中含有fd_nextsize与bk_nextsize指针与fd,bk指针的操作类似
这就是Unlink漏洞的根源

_int_free中的unlink

在_int_free()中与unlink相关的代码如下:

consolidate begin

无论是向前合并consolidate_forward 还是向后合并consolidate_backward,都必须满足以下的条件:

chunk必须不是mmap分配的
M_MMAP_THRESHOLD用于设置mmap分配阈值,默认值为128KB
所以用于申请的内存大于128kb时候才会调用sysmalloc 使用mmap分配内存,其余都用brk()来分配内存

并且注意释放的内存不是fastbin大小范围内的chunk,否则不会利用unlink合并,fastbin有自己单独的内存释放机制

范围max(fastbinSize)<Size<128kb

consolidate backward

判断free的chunk的P位为是否为0,如果为0,说明后一个chunk本就是一个free chunk
则consolidate backward(向后合并),通过unlink将后一块从freelist中删除,将两个freechunk合并为一块,管理结构在后一个chunk上(低地址的chunk)

将chunk的size大小更改为两个chunk之和,并且调整chunk的指针到后一个chunk的开始位置(就相当于将两个chunk合并为了一个,并且chunk的管理结构都在后一个chunk),接下来使用unlink宏,将本chunk(注意这是p指针已经指向了后一个chunk,指代的就是我们图中FristChunk)从双向循环链表中移除。

consolidate_forward

将free的chunk的下下一块的P位取出,判断它是否为0,如果为0,说明free chunk的前一块是一个free chunk,则通过consolidate forward 向前合并将两个free chunk合并为一块,如图

首先将nextchunk(即ThirdChunk)在链表中移除,p当前指向的是当前的chunk,更改p的大小为两块chunk之和,相当于将下一块并到了前一块(即p指向的当前chunk中)

consolidate over

合并结束,将合并后的chunk插入到unsorted bin中

然后通过设置自己的size字段将前一个chunk标记为已使用;
再更改后一个chunk的prev_size字段,将其设置为当前chunk的size。

整个_int_free的基本流程简要介绍如上

对抗技术

在glibc很早的版本中,对unlink没有保护,随着glibc版本升级,也加入了Patch防止DoubleFree与unlink

Double Free检测

不允许释放一个ptr=NULL的chunk

上述几个判断都在int_free中对于double free进行检测,但都存在一个致命缺陷
这个缺陷是经常犯的错误,即free之后没有将指针置为空,野指针的存在能绕过上述检测

next_size非法检测

判断nextsize的大小是否是一个正常的值,如果我们fake glibc时将size改成了很大的数,期望达到相应的效果
在nextsize的检测中就会出错

双向链表冲突检测

这个检测在unlink宏中是限制unlink威力的罪魁祸首

FD->bkBK->fd在正常的双向链表中都会指向自己P,所以必须保证FD->bk,Bk->fd指针的内容与P的内容一致。
于是就有了这样一条新增加到unlink中的检测,为了绕过这条检测,我们必须构造好相应的fd与bk,并满足上述要求
这样一条检测的加入,使得原本我们拥有的两个任意地址写的能力受到了很大的限制,成为了一个arbitrary 4 bytes mirrored overwrite。

How2Heap

heap overflow unlink

感谢Shellphish Unsafe-Unlink精辟的源代码与解释

上述POC正是目前高linux版本下(加入unlink检测后)的一个fakechunk伪造块利用unlink进行任意地址写的经典做法
运行结果如下:

首先chunk0_ptr 是一个挂在bss段上的指针

这样我们通过向后合并的unlink想要实现的目的就是修改这个bss段上指针让它不再指向chunk0_ptr,而是一个我们想要修改的地址,这样我们修改chunk0_ptr实际上就造成了任意地址写
下面,为了要绕过unlink的检测,我们需要进行伪造chunk0为一个free chunk,这样free(chunk1)时chunk1与chunk 0会向后合并,会触发Unlink,从而触发漏洞利用
如何伪造:
伪造FreeChunk
我们在调试时可以看到填入的fake chunk的内容

首先,我们需要利用漏洞,修改chunk1中的prev_size=0x80,并将P位置为0
将P为置0,表明prev chunk是一个freed chunk
修改chunk1中的prev_size=0x80欺骗glibc认为上一个chunk的大小为0x80,实际上chunk0的大小应该是0x90
但glibc是确定next chunk的metadata是next chunk的prev_size的大小确定的:

按照chunk1的prev_size确定上一块的边界(利用chunk设计中边界标记法),构造chunk[2]中存放fd指针,chunk[3]中存放bk指针即可,所以填入的prev_size应为0x80。

绕过双向链表冲突检测

其中:
*FD->bk = (&chunk0_ptr-0x18+0x18) = chunk0_ptr=p
*BK->fd = (&chunk0_ptr-0x10+0x10) = chunk0_ptr=p
绕过了unlink的双向链表冲突检测
但是我们可见,为了绕过该Patch,我们的fd与bk必须相应地填写,也就是原来可以造成的两个任意地址写,变成了一个
arbitrary 4 bytes mirrored overwrite,写入的值只能是address-0x18

1.Set P->fd->bk=P->bk.
2.Set P->bk->fd=P->fd.
In this case,both P->fd->bk and P->bk->fd point to the same location so only the second update is noticed.

注意在unlink向后合并时,P指针首先进行了位移prevsize = p->prev_size

ArbitraryWrite
free(chunk1_ptr)
Unlink中BK->fd=FD :chunk0_ptr=&chunk0_ptr-0x18,我们修改chunk0_ptr[3]实际就在修改chunk0_ptr的指向
我们申请的局部变量申请在栈中,本例的地址为x7ffffffffd800
chunk0_ptr[3]=(uint64_t)victim_string
将chunk0_ptr指向了victim_string,即0x7ffffffffd800

chunk0_ptr[0]=0x4141414142424242LL;

上面的fake chnnk绕过检测造成unlink的手法经常用到,在目前的glibc版本中,只能利用使用上述的unsafe-unlink

double free unlink

上述unlink利用的是heap overflow 修改next chunk的size从而实现的unlink
很多时候我们也可以通过double free触发unlink,如下见源代码:

通过double free触发unlink,我们首先申请两个chunk,chunk1大小为0x80,chunk2大小为0x90,在free的时候,free掉了chunk1与chunk2,但是chunk2没有将指针置NULL,可能造成double free。之后申请一个大的chunk包含chunk1与chunk2并在chunk中构造fake chunk如下图:

利用double free再次触发unlink,即可将chunk_ptr修改为chunk_ptr-0x18,从而修改victim_string为pwned

最终程序的运行结果如下:

double-free 只是换了一种姿势在heap中构造fake-chunk,实质与overflow unlink是一样的

unlink的利用姿势很多,上述两种只是比较常用的,但unlink的本质是基本一致的。
unlink提供的arbitrary 4 bytes write的能力,可以用来敏感的.bss指针,heap stack上的关键数据等,进而进一步劫持控制流。

参考资料

Linux堆溢出漏洞利用之unlink
How2heap
PWN之堆内存管理

发表评论