启动musl

提示: musl 的库函数就只有一个libc.so ,这个libc.so 既充当ld,又充当libc

指定 libc.so 启动一个musl 程序

替换默认环境下的libc.so

  • 把下载的musl的默认libc.so 换为指定的libc.so, 默认路径:/usr/lib/x86_64-linux-musl/libc.so ,这里可以先备份再替换
  • p = process(rbin) 直接启动程序,他会自动使用默认的libc

或者:patchelf

把libc.so 当作ld 来patch (这种方式也可以)

patchelf –set-interpreter ./libc.so ./rbin

此方式经过测试也是与直接替换默认的libc.so 的结果是相同的。

(看过一些观点,会出现偏移不对的情况。)

插件

https://github.com/xf1les/muslheap.git

指令●mchunkinfo●mfindslot●mheapinfo●mmagic

musl v1.1.x

跟glibc大差不差,最近趋势也不是这个系列

简单记录结构体: 不做重点

1.1.24

结构体

1.1.24代表了比较老版本的Musl libc,该版本的内存管理有以下几个相关的结构体:

struct chunk {
   size_t psize, csize; // 与 glibc 的 prev size 和 size类似
   struct chunk *next; 
   struct chunk *prev;
};

static struct {
   volatile uint64_t binmap;#记录bins中bins[i]是否非空
   struct bin bins[64];
   volatile int free_lock[2];
} mal;

struct bin {
   volatile int lock[2];
   struct chunk *head;
   struct chunk *tail;
};

chunkd的结构与glibc类似,mal与glibc的arena相似,记录堆的状态,binmap表示bins[i]中的非空链,1代表bin为非空。bin如果为空,head和tail为0或者指向bin自身。bin如果非空,head和tail分别指向头和尾的chunk,头chunk的prev和尾chunk的next指向bin,构成双向链表。

malloc

分配过程主要有一下几个步骤:

增加chunk头部,并且size对其0x20

如果size大于MMAP_THRESHOLD,则使用mmap分配一块大小为size的内存

如果size小于MMAP_THRESHOLD,计算size对应的bin下标:

如果所有的bin都为空,延展堆空间,分配一个新的chunk;

如果存在非空bin,选择与size最接近的bin,取bin首部的chunk,如果该chunk大小远大于size,使用pretrim分割chunk,否则unbin从链表中取出chunk。

最后对chunk进行trim,主要是回收超过size的内存,减少内存浪费。

静态堆内存

程序初始化之后,查看mal结构体,会发现bins非空,有一个libc中的堆区和程序段的堆区。

img

musl v1.2.2

与glibc管理有了较大的区别,也是最近比赛喜欢搞的系列

数据结构

首先介绍chunk

chunk

http://www.hackdig.com/05/hack-656445.htm#toc_chunk

struct chunk{
 char prev_user_data[];
   uint8_t idx;        //低5bit为idx第几个chunk
   uint16_t offset;    //与第一个chunk起始地址的偏移,实际地址偏移为offset * UNIT,详细请看get_meta源码中得到group地址的而过程!
   char data[];
};

每个chunk 都有个4 字节的chunk头记录 idx 和 offset

(第一个chunk 比较特殊,因为它上面是 group结构 + chunk 头=0x10 )

很多没有遇见过的问题:且不是实验内容类的,比如虚拟机网卡,服务器环境,实验难度

在释放后 chunk 头的 idx会变成0xff offset 会清零

这里 offset 和 idx 比较重要

细节:和glibc 的chunk 类似 glibc chunk 可以占用下一个chunk 的prev_size 空间

而musl 可以使用 下一个chunk 头的低4B 来储存数据

group

struct group {   
    struct meta *meta;// meta的地址
   unsigned char active_idx:5;
   char pad[UNIT - sizeof(struct meta *) - 1];// 保证0x10字节对齐
   unsigned char storage[];# chunk
};

img

  • 在musl 中同一类大小的chunk 都是被分配到 同一个group 中进行管理
  • musl 是通过 chunk addr 和chunk 头对应的 offset 来索引到 group 地址的
  • 整体作为一个 group,其中开头的0x10我们当作group 头,这里的group头 涵盖了第一个chunk的头数据
  • 如这里的第一个chunk是0x7f242f97fd20开始
  • group开头的8个字节存的 meta 的地址,后面8个字节存了第一个chunk 的头数据 和 active_idx
  • 这里active_idx 代表能存下的多少个 可以用的同类型chunk
  • 如图这里可以存下的chunk [0,0x1d] 共 0x1e 个

