malloc源码调试(二)
bkbqwq 发表于 湖北 二进制安全 455浏览 · 2025-06-13 03:47

unsorted bin

1malloc后续进入到对unsorted bin的处理:

C
复制代码
2 最开始时对last_remainder的处理过程:

这里先构造一个 last_remainder,与unsorted bin中的第一个chunk相同:

image.png
图片加载失败


顺利通过四个条件判断,进入对last_remainder的处理:

image.png
图片加载失败


下面对last_remainder chunk进行切割:

拿到新的remainder chunk的size、找到新的remainder chunk地址、更新unsorted bin的链首、为新的remainder chunk附上fd 和 bk

image.png
图片加载失败


更新申请到的chunk的size,更细新的remainder chunk的size,最后更新相邻高地址处的chunk的prev_size:

image.png
图片加载失败


最后从原last_remainder chunk中,切割出申请的chunk:

image.png
图片加载失败


3存在的利用:

最后切割完成之后可以看到,在申请出来的chunk上存在main_arena地址,可以用来泄漏libc地址(在没有UAF时) ==> 很常用

4 下面对unsorted bin中的chunk处理:将其移出unsorted bin,置入到对应的bin(small 、large bin中):

在将chunk置入到对应的bin里面之前,先对移出unsorted bin 的chunk进行下面的处理:

存在的利用:

只需要能任意修改size值 即可完成overlapping。(off_by_one)

演示:

原始的堆分布,这里unsorted bin 中的chunk在main_arena中的last_remainder字段是要有标记的

image.png
图片加载失败


伪造,将size改大,在对应的位置放上适当的prev_size 和 chunksize(过检查):

image.png
图片加载失败


再申请size完全相同的chunk,就能申请到包含chunk0x20的chunk了,造成overlapping:

这里通过 对unsorted bin中的chunk的 prev_size检查

image.png
图片加载失败


这里通过prev_inuse检查 (上面伪造0xe0 和 0x20的原因就是通过这个检查):

image.png
图片加载失败


将chunk 移出 unsorted bin:

image.png
图片加载失败


检查 chunk的size和 申请的大小是否完全相同:

image.png
图片加载失败


将取出的chunk 标记为已使用,即 相邻的高地址处的chunk的prev_inuse位为1

image.png
图片加载失败


tcache_nb不为0 且 tcache未满,就将取出的chunk放入到对应的tcache中,return_cached置为1(后续直接用tcache来返回):
image.png
图片加载失败


随后循环结束,该轮对unsorted bin的处理结束,再从tcache中取出:

image.png
图片加载失败


最后申请到伪造的chunk ,成功overlapping:

image.png
图片加载失败


5 当上面的size(unsorted bin 中取出的chunk的size) 和 申请的大小不完全相同时,会将移出的chunk 置入到对应的bin(small 、large bin中):

6先看置入small bin 的情况:

先准备一个unsorted bin chunk

image.png
图片加载失败


再任意申请一个chunk,开始拿到unsorted bin中chunk时会进行双向链表检查:

image.png
图片加载失败


这里将拿到的chunk移出unsorted bin:

image.png
图片加载失败


这里开始进入small bin 的处理:

image.png
图片加载失败


在main_arena中找到与chunk size 对应的small bin链:

image.png
图片加载失败


最后在这里将其放入到small bin链中,再继续进行循环:

image.png
图片加载失败


7再看置入large bin的情况(large bin attack 利用):

image.png
图片加载失败


再申请一个chunk,将该unsorted bin中的chunk放入large bin中,这里根据large bin的特性有4种情况来更新fd\bk_nextsize字段,来保证large bin处于有序

找到的large bin是空链

unsorted bin中的chunk 与 large bin中存在的chunk大小相同

unsorted bin中的chunk size 小于 对应large bin中最小的chunk (高版本上普遍利用)

unsorted bin中的chunk size 大于 对应large bin中最小的chunk 且 其中不存在size相同的chunk

主要看第三种情况,在glibc-2.30开始,对地四种情况的处理上会增加跳表检查,而对于第三种情况则没有检查:

image.png
图片加载失败


