启动musl 程序
这里探索了很多方式,勘误之前的启动方式实际上存在一些问题,这里终于探索到了最完备的musl 调试环境,这里特别感谢我的学弟gxh为勘误做出的贡献
提示:
musl 的库函数就只有一个libc.so ,这个libc.so 既充当ld,又充当libc
最佳环境配置:
本机环境ubuntu:20.04
musl 环境下载
1 | sudo dpkg - i musl_1. 2.2 - 1_amd64 .deb
|
或:通过包管理器安装,
1 | sudo apt - get install - y musl musl - dev
|
关于自己编译源码:
自己编译源码具体可以参考下面这篇文档
blog.fpliu.com/it/software/musl-libc
调试符号:
musl-dbgsym_1.2.2-1_amd64.ddeb
1 | sudo dpkg - i musl - dbgsym_1. 2.2 - 1_amd64 .ddeb
|
这里安装调试符号非常重要,关系到后面调试!!!
在安装调试符号后,即使musl 程序的libc.so不带调试信息,gdb 还是会从我们安装的调试符号中自动寻找匹配,还是能做到带符号调试!!!
安装GDB Musl小插件:
安利xf1les 师傅编写的musl heap gdb 插件
1 | git clone https: / / github.com / xf1les / muslheap.git
|
Installation
1 | echo "source /path/to/muslheap.py" >> ~ / .gdbinit
|
Requirements:(环境需求)
安装后非常方便,可以很方便的看__malloc_context 结构体,和meta 链表的情况,还可以直接得到一些musl利用的必要函数的偏移

指定 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 (这种方式也可以)
1 | patchelf - - set - interpreter . / libc.so . / rbin
|
此方式经过测试也是与直接替换默认的libc.so 的结果是相同的

调试细节:

- 在调试的时候,直接在gdb 中dir 源码,可以直观的跟踪函数执行流程进行调试
1 | dir / path / to / musl - 1.2 . 2 / src / malloc / 和 dir / path / to / musl - 1.2 . 2 / src / malloc / mallocng便可
|
致歉:再次强调
1 | io = process([libc.so,rbin]) 这种启动方式是错误的!!!!
|
Musl 1.2.2源码分析
0x00 基本数据结构
chunk->group->meta 分析顺序
先从musl 的基本数据结构 这里我选择从小到大来理解,因为在源码里它就是从小到大索引的
从chunk 一路索引到 meta。
首先介绍chunk
chunk:
1 2 3 4 5 6 | 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:
1 2 3 4 5 6 7 8 | struct group {
struct meta * meta;
unsigned char active_idx: 5 ;
char pad[UNIT - sizeof(struct meta * ) - 1 ]; / / padding = 0x10B
unsigned char storage[]; / / chunks
};
|

- 在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:
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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)中得到
1 2 3 4 | static inline int get_slot_index(const unsigned char * p)
{
return p[ - 3 ] & 31 ;
}
|
meta
1 2 3 4 5 6 7 8 9 | 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 ;
};
|
其中如果这个meta 前后都没有,那么它的prev next 就指向它自己
avail_mask,free_mask 是bitmap 的形式体现 chunk 的状态
这里例子是我申请了3个 0x30的chunk1、2、3, 然后free 掉chunk2


