DefCon Quals 2021 mooosl的解题思路

本文首发于安全客:借助DefCon Quals 2021的mooosl学习musl mallocng(源码审计篇)

title
本题来源于DefCon Quals 2021的mooosl,考察点是最新版本musl libc 1.2.2利用。

关于musl libc的资料比赛期间找到过一篇从一次 CTF 出题谈 musl libc 堆漏洞利用,碍于musl libc在1.2.x之后的堆管理机制有较大的改版,因而有了该文章。本次文章分上下两篇,从musl libc 1.2.2的源码审计、调试,以及其中的利用机会,再到mooosl这道题的解题过程做一个分析。

musl libc 1.2.2的源码可以从此处下载获得。1.2.x采用src/malloc/mallocng内的代码,其堆管理结构与早期版本几乎完全不同,而早期的堆管理器则放入了src/malloc/oldmalloc中。
title

调试带符号的musl libc

0x01源码编译

题目提供的libc.so不带符号,很难通过调试去理解musl堆管理器的数据结构,可以通过源码编译,生成一份带调试符号的libc.so,进行源码级debug。

1
2
3
4
5
6
7
8
9
10
11
12
tar -xzvf ./musl-1.2.2.tar.gz
cd musl-1.2.2
mkdir build x64
cd build

CC="gcc" CXX="g++" \
CFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error -fno-stack-protector" \
CXXFLAGS="-g -g3 -ggdb -gdwarf-4 -Og -Wno-error -fno-stack-protector" \
../configure --prefix=/home/sung3r/workspace/sharefd/glibc/glibc-2.32/x64 --disable-werror

make
make install

/src/x64/下找到编译好的libc.so

通过patchelf将ld.so改成libc.so即可,gdb调试时加上dir /path/to/musl-1.2.2/src/malloc/dir /path/to/musl-1.2.2/src/malloc/mallocng便可源码调试。

0x02安装调试符号

此方法要在ubuntu 20.04下才能成功

下载musl_1.2.2-1_amd64.debmusl-dbgsym_1.2.2-1_amd64.ddeb

在ubuntu20.04安装

1
2
sudo dpkg -i musl_1.2.2-1_amd64.deb
sudo dpkg -i musl-dbgsym_1.2.2-1_amd64.ddeb

gdb调试时通过dir加载源码即可。推荐此方法,比较简单,而且该deb里的libc.so与题目提供的libc.so md5一致。

源码审计

meta.h

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
//line:124~127
static inline int get_slot_index(const unsigned char *p)
{
//chunk地址往前的第3个byte就是该chunk的下标
return p[-3] & 31;
}

//line:129~157
static inline struct meta *get_meta(const unsigned char *p)
{
assert(!((uintptr_t)p & 15));//16字节对齐
//获取slot的偏移offset,offset*0x10才是真实偏移
int offset = *(const uint16_t *)(p - 2);
//获取slot的下标,这里的slot就是我们习惯中理解的chunk
int index = get_slot_index(p);
if (p[-4]) {
//如果offset不为0,表示不是group里的首个chunk,抛出异常
assert(!offset);
offset = *(uint32_t *)(p - 8);
assert(offset > 0xffff);
}
//获取group首地址,也即`meta->mem`这个地址
const struct group *base = (const void *)(p - UNIT*offset - UNIT);
//获取meta地址,group首地址指向meta结构的地址
const struct meta *meta = base->meta;
assert(meta->mem == base);
assert(index <= meta->last_idx);
assert(!(meta->avail_mask & (1u<<index)));
assert(!(meta->freed_mask & (1u<<index)));
const struct meta_area *area = (void *)((uintptr_t)meta & -4096);
//校验Page的secret是否正确,防止伪造Page
assert(area->check == ctx.secret);
if (meta->sizeclass < 48) {//一般都为48个sizeclass
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;
}

//line:229~238
//16字节对齐向上取整,最后换算成size_classes的下标,对group进行分类
static inline int size_to_class(size_t n)
{
n = (n+IB-1)>>4;
if (n<10) return n;
n++;
int i = (28-a_clz_32(n))*4 + 8;
if (n>size_classes[i+1]) i+=2;
if (n>size_classes[i]) i++;
return i;
}

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
//line:174~284
static struct meta *alloc_group(int sc, size_t req)
{
...
} else {///通过brk分配
int j = size_to_class(UNIT+cnt*size-IB);
int idx = alloc_slot(j, UNIT+cnt*size-IB);
if (idx < 0) {
free_meta(m);
return 0;
}
struct meta *g = ctx.active[j];
p = enframe(g, idx, UNIT*size_classes[j]-IB, ctx.mmap_counter);
m->maplen = 0;
p[-3] = (p[-3]&31) | (6<<5);
for (int i=0; i<=cnt; i++)
p[UNIT+i*size-4] = 0;///根据size清零mem
active_idx = cnt-1;
}
...
}

