Largebin介绍
Largebin用来收容超过0x400大小以上的chunk(64位) 其是一个双向链表
一共可以容纳63个chunk 和fastbin等不同的是 其对于链表对应存储chunk的大小没有明确规定 而是一个范围
一共分为6组
这里的差值(以字节为单位)是一个什么意思呢 比如在组别1中 现在释放三个chunk到largebin中 chunkA的大小是0x400 chunkB的大小是0x410 chunkC的大小是0x450
此时由于chunkC和chunkA的差值大于了64字节 所以chunkA和chunkB是位于同一组中 chunkC是另外一组
这在largebin这个双向链表中是一个什么情形呢 我们知道 largebin相对于unsortedbin多出来两个域 一个fd_nextsize 一个bk_nextsize
这两个域和fd和bk的域差距在哪里呢?
在largebin中 不同组的排列是根据从大到小来的 方便其遍历
fd_nextsize指向的是比当前组别小的组中最大的组
bk_nextsize指向的是比当前组别大的组中最小的组
而fd和bk则是用来指向组内的chunk
这么说可能不太好理解 用一张图来演示一下
size最大的chunk的bk_nextsize指向最小的chunk
size最小的chunk的fd_nextsize指向最大的chunk
并且相同大小的chunk只有链表头的fd_nextsize和bk_nextsize才有值 其余为0
Largebin中chunk的插入取出机制
插入
源码理解
来看看glibc源码是如何逐步使得chunk插入到largebin链表中
/* place chunk in bin */ |
上述的源码注释来源自
[原创]Largebin attack总结-二进制漏洞-看雪论坛-安全社区|安全招聘|bbs.pediy.com (kanxue.com)
以下是我自己对于这个过程的理解
1.释放一个chunk后 首先对其大小进行判断 区分到smallbin或者是largebin 这里不讨论smallbin的情况
2.根据当前chunk的size 来索引对应的index 并且获得两个位于链表中chunk的指针 fwd指向链表头 也就是最大的chunk
bck指向最小的chunk
3.对于fwd和bck进行判断 如果二者相等 那么此时链表中就为空 直接将chunk放置为链表头 如果二者不相同 那么链表不为空 分为两种情况 如果chunk的size不是当前链表中最小的 从链表头开始 根据fd_nextsize指针来从大到小依次对比链表中原有的chunk大小和要插入的chunk大小 如果没有找到 那么就在对应合适的位置将当前chunk置为对应的链表头 其fd_nextsize和bk_nextsize各自指向对应的链表 如果找到了 就接入对应链表中 fd_nextsize和bk_nextsize为0
4.如果当前chunk的size是当前链表中最小的 那么就直接放置到链表末尾 如果作为链表头 fd_nextsize指向最大的chunk的链表头 构成一个循环 bk_nextsize指向比当前链表更大一点的链表 如果链表尾的大小与要插入的chunk大小一致 那么就接在对应链表中
调试
接下来我们来调试一番
调试环境
add(0x410,b'aaaa')#0 |
chunk1用来防止chunk0释放以后和top chunk合并
此时chunk0释放以后优先进入unsortedbin
要使得重新分配unsortedbin中的chunk 就需要我们申请一个超过unsortedbin中所有chunk大小的堆块 这样就会把unsortedbin中所有的chunk分配到largebin或者smallbin中
否则则将大小足够分配申请的chunk的free chunk分配出所需要的大小 其余unsortedbin中的chunk各自检验大小放入到largebin中
add(0x450,b'aaaa') |
接着我们来看看双链表结构大概是一个什么样子
add(0x450,b'aaaa') |
申请三个大小足够放入到largebin的chunk 并且为了防止物理相邻合并用0x10大小的chunk隔开 最后申请一个大chunk将unsortedbin的chunk分配到largebin中 此时预期这三个chunk应该各自成为链表头
此时我们在原来的基础上再多申请一个0x450大小的chunk 不出意外应该是分配到0x440链表后
可以看到只有位于链表头的chunk的fd_nextsize和bk_nextsize才有值
具体的利用手法等下来讲吧 更进一步的调试可以自己尝试
取出
源码理解
/* |
可以总结为以下流程:
1.首先读取largebin中最大chunk的大小 与用户申请的大小对比 如果小于则另寻办法申请chunk 如果大于就进入下一步
2.根据bk_nextsize来索引最小的chunk 顺着fd_nextsize来寻找与申请大小最为接近的chunk
3.如果查找到了合适的free chunk 先判断其是否只有单一chunk位于链表头 如果链表中有其他chunk的话 则分配其他chunk 这样是为了节省重新分配fd_nextsize和bk_nextsize的麻烦
4.判断分配完了的free chunk 如果剩余的大小大于MINSIZE 那么就放入到unsortedbin中 如果剩余大小小于MINSIZE 则一并分配给用户
调试
首先是我自己的第一个疑问 如何申请到单位不是MINSIZE的chunk 先来尝试一下手动修改size值 看看会不会按照预期效果分配chunk
将这一个chunk的size域从0x461修改为0x466 按照源代码的逻辑 此时申请一个0x410大小的chunk 剩下被分配到unsortedbin的chunk大小应该为0x40
失败了 看来是无法单单通过修改size域来实现预期效果
到这里转念一想 64位构成一个chunk最起码也要0x20字节 毕竟还需要size域和prev_size域 也就是说如果此时largebin中有一个0x460的free chunk 我们申请一个0x450的chunk 显然会剩下0x10字节 小于MINSIZE 那么按照逻辑 就应该一起给了用户申请的chunk
可以看到确实是这样
漏洞利用
修改bk_nextsize来造成overlap
漏洞的原理在于将chunk从largebin中取出的时候 其是从最小的chunk开始索引 以此找到适合的free chunk用来分配
if ((victim = first (bin)) != bin && |
关键点在于victim = victim->bk_nextsize这一句 如果我们修改了victim的bk_nextsize域 再构造一个fake chunk 就可以申请到fake chunk
通常这一做法被用来构造overlap chunk 接下来详细分析一下
环境: libc2.23
漏洞目的:实现overlap chunk
漏洞需求:拥有向largebin中释放堆块的能力 能够泄露出堆地址 拥有堆溢出
演示二进制程序:由笔者自己编写 基本所有漏洞都有
首先我们需要先泄露libc基址 在没有UAF的前提下 我们可以通过申请两个chunk 将其释放到fastbin中 此时后释放的chunk位于链表头 其fd指向先释放的chunk 由于malloc函数在申请chunk的后并不会对chunk的内容进行清空 所以我们可以再次申请同样大小的chunk 将链表头的chunk申请出来 随后打印出chunk的内容 也就是泄露基址
add(0x10,b'aaaa')#0 |
我们一共需要两个chunk 下面我们分别称这些chunk为chunkA B
chunkA是要放入到largebin中的 并且其要为largebin中最大的chunk 这样修改chunkA的bk_nextsize域才能索引到fake chunk
chunkB则是用来构造fake chunk的
在学习unlink的时候 当时的unlink可以做到任意地址申请 因为最后chunk的ptr和fd、bk域有关
largebin的unlink则是用来申请一个正在使用的chunk 从而导致overlap 为此我们只需要绕过一个判断即可
if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) |
我们需要使得 fake chunk的fd域或者是bk域指向的地址 以此地址为chunk首地址 其bk域和fd域相应的存放fake chunk的首地址
此时的chunkB的内部构造应该是这个样子 这里之所以在fake_nextsize域后还要再加上fakechunk的首地址 就是为了绕过unlink检查
这里的fakechunk首地址放到哪里都行 只需要修改fd或者bk域 就比如图中的情况来说 我们需要保证这个值+0x20以后的地址存放着fakechunk的首地址 也就是我们需要填入chunkB+0x28 由于unlink的判断只需要满足一个就行 所以图中的构造其实是多余的
还需要注意的是nextsize域需要设置为0 因为如果nextsize域有值 plmalloc就会去申请下一个堆块 而非链表头的堆
bss_addr = 0x602200 |
代码中注释的部分是bss段上构造双向链表 不过感觉正常的题不会给这个机会 也就我自己编写的题会给一个bss_write函数了
修改bk域和bk_nextsize域实现任意地址写堆地址
这种利用手法的意义在于 fastbin对于申请出来的chunk的大小和对应链表有检测 如果利用这个的话 就可以绕过这个检测
利用的关键在于源码中的这两处地方
victim->bk_nextsize = fwd->bk_nextsize; #1 |
第一句 此时的victim指向的是要放入largebin的chunk 其bk_nextsize域的值由fwd的bk_nextsize域决定
而victim的bk_nextsize指向的地址的fd_nextsize域会存入victim的地址 所以如果我们修改fwd的bk_nextsize域 就可以做到堆地址写
第二句 fwd的bk域指向的地址会存入victim的地址 这里同样可以利用
所以我们只需要修改已经位于largebin中的一个chunk的bk域和bk_nextsize域 同时释放一个size大于其的chunk进入largebin 就可以利用漏洞
bss_addr = 0x602200 |
之所以chunk3的大小要同为可以被释放进largebin 是因为防止过小从chunk0中分配 导致chunk0被放入到unsortedbin 调大chunk0的值同样可行
chunk4的目的在于将chunk2放入到largebin
此时的bss_addr内容如图所示 以0x602200为首地址 两字长后为bk域 是fwd->bk = victim的效果 也就是如果我们修改fwd的bk域 那么任意写的地址在于ptr_addr + 0x10
bk_nextsize的值则是victim->bk_nextsize->fd_nextsize = victim的效果 也就是我们修改fwd的bk_nextsize域 任意写的地址在于ptr_addr + 0x20
largebin的利用在高版本中还是比较常见的 许多house of系列就是基于largebin的 需要好好掌握
2.31以上漏洞利用
2.31对于largebin的检查做了一些增强 虽然还是能够largebinattack 往任意地址写堆地址 但是攻击效果没有那么强大了
新版本针对largebin 新增了两个检查 导致我们原本的方法行不通了
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); |
但是还是有办法 原本我们利用的是比largebin中大的chunk放入largebin 引起的那些操作
与之相对的 还有小chunk放入largebin中的操作 不过只能往一个地址写入堆地址 相比之攻击效果不够强大 所以一开始没有使用
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){ |
上面就是我们要利用的代码 直接跟着我来源码调试吧 这样就清楚了
我写的POC:
|
申请四个chunk chunk1和4用来防止合并 接着释放chunk0 随后申请一个大chunk 把chunk0放入到largebin中 并且把chunk2也放入到unsortedbin中
接着我们需要伪造chunk0的bk_nextsize 将其修改为ptr_addr-0x20 然后把chunk2释放到largebin中
s进入malloc函数 接着n到int_malloc函数 再次s进入
断点打在这三行源代码所在的行数 我所用的源码是3846 然后c到这里即可
此时的victim->fd_nextsize即chunk2的fd_nextsize域 fwd->fd指向chunk0
执行完这步后 chunk2的fd_nextsize域写入chunk0的地址
下一句相当于 chunk2的bk_nextsize域写入chunk0的bk_nextsize域内容
接下来一句就是我们任意写的关键了
往chunk0的bk_nextsize域 以及chunk2的bk_nextsize的fd_nextsize域写入chunk2地址 但是前一句 已经修改了chunk2的bk_nextsize域为chunk0的bk_nextsize域 所以此时是往我们修改的Ptr_addr+0x20写入chunk2地址
总结一下 就是修改largebinchunk的bk_nextsize为ptr_addr-0x20 就可以往ptr_addr写入unsortedchunk的地址