MENU

Large Bin Attack 学习记录

December 30, 2020 • Read: 217 • Pwn

这篇文章是在石墨上编写的,可能会存在格式上的一些问题,导致阅读不够流程。
如果有需要格式较为完善的文件,可以发邮件找我要PDF格式的文件。

Largebin Attack是我一直以来未能攻克的一道难题,不过终于在最近把他弄懂了,其中也少不了长时间的琢磨。
根据我自己浏览各个文章后得到的经验,我建议师傅在学习Large Bin Attck的内容时可以对其中的结构进行绘图,并且手动模拟一遍源码的流程以加深理解。

Large Bin Attack

利用范围

Large Bin Attack是一种较为困难的攻击方式,他对攻击的条件要求较多,实现也较为复杂,通常和Unsorted Bin打配合来实现house of storm,来达到提升影响力的作用。

glibc2.29及以下版本,可以利用Large Bin Attack来写两个地址。

但是在glibc2.30及以上版本中,只能利用Large Bin Attack来写一个地址。

git commit:<TODO>

利用原理

我们在unsorted bin attack的时候就已经了解过,当一个块将要malloc的时候,堆管理器会根据返回去不同的地方查找是否有空闲的chunk,其中一个地方就是unsorted bin,而在遍历unsorted bin堆块的时候会先将当前遍历的堆块进行脱链,脱链根据大小放入small bin或者largebin当中去。

而largebin attack就发生在堆块中unsorted bin 放入 largebin的过程当中去。

这个过程较为复杂,所以我们需要先来了解一下largebin是什么?有什么?

largebin是什么?

堆块管理器中最慢的一种管理方式,largebin的范围是 size > 0x400(x64)

large bin采用双链表结构,里面的chunk从头结点的fd指针开始,按大小顺序进行排列。

largebin有什么?

在bins中占据64到126这个范围的位置,

index公差
64[0x400,0x440) 0x40
65[0x440,0x480) 0x40
...0x40(共32个)
970x200
980x200
...0x200(共16个)
1120x1000
1130x1000
...0x1000(共8个)
1200x8000
1210x8000
1220x8000
1230x8000(共4个)
1240x40000
1250x40000(共2个)
126> 0x40000

根据上表可以知道,就算在同一个index中,其大小也有差别,这和fastbin是不一样的。

所以堆管理器就利用

/*
  This struct declaration is misleading (but accurate and necessary).
  It declares a "view" into memory allowing access to necessary
  fields at known offsets from a given base. See explanation below.
*/
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;
};

这个结构中的fd_nextsizebk_nextsize来链接到下一个size的堆块头部和上一个size的堆块头部。
然后在相同size的堆块内部再通过fdbk来进行内部的管理。

为了表述方便,我们在下述描述中,称用fd_nextsizebk_nextsize链接的链表为横向链表,称用fdbk链接的链表为纵向链表,有心的师傅可以画个图理解一下。

而且,为了管理的高效,在横向链表中,堆管理器维护一个循环的单调链表,由最大的size(在这个index下的最大size)作为表头,最小的size作为表尾,且首尾相连。

而large bin attack就发生在,malloc过程中把某个之前不存在的size插入到横向链表的过程中。

PS:如果之前存在,那么就直接加入到对应size的纵向链表即可。

下面来看这一块的代码,这一块的代码也在遍历unsorted 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;

这一块内容摘选自:https://www.cnblogs.com/Rookle/p/13140339.html
这位师傅对这些内容的了解非常深入,推荐通过他的文章来学习。

从上述代码中可以看出,如果我们可以构造一个unsorted bin:X;构造一个largebin:Y。

那么我们需要满足的情况下,这时候申请一块堆块,就会触发unsorted bin的脱链,让X插入到largebin中,那么如果满足size(X) > size(Y) && index(size(X)) == index(size(Y)),那么X就会插入到Y的前面,从而触发这一串代码进行插入。

victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;

强烈建议可以画一个图理清变量之前的关系,手动模拟一下。
这里我推算出的关系是,victim就是当前要插入的unsorted bin 的chunk,fwd是依次查找到的小于size(victim),这个chunk就是我们构造中的Y。

这里只需要重点关注这一行,如果我们对victim->bk_nextsize进行伪造,那么就可以控制程序写一个堆块地址到目标位置。

victim->bk_nextsize->fd_nextsize = victim;

而下面我们还注意到了,他做了一个类似unsorted bin attack的操作

我们只需要重点关注这一行,如果我们可以控制fwd->bk位置,那么就可以写一个堆块地址到目标位置

bck->fd = victim;

在上面的构造位置中,bck也就是fwd->bk

glibc2.30的检测

if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
    malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");

在glibc2.29中,增加了对双向链表完整性的检查,这样的检查方式正如同glibc2.29中增加的对unsorted bin类似。但是与其不同的是,这个检查只存在于插入的unsorted chunk size大于chunk时候,也就是说,如果我们构造一个小于所有largebin中堆块的unsorted chunk,那么就可以成功利用上面那个分支操作。
代码来源:https://code.woboq.org/userspace/glibc/malloc/malloc.c.html

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

如上代码所示,在插入之前首先会判定一下该chunk的大小是否小于largebin中的最小块,如果小于那么就会走上面的分支,如果大于那么就会走下面的分支,而检测则只存在于下面的分支中。
所以我们绕过这个检测的最好方法就是,不经过这个检测。[感觉这个检测方法是不是有些敷衍啊]

我们通过上面分支的这些代码来达到写出地址的目的。

victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;

以上两行代码,我们可以需要的其实只有一行,我为了便于理解稍加更改

fwd->fd->bk_nextsize->fd_nextsize = victim;

其中,fwd->fd就是largebin chunk,我们只需要修改他的bk_nextsize为目标地址 - 0x20,那么就可以对目标地址进行写入,进而达到利用的目的。
但是由于少了一块地址的写入就不能用于house of storm

利用方向

目前只了解一个house of storm,之后学到新的知识后会回来补充。

总结

可以利用largebin attack干很多和unsorted bin attack类似的事情,而且我们还可以利用largebin attack的fd_nextsize和bk_nextsize来泄露堆地址(例如house of orange)。

总的来说,这种方法还有很多操作没有被开发利用(或者是我还没有了解),相信在之后这个方向的题目会出的越来越多!掌握它也成为了非常必要的事情。

参考链接

[1] house_of_storm 详解 - Rookie - 博客园 https://www.cnblogs.com/Rookle/p/13140339.html

Last Modified: January 27, 2021
Archives QR Code Tip
QR Code for this page
Tipping QR Code