先将一个大chunk置入到large bin中,再安排一个相对小的chunk放入unsorted bin中 (两个chunk的size要在同一个large bin链的范围之内),满足第三种情况的条件:

chunk_0x440 和 chunk_0x460 都在0x440-0x470范围之内

image.png
图片加载失败


再申请一个更大的chunk,来将unsorted bin中的chunk 置入到该large bin中(不损坏上面两个chunk就行):

unsorted bin 的双向链表检查

image.png
图片加载失败


后续取出chunk,进入到large bin得到处理:

拿到main_arena.bins数组中 对应large bin 的下标,并拿到对应链的头 bck

image.png
图片加载失败


这里将取出的chunk的size找到的large bin链中的最小chunk的size进行比较:

image.png
图片加载失败


通过比较后进入fd\bk_nextsize赋值,这里存在large bin attack 的利用:

通过前面的调试,可以看到,直到进入这里都没有对 large bin中本身的chunk(这里的fwd->fd的值) 的fd\bk_nextsize字段进行检查,所以即使修改了上面的值 也不会影响程序

image.png
图片加载失败


这里将fwd->fd->bk_nextsize字段上的值 修改为 t arget_addr ,那么在后面一句赋值的时候:

就可以在 target_addr + 0x20 的位置放上victim 即一个堆地址(unsorted bin中拿出的那个chunk地址)

如果要多次利用large bin attack ==> 只需要每次修改哪个large bin中最大的chunk的bk_nextsize字段的值即可

最后去更新large bin链的fd\bk字段(这里也没有检查),将chunk链入对应的large bin链:

image.png
图片加载失败


image.png
图片加载失败


这里可以看到 fwd的bk指向最小的chunk地址fd指向最大的chunk地址

bin处理

1上面结束对unsorted bin的处理后,会进入到对large bin 的处理:

这里再重新看一下unlink函数:

这里调试看一下对large bin的切割处理:

后续申请的chunk要在该large bin的size范围之内 (这里可以看出,如果不是跳表的链首,则fd\bk_nextsize字段为空)

image.png
图片加载失败


申请0x400的chunk,刚好在切割掉最后的chunk_0x460后还能剩下0x20,从而保证一个chunk:

这里开始进入对large bin的处理

image.png
图片加载失败


通过前面用 idx = largebin_index (nb) 取得的下标idx, 拿到了该large bin链 ,这里检查该链是否为空、其中的chunk的size是否满足申请的大小:

image.png
图片加载失败


这里拿到了large bin链中size最小的chunk,开始从小到大寻找适合的chunk:

image.png
图片加载失败


这里找到的最小chunk的size满足申请大小,所以直接退出循环:

image.png
图片加载失败


这里由于找到的chunk 是large bin链中的最末尾的chunk,所以肯定不存在后继的空闲块,所以只能使用跳表的链首来解链(这里如果不是large bin链的最末尾的chunk,再判断该chunk的后继的空闲块size与其是否相同,从而决定是否更新):

image.png
图片加载失败


unlink中对large bin进行双向链表检查,随后从large bin中取出:

image.png
图片加载失败


然后处理large bin中的fd\bk_nextsize字段,其中 p->fd_nextsize != NULL 保证 解链的是跳表的链首(不是链首不需要处理fd\bk_nextsize字段):

image.png
图片加载失败


跳表的完整性检查:

image.png
图片加载失败


这里没有通过判断 所以进入else 处理:

image.png
图片加载失败


最后直接更新跳表前后的 fd\bk_nextsize这字段:

image.png
图片加载失败


2再看一下 如果解链的chunk 不是跳表的链首:

image.png
图片加载失败


再申请 chunk_0x440 :

这里经过判断后 确认victim 即跳表的链首 不是该large bin链的最末尾的chunk,即存在后继空闲块 ,且后续判断size与其相同,则更新待解链的chunk 为victim的后继空闲块(size相同) ,cmove指令是一个条件赋值指令条件:ZF=1 => 即相等,才赋值:

image.png
图片加载失败


进入unlink,这里由于取的不是 跳表的链首 所以 p->fd_nextsize != NULL 没通过,即不处理后续large bin的fd\bk_nextsize字段直接ret(也能保证large bin有序):