//line:300~381
//malloc的实现,lite_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;

//大于某一个阈值,通过mmap分配
if (n >= MMAP_THRESHOLD) {///p MMAP_THRESHOLD; $10 = 0x1ffec
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();
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;
}
//否则通过brk分配
//根据传入size,转换成size_classes的下标,根据sc申请相对应group的chunk
sc = size_to_class(n);

rdlock();
//根据sc,获取存放着对应size group的meta,如果还没申请过这类group,对应ctx.active[sc]为0
g = ctx.active[sc];

// 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]) {
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
&& !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();

//申请分配sc类别的chunk,size为n
idx = alloc_slot(sc, n);
if (idx < 0) {
unlock();
return 0;
}
g = ctx.active[sc];

success:
ctr = ctx.mmap_counter;
unlock();
return enframe(g, idx, n, ctr);
}

//line:286~298
//申请chunk
static int alloc_slot(int sc, size_t req)
{
uint32_t first = try_avail(&ctx.active[sc]);
if (first) return a_ctz_32(first);

//申请group,group信息存放于meta结构
struct meta *g = alloc_group(sc, req);
if (!g) return -1;

g->avail_mask--;
queue(&ctx.acti
ve[sc], g);
return 0;
}

free.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
//line:101~143
void free(void *p)
{
if (!p) return;//地址为0,直接返回

//获取meta结构,以及做一些检查
struct meta *g = get_meta(p);
//获取chunk的下标
int idx = get_slot_index(p);
size_t stride = get_stride(g);
unsigned char *start = g->mem->storage + stride*idx;
unsigned char *end = start + stride - IB;
get_nominal_size(p, end);
uint32_t self = 1u<<idx, all = (2u<<g->last_idx)-1;
//将对应chunk的下标置0xff
((unsigned char *)p)[-3] = 255;
// invalidate offset to group header, and cycle offset of
// used region within slot if current offset is zero.
//将chunk的offset清0
*(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的avail_mask`freed_mask
for (;;) {
uint32_t freed = g->freed_mask;
uint32_t avail = g->avail_mask;
uint32_t mask = freed | avail;
assert(!(mask&self));
if (!freed || mask+self==all) break;
if (!MT)
g->freed_mask = freed+self;
else if (a_cas(&g->freed_mask, freed, freed+self)!=freed)
continue;
return;
}

wrlock();
struct mapinfo mi = nontrivial_free(g, idx);
unlock();
if (mi.len) munmap(mi.base, mi.len);
}

meta、group、chunk的具体结构,以下通过debug进行分析。

分配释放

1
2
3
4
store('a0a0', 'b0b0')
store('a1a11', 'b1b1111')

delete('a0a0')

__malloc_context是musl libc的全局管理结构指针,存放在libc.so的bss段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gef➤  p __malloc_context
$1 = {
secret = 0x69448097523526a7,
init_done = 0x1,
mmap_counter = 0x0,
free_meta_head = 0x0,
avail_meta = 0x56042ee901f8,
avail_meta_count = 0x59,
avail_meta_area_count = 0x0,
meta_alloc_shift = 0x0,
meta_area_head = 0x56042ee90000,
meta_area_tail = 0x56042ee90000,
avail_meta_areas = 0x56042ee91000 <error: Cannot access memory at address 0x56042ee91000>,
active = {0x56042ee901d0, 0x0, 0x0, 0x56042ee901a8, 0x0, 0x0, 0x0, 0x56042ee900b8, 0x0, 0x0, 0x0, 0x56042ee90090, 0x0, 0x0, 0x0, 0x56042ee90068, 0x0, 0x0, 0x0, 0x56042ee90040, 0x0, 0x0, 0x0, 0x56042ee90018, 0x0 <repeats 24 times>},
usage_by_class = {0x1e, 0x0, 0x0, 0x7, 0x0 <repeats 44 times>},
unmap_seq = '\000' <repeats 31 times>,
bounces = '\000' <repeats 31 times>,
seq = 0x0,
brk = 0x56042ee91000
}

active = {0x56042ee901d0,0,0...:堆管理器依据申请的size,将chunk分成48类chunk,由sizeclass指定。每类chunk由一个meta结构管理,meta管理的chunk个数有限,由small_cnt_tab指定。当申请个数超出一个meta所能管理的最大数量,堆管理器会再申请同类型meta管理更多的chunk,并且以双向链表结构管理这些相同类型的meta。
usage_by_class = {0x1e, 0x0, 0x0, 0x7,...:表示当前各meta管理着的chunk个数。

secret = 0x69448097523526a7:在meta域每个page大小的首8个byte,都会存在一个校验key。
title

musl libc用以下的结构管理着meta、group以及chunk
title

分配了两个0x30的chunk,未释放。

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x56042ee901a8
$2 = {
prev = 0x56042ee901a8,
next = 0x56042ee901a8,
mem = 0x7f79e1df5c50,
avail_mask = 0x7c,
freed_mask = 0x0,
last_idx = 0x6,
freeable = 0x1,
sizeclass = 0x3,
maplen = 0x0
}

prevnext都指向本身,表示只有一个meta页,meta页由一个双向链表进行维护;
0x7f79e1df5c50user data域;
avail_mask = 0x7c = 0b1111100表示第0、1个chunk不可用(已经被使用);
freed_mask = 0x0表示没有chunk被释放;
last_idx = 0x6表示最后一个chunk的下标是0x6,总数是0x7个
sizeclass = 0x3表示由0x3这个group进行管理。

title
0x000056042ee901a8指向meta结构的地址;
后面8个byte表示chunk的头部结构:
0x00000x0004表示当前chunk,距离group首地址0x00007f79e1df5c58的偏移为00x40
0xa00xa1表示当前chunk是group中的第0和1个chunk;
再往后0x28个byte就是user data域,最多接收输入0x28+4个byte,占用下一个chunk的前4个byte。

同时,也分配了四个0x10的chunk,未释放

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x56042ee901d0
$3 = {
prev = 0x56042ee901d0,
next = 0x56042ee901d0,
mem = 0x56042db99c50,
avail_mask = 0x3ffffff0,
freed_mask = 0x0,
last_idx = 0x1d,
freeable = 0x1,
sizeclass = 0x0,
maplen = 0x0
}

prevnext都指向本身,表示只有一个meta页,meta页由一个双向链表进行维护;
0x56042db99c50user data域;
avail_mask = 0x3ffffff0 = 0b111111111111111111111111110000表示第0、1、2、3个chunk不可用(已经被使用);
freed_mask = 0x0表示没有chunk被释放;
last_idx = 0x1d表示最后一个chunk的下标是0x1d,总数是0x1e个
sizeclass = 0x3表示由0x3这个group进行管理。

title
0x00000x00010x00020x0003表示距离group首地址偏移为00x100x200x30byte;
0xa00xa10xa20xa3表示group中的chunk下标;
往后8byte是user data,user data最多接收输入8+4个byte,占用下一个chunk header的前4个byte(与x86的glibc类似)

释放两个0x10的chunk

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x56042ee901d0
$9 = {
prev = 0x56042ee901d0,
next = 0x56042ee901d0,
mem = 0x56042db99c50,
avail_mask = 0x3fffffe0,
freed_mask = 0x3,
last_idx = 0x1d,
freeable = 0x1,
sizeclass = 0x0,
maplen = 0x0
}

freed_mask = 0x3 = 0b11表示前两个chunk被释放;
avail_mask = 0x3fffffe0 = 0b111111111111111111111111100000可以发现,此时前两个chunk仍然为不可分配的状态;

title
已释放的chunk会将chunk header的offset清零,并且将chunk下标置成0xff,不清空user data域。

释放一个0x30的chunk

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x56042ee901a8
$13 = {
prev = 0x56042ee901a8,
next = 0x56042ee901a8,
mem = 0x7f79e1df5c50,
avail_mask = 0x7c,
freed_mask = 0x1,
last_idx = 0x6,
freeable = 0x1,
sizeclass = 0x3,
maplen = 0x0
}

freed_mask = 0x1表示有1个已被释放的chunk。

title
同样,chunk header的offset清零,且chunk下标置0xff

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

static const uint8_t small_cnt_tab[][3] = {
{ 30, 30, 30 },
{ 31, 15, 15 },
{ 20, 10, 10 },
{ 31, 15, 7 },
{ 25, 12, 6 },
{ 21, 10, 5 },
{ 18, 8, 4 },
{ 31, 15, 7 },
{ 28, 14, 6 },
};

static struct meta *alloc_group(int sc, size_t req)
{
size_t size = UNIT*size_classes[sc];
int i = 0, cnt;
unsigned char *p;
struct meta *m = alloc_meta();///分配内存,用于建立一个group
if (!m) return 0;
size_t usage = ctx.usage_by_class[sc];
size_t pagesize = PGSZ;
int active_idx;
if (sc < 9) {
while (i<2 && 4*small_cnt_tab[sc][i] > usage)
i++;
cnt = small_cnt_tab[sc][i];
} else {
...
ctx.usage_by_class[sc] += cnt;
...

几个有用的结构
group分类表,由sc指定由哪个group管理:usage_by_class = {0,0,0,…}
要申请的chunk大小,由这个大小计算出sc: req = 0x30 -> sc = 0x3
malloc的chunk大小: UNIT*size_classes = 0x10 * 0x3 = 0x30
设定该group最多有多少个chunk:ctx.usage_by_class[sc] = 30 = 0x1e

漏洞点(Info Leak)

0x30 chunk, malloc 6次,free 5次

1
2
3
store('A', 'A')
for _ in range(5):
query('A' * 0x30)

avail_mask = 0x40 = 0b1000000除了最后一个chunk,其余chunk不可分配;
freed_mask = 0x3e = 0b111110除第一个以及最后一个chunk,其余chunk已被释放

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x55b9b0b551a8
$2 = {
prev = 0x55b9b0b551a8,
next = 0x55b9b0b551a8,
mem = 0x7fccf5fdcc50,
avail_mask = 0x40,
freed_mask = 0x3e,
last_idx = 0x6,
freeable = 0x1,
sizeclass = 0x3,
maplen = 0x0
}

可以发现,free掉的chunk不会优先分配

chunk在被free后不会清空user data域
title

增加到malloc 8次,free 7次

1
2
3
4
5
store('A', 'A')
for _ in range(5):
query('A' * 0x30)
query('A' * 0x30)
query('B' * 0x30)

avail_mask = 0x7c = 0b1111100被释放的chunk重新分配,也就是当耗尽该group的7个chunk时,堆管理器才会检查是否有已被free掉的chunk,将这些chunk的avail_mask置1,再重新分配。

1
2
3
4
5
6
7
8
9
10
11
12
gef➤  p *(struct meta*)0x5575a83401a8
$2 = {
prev = 0x5575a83401a8,
next = 0x5575a83401a8,
mem = 0x7f54fbdeec50,
avail_mask = 0x7c,
freed_mask = 0x2,
last_idx = 0x6,
freeable = 0x1,
sizeclass = 0x3,
maplen = 0x0
}

现在可以分配回先前已被释放的chunk,这样就有了uaf的利用机会。通过重新将带指针的结构体chunk分配回来,可leak出内存信息。
title

漏洞点(Hijack)

meta.h

1
2
3
4
5
6
7
8
9
10
11
12
//line:90~100
static inline void dequeue(struct meta **phead, struct meta *m)
{
if (m->next != m) {
m->prev->next = m->next;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0;
}

在审计源码时,可以发现这个经典的unsafe-unlink漏洞,跟早期glibc版本unlink宏出现的问题十分类似。

通过伪造fake meta,在删除该meta时,便会产生一次任意写,那么就有了劫持的机会。关于mooosl这道题的完整利用过程会在下篇文章中分析。

⬆︎TOP