从chunk 索引到 group:

源码:

// musl-1.2.2\src\malloc\mallocng\meta.h //line:129
static inline struct meta *get_meta(const unsigned char *p)
{
   assert(!((uintptr_t)p & 15));
   int offset = *(const uint16_t *)(p - 2);
   int index = get_slot_index(p);
   if (p[-4]) {
     assert(!offset);
     offset = *(uint32_t *)(p - 8);
     assert(offset > 0xffff);
   }
   const struct group *base = (const void *)(p - UNIT*offset - UNIT);// base 指向的就是group 地址
  ............
}

根据源码我们可以知道 从chunk 索引到group 起始地址的计算式子为

group_addr = chunk_addr - 0x10 * offset - 0x10

补充

offset = p[-2](这里的p 就是代指chunk)

index 从 get_slot_index(p)中得到

static inline int get_slot_index(const unsigned char *p)
{
   return p[-3] & 31;
}

meta

其中如果这个meta 前后都没有,那么它的*prev *next 就指向它自己

struct meta {
   struct meta *prev, *next;   //双向链表
   struct group *mem;          // 这里指向管理的group 地址
   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;
};

avail_mask,free_mask 是bitmap 的形式体现 chunk 的状态

这里例子是我申请了3个 0x30的chunk1、2、3, 然后free 掉chunk2

img

avail_mask == b”01111000”

(最前面那个0 不算只是为了对齐)

  • 在 avail_mask 中 2 进制的 0 表示不可分配 1表示可分配,顺序是从后到前
  • 如01111000 中最后的 3个0 , 表示第1、2、3个 chunk 是不可分配的 前面4个chunk 是可以分配的

free_mask == 2 =b”0010”

在 free_mask 中的 1 表示已经被释放

如第二个chunk2已经被释放:free_mask =b”0010”

last_idx 可以表示最多可用堆块的数量 最多数量=last_idx+1(因为是从0 - last_idx)

freeable=1

根据源码 代表meta否可以被回收 freeable=0 代表不可以 =1 代表可以

// musl-1.2.2\src\malloc\mallocng\free.c line 38
static int okay_to_free(struct meta *g)
{
   int sc = g->sizeclass;

   if (!g->freeable) return 0;
   ...........
}

sizeclass=3

表示由0x3这个group进行管理这一类的大小的chunk

const uint16_t size_classes[] = {
   1, 2, 3, 4, 5, 6, 7, 8,
   9, 10, 12, 15,
   18, 20, 25, 31,
   36, 42, 50, 63,
   72, 84, 102, 127,
   146, 170, 204, 255,
   292, 340, 409, 511,
   584, 682, 818, 1023,
   1169, 1364, 1637, 2047,
   2340, 2730, 3276, 4095,
   4680, 5460, 6552, 8191,
};

maplen

maplen >= 1表示这个meta里的group 是新mmap出来的,长度为多少

meta->maplen = (needed+4095)/4096;

并且这个group 不在size_classes里

maplen =0 表示group 不是新mmap 出来的在size_classes里

细节:

  • meta 一般申请的是堆空间brk 分配的,有可能是mmap 映射的,而group 都是使用的mmap 的空间
  • 由于bitmap的限制, 因此一个group中最多只能有32个chunk

其中如果这个meta 前后都没有,那么它的prev next 就指向它自己

img

img


meta_area

struct meta_area {
   uint64_t check;
   struct meta_area *next;
   int nslots;
   struct meta slots[];
};

img

meta_area 是管理meta的合集 meta_area 以页为单位分配 所以计算地址如下

meta_area_addr = meta & ( -4096 )

const struct meta_area *area = (void* )((uintptr_t)meta & -4096)

check:是个校验数字 保护meta_area 里的meta,防止meta被 伪造

meta_area *****next 指向下一个meta_area 如果没有 就默认为0

nslots: meta 槽的数量

细节:在这个meta_area 页被使用的时候 上一个临近的页 会被设置为不可写

是为了防止 使用者覆盖check 校验值

__malloc_context

http://www.hackdig.com/05/hack-656445.htm#toc___malloc_context

malloc_context

img

是musl libc 记录结构状态的表,记录各个meta 和 secret 队列信息等

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

小总结一下

  • musl 中堆的管理由meta 管理 group ,group 管理 chunk
  • 在free 或者 malloc chunk 的时候又是从 chunk 到group 再到meta 从小到大索引
  • meta 间 通过meta 中prev next 结构形成循环链表连接