image.png
图片加载失败


3最后进入切割:

比较切割后的大小 是否 保证能达到最小chunk

image.png
图片加载失败


切割后将剩余的chunk放入到unsorted bin中:

image.png
图片加载失败


4如果没有进入到上面的large bin中,或者进入了但是对应的large bin链中没有满足申请大小的chunk,就会进入下面这段,在small bin 和 large bin中依次从小到大查找适合的bin链:

首先看一下,main_arena中bins数组binmap的结构

bin数组中前两个元素为unsorted (2),后面的都是 small bin(62*2) 和 large bin(63 *2)。一共125个bin链

binmap中有4个元素,每一个为无符号整型 ==> 4字节 ==> 4*32= 128 位 (能标志128个bin链) ,可以标识所有的samll bin 和 large bin链 ==> 将125个bin链 分成4组,每一组里面都有1bit来标记一个bin 链

image.png
图片加载失败


这里调试看一下,使用binmap寻找适合的bin链的过程,(binmap数组是在对unsorted bin处理,将chunk置入到small bin和large bin时进行维护的):

5维护过程:

先准备一个属于small bin 的chunk在unsorted bin中

image.png
图片加载失败


再置入到small bin中:

image.png
图片加载失败


开始更新binmap:

image.png
图片加载失败


更新完成,大小位0xb0的smnall bin就会被binmap标记 表示该bin链不为空

image.png
图片加载失败


如果将0x20的chunk放入到small bin中,binmap会不会用最小的那个bit位来标记这个bin链:

image.png
图片加载失败


最小的chunk_0x20,是用0b100 即第3位来标记的 ,后面的chunk依次类推:

image.png
图片加载失败


6看一下寻找bin链的过程,将chunk_0xb0置入到small bin中,再申请chunk_0x20:

small bin中的chunk成功被binmap标记 (这里不放入到small bin中也可以,因为没有被last_remainder标记,所以开始处理unsorted bin时不会被切割,还是会被先放入到small bin中)

image.png
图片加载失败


再申请0x20的chunk:

这里bin跳过0x20,直接检查0x30的bin 链,被标记再bimmap[0]中,所以从binmap[0]中拿map,bit ==> 0x8 =>0b1000,说明chunk_0x30在map中的标记位是第4位

image.png
图片加载失败


这里bit比map小,说明map标记的bin链中,有满足申请大小的空闲chunk

image.png
图片加载失败


这里开始逐位寻找被标记的bin链(从小到大),bit左移1位,将相当于寻找的chunk的size大0x10:

image.png
图片加载失败


最后找到了被map标记的bin链,代表0xd0大小的空闲块:

image.png
图片加载失败


检查找到的bin链是否为空:

image.png
图片加载失败


最后unlink从bin链中取出 chunk,开始切割:

image.png
图片加载失败


切割后剩下的chunk,放入unsorted bin中:

image.png
图片加载失败


如果申请的大小在small bin范围内,还要标记一下main_arena中的last_remainder字段:

image.png
图片加载失败


完成申请,剩余的chunk:

image.png
图片加载失败


top

1当上面的情况都无法满足时,会启用top 直接从top chunk中拿堆:

利用1:

1 在glibc-2.29之前 没有新增这个检查之前 __glibc_unlikely (size > av->system_mem) ,通过将top chunk的size改大,存在一个house of force的利用,可以将top chunk延申到任意位置,从而任意地址申请chunk:

2用glibc-2.25演示:

将top chunk的size改大,并计算出,top chunk到 目标地址的距离,这里我申请到_IO_list_all:

image.png
图片加载失败


往高地址申请chunk,申请的大小就是刚才计算出来的值:

这里从main_arena中,取出原本的top chunk地址,并用申请的size和原先的top chunk的size比较

image.png
图片加载失败


通过检查后计算新的top chunk地址,并将其放入到main_arena的top字段中:

image.png
图片加载失败


在glibc-2.29之后,house of force就不适用了:

可以看到这里由于伪造的top chunk的size比main_arena中的system_mem限制的要大,所以会报错退出,所以不是适用:

