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 libc 浅析 (f5.pm)

[原创]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有了大概的理解

posted @ 2022-04-25 20:52  庄周恋蝶蝶恋花  阅读(594)  评论(0编辑  收藏  举报