初级堆溢出-unlink漏洞

Linux下堆的unlink漏洞

参考文章:https://blog.csdn.net/qq_25201379/article/details/81545128

首先介绍一下Linux的堆块结构:

struct malloc_chunk {
INTERNAL_SIZE_T prev_size;
INTERNAL_SIZE_T size;
struct malloc_chunk *fd;
Struct malloc_chunk *bk;
}

0x01、其中前两个结构体成员组成了堆块的块首:1、prev_size字段仅在该堆块是空闲时有意义,代表了前一个堆块的size(包括块首的大小在内),注意国内很多相关blog中将prev解释成“后一个”堆块,这非常的别扭,此处我们将prev当作前一个。 2、size字段表示该堆块的大小,由于堆块的大小必须是8字节的整数倍,因此size字段的后三个二进制位是不表示大小的,因此作为其他标志位,我们需要知道的是,最后一位表示prev堆块是否空闲,若prev空闲,则最后一位为0。 3、至于fd和bk,也是在空闲堆块中才有意义的数据,应用在双链表中表示前后堆块(指向块首),而在in_use堆块中它们是用户数据。

0x02、关于malloc的返回值:malloc返回的指针是用户态指针,是指向chunk_body的,不包括块首,而malloc的参数表示的size也是用户态size

0x03、链表安全检查:Linux的堆内存管理有一个很重要的机制,会检查  p->bk->fd==p && p->fd->bk==p,我们不讨论宏中没有这个机制的漏洞利用

0x04、漏洞利用详解:我们不先讲原理,直接看过程来体会

(图中8byte有误,应该是16byte,图片不方便换了,聊以填坑。。)

我们需要至少两个堆块,堆内存分布如上,此处不要引起误会,虽然每个chunk都列出了fd和bk字段,但是只是为了方便读者参考,并不代表这些堆块是空闲的

首先malloc p堆块和引线堆块,那么如果放到空闲链表中看,p就是引线堆块的prev_chunk,但是此时两个堆块都是非空闲的。

往下方是高地址这个不用多说,我们在p堆块中打一个溢出,踩到引线堆块,怎么踩呢?要把引线堆块的prev_size覆盖成p堆块的用户区大小,把引线堆块的size字段的最后一个二进制位弄成0,这样就造成了p堆块是free态的假象;此外在这个溢出过程中,由于写操作是从p发起的,要顺便看一下能否写到p_user的2和3个的偏移,也就是p_user[2]和p_user[3],如果能,就把它们分别覆盖成fake的fd和bk,不能的话也不慌,寻找一下有没有别的可行写操作或者可以从更低地址堆块打来溢出。

那么fake的fd和bk应该改成多少呢?注意改了以后还要过链表安全检查的!我们来看一个巧夺天工的构造:

fd = &p - 3*size(int); bk = &p - 2*size(int) 我们来分析一下这个小小的艺术品奇妙在哪里:

首先我们需要清楚结构体的寻址是按偏移来寻址的,然后我们来看一下free函数的具体实现:

FD = p->fd;
BK = p->bk;
FD->bk = BK;
BK->fd = FD;

 其实就是一个很简单的双向链表拆卸,不多说;

然后我们来看,fd = &p - 3*size(int),bk = &p - 2*size(int) ,所以FD=&p - 3*size(int) , BK=&p - 2*size(int)

FD->bk按照偏移寻址,就是FD+3*size(int)==&p,FD->bk==p,同理 BK->fd==p,这样一来就绕过了安全检查

检查通过就按照上述代码来free,第三行等价于p=&p - 2*size(int)

但是第四行又把值覆盖回来了,最终执行完毕后就变成了p=&p - 3*size(int)

回到本例中,此时如果程序free(引线堆块),那么由于检查到引线堆块的size最后一位是0,因此认为p堆块空闲,进行合并,触发p的unlink

现在大家应该就明白“引线堆块”这个名字是怎么来的了,free相当于点火,堆块就是引线,触发prev的unlink爆炸

还记得我们已经将引线堆块的prev_size覆盖成了prev用户区的大小,因此会造成一种假象,认为prev用户区的起始就是prev的块首起始,因此做unlink时,进行fd和bk操作的时候,fd和bk就能成功定位到之前的fake值!

这样一来,在没有触发检查报警的情况下,成功将指针p_user劫持到了存放p_user自己的内存往上三个单位的内存处:

(图中p为p_user)

 

此时p_user=&p_user - _3

但是,此时在受害程序视角上,堆块p并非空闲态,也就是说,此时程序可能继续以指针p_user为接口继续进行读写操作。此时,便可以为所欲为。

这里举例一个具体操作:(p指p_user)

比如接下来存在p堆块的写操作,原程序中正常的堆块操作是,首先写p[3],然后写p[0]

假定我们现在已经知道了libc在内存中的映像地址,即得知了&free_got(free_plt)、system_got

那么我们就可以在写操作中执行p[3]=&free_got,p[0]=system_got

这两个操作分别实现的效果是,p[3]指向p即p[0],将p[0]的值即p的值改成了&free_got,之后p[0]=system_got,就相当于实现了*(&free_got)=system_got

这样一来,在程序执行任何free操作的时候,都会被劫持到system函数,get shell

当然有一个问题忘了提到,就是伪造fd和bk的时候,我们需要事先知道&p的值,这个估计需要具体的内存泄露,不再赘述。

希望对各位pwn🐶们有所裨益,有不当之处欢迎指正!

(尊重版权,转载请注明出处,谢谢!)

 

posted @ 2018-09-30 11:34  Magpie#Canary  阅读(1286)  评论(0编辑  收藏  举报