函数

网上分析比较多,不细分析了

找到了,如下:

0x01 释放与分配

http://www.hackdig.com/05/hack-656445.htm#toc_0x01

(如果不想看源码 可以跳下面看总结)

malloc

http://www.hackdig.com/05/hack-656445.htm#toc_malloc

源码路径

/src/malloc/mallocng/malloc.c

源码:

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) {// size >= 阈值 会直接通过mmap 申请空间
     size_t needed = n + IB + UNIT; //UNIT 0x10 IB 4 定义在meta.h 里 这里UNIT + IB 是一个基本头的大小
     void *p = mmap(0, needed, PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON, -1, 0);//新mmap group 空间
     if (p==MAP_FAILED) return 0;
     wrlock();
     step_seq();
     g = alloc_meta();
     if (!g) { // 如果申请meta 失败 会把刚刚mmap 出来的group 回收
       unlock();
       munmap(p, needed);// 回收group
       return 0;
     }
     g->mem = p;// mem = group 地址
     g->mem->meta = g; //group 头部 指向meta (g 为 meta)
     g->last_idx = 0;//mmap的group last_idx默认值=0
     g->freeable = 1;
     g->sizeclass = 63; // mmap 的申请的 sizeclass 都为63
     g->maplen = (needed+4095)/4096;
     g->avail_mask = g->freed_mask = 0;
     ctx.mmap_counter++;// mmap 内存记载数量++
     idx = 0;
     goto success;
   }
   //否则直接根据传入size,转换成size_classes的对应大小的 下标,
   sc = size_to_class(n);

   rdlock();
   g = ctx.active[sc]; // 从现有的active中取出对应sc 的 meta ,不同sc 对应不同的meta

  /*
   如果从ctx.active 中没找到对应的meta 会执行下面的if分支
   这里!g<=> g==0 ,说明ctx.active[sc] 没有对应的meta
   */
   if (!g && sc>=4 && sc<32 && sc!=6 && !(sc&1) && !ctx.usage_by_class[sc]) {
     size_t usage = ctx.usage_by_class[sc|1];// 如果在 ctx.active 没找到 就使用更大size group 的meta
     // 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
       && !ctx.active[sc|1]->freed_mask))
       usage += 3;
     if (usage <= 12)
       sc |= 1;
     g = ctx.active[sc];
   }

   for (;;) { 
     mask = g ? g->avail_mask : 0;
     first = mask&-mask;
     if (!first) break;
     if (RDLOCK_IS_EXCLUSIVE || !MT)
       g->avail_mask = mask-first;
     else if (a_cas(&g->avail_mask, mask, mask-first)!=mask)
       continue;
     idx = a_ctz_32(first);
     goto success;
   }
   upgradelock();

   idx = alloc_slot(sc, n);
  /*
  如果当前group 不满足就会来到这里:
   alloc_slot 从group 中取出对应大小chunk 的idx
   这里先从对应sc 的ctx.active[sc] 中找对应的meta的group 有无空闲chunk可以使用
     再从队列中其他meta的group 中找
   如果队列中其他meta的group 有可利用的chunk,就使用
   如果没有就重新分配一个新的group
  */
   if (idx < 0) {
     unlock();
     return 0;
   }
   g = ctx.active[sc];// 取出 sc 对应active meta

success:
   ctr = ctx.mmap_counter;
   unlock();
   return enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk  n = size
}

!!!! 关键: 一般分配先进入这个循环

for (;;) { 
   mask = g ? g->avail_mask : 0; //先检查g所指meta是否存在,若存在mask = g->avail_mask
   first = mask&-mask;           //这里只有mask=0时,first才会为0
   if (!first) break;            //mask为0,first=0,无可用空闲chunk,跳出循环
   if (RDLOCK_IS_EXCLUSIVE || !MT)//如果是排它锁, 那么下面保证成功
     g->avail_mask = mask-first;
   else if (a_cas(&g->avail_mask, mask, mask-first)!=mask) //成功找到并设置avail_mask之后,continue 后设置idx,然后跳出
     continue;
   idx = a_ctz_32(first);
   goto success;
}
   upgradelock();
   如果

   idx = alloc_slot(sc, n);

alloc_slot

http://www.hackdig.com/05/hack-656445.htm#toc_alloc_slot

