导言
在libc版本越来越高的情况下,许多旧的攻击方式已然失效,而large_bin_attack始终屹立不倒,是许多攻击方式的先决条件,这也是我们要学习它的原因
large_bin
概念
large_bin是一种堆分配的管理方式,是双向链表,用于管理大于某个特定大小阈值的内存块。一般而言,进入large_bin的最低字节为0x200(512)。但由于引入了tcache_bin,使得在tcache_bin尚未填满的情况下,进入large_bin的最低字节为0x410(1040),所以一般我们设置大堆块都是0x410起步
结构
large_bins中含有63个bin,而large_bins总体又被分成6个组,每个组对应一个区间,且容纳个数呈指数性减少,示意图如下

说完组成部分,我们来看看链表的结构

1.在large_bin中的排列顺序是从大到小的顺序,所以越大的chunk越靠前,越小的chunk越靠后,最小的chunk指向main_arena+一定偏移。也就是说,非尾部的fd_nextsize指向的是更小的chunk,非头部的bk_nextsize指向的是更大的chunk
2.在相同大小的情况下,按照free的时间进行排序
3.只有首堆块的fd_nextsize,bk_nextsize会指向其它大小的堆块,而其后的堆块中fd_nextsize,bk_nextsize无效,通常为0
说完了large_bin的概念和结构,那么我们现在该写如何实现large_bin_attack了。large_bin_attack也是有分水岭的,这个分水岭就是glibc-2.31,所以本文会分为两个板块,一个讲解2.23版本的large_bin_attack,另一个讲解2.31版本的large_bin_attack。这两种攻击方式我们都利用how2heap项目团队编写的源码来进行讲解
glibc-2.23版本的攻击方式
适用条件
存在能够修改堆内容的函数
从unsorted_bins里提取出来的堆块要紧挨着我们伪造过的large_bins里的堆块
how2heap源码及动调
首先展示源码
1 |
|
简单解释一下这段代码的意思。首先设置了stack_var1,2的值并展示了各自的地址,接下来申请了p1,p2,p3三个分别大小为0x420,0x500,0x500的大堆块(中间的0x20堆块起保护作用,防止合并)。free掉p1,p2,此时在unsorted_bins里。申请出一个0x90的堆块后,会把p2放进large_bins里,而p1先被分进small_bins里又被切割了0x90大小的堆块并被放回了unsorted_bins里。free了p3之后伪造p2的size,bk和bk_nextsize。再次申请一个0x90的堆块后,p1又会被分割,而p3会被放进large_bins,且p3的bk被赋值为了stack_var1-0x10的地址,bk_nextsize被赋值为了stack_var2-0x20的地址(至于为什么有减去这两个数,参考chunk的结构),而stack_var1,2的值都是p3的头指针(prev)
啰里八嗦一大堆估计大家也看不懂,所以先动调来看看
申请三个堆
1 | unsigned long stack_var1 = 0; |
记住此时的var值和堆结构


逐步释放三个堆
1 | free(p1); |
首先释放完p1,p2,一切都很正常

到了比较关键的一步,申请个0x90的堆块

可以看到,p2被放进了large_bins里,而p1地址增长了0xa0后还待在unsorted_bins里。一个malloc(0x90)干的事其实还真不少
首先它将p1提取出来放进了small_bins里,将p2提取出来放到了large_bins里。为了满足malloc(0x90),系统遍历bins链表,依次从fast–>unsorted–>small–>large–>top的顺序扫描。而我们的p1很不幸就再次被提了出来,被切割,被丢回unsorted_bins等待分配
然后它的hxd p3就来陪它了

修改p2的结构体
1 | p2[-1] = 0x3f1; //size |
这是整个修改过程中最重要的部分,修改p2的结构,为后续的攻击做铺垫
原结构和内容

经过这段代码修改过后的结构和内容

可以看到我们已经成功完成了修改,此时p2的结构如下图所示

