Heap Exploitation - _libc_malloc源码分析 堆初始化 堆初始化是在用户第一次申请内存时执行 malloc_consolidate 再执行 malloc_init_state 实现的。
malloc_consolidate函数是定义在malloc.c中的一个函数,用于将fastbin中空闲的chunk合并整理到 unsorted_bin 中以及进行初始化堆的工作。该函数是free的一个小的变体,专门用于处理fastbin中的空闲chunk,并且负责堆管理中的初始化工作。
malloc_state 的初始化操作由函数 malloc_init_state(av) 完成,该函数先初始化除 fastbins 之外的所有的bins,再初始化 fastbins,清空 fastbins。判断当前 malloc_state 结构体中的 fastbin 是否为空,如果为空就说明整个 malloc_state 都没有完成初始化,需要对malloc_state 进行初始化。
源码:
static void malloc_consolidate (mstate av) { mfastbinptr* fb; mfastbinptr* maxfb; mchunkptr p; mchunkptr nextp; mchunkptr unsorted_bin; mchunkptr first_unsorted; mchunkptr nextchunk; INTERNAL_SIZE_T size; INTERNAL_SIZE_T nextsize; INTERNAL_SIZE_T prevsize; int nextinuse; mchunkptr bck; mchunkptr fwd; if (get_max_fast () != 0 ) { clear_fastchunks(av); unsorted_bin = unsorted_chunks(av); maxfb = &fastbin (av, NFASTBINS - 1 ); fb = &fastbin (av, 0 ); do { p = atomic_exchange_acq (fb, NULL ); if (p != 0 ) { do { check_inuse_chunk(av, p); nextp = p->fd; 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); } }
未初始化 首先会通过get_max_fast()判断当前堆是否已经完成初始化,第一次调用malloc申请分配时,该函数返回值为0,此时会进行堆的初始化工作。初始化会调用malloc_init_state()函数。
if (get_max_fast () != 0 ) { ... } else { malloc_init_state(av); check_malloc_state(av); }
关于malloc_init_state函数
static void malloc_init_state (mstate av) { int i; mbinptr bin; for (i = 1 ; i < NBINS; ++i) { bin = bin_at (av, i); bin->fd = bin->bk = bin; } #if MORECORE_CONTIGUOUS if (av != &main_arena) #endif set_noncontiguous (av); if (av == &main_arena) set_max_fast (DEFAULT_MXFAST); av->flags |= FASTCHUNKS_BIT; av->top = initial_top (av); }
在malloc_init_state()函数会进行堆的初始化工作,并且调用set_max_fast()设置global_max_fast为DEFAULT_MAXFAST,
首先会通过循环迭代初始化内存分配中的bins,bin->fd和bin->bk被初始化为指向自己,以建立循环链表。
如果内存管理状态不是主分配区(main_arena
),则调用 set_noncontiguous
函数。这个函数用于设置内存管理状态的 noncontiguous
标志,表示内存分配不是连续的。
如果内存管理状态是主分配区(main_arena
),则调用 set_max_fast
函数,将 DEFAULT_MXFAST
设置为最大的快速分配块大小。这个值用于确定哪些块可以放入 fastbin 中。
设置 FASTCHUNKS_BIT
标志:将 FASTCHUNKS_BIT
标志设置为内存管理状态的 flags
中,以表示 fastbin 可用。
初始化 top
:调用 initial_top
函数,为内存管理状态初始化 top
指针,指向内存池的起始位置。
check_malloc_state()函数,用来检查内存管理器的状态是否合法,并且用于內部调试,用于内部调试和错误检测的函数,用于验证内存管理器的状态是否正确。
av:表示内存管理的状态(memory state)。这个参数是一个指向 malloc_state
结构体的指针,用于跟踪和管理内存分配和释放的状态信息。
DEFAULT_MXFAST 在 32 位系统上为 64,在 64 位系统上为 128。后面再次调用 malloc_consolidate() 的时候, get_max_fast() 返回值都不会等于 0,就不会再次进行初始化。
已初始化 如果get_max_state返回值不为0,那么就是堆已经完成初始化,接下来会将fastbin中的每一个chunk合并整理到unsorted_chunk或者top_chunk。
if (get_max_fast () != 0 ) { clear_fastchunks(av); ... }
因为malloc_consolidate()会清空fastbin,因此先调用clear_fastchunks()清除标志位
接下来就会先遍历fastbinY数组,得到每一个固定尺寸的fastbin单链表,然后再遍历fastbin单链表得到相同大小的空闲chunk。
maxfb = &fastbin (av, NFASTBINS - 1 ); fb = &fastbin (av, 0 ); do { p = atomic_exchange_acq (fb, NULL ); if (p != 0 ) { do { check_inuse_chunk(av, p); nextp = p->fd; ... } while ( (p = nextp) != 0 ); } } while (fb++ != maxfb);
对于每一个chunk会先尝试向后合并,合并操作就是更新p的size和指向,然后调用unlink宏将其从bin中脱离。
然后会尝试向前合并,如果前一个相邻top_chunk,那么就直接合并到top_chunk后,不管unsorted_bin;如果前一个不相邻top_chunk,则向前合并后插入到unsorted_bin。
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; }
申请内存 _libc_malloc malloc函数一般被用来申请内存块,该函数调用了_libc_malloc函数,该函数核心为int_malloc函数。
该函数会先检查是否有内存分配函数的hook函数(_malloc_hook),如果存在,它会调用该钩子函数来执行内存分配操作。如果没有用户定义的钩子函数,它会继续执行内存分配的默认逻辑。
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 ));
然后会寻找一个arena来试图分配内存,再调用_int_malloc函数去申请内存
arena_get(ar_ptr, bytes); victim = _int_malloc(ar_ptr, bytes);
分配失败就会去尝试寻找一个可以用的arena,并分配内存
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 ) __libc_lock_unlock(ar_ptr->mutex);
然后会判断目前的状态是否满足以下条件:
assert(!victim || chunk_is_mmapped(mem2chunk(victim)) || ar_ptr == arena_for_chunk(mem2chunk(victim)));
没有申请到内存
是mmap的内存
申请到的内存必须在其分配的arena中
_int_malloc 上面说过,_int_malloc是内存分配的和核心函数,大致流程为:
会根据用户申请的内存块大小和相应大小的chunk使用频度来实现不同的分配方法
由小到大依次检查各种bin中是否有相应的内存块能够满足请求的内存
如果所有的空闲chunk都无法满足就会从top_chunk去找
如果top_chunk也无法满足,堆分配器就会进行内存申请
_int_malloc函数太长了,这里就不放全部的源码了
函数首先定义了一些变量,并且将用户申请的内存大小转换为内部的chunk大小
static void *_int_malloc (mstate av, size_t bytes) { INTERNAL_SIZE_T nb; unsigned int idx; mbinptr bin; mchunkptr victim; INTERNAL_SIZE_T size; int victim_index; mchunkptr remainder; unsigned long remainder_size; unsigned int block; unsigned int bit; unsigned int map ; mchunkptr fwd; mchunkptr bck; const char *errstr = NULL ; checked_request2size (bytes, nb);
首先调用checked_request2size
将需要分配的内存大小bytes转换为chunk的大小。checked_request2size
是个宏定义,主要调用request2size进行计算
#define request2size(req) \ (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \ MINSIZE : \ ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)
在学习结构的时候有这样一个地方,当一个chunk为空闲时,至少要有prev_size、size、fd和bk四个参数,因此MINISIZE就代表了这四个参数需要占用的内存大小,当一个chunk被使用时,prev_size可能会被前一个chunk用来存储,fd、bk会被用来存储数据,所以只剩下了size参数需要设置,request2size中的SIZE_SZ就是INTERNAL_SIZE_T
类型的大小,因此至少需要req+SIZE_SZ
的内存大小。MALLOC_ALIGN_MASK
用来对齐,因此request2size就计算出了所需的chunk的大小。
参数av就是分配区指针,为null表示没有分配区可用,这时会调用sysmalloc通过mmap获取chunk。
arena if (__glibc_unlikely (av == NULL )) { void *p = sysmalloc (nb, av); if (p != NULL ) alloc_perturb (p, bytes); return p; }
这段代码会先检查是否存在可用的分配区(arena),如果没有,就使用malloc函数通过mmap系统调用来获取一块内存块,获取内存块成功后,还会对这块内存块进行填充(perturb),以提高内存分配的安全性。
fast bin if ((unsigned long )(nb) <= (unsigned long )(get_max_fast())){ idx = fastbin_index(nb); mfastbinptr *fb = &fastbin(av, idx); mchunkptr pp = *fb; do { victim = pp; if (victim == NULL ) break ; } while ((pp = catomic_compare_and_exchange_val_acq(fb, victim->fd, victim)) != victim); if (victim != 0 ) { 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 ; } check_remalloced_chunk(av, victim, nb); void *p = chunk2mem(victim); alloc_perturb(p, bytes); return p; } }
这段代码主要用于快速分配内存块,首先检查请求的内存大小是否符合快速分配的条件,然后在快速分配区块中查找可用的内存块。如果找到符合要求的内存块,就返回该内存块的指针,否则返回NULL。同时,如果发现内存块的大小与索引不匹配,会报告内存损坏错误。从fastbin的头结点开始取chunk。
small bin if (in_smallbin_range (nb)){ idx = smallbin_index (nb); bin = bin_at (av, idx); if ((victim = last (bin)) != bin) { if (victim == 0 ) 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; } } }
用于处理小内存块的分配请求。它首先检查请求的内存大小是否在小内存块的分配范围内,然后在相应的小内存块分配区(smallbins)中查找合适的内存块。如果找到一个合适的内存块,就将其标记为已分配并从分配区中移除,然后返回该内存块的指针。如果分配区尚未初始化,会进行内存整理。同时,还会检查链表完整性,以确保内部数据结构的正确性。最后,分配成功后,还会对内存块进行检查和填充,以提高安全性。
large bin 当fast bin和large bin中的chunk都不能满足用户申请的chunk大小时,就会从large bin中尝试寻找。但是在这里并没有直接扫描对应bin中的chunk,调用了malloc_consolidata函数处理fast bin中的chunk,如果能合并就合并后放到unsorted bin中,不能合并就直接放到unsorted bin中。这里在还不知道是否有可用内存就执行合并操作,以便减少内存碎片。实际上,合并操作在大多数程序并不频繁,需要频繁调用合并操作的程序通常容易出现内存问题。
else { idx = largebin_index (nb); if (have_fastchunks (av)) malloc_consolidate (av); }
大循环 如果程序执行到了这里,说明与chunk大小一致的bin没有符合要求的,就会进入下面的大循环。
在大循环中,主要完成了以下操作:
按照FIFO的方式将unsored bin取出,如果是small request并且满足,满足就返回,不是的话就返回bin
从large bin分配所需内存
虽然会首先使用large bin、top chunk来满足请求,但是在上面由于没有分配small bin,并没有对fast bin中的chunk进行合并,所以在这儿会进行fast bin chunk的合并,并使用一个大循环来尝试再次分配small bin chunk。
遍历unsorted bin 先遍历unsorted bin,遍历顺序为bk
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)) { bck = victim->bk; if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0 ) || __builtin_expect (victim->size > av->system_mem, 0 )) malloc_printerr (check_action, "malloc(): memory corruption" , chunk2mem (victim), av); size = chunksize (victim);
如果用户的请求是small bin chunk,那么首先考虑last remainder,如果unsorted bin中唯一
if (in_smallbin_range (nb) && bck == unsorted_chunks (av) && victim == av->last_remainder && (unsigned long ) (size) > (unsigned long ) (nb + MINSIZE)) { remainder_size = size - nb; remainder = chunk_at_offset (victim, nb); unsorted_chunks (av)->bk = unsorted_chunks (av)->fd = remainder; av->last_remainder = remainder; remainder->bk = remainder->fd = unsorted_chunks (av); 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; } unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);c
如果取出的大小正好合适,就直接使用
if (size == nb) { set_inuse_bit_at_offset (victim, size); 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; }
判断大小,取出来的chunk放到small bin和large 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; if (fwd != bck) { size |= PREV_INUSE; 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) 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; }
取出,并设定最多循环1000次
mark_bin (av, victim_index); victim->bk = bck; victim->fd = fwd; fwd->bk = victim; bck->fd = victim; #define MAX_ITERS 10000 if (++iters >= MAX_ITERS) break ; }
large chunk 如果申请的chunk在large chunk范围内,就在对应的bin中从小到大扫描,找到合适的
if (!in_smallbin_range(nb)) { bin = bin_at(av, idx); if ((victim = first(bin)) != bin && (unsigned long )(victim->size) >= (unsigned long )(nb)) { victim = victim->bk_nextsize; while ((unsigned long )(size = chunksize(victim)) < (unsigned long )(nb)) victim = victim->bk_nextsize; if (victim != last(bin) && victim->size == victim->fd->size) victim = victim->fd; remainder_size = size - nb; unlink(av, victim, bck, fwd); if (remainder_size < MINSIZE) { set_inuse_bit_at_offset(victim, size); if (av != &main_arena) victim->size |= NON_MAIN_ARENA; } else { remainder = chunk_at_offset(victim, nb); 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; } }
执行到这里,说明没有从对应的合适的bin中获取合适的chunk,所以需要查找比当前bin更大的fast bin、small bin或者是large bin。
++idx; bin = bin_at(av, idx); block = idx2block(idx); map = av->binmap[ block ]; bit = idx2bit(idx); for (;;) {
找到合适的map
if (bit > map || bit == 0 ) { do { if (++block >= BINMAPSIZE) goto use_top; } while ((map = av->binmap[ block ]) == 0 ); bin = bin_at(av, (block << BINMAPSHIFT)); bit = 1 ; }
找到合适的bin
while ((bit & map ) == 0 ) { bin = next_bin(bin); bit <<= 1 ; assert(bit != 0 ); }
检查chunk
victim = last(bin); if (victim == bin) { av->binmap[ block ] = map &= ~bit; bin = next_bin(bin); bit <<= 1 ; }
取出chunk
else { size = chunksize(victim); assert((unsigned long ) (size) >= (unsigned long ) (nb)); remainder_size = size - nb; unlink(av, victim, bck, fwd); if (remainder_size < MINSIZE) { set_inuse_bit_at_offset(victim, size); if (av != &main_arena) set_non_main_arena(victim); } else { remainder = chunk_at_offset(victim, nb); bck = unsorted_chunks(av); fwd = bck->fd; if (__glibc_unlikely(fwd->bk != bck)) { errstr = "malloc(): corrupted unsorted chunks 2" ; goto errout; } remainder->bk = bck; remainder->fd = fwd; bck->fd = remainder; fwd->bk = remainder; if (in_smallbin_range(nb)) av->last_remainder = 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; }
使用top chunk use_top: 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; } else if (have_fastchunks(av)) { malloc_consolidate(av); if (in_smallbin_range(nb)) idx = smallbin_index(nb); else idx = largebin_index(nb); }
堆内存不够 else { void *p = sysmalloc(nb, av); if (p != NULL ) alloc_perturb(p, bytes); return p; }
_libc_calloc calloc 也是 libc 中的一种申请内存块的函数。在 libc
中的封装为 _libc_calloc
void * __libc_calloc(size_t , size_t );
未完待续。。。
参考