static int alloc_slot(int sc, size_t req)
{  // 尝试从限制active 中找到合适可用的
   uint32_t first = try_avail(&ctx.active[sc]);
   if (first) return a_ctz_32(first);

  // 如果没找到 重新创造一个meta,然后重新分配一个size大小对应sc的group,给这个新分配的meta
   struct meta *g = alloc_group(sc, req);
   if (!g) return -1;

   g->avail_mask--;
   queue(&ctx.active[sc], g); //把新meta 加入队列 
   return 0;
}

try_avail

http://www.hackdig.com/05/hack-656445.htm#toc_try_avail

static uint32_t try_avail(struct meta **pm)
{
   struct meta *m = *pm;
   uint32_t first;
   if (!m) return 0;
   uint32_t mask = m->avail_mask;
   if (!mask)//mask = m->avail_mask (!mask) 表示没有可用的chunk了
   {                    
     if (!m->freed_mask) // if (!m->freed_mask) <=> 没有已经释放的chunk
     {                
    /*
    进入这个分支的条件:既没有可用的chunk,也没有被释放还未回收的chunk,即chunk都被使用,且都没被释放
    */
       dequeue(pm, m); // freed_mask==avail_mask=0, group 空间已满 让对应的meta 出队
       m = *pm;
       if (!m) return 0;
     } 
     /*
     这里else表示的是:无可用空闲chunk,但是有已经释放的chunk
     !!! free释放的chunk 不能马上被复用的 !!! 
     */
     else 
     {
    /*
    进入这个分支的条件:没有可用的chunk,有被释放还未回收的chunk。
    有点好奇这里,如果达成这个条件,然后利用指针互写,修改m->next 伪造的meta,是不是就可以制造fake meta 入队的假象
    若meta链表中没有,一般meta 的next和prev 都是指向自己
    */
       m = m->next;
       *pm = m;
     }

     mask = m->freed_mask;
     // 如果这个meta 的group 只含有一个chunk ,且被释放就跳过,
     // 或者 这个meta 的group 根本不能被释放 如mmap 的 group last_idx = 0 freeable=1
     if (mask == (2u<<m->last_idx)-1 && m->freeable)
     {
       m = m->next;
       *pm = m;
       mask = m->freed_mask;
     }

     // activate more slots in a not-fully-active group
     // if needed, but only as a last resort. prefer using
     // any other group with free slots. this avoids
     // touching & dirtying as-yet-unused pages.
     if (!(mask & ((2u<<m->mem->active_idx)-1))) 
     {
       if (m->next != m) 
       { // 如果这个meta 后还有meta 就切换到 下一个meta
         m = m->next;
         *pm = m;
       } 
       else 
       {
         int cnt = m->mem->active_idx + 2;
         int size = size_classes[m->sizeclass]*UNIT;
         int span = UNIT + size*cnt;
         // activate up to next 4k boundary
         while ((span^(span+size-1)) < 4096) // 页对齐
         {
           cnt++;
           span += size;
         }
         if (cnt > m->last_idx+1)
           cnt = m->last_idx+1;
         m->mem->active_idx = cnt-1;
       }
     }
     mask = activate_group(m);// 这里是给 m的 avail_mask 打上标记
     assert(mask);
     decay_bounces(m-> sizeclass);
   }
   first = mask&-mask; // 若 mask%2==0 则first =结果是能整除这个偶数的最大的2的幂 若 mask%2==1 则first永远为1
   m->avail_mask = mask-first;
   return first;
}

malloc 的流程

http://www.hackdig.com/05/hack-656445.htm#toc_malloc_1

一、判断是否超过size 阈值

  • 先检查 申请的chunk的 needed size 是否超过最大申请限制
  • 检查申请的needed 是否超过需要mmap 的分配的阈值 超过就用mmap 分配一个group 来给chunk使用
  • 若是mmap 则设置各种标记

二、分配chunk

  1. 若申请的chunk 没超过阈值 就从active 队列找管理对应size大小的meta
  2. 关于找对应size的meta 这里有两种情况:
  3. 如果active 对应size的meta 位置上为空,没找到那么尝试先找size更大的meta
  4. 如果active 对应size的meta位置上有对应的meta,尝试从这个meta中的group找到可用的chunk(这里malloc 那个循环:for (;;),

这里不清楚建议看malloc源码分析那里)

  • 如果通过循环里,通过meta->avail_mask 判断当前group 中是否有空闲chunk

    • 有,就直接修改meta->avail_mask,然后利用enframe(g, idx, n, ctr);// 从对应meta 中的group 取出 第idx号chunk分配
    • 无,break 跳出循环
  • 跳出循环后执行idx = alloc_slot(sc, n); alloc_slot有三种分配方式

    • 使用group中被free的chunk
    • 从队列中其他meta的group 中找
    • 如果都不行就重新分配一个新的group 对应一个新的meta
  • enframe(g, idx, n, ctr) 取出 对应meta 中对应idx 的chunk

