写在前面

大部分的分析都写在了注释里,参考了一些师傅的博客和CTFwiki

概述

malloc_fastbin:

  • malloc函数执行时,如果size在fastbin范围内,且fastbin内有合适的chunk,直接返回,否则去unsorted

malloc_small_bin:

  • malloc函数执行时,如果size在smallbin范围内,且smallbin内有合适的chunk,直接返回,否则去unsorted
  • size在largebin,consolidate合并,然后去unsorted

malloc_unsorted_bin:

  • size在small_bin范围内,且存在chunk时last_remainder且size大于nb,直接切割然后返回,如果不是last_remainder,如果unsorted_bin里的chunk在small范围内,先放入small_bin,然后再切割取出,剩余的部分作为last_remainder放回unsorted。如果size再large范围内,先放入large,然后再切割取出(合适直接取出),剩余的部分作为last_remainder放回unsorted
1
2
3
4
5
6
7
8
1. 查找unsortedbin 是否为空
2. unsortedbin 中只有一个chunk且为last_remainder时,进行last_remainder切分,返回
3.否则:
4. 将chunk从 unsortedbin中解链,如果大小满足则返回
5. 如果chunk大小不满足, 是否满足small bin大小,并插入small bin中
6. 如果chunk大小不满足small bin,是否满足larg bin,并插入 large bin中
7. 对下一个chunk 进行处理
8.当unsorted bin中没有合适的chunk时,则会遍历查找 请求的size 所对应的 bin中chunk

malloc_largebin:

  • 如果用户申请的size所对应的large bin中不存在符合要求的chunk的情况,则在其他 size 的large bin中进行寻找。
  • 如果符合用户请求的size 的bins 中没有满足要求的 chunk,则会继续在 large bin中寻找。不断遍历,直到找到符合要求的chunk,随后进行 chunk 划分等操作。
  • 如果 large bin 中遍历结束,仍然没有找到相应的 chunk,则需要在 top chunk中寻找。
  • 最后就是在top chunk进行分配,如果top chunk满足,则划分top chunk。如果不满足,首先会进行合并fastbin到large bin或者 small bin中,在继续进行遍历。如果没有fastbin可合并,程序会直接调用 sysmalloc 函数进行分配区分配。

sysmalloc:

1
2
3
4
1.old-top chunk size 必须要对齐到内存页
2.size 要大于 MINSIZE(0x10)
3.size 要小于之后申请的 chunk size + MINSIZE(0x10)
4.size 的 prev inuse 位必须为 1

一些自己容易忘记的小知识

  • mmap出来的堆空间和libc的基地址的偏移是固定的,所以可以通过这种手段泄露libc基地址

  • 堆空间大概长这样:

    Snipaste_2022-06-24_13-02-15.png

  • 还有就是关于bins数组,fastbin是单独的,其他的0—1是unsorted_bin,2-63是small_bin,64-128是largebin

malloc

__libc_malloc

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
void *
__libc_malloc (size_t bytes)
{
mstate ar_ptr;
void *victim;

void *(*hook) (size_t, const void *)
= atomic_forced_read (__malloc_hook);
if (__builtin_expect (hook != NULL, 0))
return (*hook)(bytes, RETURN_ADDRESS (0));//检查是否有hook,如果有的话,就去执行hook

arena_get (ar_ptr, bytes); //寻找arena分配内存

victim = _int_malloc (ar_ptr, bytes);//调用malloc来申请内存
/* Retry with another arena only if we were able to find a usable arena
before. */
if (!victim && ar_ptr != NULL)
{
LIBC_PROBE (memory_malloc_retry, 1, bytes);
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}//如果失败就再次寻找

if (ar_ptr != NULL)
(void) mutex_unlock (&ar_ptr->mutex);//申请到了arena,释放分配区锁

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
ar_ptr == arena_for_chunk (mem2chunk (victim)));//再次进行判断,是否申请到了内存,内存是否mmap,内存是否在其分配耳朵arena中
return victim; //返回堆地址
}

来简单看一下malloc的过程:

开始->获取分配区锁->随后一次遍历fastbin(小于max_fast),smallbin(小于512B),unsortedbin,largebin,top_chunk.如果大于mmap阈值,则使用mmap调用,如果不是main_arena,也采用mmap。大概就是这样了。