image.png
图片加载失败


利用2: 当空间不足时,会适用syamalloc进行扩容

1如果top chunk的size 不能满足申请的大小,且fastbin中没有空闲块,即进入到最后的else中:

这里直接把top chunk的size改小:

image.png
图片加载失败


再申请一个大于top chunk sizede 堆,在use_top 这个标签之前,对top chunk的size没有任何检查的:

我们是将top chunk的size该小,所以这里的system_mem检查能通过

image.png
图片加载失败


这里由于申请的大小大于top chunk剩余的size,所以不会进入切割,后面fastbin中没有空闲块所以else if也不会进入:

image.png
图片加载失败


这里最后调用sysmalloc函数,传入的参数是申请的大小 和 main_arena地址,接收一个指针p作为返回值:

image.png
图片加载失败


进入sysmalloc函数:

这里如果申请的大小 nb > 0x2000(mmap的阈值)并且 mmap分配的堆的数量n_mmap 要小于 最大值n_mmap_max,那么就会以mmap的形式进行扩容

image.png
图片加载失败


最后扩容出来的效果,和使用main_arena管理的堆不在一个段上面:

image.png
图片加载失败


2 这里看另外一种,对主内存区的分配中使用brk方式扩容:

这里检查了top chunk,根据old_top的地址(main_arena中取出) 和 old_size 计算出top chunk的顶地址(即将top chunk申请完时的地址)。

image.png
图片加载失败


old_size至少要大于最小的chunkprev_inuse位必须为1old_end & (pagesize - 1) ==> 要保证top按0x1000页对齐

image.png
图片加载失败


这里再次检查,top chunk的size是否真的不够申请,如果条件为假,则报错退出:

image.png
图片加载失败


这里判断是不是主分配区扩容,如果是主分配区,则直接扩容Top chunk

image.png
图片加载失败


扩容的size 等于 nb(此次分配的容量) + top_pad(每次分配扩展值128K = 0x020000)+ MINSIZE对齐字节,最后得出size大小。是连续性分配的(contiguous (av)),可以减去老的Top chunk剩余的old_size值,:

image.png
图片加载失败


然后使用系统调用(sbrk)分配size大小的内存,可以看到在原有的堆的基础上又增加了0x21000的空间,而且和之前的空间时连续的(这里如果分配失败的话,会从新计算size大小,并采用mmap的方式分配内存):

image.png
图片加载失败


调整main_arena中系统内存大小(整个top chunk的大小):

image.png
图片加载失败


如果通过brk扩容的空间是连续的(想对上次的top chunk顶地址),则直接更新原来top chunk的size即可。但是我们修改了top chunk的size,所以导致了计算出来的old_end 和 新扩展的堆的起始地址brk不相等,即地址不连续:

image.png
图片加载失败


这里进行第二次扩容,correction = brk前置的对齐字节 + 老的top的size + 新的brk尾部的对齐字节,可以看到从新增加 了0x1000大小。如果这里不阔容,而是直接使用第一次扩容的起始地址,可能:

image.png
图片加载失败


第二次扩容完成后,调整top chunk,top指向调整过的aligned_brk地址,后续释放掉原来的top chunk:

image.png
图片加载失败


这里,减小top chunk的size来释放,是为了在新的top和原来的top 之间插入一个栅栏,保证原来的top chunk能被正常释放 (不会报错),:

image.png
图片加载失败


设置栅栏,这么设置的原有和free函数中的实现有关(为了防止原来的top chunk向前合并(向高地址)),导致free top时出错,这里在这篇文章中有解释 伪造unsortedbin释放时 top chunk的衔接问题 ,这里采用的就是类似的方法来防止伪造的fake_chunk向高地址合并:

image.png
图片加载失败


最后释放掉原来的top chunk。进入到unsorted bin中:

image.png
图片加载失败


最后,直接从新的top 中切割出了我们申请的chunk(和前面切割top的操作一样):

image.png
图片加载失败


3最后在pwngdb中使用vis 命令时,不会显示新的top,新的top要在main_arena中查看:

image.png
图片加载失败


0 条评论
某人
表情
可输入 255

没有评论

目录