musl-1.22部分源码分析
群里的师傅都在卷musl了,比我强还比我努力!
musl相对于libc体积更小,听说为了嵌入式,musl在ctf出现的概率正在增加,但是其结构可以说完全跟libc不同,尤其是1.22版本,特以此篇,记录我的源码分析理解
malloc
void *malloc(size_t n) { if (size_overflows(n)) return 0;//先判断是否溢出,溢出就返回 struct meta *g; uint32_t mask, first; int sc; int idx; int ctr; if (n >= MMAP_THRESHOLD) {//如果申请的内存太大,就直接通过mmap去申请 size_t needed = n + IB + UNIT; void *p = mmap(0, needed, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANON, -1, 0); if (p==MAP_FAILED) return 0; wrlock(); step_seq(); g = alloc_meta(); //获取一个meta if (!g) { unlock(); munmap(p, needed); return 0; }//将内存信息保存在如下的结构里面 g->mem = p; g->mem->meta = g; g->last_idx = 0; g->freeable = 1; g->sizeclass = 63; g->maplen = (needed+4095)/4096;//映射内存长度 g->avail_mask = g->freed_mask = 0; // use a global counter to cycle offset in // individually-mmapped allocations. ctx.mmap_counter++; idx = 0; goto success; } sc = size_to_class(n);//计算size的类型 rdlock();//对malloc上死锁 g = ctx.active[sc]; //根据size的类别去寻找对应的meta // use coarse size classes initially when there are not yet // any groups of desired size. this allows counts of 2 or 3 // to be allocated at first rather than having to start with // 7 or 5, the min counts for even size classes. if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {//对应meta为空 AND 4<=sc<32 AND sc!=6 AND sc是偶数 AND 这个sc没使用过内存 size_t usage = ctx.usage_by_class[sc|1]; // if a new group may be allocated, count it toward // usage in deciding if we can use coarse class. if (!ctx.active[sc|1] || (!ctx.active[sc|1]->avail_mask //下面大概意思就是如果这个sc是空的, 那么就是使用更大的sc中的meta && !ctx.active[sc|1]->freed_mask)) usage += 3; if (usage <= 12) sc |= 1; g = ctx.active[sc]; } for (;;) { //在此meta中寻找一个chunk mask = g ? g->avail_mask : 0;//meta中的可用内存的bitmap, 如果g为0那么就设为0, 表示没有可用chunk first = mask&-mask;//一个小技巧, 作用是找到mask的bit中第一个为1的bit if (!first) break; //如果没找到就停止 if (RDLOCK_IS_EXCLUSIVE || !MT) //设置avail_mask中first对应的bit为0//如果是排它锁, 那么下面保证成功 g->avail_mask = mask-first; else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) //如果是cas原子操作则需要for(;;)来自旋 continue; idx = a_ctz_32(first); //成功找到并设置avail_mask之后转为idx, 结束 goto success; } upgradelock(); /* - 如果这个group没法满足, 那就尝试从别的地方获取: - 使用group中被free的chunk - 使用队列中别的group - 分配一个group */ idx = alloc_slot(sc, n); if (idx < 0) { unlock(); return 0; } g = ctx.active[sc];//然后找到对应meta success: ctr = ctx.mmap_counter; unlock(); return enframe(g, idx, n, ctr); //从g中分配第idx个chunk, 大小为n }
如果找不到内存可用情况下就会使用alloc_meta去申请
struct meta *alloc_meta(void)//分配一个meta对象, 有可能是用的空闲的meta, 也可能是新分配一页划分的 { struct meta *m; unsigned char *p; if (!ctx.init_done) { //如果还没初始化, 就设置secret #ifndef PAGESIZE ctx.pagesize = get_page_size(); #endif ctx.secret = get_random_secret();//设置secret为随机数 ctx.init_done = 1; } size_t pagesize = PGSZ; //设置pagesize if (pagesize < 4096) pagesize = 4096; if ((m = dequeue_head(&ctx.free_meta_head))) return m; //如果能从空闲meta队列free_meta_head中得到一个meta, 就可直接返回 if (!ctx.avail_meta_count) {//如果没有空闲的, 并且ctx中也没有可用的, 就通过mmap映射一页作为meta数组 int need_unprotect = 1; if (!ctx.avail_meta_area_count && ctx.brk!=-1) { //如果ctx中没有可用的meta, 并且brk不为-1 uintptr_t new = ctx.brk + pagesize;//新分配一页 int need_guard = 0; if (!ctx.brk) {//如果cnt中brk为0 need_guard = 1; ctx.brk = brk(0);//那就调用brk()获取当前的heap地址 // some ancient kernels returned _ebss // instead of next page as initial brk. ctx.brk += -ctx.brk & (pagesize-1); //设置ctx.brk与new new = ctx.brk + 2*pagesize; } if (brk(new) != new) {//brk()分配heap到new地址失败 ctx.brk = -1; } else {//如果brk()分批额成功 if (need_guard)//保护页, 在brk后面映射一个不可用的页(PROT_NONE), 如果堆溢出到这里就会发送SIGV mmap((void *)ctx.brk, pagesize, PROT_NONE, MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0); ctx.brk = new; ctx.avail_meta_areas = (void *)(new - pagesize); //把这一页全划分为meta ctx.avail_meta_area_count = pagesize>>12; need_unprotect = 0; } } if (!ctx.avail_meta_area_count) {//如果前面brk()分配失败了, 直接mmap匿名映射一片PROT_NONE的内存再划分 size_t n = 2UL << ctx.meta_alloc_shift; p = mmap(0, n*pagesize, PROT_NONE, MAP_PRIVATE|MAP_ANON, -1, 0); if (p==MAP_FAILED) return 0; ctx.avail_meta_areas = p + pagesize; ctx.avail_meta_area_count = (n-1)*(pagesize>>12); ctx.meta_alloc_shift++; } p = ctx.avail_meta_areas; //如果avail_meta_areas与4K对齐, 那么就说明这片区域是刚刚申请的一页, 所以需要修改内存的权限 if ((uintptr_t)p & (pagesize-1)) need_unprotect = 0; if (need_unprotect) if (mprotect(p, pagesize, PROT_READ|PROT_WRITE) && errno != ENOSYS) return 0; ctx.avail_meta_area_count--; ctx.avail_meta_areas = p + 4096; if (ctx.meta_area_tail) { ctx.meta_area_tail->next = (void *)p; } else { ctx.meta_area_head = (void *)p; } //ctx中记录下相关信息 ctx.meta_area_tail = (void *)p; ctx.meta_area_tail->check = ctx.secret; ctx.avail_meta_count = ctx.meta_area_tail->nslots = (4096-sizeof(struct meta_area))/sizeof *m; ctx.avail_meta = ctx.meta_area_tail->slots; } //ctx的可用meta数组中有能用的, 就直接分配一个出来 ctx.avail_meta_count--; m = ctx.avail_meta++;//取出一个meta m->prev = m->next = 0; //这俩指针初始化为0 return m; }
musl的结构如下
其中meta,group结构的定义分别如下
#define UNIT 16 #define IB 4 struct group { //以下是group中第一个slot的头0x10B struct meta *meta; //0x80B指针 unsigned char active_idx : 5; //5bit idx char pad[UNIT - sizeof(struct meta *) - 1]; //padding为0x10B //以下为第一个chunk的用户数据区域+剩余所有chunk unsigned char storage[]; //chunk };
struct meta { struct meta *prev, *next; //双向链表 struct group *mem; //管理的内存 volatile int avail_mask, freed_mask; uintptr_t last_idx : 5; uintptr_t freeable : 1; uintptr_t sizeclass : 6; uintptr_t maplen : 8 * sizeof(uintptr_t) - 12; };
struct malloc_context {
uint64_t secret;
/
/
和meta_area 头的check 是同一个值 就是校验值
#ifndef PAGESIZE
size_t pagesize;
#endif
int
init_done;
/
/
是否初始化标记
unsigned mmap_counter;
/
/
记录有多少mmap 的内存的数量
struct meta
*
free_meta_head;
/
/
被free 的meta 头 这里meta 管理使用了队列和双向循环链表
struct meta
*
avail_meta;
/
/
指向可用meta数组
size_t avail_meta_count, avail_meta_area_count, meta_alloc_shift;
struct meta_area
*
meta_area_head,
*
meta_area_tail;
unsigned char
*
avail_meta_areas;
struct meta
*
active[
48
];
/
/
记录着可用的meta
size_t u sage_by_class[
48
];
uint8_t unmap_seq[
32
], bounces[
32
];
uint8_t seq;
uintptr_t brk;
};
通过图像我们可以明显的看出meta管理group,而group管理着chunk
free
void free(void *p) { if (!p) return; struct meta *g = get_meta(p);//获取chunk所属的meta int idx = get_slot_index(p); //这是group中第几个chunk size_t stride = get_stride(g);//这个group负责的大小 unsigned char *start = g->mem->storage + stride*idx; unsigned char *end = start + stride - IB; get_nominal_size(p, end);//这个group负责的大小 uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1; //计算这个chunk的bitmap ((unsigned char *)p)[-3] = 255; //idx与offset都无效 // invalidate offset to group header, and cycle offset of // used region within slot if current offset is zero. //释放slot中的一整页 *(uint16_t *)((char *)p-2) = 0; // release any whole pages contained in the slot to be freed // unless it's a single-slot group that will be unmapped. if (((uintptr_t)(start-1) ^ (uintptr_t)end) >= 2*PGSZ && g->last_idx) { unsigned char *base = start + (-(uintptr_t)start & (PGSZ-1)); size_t len = (end-base) & -PGSZ; if (len) madvise(base, len, MADV_FREE); } // atomic free without locking if this is neither first or last slot //在meta->freed_mask中标记一下, 表示这个chunk已经被释放了 //如果既不是中间的slot也不是末尾的slot, 那么释放时不需要锁 for (;;) { uint32_t freed = g->freed_mask; uint32_t avail = g->avail_mask; uint32_t mask = freed | avail;//mask = 所有被释放的chunk + 现在可用的chunk assert(!(mask&self)); //要释放的chunk应该既不在freed中, 也不在avail中 /* - 两种不能只设置meta的mask的情况, 这两种情况不设置mask, break后调用nontrivial_free()处理 - 如果!freed, 就说明meta中没有被释放的chunk, 有可能这个group全部被分配出去了, 这样group是会弹出avtive队列的, 而现在释放了一个其中的chunk, 需要条用nontrivial_free()把这个group重新加入队列 - 如果mask+self==all, 那就说明释放了这个chunk, 那么这个group中所有的chunk都被回收了, 因此这个meta需要调用nontrivial_free()回收这个group */ if (!freed || mask+self==all) break;//设置freed_mask, 表示这个chunk被释放了 if (!MT)//如果是单线程,直接写就好了 g->freed_mask = freed+self; else if (a_cas(&g->freed_mask, freed, freed+self)!=freed) //如遇多线程使用原子操作, 一直循环到g->freed_mask为freed+self为止 continue; return; } wrlock(); struct mapinfo mi = nontrivial_free(g, idx);//处理涉及到meta之间的操作 /*nontrivial_free() 根据free()进入这个函数的方式可以知道, 此时还没有设置freed_mask 如果发现这个group中所有的chunk要么被free, 要么是可用的, 那么就会回收掉这个group 先调用dequeue从队列中出队 如果队里中后面还有meta的话, 就会激活后一个meta 然后调用free_group()释放整个group 如果发现mask为空 那么说明malloc分配出最后一个chunk的时候已经把这个meta给弹出队列了 但是现在里面有一个chunk被释放了, 这个meta就应该再次回归队列, 因此调用queue()再次入队*/ unlock(); if (mi.len) munmap(mi.base, mi.len); }
nontrivial_free
static struct mapinfo nontrivial_free(struct meta *g, int i) { uint32_t self = 1u<<i; int sc = g->sizeclass; uint32_t mask = g->freed_mask | g->avail_mask; if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) {//如果group中所有chunk要么被释放要么可使用, 并且g可以被释放, 那么就要回收掉整个meta // any multi-slot group is necessarily on an active list // here, but single-slot groups might or might not be. if (g->next) {//如果g有下一个 assert(sc < 48);//检查: sc合法, 不是mmap的 int activate_new = (ctx.active[sc]==g);//如果g是队列中开头的meta, 那么弹出队列后, 要激活后一个 dequeue(&ctx.active[sc], g);//这个meta丢出队列 if (activate_new && ctx.active[sc]) //如果队列存在后一个meta, 那么就激活他, 因为之前为了free的快速, 只是用freed_mask标记了一下而已, 现在要转移到avail_mask中了 activate_group(ctx.active[sc]); } return free_group(g); //meta已经取出, 现在要释放这个meta } else if (!mask) { //如果mask==0, 也就是这个group中所有的chunk都被分配出去了 assert(sc < 48); //那么这个meta在malloc()=>alloc_slot()=>try_avail()最终就被弹出队列了, 目的取出队列中不可能再被分配的, 提高效率 //现在这个全部chunk被分配出去的group中有一个chunk被释放了, 因此这个meta要重新入队 // might still be active if there were no allocations // after last available slot was taken. if (ctx.active[sc] != g) { queue(&ctx.active[sc], g); //重新入队 } } a_or(&g->freed_mask, self); return (struct mapinfo){ 0 };
部分关键源码分析如上,非常感谢师傅对我的帮助
musl-1.2.x堆部分源码分析 - 安全客,安全资讯平台 (anquanke.com)
[原创]musl 1.2.2 总结+源码分析 One-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com
通过源代码,我们可以发现开发者讲mate和group进行分离,同时在meta所在的页前后各放置了PROT_NONE,来防止group上面的堆溢出影响到meta,所以作者就认为meta不可写,就安全,但是并没有对meta的双向链表进行检查,在师傅的博客上,有一个攻击思路
- 我们可以溢出一个chunk, 伪造他的offset与next, 使其指向我们伪造的group,
- 然后伪造group中的meta指针, 使其指向我们伪造的meta
- 然后伪造meta中的prev next指针, 并且伪造freed_mask与avail_mask, 做出一副这个meta中的chunk已经全部被释放了的样子, 这样就会调用: free()=>nontrivial_free()=>dequeue()完成攻击
感谢师傅们的博客对我的帮助,让我大体上对musl有了大概的理解