_int_malloc

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
static void *_int_malloc(mstate av, size_t bytes) {
INTERNAL_SIZE_T nb; /* normalized request size */
unsigned int idx; /* associated bin index */
mbinptr bin; /* associated bin */

mchunkptr victim; /* inspected/selected chunk */
INTERNAL_SIZE_T size; /* its size */
int victim_index; /* its bin index */

mchunkptr remainder; /* remainder from a split */
unsigned long remainder_size; /* its size */

unsigned int block; /* bit map traverser */
unsigned int bit; /* bit map traverser */
unsigned int map; /* current word of binmap */

mchunkptr fwd; /* misc temp for linking */
mchunkptr bck; /* misc temp for linking */
//定义了一些后续用的到的变量
const char *errstr = NULL;

/*
Convert request size to internal form by adding SIZE_SZ bytes
overhead plus possibly more to obtain necessary alignment and/or
to obtain a size of at least MINSIZE, the smallest allocatable
size. Also, checked_request2size traps (returning 0) request sizes
that are so large that they wrap around zero when padded and
aligned.
*/

checked_request2size(bytes, nb);

_int_malloc fastbin部分

这里唯一的一个检查就是malloc的size大小与fastbin索引的大小是否一致,fastbin使用bin的fd来获取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
/*
If the size qualifies as a fastbin, first check corresponding bin.
This code is safe to execute even if av is not yet initialized, so we
can try it without checking, which saves some time on this fast path.
*/

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast())) {
// 得到对应的fastbin的下标
idx = fastbin_index(nb);
// 得到对应的fastbin的头指针
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp = *fb;
// 利用fd遍历对应的bin内是否有空闲的chunk块,
do {
victim = pp;
if (victim == NULL) break;
} while ((pp = catomic_compare_and_exchange_val_acq(fb, victim->fd,
victim)) != victim);
// 存在可以利用的chunk
if (victim != 0) {
// 检查取到的 chunk 大小是否与相应的 fastbin 索引一致。
// 根据取得的 victim ,利用 chunksize 计算其大小。
// 利用fastbin_index 计算 chunk 的索引。
if (__builtin_expect(fastbin_index(chunksize(victim)) != idx, 0)) {
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr(check_action, errstr, chunk2mem(victim), av);
return NULL;
}
// 细致的检查。。只有在 DEBUG 的时候有用
check_remalloced_chunk(av, victim, nb);
// 将获取的到chunk转换为mem模式
void *p = chunk2mem(victim);
// 如果设置了perturb_type, 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}

_int_malloc smallbin部分

这里的检查也只有一个,检查bck ->fd = victim

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
/*
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)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset(victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena) set_non_main_arena(victim);
// 细致的检查,非调试状态没有作用
check_malloced_chunk(av, victim, nb);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}

_int_malloc largebin部分

当 fast bin、small bin 中的 chunk 都不能满足用户请求 chunk 大小时,就会考虑是不是 large bin。但是,其实在 large bin 中并没有直接去扫描对应 bin 中的 chunk,而是先利用 malloc_consolidate(参见 malloc_state 相关函数) 函数处理 fast bin 中的 chunk,将有可能能够合并的 chunk 先进行合并后放到 unsorted bin 中,不能够合并的就直接放到 unsorted bin 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
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 {
// 获取large bin的下标。
idx = largebin_index(nb);
// 如果存在fastbin的话,会处理 fastbin
if (have_fastchunks(av)) malloc_consolidate(av);
}

后续部分代码比较多,就简单描述一下吧,大致意思就是合并后的chunk,首先放到unsorted bin中,然后便历unsorted_bin,如果有合适的就取出,如果chunk过大则进行切割,剩下的根据大小放入small bin或者 large_bin中去。

如果合并后依然没有足够的空闲空间,那么就会切割top_chunk,依然不够的话就mmap。

free

__libc_free

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
void __libc_free(void *mem) {
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */
// 判断是否有钩子函数 __free_hook
void (*hook)(void *, const void *) = atomic_forced_read(__free_hook);
if (__builtin_expect(hook != NULL, 0)) {
(*hook)(mem, RETURN_ADDRESS(0));
return;
}
// free NULL没有作用
if (mem == 0) /* free(0) has no effect */
return;
// 将mem转换为chunk状态
p = mem2chunk(mem);
// 如果该块内存是mmap得到的
if (chunk_is_mmapped(p)) /* release mmapped memory. */
{
/* See if the dynamic brk/mmap threshold needs adjusting.
Dumped fake mmapped chunks do not affect the threshold. */
if (!mp_.no_dyn_threshold && chunksize_nomask(p) > mp_.mmap_threshold &&
chunksize_nomask(p) <= DEFAULT_MMAP_THRESHOLD_MAX &&
!DUMPED_MAIN_ARENA_CHUNK(p)) {
mp_.mmap_threshold = chunksize(p);
mp_.trim_threshold = 2 * mp_.mmap_threshold;
LIBC_PROBE(memory_mallopt_free_dyn_thresholds, 2,
mp_.mmap_threshold, mp_.trim_threshold);
}
munmap_chunk(p);
return;
}
// 根据chunk获得分配区的指针
ar_ptr = arena_for_chunk(p);
// 执行释放
_int_free(ar_ptr, p, 0);
}

__int_free

三个检查一个是chunk的size是否对齐,一个是指针地址是否合法,还有就是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
static void _int_free(mstate av, mchunkptr p, int have_lock) {
INTERNAL_SIZE_T size; /* its size */
mfastbinptr * fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */

const char *errstr = NULL;
int locked = 0;

size = chunksize(p);
//定义一些用的的变量


/* Little security check which won't hurt performance: the
allocator never wrapps around at the end of the address space.
Therefore we can exclude some size values which might appear
here by accident or by "design" from some intruder. */
// 指针不能指向非法的地址
// 指针必须得对齐,2*SIZE_SZ
if (__builtin_expect((uintptr_t) p > (uintptr_t) -size, 0) ||
__builtin_expect(misaligned_chunk(p), 0)) {
errstr = "free(): invalid pointer";
errout:
if (!have_lock && locked) __libc_lock_unlock(av->mutex);
malloc_printerr(check_action, errstr, chunk2mem(p), av);
return;
}
/* We know that each chunk is at least MINSIZE bytes in size or a
multiple of MALLOC_ALIGNMENT. */
// 大小没有最小的chunk大,或者说,大小不是MALLOC_ALIGNMENT的整数倍
if (__glibc_unlikely(size < MINSIZE || !aligned_OK(size))) {
errstr = "free(): invalid size";
goto errout;
}
// 检查该chunk是否处于使用状态,非调试状态下没有作用
check_inuse_chunk(av, p);

__int_free fastbin部分

这部分主要进行了以下检查:

  • double free的检查
  • size大小的检查,如果fastbinY的大小是0x70,此时size 0x70-0x7f(64bit)都是合法的
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_free (mstate av, mchunkptr p, int have_lock)
{
size = chunksize (p); //获取p的size
check_inuse_chunk(av, p);//检查p的物理相邻的下一个堆块的inuse位是否置1

//检查p的大小是否小于global_max_fast
if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())
#if TRIM_FASTBINS
//检查p物理相邻的堆块是否是top chunk
&& (chunk_at_offset(p, size) != av->top)
#endif
)
{
//检查p的物理相邻下个堆块是否存在,且大小是否满足最小和最大要求
if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize (chunk_at_offset (p, size))
>= av->system_mem, 0))
{.......}