avail_mask == b"01111000" (最前面那个0 不算只是为了对齐)
free_mask == 2 =0010
在 free_mask 中的 1 表示已经被释放
如第二个chunk2已经被释放:free_mask =b"0010"
last_idx 可以表示最多可用堆块的数量 最多数量=last_idx+1(因为是从0 - last_idx)
freeable=1根据源码 代表meta否可以被回收 freeable=0 代表不可以 =1 代表可以
1 2 3 4 5 6 7 8 | static int okay_to_free(struct meta * g)
{
int sc = g - >sizeclass;
if (!g - >freeable) return 0 ;
...........
}
|
sizeclass=3 表示由0x3
这个group进行管理这一类的大小的chunk
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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_area
1 2 3 4 5 6 | struct meta_area {
uint64_t check;
struct meta_area * next ;
int nslots;
struct meta slots[];
};
|
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
是musl libc 记录结构状态的表,记录各个meta 和 secret 队列信息等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | struct malloc_context {
uint64_t secret; / / 和meta_area 头的check 是同一个值 就是校验值
size_t pagesize;
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 释放与分配
(如果不想看源码 可以跳下面看总结)
malloc
源码路径
/src/malloc/mallocng/malloc.c
源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | 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
}
|
!!!! 关键: 一般分配先进入这个循环
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 | 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 的流程:
一、判断是否超过size 阈值
- 先检查 申请的chunk的 needed size 是否超过最大申请限制
- 检查申请的needed 是否超过需要mmap 的分配的阈值 超过就用mmap 分配一个group 来给chunk使用
- 若是mmap 则设置各种标记
二、分配chunk
- 若申请的chunk 没超过阈值 就从active 队列找管理对应size大小的meta
- 关于找对应size的meta 这里有两种情况:
- enframe(g, idx, n, ctr) 取出 对应meta 中对应idx 的chunk
仔细观察分配的过程,我们也可以看出为什么free 的chunk不能立即回收使用,因为有空闲的chunk的时候,分配chunk是直接设置meta->avail_mask
然后直接enframe() 直接从group中取出 chunk即可,不会设置meta->freed
free
源码路径
/src/malloc/mallocng/malloc.c
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | 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 的流程:
free:
1 2 3 4 5 6 7 8 9 | 通过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 循环外 还未设置)
|
细节:!!!
1 2 3 4 5 | 释放chunk的时候,先只会修改freed_mask,不会修改avail_mask,说明chunk 在释放后,不会立即被复用
注意进入nontrivial_free 是在 for 循环外 还未设置freed_mask 跳出循环的条件是 if (!freed || mask + self = = all ) break ;
free 中chunk 的起始位置可以通过 chunk的idx 定位
|
get_meta
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | 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
关于nontrivial_free()函数很重要 ,这里尽量详细说明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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 };
}
|
1 2 3 4 5 6 7 8 9 10 11 | 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 触发条件

self = 1 << idx
下面是几种简单的触发情况
1.avail_mask 表示只有一个chunk 被使用 ,freed_mask=0,而free 刚好要free 一个chunk
满足 okay_to_free() 条件 就可以进入dequeue 进行出队操作
如add(1,0x20) 再free(1) 就会使得meta 被回收
2.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出队

3.如果发现这个group中所有的chunk要么被free, 要么是可用的, 那么就会回收掉这个group,调用dequeue从队列中出队
0x02 CTF 中musl题利用
一般有如下几种利用方法,核心原理都是构造假的chunk 索引到假的group 从而所引导假的meta
或覆盖group 中指向meta 的指针 覆盖为假的meta ,然后使得假的meta dequeue 最终实现unlink
(构造fake_meta 需要先泄露 secret 校验值)
dequeue 的两种流程
一、
通过构造假的meta 满足各种条件 通过以下流程
free()->nontrivial_free()->dequeue

这里通过free 到 dequeue
二、
通过realloc 里也带有free
realloc()->free(old)->nontrivial_free()->dequeue
伪造meta后控制程序流的方法
注意: musl 是没有malloc_hook和 free_hook 这种一般的hook 位
且musl 程序的IO_FILE 结构体格式和libc 不一样 没有IO_jump_t的vtable
但是存在read,write,seek,close 四个函数指针

讲一下思路
- 伪造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上
- 第二种方式更麻烦 也是伪造fake_meta 也是任意地址指针互写,先进行布局使得 fake_meta dequeue 实现unlink,
在利用指针互写 修改fake_meta 中的mem(mem 就是group 区域) ,把mem 修改为我们想要的地址,
然后让fake_meta 通过queue 入队,可以实现任意地址分配的,然后同样是打 IO_FILE 通过修改stdout stdin 和stderr 结构体 劫持程序流
0x03 补充:
前面提到过
- 第一种:如果一个group 中所有的chunk 都已经被使用,且没有free掉的chunk
- 第二种:group 中的chunk 当free掉最后一个chunk,都处于freed的状态
处于这两种状态的group 对应的meta 会被dequeue 出队
- 第一种状态,在malloc 时候触发dequeue ,当前meta出队
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | / *
/ 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;
}
....
}
.....
}
|
- 第二种状态在free时候,在nontrivial_free()中触发dequeue ,当前meta 出队,不被使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | 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
参考文章
VMProtect分析与还原
最后于 2022-5-14 21:46
被0xRGz编辑
,原因: 更新错误