堆漏洞挖掘:07---chunk的NON

it2022-05-08  7

struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; INTERNAL_SIZE_T mchunk_size; struct malloc_chunk* fd; struct malloc_chunk* bk; struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };

一、三者简介

NON_MAIN_ARENA:记录当前 chunk 是否不属于主线程IS_MAPPED:记录当前chunk是否是由mmap分配的PREV_INUSE:如果前面一个chunk处于分配状态,那么此位为1。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个chunk 的size 的P位为0时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲chunk之间的合并

二、glibc的实现

上篇文章中有介绍struct malloc_chunk结构体的大小的对齐原则,在64位系统中为16字节对齐,在32位的系统中为8字节对齐。因此在64位系统中,struct malloc_chunk结构体的大小的最后4个字节是没有被使用的,32位系统中,最后3个字节是没有被使用的。如下图所示:

所以,glibc用struct malloc_chunk结构体的fd成员的最后3个比特位来表示当前chunk处于什么状态。只需要将fd成员相应的比特位置为1,就可以代表当前chunk处于什么状态

PREV_INUSE:值为1

IS_MMAPPED:值为2

NON_MAIN_ARENA:值为4

三、PREV_INUSE位导致的chunk合并

系统会根据chunk的PREV_INUSE位来进行合并操作。如果PREV_INUSE为1,说明该chunk前面的chunk正在被使用,那么该chunk不能用于合并;如果PREV_INUSE为0,说明该chunk前面的chunk为空闲状态,那么该chunk可以用于合并 合并的操作发生在_int_free函数中  #define prev_inuse(p) ((p)->mchunk_size & PREV_INUSE) static void _int_free (mstate av, mchunkptr p, int have_lock) { /* consolidate backward */ if (!prev_inuse(p)) { prevsize = prev_size (p); //1 size += prevsize; //2 p = chunk_at_offset(p, -((long) prevsize)); //3 unlink(av, p, bck, fwd); //4 } }

 函数解析:

首先使用prev_inuse宏定义判断p的PREV_INUSE位是否为0如果PREV_INUSE位为0,那么prev_inuse宏返回0,if条件为1,进入if语句体中1.通过recv_size函数得到前面要合并的chunk的大小2.将自己的大小与前面的presize相加得到合并后的chunk的大小3.将p的指针向前移动,移动到前面那个chunk的开头处4.unlink是将两个要合并的chunk各自从原来的bin链上面断下来,然后进行合并

四、特殊情况:fast chunk的PREV_INUSE位

如果chunk的大小为32字节~128字节(0x20~0x80)大小之间,我们称该chunk位fast chunk

系统规定:

①无论fastchunk前面的chunk是否处于分配状态,PREV_INUSE始终为1。因为系统不希望fastchunk与前面的chunk之间进行合并②不论当前的fastchunk是否处于分配状态,该chunk后面的chunk的PREV_INUSE位一定为1,也是为了防止后面的chunk与fastchunk合并

为什么这么设计?

大多数程序经常会经常申请和释放一些较小的chunk,如果将一些较小的chunk快free之后并合并,下次malloc时申请一个比合并的chunk小的chunk时,此时有需要切割合并的chunk。程序如果把大量的时间放在合并和切割以及中间检查上面,这样就大大的降低了效率,因为ptmalloc专门设计了fastchunk,且fastchunk是不会合并的

五、演示案例(查看chunk的PREV_INUSE位)

#include <stdio.h> #include <unistd.h> #include <malloc.h> int main() { int size=0x10; void *p=malloc(size); void *p2=malloc(size); sleep(0); //只为了打断点使用,无其他用处 free(p); free(p2); return 0; } 第一步:在sleep处打断点,并且运行起来

第二步:查看当前的所有堆块

看到第一个chunk(地址0x602020)的size为33(chunk大小32+PREV_INUSE位为1),因为它的前面的chunk是出于分配状态的

第二个chunk(地址0x602000)的size为33(chunk大小32+PREV_INUSE位为1),虽然其前面没有chunk了,但是PREV_INUSE位仍然为1,这也就验证了前面的说法(fast chunk的PREV_INUSE位永远为1)

六、演示案例(PREV_INUSE的合并操作)

因为fast chunk不会进行合并,因为其PREV_INUSE永远为1,下面我们申请更大的chunk来演示一下chunk的合并

#include <unistd.h> #include <malloc.h> int main() { int size=0x100; void *p1=malloc(size); void *p2=malloc(size); void *p3=malloc(size); //为了达到演示效果,防止合并后的chunk与top chunk合并 sleep(0); //只为了打断点使用,无其他用处 free(p1); free(p2); return 0; } 第一步:断点打到sleep上,并运行起来

第二步:先释放p1(可以看到p1被放入到unsortedbin中)

第三步:再释放p2(可以看到两个chunk被合并),合并后的大小为:273+272=545

程序更改:

现在我们在p1和p2之间再申请一块内存,来验证p1和p2不能合并的情景

第一步:断点打到sleep上,并运行起来

第二步:先释放p1

第三步:再释放p2

可以看到p1和p2没有被合并,因为其前面都有chunk正在使用(PREV_INUSE为1)


最新回复(0)