这种方法适用于glibc版本<2.26
glibc malloc基础
在线malloc.c源码
在malloc.c中找到chunk结构的相关代码:
1 | struct malloc_chunk { |
heap chunk结构如下
1 | +-----------+---------+------+------+-------------+ |
如果当前chunk前面相邻的chunk空闲,那么prev_size
记录前一个chunk的大小,如果不空闲,prev_size区域是前面chunk的数据部分。size
是当前chunk的大小,因为chunk的大小都是8字节对齐,size的低三位一定会空闲出来,低三位就用作三个Flag
标识位。fd
和bk
是在当前chunk为空闲时,分别指向下一个和上一个空闲chunk,串联成一个空闲chunk的双向链表
。
详细介绍请参考我的另一篇Blog
经典的unlink利用方法
有漏洞的演示程序:
1 |
|
strcpy
函数导致堆溢出,argv[1]
大于666字节时,会覆盖下一个chunk的chunk头,能够导致任意代码执行。
堆的结构如下:
1.没有攻击者时,第一个free做了以下工作:
不是mmap创建的chunk,会向前或向后合并
向后合并:
判断前一个chunk是否空闲
如果当前释放的chunk的PREV_INUSE(P)位设为0,则前一个chunk空闲。本例中,因为”first”的PREV_INUSE位设为1,所以前一个chunk不是空闲的,默认堆内存的第一个chunk的前一个chunk是allocated(即使它不存在)。如果空闲,则合并
例如,从binlist中将前一个chunk移除(unlink),把前一个chunk的大小加到当前大小,改变chunk指针指向前一个chunk。本例中,前一个chunk是allocated,所以没有调用unlink。因此当前的空闲chunk “first”不能向后合并。
向前合并:
判断下个chunk是否空闲
如果下下个chunk(相对当前释放的chunk而言)的PREV_INUSE(P)位设为0,则下个chunk是空闲的。为了找到下下个 chunk,将当前释放chunk的大小加到它的chunk指针就得到下个chunk的指针,然后将下个chunk的大小加到下个chunk指针就能得到下下个chunk。本例中,当前释放的”first” chunk的下下个chunk是top chunk,它的PREV_INUSE位是1,因此下个chunk “second”没有被释放。如果空闲,则合并
例如,将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下一个chunk是allocated,因此没有调用unlink。因此,当前释放的chunk “first”不能向前合并。
将合并后的chunk加到unsorted bin。本例中,因为没有合并发生,所以把”first” chunk加到unsorted bin。
2.攻击者在strcpy中覆盖了”second” chunk的chunk头:
1 | prev_size = 偶数 |
有攻击者时,第一个free做了如下工作:
向前合并:
- 当前释放的”frist” chunk的下下个chunk不是top chunk。因为攻击者将”second” chunk的大小覆盖为-4, 所以下下个chunk在”second” chunk偏移为-4的位置。因此现在
glibc malloc
把”second” chunk的prev_size当做下下个chunk的size。因为攻击者把prev_size覆盖为偶数(PREV_INUSE位为0),glibc malloc
以为”second” chunk是空闲的。 - 如果空闲,则合并 将下个chunk从它的binlist中移除(unlink),把下个chunk的大小加到当前大小。本例中,下个chunk(second)是空闲的,因此会发生合并,触发
unlink(second)
宏:1
2
3
4FD = second->fd (本例中fd是free@got-12)
BK = second->bk (本例中bk是shellcode的地址)
FD->bk = BK
BK->fd = FD - 把”second” chunk的fd和bk拷贝到变量
FD
和BK
。本例中,FD = free@got-12
,BK = shellcode address
(攻击者将shellcode放在”first”堆空间中) - FD是
malloc_chunk
结构体指针,因此FD->bk相当于FD+12。本例中,FD+12 = free@got-12+12
,即指向free
对应的GOT条目。 - 因此FD->bk = BK相当于
free@got = shellcode address
(free对应的GOT条目被覆盖成了shellcode地址)。 - 现在第二个free调用时就会执行shellcode。
漏洞程序的poc如下:
1 | #!/usr/bin/python |
这个poc只是说明一下利用方法,实际不能运行,下面就是原因。
unlink 2.0
现在的glibc都是采取了保护机制的,想要利用unlink没有以前那么方便了。
新版本glib中的unlink宏:
1 | /* Take a chunk off a bin list */ |
最大的阻碍就是下面这部分代码:
1 | if (__builtin_expect(FD->bk != P || BK->fd != P, 0)) |
这段代码加到unlink
宏后,再调用unlink宏时,chunk指针FD->bk
(即P->fd->bk)应该是p指针自己,BK->fd
也是。
上一部分中,我们修改了
1 | p->bk = shellcode address |
这样改使得FD->bk
为free@got
,而BK->fd
是shellcode+8
不能通过,上文的利用方式不能成功。
想要绕过这段代码需要在内存中找到一个已知的地址X指向p(地址X中保存p的值),修改如下
1 | fd = 0x0804bfa0 - 0xc // 0x0804bfa0是地址X |
这样修改,unlink执行
1 | FD = P->fd; // 0x0804bfa0 - 0xc |
使得FD->bk == P
且BK->fd == P
,即可绕过检查代码。unlink接着执行
1 | FD->bk = BK; |
再对p指向的chunk进行写入,’A’ * 0xc + overwrite,就可以将p覆盖成某一值。
具体过程借一个示例讲解
这个程序在free时没有检验指针的有效性,没有在free之后将野指针清空。可以任意指定每一个chunk的大小。
利用步骤如下:
1.分配两个长度合适的chunk
1 | chunk0 malloc返回的ptr chunk1 malloc返回的ptr |
chunk0大小为504,chunk1大小为512,内容随意。
2.对chunk0进行编辑,设置好chunk0的fd、bk并溢出chunk1,改好chunk0的chunk头控制信息如下:
1 | chunk0 malloc返回的p chunk1 malloc返回的p |
为了欺骗glibc,让它以为malloc chunk0时返回的指针p就是chunk0指针,因此改写chunk1的prev_size为上图所示的new_size
,将chunk1 size部分设为chunk1的实际大小(PREV_INUSE
为0)。如此就做好了unlink触发的准备。
3.再新分配一个chunk2,内容为/bin/sh
,为后面调用system()
做准备。
4.删掉chunk1,触发unlink(p),将p改写。
删除chunk1时,glibc会检查PREV_INUSE的值,发现前一个chunk是空闲的(实际是我们伪造的),glibc会合并两个相邻的空闲块。glibc会先将chunk0从binlist中解引用,触发unlink(p)。
1 | FD = p->fd(实际是0x0804bfa0-0xc,因为全局数组保存p值的地址是0x0804bfa0) |
存储指针的全局变量a的结构如图所示
1 | p 0 1 2 |
5.对chunk0进行edit,填写内容为'A'*0xc + p32(0x0804a014)
其中0x0804a014为free的got地址,edit之后p指针被free的got地址覆盖,此时print就会打印出所有的got地址。
6.根据泄露出的实际内存地址,计算出system函数的实际内存地址,将system的地址当做free的地址,其它地址不变,重新写回got。
7.Free chunk2就相当于调用system("/bin/sh")
,即可获得shell。
reference
https://sploitfun.wordpress.com/2015/02/26/heap-overflow-using-unlink/
http://drops.wooyun.org/tips/7326