how2heap 深入学习(7)

how2heap下载网址: 传送门
Glibc源码查看网址:传送门
参考书籍:CTF竞赛权威指南-pwn篇

测试环境:Ubuntu 18.04
Glibc 版本:Ubuntu GLIBC 2.27-3ubuntu1.5

按照顺序,本文将分析glibc 2.27文件夹下的第7~8源码,对house_of_storm进行了深入的分析。
如果本文的分析有任何错漏之处,还请各位读者不吝赐教,不胜感激。

7. house_of_mind_fastbin

这是一种伪造arena以将一个大chunk中的一处值改为很大的利用方式,和glibc 2.23差别不大,但是2.23的分析感觉逻辑不是太清晰,还是再写一遍吧。

Step 1: 分配0x1010的chunk,要改写的地址为(chunk head + 0x40)。

这里解释一下为什么改写的是chunk head + 0x40。
这个chunk是要作为伪造的arena使用的,参考2.27的arena结构体——malloc_state如下:

struct malloc_state
{
  /* Serialize access.  */
  __libc_lock_define (, mutex);

  /* Flags (formerly in max_fast).  */
  int flags;

  /* Set if the fastbin chunks contain recently inserted free blocks.  */
  /* Note this is a bool but not all targets support atomics on booleans.  */
  int have_fastchunks;

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;

  /* The remainder from the most recent split of a small request */
  mchunkptr last_remainder;

  /* Normal bins packed as described above */
  mchunkptr bins[NBINS * 2 - 2];

  /* Bitmap of bins */
  unsigned int binmap[BINMAPSIZE];

  /* Linked list */
  struct malloc_state *next;

  /* Linked list for free arenas.  Access to this field is serialized
     by free_list_lock in arena.c.  */
  struct malloc_state *next_free;

  /* Number of threads attached to this arena.  0 if the arena is on
     the free list.  Access to this field is serialized by
     free_list_lock in arena.c.  */
  INTERNAL_SIZE_T attached_threads;

  /* Memory allocated from the system in this arena.  */
  INTERNAL_SIZE_T system_mem;
  INTERNAL_SIZE_T max_system_mem;
};

后面分配的是0x60大小的fastbin chunk,会被链入到这个假chunk中,而链入的地址就是chunk head + 0x40。在2.23中这个地址为0x38,2.27由于添加了一个have_fastchunks这个成员,因此地址往后移动了8字节。

addr 0x0 0x4 0x8 0xC
0x603420 mutex flag have_fastchunks -
0x603430 fastbinsY[0] (for chunk size=0x20) fastbinsY[1] (for chunk size=0x30)
0x603440 fastbinsY[2] (for chunk size=0x40) fastbinsY[3] (for chunk size=0x50)
0x603450 fastbinsY[4] (for chunk size=0x60) ......

Step 2: 设置假arena的system_mem为0xFFFFFF。

system_mem标志的是这个arena管理的空间大小。在_int_malloc函数中有这么一项检查:

if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
              || __builtin_expect (chunksize_nomask (victim)
				   > av->system_mem, 0))
            malloc_printerr ("malloc(): memory corruption");

这个检查是在分配unsorted bin和large bins前进行的,表明请求的内存不能大于system_mem。

Step 3: 计算假arena管理的空间位置。

在系统堆初始化之后,将堆的大小定为0x4000000,因此后面申请的假arena管理的地址在这个堆之后。要计算这个堆的起始地址。

Step 4: 一直分配chunk直到系统heap被占满。

在源码中,这里一直分配大小为0x1ff00的chunk,因为mmap_threshold=0x20000,它表示当用户分配大于0x20000的空间时,就不使用堆而是直接通过mmap获取了,这种情况需要避免,因此最大分配0x1ff00的chunk。

Step 5: 分配一个0x60的chunk在堆空间之上。

Step 6: 填满0x60的tcache。

Step 7: 修改系统heap之上的假heap的控制信息。

