dig into musl

经历一星期的musl源码审计,和拜读大神们的文章,进行个人的musl总结

1.数据结构

1.malloc_context

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的合法性
#ifndef PAGESIZE
size_t pagesize;//0x1000
#endif
int init_done;//是否初始化
unsigned mmap_counter;//用mmap开辟空间的数量
struct meta *free_meta_head;//free掉的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];//active的meta链表头指针的数组
size_t usage_by_class[48];//对应大小meta的group管理的chunk数
uint8_t unmap_seq[32], bounces[32];
uint8_t seq;
uintptr_t brk;
};

2.meta_area

1
2
3
4
5
6
struct meta_area {
uint64_t check;//与secret对应,检查meta_area的合法性
struct meta_area *next;//下一个meta_area
int nslots;//管理的meta数量
struct meta slots[];
};

3.meta

1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next;//meta链表指针
struct group *mem;//管理的group指针
volatile int avail_mask, freed_mask;//bitmap,用一个bit表示是与否
uintptr_t last_idx:5;//占5bit,表示最后一个chunk的索引(从左往右数)
uintptr_t freeable:1;//占1bit,表示是否可以free
uintptr_t sizeclass:6;//占6bit,表示此meta对应chunk的大小索引
uintptr_t maplen:8*sizeof(uintptr_t)-12;//如果是由mmap分配的,则为内存页数,否则为0
};

4.group

1
2
3
4
5
6
struct group {
struct meta *meta;//对应meta指针
unsigned char active_idx:5;//5bit,还多少chunk可用
char pad[UNIT - sizeof(struct meta *) - 1];//对齐0x10byte
unsigned char storage[];
};

5.chunk

1
2
3
4
5
6
struct chunk{
char prev_user_data[];
uint8_t idx; //低5bit为idx第几个chunk
uint16_t offset; //group_addr = chunk_addr - offset*0x10 - 0x10
char data[];
};

2.利用思路

在nontrivial_free函数中存在dequeue函数

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;
m->next->prev = m->prev;
if (*phead == m) *phead = m->next;
} else {
*phead = 0;
}
m->prev = m->next = 0; // 清理m(meta)的头尾指针
}

存在next指针和prev指针互写,且没有检查

free()–>nontrival_free()–>dequeue()

我们需要伪造chunk,甚至group,meta等等才能实现两个地址互写.

并且想要到达dequeue()函数,也要经过一系列检查.


free中调用的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
30
(/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);
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);
assert(area->check == ctx.secret);
if (meta->sizeclass < 48) {
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;
}

1.meta->mem == base : 检查meta的mem指针是否是原来通过group->meta找到自己的group,即检查双向闭合

2.index <= meta->last_idx : 检查idx的合法性

3.area->check == ctx.secret : 检查area的合法性

4.offset >= size_classes[meta->sizeclass]*index

5.offset < size_classes[meta->sizeclass]*(index+1) : 检查offset和chunk大小是否匹配

6.assert(offset <= meta->maplen*4096UL/UNIT - 1) : 检查offset是否越界


然后再看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
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)) {
// any multi-slot group is necessarily on an active list
// here, but single-slot groups might or might not be.
if (g->next) {
assert(sc < 48);
int activate_new = (ctx.active[sc]==g);
dequeue(&ctx.active[sc], g);
if (activate_new && ctx.active[sc])
activate_group(ctx.active[sc]);
}
return free_group(g);
} else if (!mask) {
assert(sc < 48);
// 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 };
}

这里要求mask+self == (2u<last_idx)-1 && okay_to_free(g), 因此要合理设置meta的两个mask的值

(1) avail_mask 表示只有一个chunk 被使用 ,freed_mask=0,free这个chunk

(2) avail_mask=0, freed_mask 表示只有1个chunk没被释放,即当前这个chunk没释放,free这个chunk

tips: activate_group(ctx.active[sc])建议不要让这条语句执行,因为它在dequeue之后,相当于把next指针的区域链入active[]并且还要activate这个区域,但是这个区域一般是stdout_used是无法控制,来伪造成meta,所以很可能会报错. 所以为了避免执行,我们把sc改掉即sizeclass,使得ctx.active[sc]!=g,activate_new为0.


之后调用free_group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static struct mapinfo free_group(struct meta *g)
{
struct mapinfo mi = { 0 };
int sc = g->sizeclass;
if (sc < 48) {
ctx.usage_by_class[sc] -= g->last_idx+1;
}
if (g->maplen) {
step_seq();
record_seq(sc);
mi.base = g->mem;
mi.len = g->maplen*4096UL;
} else {
void *p = g->mem;
struct meta *m = get_meta(p);
int idx = get_slot_index(p);
g->mem->meta = 0;
// not checking size/reserved here; it's intentionally invalid
mi = nontrivial_free(m, idx);
}
free_meta(g);
return mi;
}

我们不能进入else分支,这样会再一次调用nontrivial_free,因此要使得maplen不为0


3.控制程序流

想控制musl题的程序流,一般都是打IO

IO_FILE

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
struct _IO_FILE {
unsigned flags;
unsigned char *rpos, *rend;
int (*close)(FILE *);
unsigned char *wend, *wpos;
unsigned char *mustbezero_1;
unsigned char *wbase;
size_t (*read)(FILE *, unsigned char *, size_t);
size_t (*write)(FILE *, const unsigned char *, size_t);
off_t (*seek)(FILE *, off_t, int);
unsigned char *buf;
size_t buf_size;
FILE *prev, *next;
int fd;
int pipe_pid;
long lockcount;
int mode;
volatile int lock;
int lbf;
void *cookie;
off_t off;
char *getln_buf;
void *mustbezero_2;
unsigned char *shend;
off_t shlim, shcnt;
FILE *prev_locked, *next_locked;
struct __locale_struct *locale;
};

IO_FILE结构体中有4个函数指针,一般利用stdinstdoutstderr

exit()调用链

1
2
3
4
5
6
7
8
_Noreturn void exit(int code)
{
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}

1
2
3
4
5
6
7
8
void __stdio_exit(void)
{
FILE *f;
for (f=*__ofl_lock(); f; f=f->next) close_file(f);
close_file(__stdin_used);
close_file(__stdout_used);
close_file(__stderr_used);
}
1
2
3
4
5
6
7
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);
}

在close_file函数中当 f->wpos != f->wbase 就会调用f->write函数

1.getshell

1.利用dequeue函数,将可控地址写到stdout_used(__stdout_FILE结构体链表头)中

2.再在FILE结构体头几字节写入”/bin/sh”

3.再修改write指针为system,

4.以及f->wpos != f->wbase

5.触发exit()

2.orw

maigic_gadget

1
mov rsp, qword ptr [rdi + 0x30] ; jmp qword ptr [rdi + 0x38]

1.利用dequeue函数,将可控地址写到stdout_used(__stdout_FILE结构体链表头)中

2.再rdi+0x30也就是FILE+0x30写入orw的首地址,在rdi+0x38(也是f->wbase)处写入ret

3.再在write指针处写入magic_gagdet

4.构造好orw链

5.触发exit()


reference

musl 1.2.2 总结+源码分析

musl pwn 入门 (1)

musl pwn 入门 (2)

musl pwn 入门 (3)