仔细观察分配的过程,我们也可以看出为什么free 的chunk不能立即回收使用,因为有空闲的chunk的时候,分配chunk是直接设置meta->avail_mask

然后直接enframe() 直接从group中取出 chunk即可,不会设置meta->freed

free

http://www.hackdig.com/05/hack-656445.htm#toc_free

源码路径

/src/malloc/mallocng/malloc.c

void free(void *p)
{
   if (!p) return;

   struct meta *g = get_meta(p);// 通过chunk p 用get_meta得到对应的meta
   int idx = get_slot_index(p);// 得到对应chunk的 idx
   size_t stride = get_stride(g); // 得到sizeclasses 中对应chunk类型的size

   unsigned char *start = g->mem->storage + stride*idx;
   unsigned char *end = start + stride - IB;
   //*start = g->mem->storage(得到group中第一个chunk地址) + stride*idx(加上对应chunk偏移);
   // start 就为对应p(chunk)的起始地址
   // end 对应结束地址

   get_nominal_size(p, end);//算出真实大小
   uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;//设置bitmap 标志
   ((unsigned char *)p)[-3] = 255;
   *(uint16_t *)((char *)p-2) = 0;
   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
   for (;;) {
     uint32_t freed = g->freed_mask;
     uint32_t avail = g->avail_mask;
     uint32_t mask = freed | avail; // 将释放的chunk 和 现在可用的 chunk 加起来
     assert(!(mask&self));
     if (!freed || mask+self==all) break; 
     //!freed 没有被释放的chunk,mask+self==all说明释放了当前chunk所有chunk 都将被回收
     // 此group 会被弹出队列 
     if (!MT)
       g->freed_mask = freed+self;// 设置free_mask 表示chunk 被释放
     else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
       continue;
     return;
   }

   wrlock();
   struct mapinfo mi = nontrivial_free(g, idx);// 含有meta 操作 ,内有unlink 是漏洞利用的关键
   unlock();
   if (mi.len) munmap(mi.base, mi.len);
}

free 的流程

http://www.hackdig.com/05/hack-656445.htm#toc_free_1

free

通过get_meta(p)得到meta (get_meta 是通过chunk 对应的offset 索引到对应的group 再索引到meta) 下面会详细介绍get_meta

通过get_slot_index(p)得到对应chunk的 idx -> 通过get_nominal_size(p, end) 算出真实大小

重置idx 和 offset idx 被置为0xff 标记chunk

修改freed_mask 标记chunk被释放

最后调用nontrivial_free 完成关于meta一些剩余操作 (注意进入nontrivial_free 是在for循环外 还未设置)

细节:!!!

释放chunk的时候,先只会修改freed_mask,不会修改avail_mask,说明chunk 在释放后,不会立即被复用

注意进入nontrivial_free 是在for循环外 还未设置freed_mask 跳出循环的条件是 if (!freed || mask+self==all) break;

free 中chunk 的起始位置可以通过 chunk的idx 定位

get_meta

static inline struct meta *get_meta(const unsigned char *p)
{
   assert(!((uintptr_t)p & 15));
   int offset = *(const uint16_t *)(p - 2);// 得到chunk offset
   int index = p[-3] & 31;;// 得到chunk idx
   if (p[-4]) {
     assert(!offset);
     offset = *(uint32_t *)(p - 8);
     assert(offset > 0xffff);
   }
   const struct group *base = (const void *)(p - UNIT*offset - UNIT);// 通过offset 和chunk 地址计算出group地址
   const struct meta *meta = base->meta;// 从group 得到 meta 地址
   assert(meta->mem == base);// 检查meta 是否指向对应的group
   assert(index <= meta->last_idx);// 检查chunk idx 是否超过 meta 最大chunk 容量
   assert(!(meta->avail_mask & (1u<<index)));
   assert(!(meta->freed_mask & (1u<<index)));
   const struct meta_area *area = (void *)((uintptr_t)meta & -4096);// 得到meta_area 地址
   assert(area->check == ctx.secret);// 检查 check 校验值
   if (meta->sizeclass < 48) { // 如果属于 sizeclasses 管理的chunk 大小
     assert(offset >= size_classes[meta->sizeclass]*index);
     assert(offset < size_classes[meta->sizeclass]*(index+1));
   } else {
     assert(meta->sizeclass == 63);
   }
   if (meta->maplen) {
     assert(offset <= meta->maplen*4096UL/UNIT - 1);
   }
   return (struct meta *)meta;
}