修改stack_var值
1 | malloc(0x90); |
短短三行代码其实包含了很多东西。
依旧是这个malloc(0x90),p1依旧惨遭剥削,p3被扔进large_bins里。此时会对p2和p3的大小做出比较,先看看比较过程
1 | while((unsigned long)size < fwd->size){ |
很显然,我们p2的0x3F0小于p3的0x510,所以会执行else里的语句,跟我注释里写的一样,为了起到明显的对比效果,我分别把修改前和修改后的截图放出来
修改前的p2和p3

修改后的p2和p3

可以看到,我们已经把p3的bk和bk_nextsize修改成了stack_var1-0x10和stack_var2-0x20,而这两个分别对应的fd,fd_nextsize指针指向的就是我们的stack_var1,stack_var2,我们就把chunk3的头指针给输入进这两个地址内部了

总结
这个攻击主要就是利用两个chunk,大的为p3,小的为p2。我们可以伪造较小堆块p2的bk和bk_nextsize,分别指向target1-0x10,target2_0x20,放进large_bins里(可以的话,large_bins在此前最好是空的)。而大堆块p3在经过malloc后放进large_bins里,将p3的头指针赋值给了target1,target2,完成了任意地址写。将p3的头指针写进target1,2里后,可以结合其它House of 系列进行攻击
glibc-2.31版本的攻击方式
新增检测
1 | else |
因为这个检测,我们在glibc-2.23的攻击方式算是彻底失效了,得另寻他路
新源码利用
1 | assert (chunk_main_arena (bck->bk));//断言bck->bk属于main_arena |
这段代码与unsorted_bins、large_bins有关,这是从unsorted_bins里提取出来的堆块((unsigned long) (size))与large_bins里的最小堆块((unsigned long) chunksize_nomask (bck->bk))进行比较,如果unsorted出来的堆块更小,就执行如上操作。
注意,这里并没有进行检测操作!,意味着我们可以对bk_nextsize进行修改,再次实现任意地址写
how2heap新源码及动调
1 |
|
跟之前相比起来其实大差不差,但是中间申请的堆块从小堆块变成了大堆块(指从0x90变成了0x438),跟前面雷同的地方我就不再演示,主要看free(p2)及其之后的动调
free(p2)及其后的变化
在刚刚完成free(p2)的时候,堆内的结构是这样的

此时还很正常,那么我们接下来修改p1的bk_nextsize

可以看到,我们把p1的bk_nextsize修改成了target-0x20字节处的地址,我们继续往下走
申请完最后一个大堆块,这是此时p2和target的变化


此时,我们已经完成了我们的目的,任意地址写,已经成功将p2的头指针写进了target里边,实现了large_bin_attack
总结
相较于旧版本的large_bin_attack,高版本的这种攻击只能实现一个地址的任意地址写,结合其他攻击方式的过程会更为繁琐。
这种large_bin_attack,是申请大于p1,p2这两个堆块的堆块,来把p1,p2塞进large_bins里,没有之前的切割行为,我们只用伪造p1这个相对较大的堆块的bk_nextsize即可
例题 LitCTF2024 Heap2.39
源代码及分析






根据题目信息可知,这是非常高版本的堆,要用比较新型的攻击方式,而在create里限制了size的大小,典型的large_bins,还看到能主动触发exit,我们可以考虑House of Apple
动调过程
先创建5个堆
1 | add(8,0x508) |
因为有UAF,所以可以据此泄露libc_base和heap_base
1 | delete(2) |


ok,泄露完这些基础条件后,就该利用large_bins_attack了,布置好堆
1 | delete(0) |
这是p1和p2的原始内容,p2尚未分配进large_bins

此时经过edit修改后,p2再分配进large_bins,注意观察p1和p2的内容


可以很明显的看到,原本在p1的bk_nextsize处的_IO_list_all-0x20转移到了p2的bk_nextsize,此时,我们已经向_IO_list_all写入了p2的地址,接下来就是要利用p2来getshell

使用过IO攻击的师傅都知道,要伪造IO,本题也不例外,我们使用的是House of Apple2的_IO_wfile_overflow函数控制执行流
1 | edit(8, b'a' * 0x500 + p32(0xfffff7f5) + b';sh\x00') |
这段代码最容易迷糊的就是开头的flag位设置,首先我们通过chunk8来修改p2的prevsize位,0xfffff7f5的0xffff设置为屏蔽高四字节,意味着我们的sh不会被检测,f7f5则设置了很多状态位,不一一赘述
设置好堆状态,让我们看看此时的堆排列

可以看到此时是已经设置好了的,只要我们主动触发exit即可