typedef struct _heap_info
{
  mstate ar_ptr; /* Arena for this heap. */
  struct _heap_info *prev; /* Previous heap. */
  size_t size;   /* Current size in bytes. */
  size_t mprotect_size; /* Size in bytes that has been mprotected
                           PROT_READ|PROT_WRITE.  */
  /* Make sure the following data is properly aligned, particularly
     that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
     MALLOC_ALIGNMENT. */
  char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

将计算得到的假heap地址的开头写入假arena的地址,即ar_ptr

Step 8: 修改0x60 chunk的non_main_arena标志位。

Step 9: 释放最后一个chunk,修改假main_arena对应位置的值。

此时,当我们free时,libc会根据_heap_info的ar_ptr找到我们的假chunk,然后在假chunk里面更改内容。这也就是我们的目的。

8. house_of_storm

源码要修改bss段的一个全局变量。

Tips: 如果使用gdb调试需要加上-no-pie参数去掉pie,否则后面的检查通不过。

Step 1: 构造堆环境并进行堆风水检查。

本漏洞利用需要的堆环境为:一个unsorted bin chunk和一个large bins chunk,且unsorted bin chunk大于large bins chunk。

首先分配0x4f0的chunk(之后将作为unsorted bin chunk),检查该chunk的地址最高非0位的值x,这里需要检查的原因在后面说明。具体检查方式:

首先判断x是否小于0x10,x小于0x10不行。

然后判断x的最低4位——bit-0~bit-3:

bit-3必须为0;
bit-2为1时bit-1不能为0;

由于要分配的大小在tcache范围,因此需要填满对应的tcache。

然后分配0x4e0的chunk,之后将作为large bins chunk。
分配一个小chunk防止top chunk合并。
随后依次释放0x4e0和0x4f0,先释放小chunk。再分配回大chunk再释放,小chunk就能顺利进入large bins。
至此,堆结构构造完成。

检查原因:因为最高非0位是作为size呈现的,因此不能小于0x10这个最小值。其次,chunk的大小应该是0x10的倍数,因此bit-3不能为1。再次,bit-2是non_main_arena标志位,bit-1是mmap标志位,这两者也不能够有一定的组合:

(_int_malloc glibc 2.27 line 3438)
  assert (!mem || chunk_is_mmapped (mem2chunk (mem)) ||
          av == arena_for_chunk (mem2chunk (mem)));

不然也无法通过检查。

Step 2: 修改unsorted bin chunk和large bins chunk的bk,large bins chunk的bk_nextsize。

由于需要修改bss段内容,设需要修改的地址为y,要在这里伪造一个chunk,那么chunk头应该在y-0x10处。将unsorted bin chunk的bk修改为y-0x10,large bins chunk的bk修改为y-0x8。将large bins chunk的bk_nextsize修改为y-0x18-(偏移)。这个偏移指的是unsorted bin chunk的地址的非零字节数-1。

addr+0x0+0x8
unsorted bin chunk + 0x10fdy - 0x10
large bin chunk + 0x10fdy - 0x8
large bin chunk + 0x20fd_nextsizey - 0x18 - <offset>

下面是修改完成后两个chunk的情况:(目标地址为0x6020A0)

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x603250
Size: 0x4f1
fd: 0x7ffff7dcdca0
bk: 0x602090

Free chunk (largebins) | PREV_INUSE
Addr: 0x603a00
Size: 0x4e1
fd: 0x7ffff7dce0c0
bk: 0x602098
fd_nextsize: 0x603a00
bk_nextsize: 0x602076

Step 3: 调用calloc返回目标地址。

这里使用calloc而不使用malloc是为了避开tcache。而在这一步中蕴含了很多操作。

由于last_remainder为空,因此unsorted bin中的这个chunk实际上并不会被切割,而是直接被分配到bins中去了。这里的unsorted bin chunk大于small bins的最大阈值,因此被分配到了large bins中。

在glibc 2.23中对large bins的插入规则没有进行详细分析,这里解释一下。

large bin的链入过程

在_int_malloc进入大循环中时,每一次会从unsorted bin中弹出一个chunk,不符合需求就将会被放入small bins或large bins中。假设unsorted bins中全部都会被放入一个large bins中。

// (line 3734)
	bck = victim->bk;
	......
// (line 3778~3779)
	unsorted_chunks (av)->bk = bck;
    bck->fd = unsorted_chunks (av);
    ......
// (line 3820)
	victim_index = largebin_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 (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;
                  fwd->bk_nextsize = victim;
                  victim->bk_nextsize->fd_nextsize = victim;
                }
              bck = fwd->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;

上面是所有涉及到bins修改的代码。每一次循环时,进行操作的chunk是victim,bck = victim->bk。

a. 将victim脱离unsorted bin链,即line 3778~3779。
b. 如果victim正好是请求的大小,直接返回,即line 3783~3808
c. 发现是large bin,进入large bin调整过程,即line 3818

调整之前,首先找到这个large bin应该被放入的bin的索引,即line 3820的调用largebin_index函数;bck设置为这个bin的头指针,fwd设置为bck的fd指针。下面是判断这个bin是否有chunk。这里有一处需要注意:如果这个bin没有chunk,那么bck会指向前一个chunk。这样找到bck->fd时找到的是下一个bin,而下一个bin指向的正好是当前bin,就像下面这样。这就可以解释为什么fwd != bck可以用来判断bin中是否有chunk。不过存放chunk的真正bin应该是bck->fd而不是bck,这点也需要注意,在gdb调试过程中可以发现。

00:0000│ r8 0x7ffff7dce0d0 (main_arena+1168) —▸ 0x7ffff7dce0c0 (main_arena+1152) —▸ 0x7ffff7dce0b0 (main_arena+1136) —▸ 0x7ffff7dce0a0 (main_arena+1120) —▸ 0x7ffff7dce090 (main_arena+1104) ◂— ...
01:0008│    0x7ffff7dce0d8 (main_arena+1176) —▸ 0x7ffff7dce0c0 (main_arena+1152) —▸ 0x7ffff7dce0b0 (main_arena+1136) —▸ 0x7ffff7dce0a0 (main_arena+1120) —▸ 0x7ffff7dce090 (main_arena+1104) ◂— ...
02:0010│    0x7ffff7dce0e0 (main_arena+1184) —▸ 0x602250 ◂— 0x0
03:0018│    0x7ffff7dce0e8 (main_arena+1192) —▸ 0x602250 ◂— 0x0
04:0020│    0x7ffff7dce0f0 (main_arena+1200) —▸ 0x7ffff7dce0e0 (main_arena+1184) —▸ 0x602250 ◂— 0x0
05:0028│    0x7ffff7dce0f8 (main_arena+1208) —▸ 0x7ffff7dce0e0 (main_arena+1184) —▸ 0x602250 ◂— 0x0
06:0030│    0x7ffff7dce100 (main_arena+1216) —▸ 0x7ffff7dce0f0 (main_arena+1200) —▸ 0x7ffff7dce0e0 (main_arena+1184) —▸ 0x602250 ◂— 0x0
07:0038│    0x7ffff7dce108 (main_arena+1224) —▸ 0x7ffff7dce0f0 (main_arena+1200) —▸ 0x7ffff7dce0e0 (main_arena+1184) —▸ 0x602250 ◂— 0x0

如果这个bin中没有chunk,则将victim链入bin中,将fd_nextsize和bk_nextsize设为其自身。如果有,则继续下面的操作。这大致可以用几张图来展示。

如果victim的size小于这个bin中最后一个chunk的size,则进行下面的操作,将victim链入到bin的最后位置。注意:每一个bin的第一个chunk的bk和最后一个chunk的fd指向的并不是这个bin的头指针,而是上一个bin的头指针!

否则,进行如下操作:

找到应该插入的位置,如果没有找到与victim大小相同的chunk,则进行插入操作,更新fd, bk, fd_nextsize和bk_nextsize。如下图:

如果找到了与victim大小相同的chunk,则在其后进行插入,使victim成为这个大小的chunk中第二靠前的chunk。如下图:

在这里插入图片描述

综上所述,large bin要维护的实际上是这样一个结构,其中每一个bin里面可以按照chunk的大小划分,bin head指向的是最大的chunk,那些大小相同的chunk中只有最靠前的chunk有fd_nextsize和bk_nextsize。


再回到这个漏洞上面来。整个漏洞的执行过程一共有十几个调整bin的步骤。在漏洞调整chunk之后,bins的结构如图:

调整的过程如下:

// line 3778~3779, Step 0
	unsorted_chunks (av)->bk = bck;
    bck->fd = unsorted_chunks (av);
// line 3820~3822, Step 1, 2
    victim_index = largebin_index (size);
    bck = bin_at (av, victim_index);
    fwd = bck->fd;
// line 3856~3859, Step 3, 4, 5, 6
	victim->fd_nextsize = fwd;
    victim->bk_nextsize = fwd->bk_nextsize;
    fwd->bk_nextsize = victim;
    victim->bk_nextsize->fd_nextsize = victim;
// line 3861, Step 7
	bck = fwd->bk;
// line 3869~3872, Step 8, 9, 10, 11
	victim->bk = bck;
    victim->fd = fwd;
    fwd->bk = victim;
    bck->fd = victim;

之后再次进行一次循环,到达line 3778时将victim赋值为target-0x10(Step 0中已经将unsorted bin head赋值为target-0x10)。后面判断target-0x10这个chunk的大小,发现正好满足(Step 6中错位赋值所致),因此返回target。

这个地方非常不好理解,要知道它为什么会返回target,我死抠源码抠了好几天才捋清楚这一整个过程到底是怎么一个流程。建议对照源码仔细消化理解,搞清楚每一步干了什么。gdb有的时候调试源码定位不准确,因此只能这样一步步去推演了。

用了这么长时间,算是把house of storm研究透了,这种攻击方式真是巧妙,能够想到这种方式的人真的是天才。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值