nontrivial_free

http://www.hackdig.com/05/hack-656445.htm#toc_nontrivial_free

关于nontrivial_free()函数很重要 ,这里尽量详细说明

static struct mapinfo nontrivial_free(struct meta *g, int i)// i = idx
{
   uint32_t self = 1u<<i;
   int sc = g->sizeclass;
   uint32_t mask = g->freed_mask | g->avail_mask;//mask=已经被free的chunk +可使用的chunk
   if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) 
   {  /*  
     如果 mask+self == (2u<<g->last_idx)-1 代表此meta中group里的chunk 都被释放 或者 都被用了
     (2u<<g->last_idx)-1 计算出的值化成二进制,其中每位含义类似于bitmap,如果每位为1表每位要不是被free 不然就是被
     okay_to_free 检测是否可以被释放

     */
     if (g->next) 
     {  // 如果队列中 有下一个meta
       assert(sc < 48);// 检测 sc 是不是mmap 分配的
    // 检测当前meta g 和 队列里的active[sc] meta 是否一样,一样则activate_new赋值为1
       int activate_new = (ctx.active[sc]==g);
       dequeue(&ctx.active[sc], g);// 当前meta 出队

       // 在出队操作后 ,ctx.active[sc]==meta ->next  是指的刚刚出队meta 的下一个meta
       if (activate_new && ctx.active[sc])
         activate_group(ctx.active[sc]);//如果有下一个meta 直接激活 然后修改avail_mask 标志位
     }
     return free_group(g);
   } 
   else if (!mask) 
   {// mask==0 group chunk 空间已被完全使用
     assert(sc < 48);
     // might still be active if there were no allocations
     // after last available slot was taken.
     if (ctx.active[sc] != g) {// 如果 g 未被加入 队列ctx.ative[sc]
       queue(&ctx.active[sc], g);// 把g 加入队列
     }
   }
   a_or(&g->freed_mask, self);// 修改对应 的freed_mask 标志 ,表示着对应的chunk 已被释放
   return (struct mapinfo){ 0 };
}
static inline void dequeue(struct meta **phead, struct meta *m)
{
   if (m->next != m) {
     m->prev->next = m->next; // 这里存在指针互写 在 prev 所指地址上 写入next 指针
     m->next->prev = m->prev; // 在next 所指地址上 写入prev 指针
     if (*phead == m) *phead = m->next;// 队列头如果为m 那就更新为m->next
   } else {
     *phead = 0;
   }
   m->prev = m->next = 0; // 清理m(meta)的头尾指针
}

dequeue 触发条件c

img

self = 1 << idx

下面是几种简单的触发情况

  1. avail_mask 表示只有一个chunk 被使用 ,freed_mask=0,而free 刚好要free 一个chunk

满足 okay_to_free() 条件 就可以进入dequeue 进行出队操作

如add(1,0x20) 再free(1) 就会使得meta 被回收

  1. avail_mask=0, freed_mask 表示只有 1个 chunk 没被 释放,这时释放的chunk 就应该是那最后一个chunk

如下面情况 avail_mask ==0 free_mask=63=00111111 last_idx = 6

已经释放6 个chunk 还有最后一个chunk没被释放 在释放最后一个chunk 时会触发dequeue使得对应meta出队

img

  1. 如果发现这个group中所有的chunk要么被free, 要么是可用的, 那么就会回收掉这个group,调用dequeue从队列中出队

利用手段

CTF 中musl题利用

http://www.hackdig.com/05/hack-656445.htm#toc_0x02-ctf-musl

一般有如下几种利用方法,核心原理都是构造假的chunk 索引到假的group 从而所引导假的meta

或覆盖group 中指向meta 的指针 覆盖为假的meta ,然后使得假的meta dequeue 最终实现unlink

(构造fake_meta 需要先泄露 secret 校验值)

dequeue 的两种流程

http://www.hackdig.com/05/hack-656445.htm#toc_dequeue

  1. free()->nontrivial_free()->dequeue

通过构造假的meta 满足各种条件 通过以下流程

free()->nontrivial_free()->dequeue

img

这里通过free 到 dequeue

  1. realloc()->free(old)->nontrivial_free()->dequeue