//对chunk的data块通过memset赋值,但是默认情况下是不进行操作
free_perturb (chunk2mem(p), size - 2 * SIZE_SZ);
//设置 malloc_state的flag
set_fastchunks(av);

//获取p对应大小的fastbinY的索引
unsigned int idx = fastbin_index(size);
//fb指向对应大小的fastbinY的地址
fb = &fastbin (av, idx);

/* Atomically link P to its fastbin: P->FD = *FB; *FB = P; */
// old为 对应大小的fastbinY的fd值,也就是第一个对块的地址
mchunkptr old = *fb, old2;
unsigned int old_idx = ~0u;

do
{
// Check that the top of the bin is not the record we are going to add
//检查 fastbin中对应的bin的第一项 是否 等于 p (新加入的堆块)
if (__builtin_expect (old == p, 0))
{
errstr = "double free or corruption (fasttop)";
goto errout;
}
//获取 fastbin中对应的bin的第一项的索引。
if (have_lock && old != NULL)
old_idx = fastbin_index(chunksize(old));
//让 p 的fd指向 顶部的fastbin块
p->fd = old2 = old;
}
while ((old = catomic_compare_and_exchange_val_rel (fb, p, old2)) != old2);
//catomic_compare_and_exchange_val_rel 功能是 如果*fb等于old2,则将*fb存储为p,返回old2;
// *fb=p 也就是 让对应fastbin的fd指向 p(新加入的堆块)

//检查fastbin中对应的bin的第一项的大小是否与p(要添加的块)的大小相同。
if (have_lock && old != NULL && __builtin_expect (old_idx != idx, 0))
{
errstr = "invalid fastbin entry (free)";
goto errout;
}
}
}

如果size大于max_fast,其余部分size的chunk会先放入unsorted_bin,等下次遍历unsorted_bin时,再放入相应大小的bin中。