【技術分享】借助DefCon Quals 2021的mooosl學習musl mallocng(源碼審計篇)

關于musl libc的資料比賽期間找到過一篇從一次 CTF 出題談 musl libc 堆漏洞利用,礙于musl libc在1.2.x之后的堆管理機制有較大的改版,因而有了該文章。本次文章分上下兩篇,從musl libc 1.2.2的源碼審計、調試,以及其中的利用機會,再到mooosl這道題的解題過程做一個分析。
musl libc 1.2.2的源碼可以從[此處],(https://musl.libc.org/releases/musl-1.2.2.tar.gz)下載獲得。1.2.x采用src/malloc/mallocng內的代碼,其堆管理結構與早期版本幾乎完全不同,而早期的堆管理器則放入了src/malloc/oldmalloc中。

調試帶符號的musl libc
0x01源碼編譯
題目提供的libc.so不帶符號,很難通過調試去理解musl堆管理器的數據結構,可以通過源碼編譯,生成一份帶調試符號的libc.so,進行源碼級debug。
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.deb、musl-dbgsym_1.2.2-1_amd64.ddeb
在ubuntu20.04安裝
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
//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< assert(!(meta->freed_mask & (1u< 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:
//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
//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<2u<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進行分析。
分配釋放
store('a0a0', 'b0b0')
store('a1a11', 'b1b1111')
delete('a0a0')
__malloc_context是musl libc的全局管理結構指針,存放在libc.so的bss段
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 ,
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 times>},
usage_by_class = {0x1e, 0x0, 0x0, 0x7, 0x0 times>},
unmap_seq = '\000' times>,
bounces = '\000' 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。

musl libc用以下的結構管理著meta、group以及chunk

分配了兩個0x30的chunk,未釋放。
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
}
prev和next都指向本身,表示只有一個meta頁,meta頁由一個雙向鏈表進行維護;
0x7f79e1df5c50是user data域;
avail_mask = 0x7c = 0b1111100表示第0、1個chunk不可用(已經被使用);
freed_mask = 0x0表示沒有chunk被釋放;
last_idx = 0x6表示最后一個chunk的下標是0x6,總數是0x7個
sizeclass = 0x3表示由0x3這個group進行管理。

0x000056042ee901a8指向meta結構的地址;
后面8個byte表示chunk的頭部結構:
0x0000和0x0001表示當前chunk,距離group首地址0x00007f79e1df5c58的偏移為0和0x40;
0xa0和0xa1表示當前chunk是group中的第0和1個chunk;
再往后0x28個byte就是user data域,最多接收輸入0x28+4個byte,占用下一個chunk的前4個byte。
同時,也分配了四個0x10的chunk,未釋放
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
}
prev和next都指向本身,表示只有一個meta頁,meta頁由一個雙向鏈表進行維護;
0x56042db99c50是user data域;
avail_mask = 0x3ffffff0 = 0b111111111111111111111111110000表示第0、1、2、3個chunk不可用(已經被使用);
freed_mask = 0x0表示沒有chunk被釋放;
last_idx = 0x1d表示最后一個chunk的下標是0x1d,總數是0x1e個
sizeclass = 0x3表示由0x3這個group進行管理。

0x0000、0x0001、0x0002、0x0003表示距離group首地址偏移為0、0x10、0x20、0x30byte;
0xa0、0xa1、0xa2、0xa3表示group中的chunk下標;
往后8byte是user data,user data最多接收輸入8+4個byte,占用下一個chunk header的前4個byte(與x86的glibc類似)
釋放兩個0x10的chunk
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仍然為不可分配的狀態;

已釋放的chunk會將chunk header的offset清零,并且將chunk下標置成0xff,不清空user data域。
釋放一個0x30的chunk
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。

同樣,chunk header的offset清零,且chunk下標置0xff。
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大小:UNITsize_classes = 0x10 0x3 = 0x30
設定該group最多有多少個chunk:ctx.usage_by_class[sc] = 30 = 0x1e
漏洞點(Info Leak)
0x30 chunk, malloc 6次,free 5次
store('A', 'A')
for _ in range(5):
query('A' * 0x30)
avail_mask = 0x40 = 0b1000000除了最后一個chunk,其余chunk不可分配;
freed_mask = 0x3e = 0b111110除第一個以及最后一個chunk,其余chunk已被釋放
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域

增加到malloc 8次,free 7次
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,再重新分配。
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出內存信息。

漏洞點(Hijack)
meta.h
//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這道題的完整利用過程會在下篇文章中分析。