通过realloc 里也带有free

realloc()->free(old)->nontrivial_free()->dequeue

musl基础知识整理

http://www.hackdig.com/05/hack-656445.htm#toc_meta_1

注意: musl 是没有malloc_hook和 free_hook 这种一般的hook 位

且musl 程序的IO_FILE 结构体格式和libc 不一样 没有IO_jump_t的vtable

但是存在read,write,seek,close 四个函数指针

讲一下思路

  1. 伪造meta 后满足各种条件 使得其进入dequeue 通过unlink,构造prev,next 实现任意地址指针互写

    通过任意地址互写指针,向stdout_used 写入我们伪造的fake_stdout地址, 通过IO_FILE 劫持程序执行流

    到我们布置好的fake_stdout 上,可以找IO_FILE 里的一些函数exit puts在fake_stdout上布置rop_chain然后通过栈迁移的gadget 利用FSOP 劫持程序到布置的fake_stdout上

  2. 第二种方式更麻烦 也是伪造fake_meta 也是任意地址指针互写,先进行布局使得 fake_meta dequeue 实现unlink,

    在利用指针互写 修改fake_meta 中的mem(mem 就是group 区域) ,把mem 修改为我们想要的地址,

    然后让fake_meta 通过queue 入队,可以实现任意地址分配的,然后同样是打 IO_FILE 通过修改stdout stdin 和stderr 结构体 劫持程序流


这部分得调试了解了解吧。

补充

http://www.hackdig.com/05/hack-656445.htm#toc_0x03

前面提到过

  • 第一种:如果一个group 中所有的chunk 都已经被使用,且没有free掉的chunk
  • 第二种:group 中的chunk 当free掉最后一个chunk,都处于freed的状态

处于这两种状态的group 对应的meta 会被dequeue 出队

  1. 第一种状态,在malloc 时候触发dequeue ,当前meta出队

    /*
    /src/malloc/mallocng/malloc.c
    */
    static uint32_t try_avail(struct meta **pm)
    {
        struct meta *m = *pm;
        uint32_t first;
        if (!m) return 0;
        uint32_t mask = m->avail_mask;
        if (!mask) 
            { //这里如果mask==0,含义就是group中的chunk已被全部使用,没有空闲了
            if (!m) return 0;
            if (!m->freed_mask)//并且没有已经释放的chunk
            {
            dequeue(pm, m);//直接让这个meta 出队,因为group已经没有可用空间了
            m = *pm;
            if (!m) return 0;
            } 
        else {
            m = m->next;
            *pm = m;
            }
            ....
            }
        .....
    }
  2. 第二种状态在free时候,在nontrivial_free()中触发dequeue ,当前meta 出队,不被使用

    static struct mapinfo nontrivial_free(struct meta *g, int i)// i = idx
    {
        uint32_t self = 1u<<i;
        int sc = g->sizeclass;
        uint32_t mask = g->freed_mask | g->avail_mask;//mask=已经被free的chunk +可使用的chunk
        if (mask+self == (2u<<g->last_idx)-1 && okay_to_free(g)) 
        {  /*  
            如果 mask+self == (2u<<g->last_idx)-1 代表此meta中group里的chunk 都被释放 或者 都被用了
            (2u<<g->last_idx)-1 计算出的值化成二进制,其中每位含义类似于bitmap,如果每位为1表每位要不是被free 不然就是被
            okay_to_free 检测是否可以被释放
        
              */
              if (g->next) 
              {  // 如果队列中 有下一个meta
                assert(sc < 48);// 检测 sc 是不是mmap 分配的 
        
        // 检测当前meta g 和 队列里的active[sc] meta 是否一样,一样则activate_new赋值为1
            int activate_new = (ctx.active[sc]==g);
            dequeue(&ctx.active[sc], g);// 当前meta 出队
        
                // 在出队操作后 ,ctx.active[sc]==meta ->next  是指的刚刚出队meta 的下一个meta
                if (activate_new && ctx.active[sc])
                  activate_group(ctx.active[sc]);//如果有下一个meta 直接激活 然后修改avail_mask 标志位
              }
              return free_group(g);
        }
    }

一些特殊的地方:

  • 若:group 中chunk都被使用了,只有一个被free的chunk, 在再次malloc 申请相同size chunk时:
  • 调用流程:malloc() –> alloc_slot() –> try_avail()
  • try_avail() 清除当前meta freed bit位,但不能立即复用,所以此时已经无可用chunk
  • alloc_slot() 重新申请一个group(通过alloc_group申请新group)对应一个新的meta1,然后把这个新的meta queue 入队
  • meta 结构体有prev,next 指针。同一size的chunk 对应同一类meta。再queue 后,同一类meta被链接,实际上是个链表结构

若:当前meta 对应group 中无可用chunk,则在alloc_slot 中,会在链表上的寻找下一个meta的group中是否有可用的chunk


没有hook函数可以劫持,往往根据IO_FILE来getshell

unbin

static void unbin(struct chunk *c, int i)
{
   if (c->prev == c->next)
     a_and_64(&mal.binmap, ~(1ULL<<i));
   c->prev->next = c->next;
   c->next->prev = c->prev;
   c->csize |= C_INUSE;
   NEXT_CHUNK(c)->psize |= C_INUSE;
}

glibc中的unlink

.....
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))            
  malloc_printerr (check_action, "corrupted double-linked list", P, AV); 
.....

unbin没有对链表合法性的判断

根据exit函数

_Noreturn void exit(int code)
{
   __funcs_on_exit();
   __libc_exit_fini();
   __stdio_exit();
   _Exit(code);
}

exit中__funcs_on_exit()

static struct fl
{
   struct fl *next;
   void (*f[COUNT])(void *);
   void *a[COUNT];
} builtin, *head;

static int slot;
static volatile int lock[1];

void __funcs_on_exit()
{
   void (*func)(void *), *arg;
   LOCK(lock);
   for (; head; head=head->next, slot=COUNT) while(slot-->0) {
     func = head->f[slot];
     arg = head->a[slot];
     UNLOCK(lock);
     func(arg); //
     LOCK(lock);
   }
}

exit中__stdio_exit()

调用链

exit()->__stdio_exit()->close_file()

close_file()函数中

static void close_file(FILE *f)
{
   if (!f) return;
   FFINALLOCK(f);
   if (f->wpos != f->wbase) f->write(f, 0, 0); //
   if (f->rpos != f->rend) f->seek(f, f->rpos-f->rend, SEEK_CUR);
}

puts函数

puts()->….->fwrite()->__fwritex()

size_t __fwritex(const unsigned char *restrict s, size_t l, FILE *restrict f)
{
    size_t i=0;

     if (!f->wend && __towrite(f)) return 0;

     if (l > f->wend - f->wpos) return f->write(f, s, l); //

     if (f->lbf >= 0) {
         /* Match /^(.*\n|)/ */
         for (i=l; i && s[i-1] != '\n'; i--);
         if (i) {
             size_t n = f->write(f, s, i);
             if (n < i) return n;
             s += i;
             l -= i;
         }
     }

     memcpy(f->wpos, s, l);
     f->wpos += l;
     return l+i;
}
size_t fwrite(const void *restrict src, size_t size, size_t nmemb, FILE *restrict f)
{
     size_t k, l = size*nmemb;
    if (!size) nmemb = 0;
    FLOCK(f);
    k = __fwritex(src, l, f);
    FUNLOCK(f);
    return k==l ? nmemb : k/size;
}

weak_alias(fwrite, fwrite_unlocked);

编译musl

musl 环境下载

http://www.hackdig.com/05/hack-656445.htm#toc_musl_1

sudo dpkg -i musl_1.2.2-1_amd64.deb

或:通过包管理器安装,

sudo apt-get install -y musl musl-dev

关于自己编译源码:

自己编译源码具体可以参考下面这篇文档

blog.fpliu.com/it/software/musl-libc

调试符号

http://www.hackdig.com/05/hack-656445.htm#toc_

musl-dbgsym_1.2.2-1_amd64.ddeb

sudo dpkg -i musl-dbgsym_1.2.2-1_amd64.ddeb

这里安装调试符号非常重要,关系到后面调试!!!

在安装调试符号后,即使musl 程序的libc.so不带调试信息,gdb 还是会从我们安装的调试符号中自动寻找匹配,还是能做到带符号调试!!!

几个博客:

Musl 程序分析和调试环境配置 && 部分源码分析_黑客技术 (hackdig.com)

muslpwn · 语雀 (yuque.com)

从musl libc 1.1.24到1.2.2 学习pwn姿势 - 安全客,安全资讯平台 (anquanke.com)

其他参考博客:

(178条消息) pwn musl libc环境配置_N1ch0l4s的博客-CSDN博客

[原创]musl1.1.24 源码分析+利用+例题-Pwn-看雪论坛-安全社区|安全招聘|bbs.pediy.com