行业新闻

从广东省强网杯——girlfriend中看realloc的艺术

从广东省强网杯——girlfriend中看realloc的艺术

很早就得知realloc一些特殊的作用,但一直以来都是硬套,导致有时候并不能很快的反应过来(太菜了…),所以在赛后对realloc做个整理,如果感兴趣的话就一起看下去吧!

realloc分析

首先先通过源码来进行分析,下面的libc-2.27realloc的源码,此函数位于glibc-2.27-master\malloc\malloc.c,为了更好理解删除了一些代码:

void *
__libc_realloc (void *oldmem, size_t bytes)
{
  mstate ar_ptr;
  INTERNAL_SIZE_T nb;         /* padded request size */

  void *newp;             /* chunk to return */

  void *(*hook) (void *, size_t, const void *) =
    atomic_forced_read (__realloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));

#if REALLOC_ZERO_BYTES_FREES
  if (bytes == 0 && oldmem != NULL)
    {
      __libc_free (oldmem); return 0;
    }
#endif

  /* realloc of null is supposed to be same as malloc */
  if (oldmem == 0)
    return __libc_malloc (bytes);

  /* chunk corresponding to oldmem */
  const mchunkptr oldp = mem2chunk (oldmem);
  /* its size */
  const INTERNAL_SIZE_T oldsize = chunksize (oldp);

  if (chunk_is_mmapped (oldp))
    ar_ptr = NULL;
  else
    {
      MAYBE_INIT_TCACHE ();
      ar_ptr = arena_for_chunk (oldp);
    }

  if ((__builtin_expect ((uintptr_t) oldp > (uintptr_t) -oldsize, 0)
       || __builtin_expect (misaligned_chunk (oldp), 0))
      && !DUMPED_MAIN_ARENA_CHUNK (oldp))
      malloc_printerr ("realloc(): invalid pointer");

  checked_request2size (bytes, nb);

  if (chunk_is_mmapped (oldp))
    {
      /* If this is a faked mmapped chunk from the dumped main arena,
     always make a copy (and do not free the old chunk).  */
      if (DUMPED_MAIN_ARENA_CHUNK (oldp))
    {
      /* Must alloc, copy, free. */
      void *newmem = __libc_malloc (bytes);
      if (newmem == 0)
        return NULL;
      /* Copy as many bytes as are available from the old chunk
         and fit into the new size.  NB: The overhead for faked
         mmapped chunks is only SIZE_SZ, not 2 * SIZE_SZ as for
         regular mmapped chunks.  */
      if (bytes > oldsize - SIZE_SZ)
        bytes = oldsize - SIZE_SZ;
      memcpy (newmem, oldmem, bytes);
      return newmem;
    }

      void *newmem;
      /* Note the extra SIZE_SZ overhead. */
      if (oldsize - SIZE_SZ >= nb)
        return oldmem;                         /* do nothing */

      /* Must alloc, copy, free. */
      newmem = __libc_malloc (bytes);
      if (newmem == 0)
        return 0;              /* propagate failure */

      memcpy (newmem, oldmem, oldsize - 2 * SIZE_SZ);
      munmap_chunk (oldp);
      return newmem;
    }

  if (SINGLE_THREAD_P)
    {
      newp = _int_realloc (ar_ptr, oldp, oldsize, nb);
      assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
          ar_ptr == arena_for_chunk (mem2chunk (newp)));

      return newp;
    }

  __libc_lock_lock (ar_ptr->mutex);

  newp = _int_realloc (ar_ptr, oldp, oldsize, nb);

  __libc_lock_unlock (ar_ptr->mutex);
  assert (!newp || chunk_is_mmapped (mem2chunk (newp)) ||
          ar_ptr == arena_for_chunk (mem2chunk (newp)));

  if (newp == NULL)
    {
      /* Try harder to allocate memory in other arenas.  */
      LIBC_PROBE (memory_realloc_retry, 2, bytes, oldmem);
      newp = __libc_malloc (bytes);
      if (newp != NULL)
        {
          memcpy (newp, oldmem, oldsize - SIZE_SZ);
          _int_free (ar_ptr, oldp, 0);
        }
    }

  return newp;
}
libc_hidden_def (__libc_realloc)

它和mallocfree都有同样的特性,就是当xxx_hook不为空的时候去执行存放在xxx_hook里面的函数,熟悉realloc调整栈帧的师傅应该都明白这个,并且还利用了__builtin_expect来优化,具体可以看下面的链接:

__builtin_expect详解

  void *(*hook) (void *, size_t, const void *) =
    atomic_forced_read (__realloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    return (*hook)(oldmem, bytes, RETURN_ADDRESS (0));

下面的代码就是它的特性之一,查找定义得知#define REALLOC_ZERO_BYTES_FREES 1,所以当bytes(也就是size)为0,且oldmem不为空的时候就执行__libc_free,同时return 0

#if REALLOC_ZERO_BYTES_FREES
  if (bytes == 0 && oldmem != NULL)
    {
      __libc_free (oldmem); return 0;
    }
#endif

__libc_free先进行一些检查,之后就进入_int_free里面,也就是平常所说到的free函数,也就是说realloc在某些特性情况下是可以充当free函数来使用的!其实也很好理解,就是重新调整堆块的大小为0,那既然堆块的大小都为0了,不就等于free了吗?

void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */

  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }

  if (mem == 0)                              /* free(0) has no effect */
    return;

  p = mem2chunk (mem);

  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      /* See if the dynamic brk/mmap threshold needs adjusting.
     Dumped fake mmapped chunks do not affect the threshold.  */
      if (!mp_.no_dyn_threshold
          && chunksize_nomask (p) > mp_.mmap_threshold
          && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
      && !DUMPED_MAIN_ARENA_CHUNK (p))
        {
          mp_.mmap_threshold = chunksize (p);
          mp_.trim_threshold = 2 * mp_.mmap_threshold;
          LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                      mp_.mmap_threshold, mp_.trim_threshold);
        }
      munmap_chunk (p);
      return;
    }

  MAYBE_INIT_TCACHE ();

  ar_ptr = arena_for_chunk (p);
  _int_free (ar_ptr, p, 0);
}

通过一个demo来加深一下印象

#include <stdio.h>
#include <stdlib.h>

int main(void){
    void *p;
    p = malloc(0x10);
    realloc(p,0);
    return 0;
}
//gcc realloc.c -o realloc -no-pie

gdbmain函数下断点之后单步走完call malloc@plt <malloc@plt>,可以看到正常的创建了一个堆块:

再往下走完call realloc@plt <realloc@plt>,再次查看的时候,它已经被free了:

往下走就是另一个特性,当oldmem为0,就调用__libc_malloc,也就是平时所说的malloc函数的入口:

if (oldmem == 0)
    return __libc_malloc (bytes);

稍微改一下上面的demo来重新验证一下:

#include <stdio.h>
#include <stdlib.h>

int main(void){

    void *p,*q;
    p = malloc(0x10);
    realloc(q,0x10);
    return 0;
}
//gcc realloc.c -o realloc -no-pie

调试过程和上面的一样,结果同样没有任何毛病:

再往下就是mmap的分配方式,本文重点不在此出,直接看下面的_int_realloc

newp = _int_realloc (ar_ptr, oldp, oldsize, nb);

下面是 _int_realloc的源码,已经做好了注释:

void*
_int_realloc(mstate av, mchunkptr oldp, INTERNAL_SIZE_T oldsize,
         INTERNAL_SIZE_T nb)
{
  //首先是检查oldmem size和next chunk的大小是否合法
  if (__builtin_expect (chunksize_nomask (oldp) <= 2 * SIZE_SZ, 0)
      || __builtin_expect (oldsize >= av->system_mem, 0))
    malloc_printerr ("realloc(): invalid old size");

  check_inuse_chunk (av, oldp);

  assert (!chunk_is_mmapped (oldp));

  next = chunk_at_offset (oldp, oldsize);
  INTERNAL_SIZE_T nextsize = chunksize (next);
  if (__builtin_expect (chunksize_nomask (next) <= 2 * SIZE_SZ, 0)
      || __builtin_expect (nextsize >= av->system_mem, 0))
    malloc_printerr ("realloc(): invalid next size");

  //判断oldsize是不是已经大于要分配的
  if ((unsigned long) (oldsize) >= (unsigned long) (nb))
    {
      newp = oldp;
      newsize = oldsize;
    }

  else
    {
      //如果堆块和top chunk相邻,就直接从top chunk中分割出来
      if (next == av->top &&
          (unsigned long) (newsize = oldsize + nextsize) >=
          (unsigned long) (nb + MINSIZE))
        {
          set_head_size (oldp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
          av->top = chunk_at_offset (oldp, nb);
          set_head (av->top, (newsize - nb) | PREV_INUSE);
          check_inuse_chunk (av, oldp);
          return chunk2mem (oldp);
        }

      //如果下一个堆块是空闲的且大小合适,就直接将它两个合并成一个大的堆块
      else if (next != av->top &&
               !inuse (next) &&
               (unsigned long) (newsize = oldsize + nextsize) >=
               (unsigned long) (nb))
        {
          newp = oldp;
          unlink (av, next, bck, fwd);
        }

      /*如果又没有和top chunk相邻,下一个堆块也不合适,就直接malloc出来一个,并将原堆块的内容复制过去,最后把原来的堆块free掉*/
      else
        {
          newmem = _int_malloc (av, nb - MALLOC_ALIGN_MASK);
          if (newmem == 0)
            return 0; /* propagate failure */

          newp = mem2chunk (newmem);
          newsize = chunksize (newp);

          /*
             Avoid copy if newp is next chunk after oldp.
          */
          if (newp == next)
            {
              newsize += oldsize;
              newp = oldp;
            }
          else
            {
              copysize = oldsize - SIZE_SZ;
              s = (INTERNAL_SIZE_T *) (chunk2mem (oldp));
              d = (INTERNAL_SIZE_T *) (newmem);
              ncopies = copysize / sizeof (INTERNAL_SIZE_T);
              assert (ncopies >= 3);

              if (ncopies > 9)
                memcpy (d, s, copysize);

              else
                {
                  *(d + 0) = *(s + 0);
                  *(d + 1) = *(s + 1);
                  *(d + 2) = *(s + 2);
                  if (ncopies > 4)
                    {
                      *(d + 3) = *(s + 3);
                      *(d + 4) = *(s + 4);
                      if (ncopies > 6)
                        {
                          *(d + 5) = *(s + 5);
                          *(d + 6) = *(s + 6);
                          if (ncopies > 8)
                            {
                              *(d + 7) = *(s + 7);
                              *(d + 8) = *(s + 8);
                            }
                        }
                    }
                }

              _int_free (av, oldp, 1);
              check_inuse_chunk (av, newp);
              return chunk2mem (newp);
            }
        }
    }
  //整理一些额外的空间

  assert ((unsigned long) (newsize) >= (unsigned long) (nb));

  remainder_size = newsize - nb;

  if (remainder_size < MINSIZE)   /* not enough extra to split off */
    {
      set_head_size (newp, newsize | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_inuse_bit_at_offset (newp, newsize);
    }
  else   /* split remainder */
    {
      remainder = chunk_at_offset (newp, nb);
      set_head_size (newp, nb | (av != &main_arena ? NON_MAIN_ARENA : 0));
      set_head (remainder, remainder_size | PREV_INUSE |
                (av != &main_arena ? NON_MAIN_ARENA : 0));
      /* Mark remainder as inuse so free() won't complain */
      set_inuse_bit_at_offset (remainder, remainder_size);
      _int_free (av, remainder, 1);
    }

  check_inuse_chunk (av, newp);
  return chunk2mem (newp);
}

我们来重点看一下这一段代码:

//如果下一个堆块是空闲的且大小合适,就直接将它两个合并成一个大的堆块
else if (next != av->top &&
         !inuse (next) &&
         (unsigned long) (newsize = oldsize + nextsize) >=
         (unsigned long) (nb))
{
    newp = oldp;
    unlink (av, next, bck, fwd);
}

例题就是利用这点来做堆叠的,为什么这么说呢?思考如下场景,如果chunk1存在堆溢出可以修改到chunk2size,在这里假设改成0x221,那么下次再申请0x200的大小的时候,chunk2的堆块就会被申请到,但是此时的chunk2size已经变成了0x220,也就是说chunk2里面还有0x20大小没分配出去,所以当我再次去扩大它的大小的时候就会像上面的那段代码一样直接将下一个堆块合并,也就是吞并了下一个堆块0x20的大小,就拥有了修改它fd指针的能力!

如果还不是很明白,再通过几张图片来巩固一下:

  1. 首先修改chunk2size为0x221,它将把下一个chunkhead包括进去:

2.申请0x200的大小:

3.对chunk2进行扩充:

下面是最后一个特性,当对newp没有成功的扩充的时候(即bin中没有合适的堆块),会直接重新分配一个新的堆块,并将原来堆块的内容复制到新的堆块当中,再将原来的堆块free

newp = _int_realloc (ar_ptr, oldp, oldsize, nb);
...
if (newp == NULL)
    {
      LIBC_PROBE (memory_realloc_retry, 2, bytes, oldmem);
      newp = __libc_malloc (bytes);
      if (newp != NULL)
        {
          memcpy (newp, oldmem, oldsize - SIZE_SZ);
          _int_free (ar_ptr, oldp, 0);
     }
}

小结

  • size为0,就等于free()函数,同时返回值为NULL
  • 当指针为0,size大于0,就等于malloc()函数
  • size小于等于原来的size,则在原堆块上缩小,多余的大小free()
  • size大于原来的size,如果bin中有多余的堆块就进行扩充,没有多余的堆块则重新分配新的堆块,并将内容复制到新的堆块中,然后再将原来的堆块free()

 

广东省强网杯—girlfriend

基本分析

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled
FORTIFY:  Enabled

保护全开,同时还开启了FORTIFY来检测格式化字符串漏洞,只能打xxx_hookgetshell或者orw

IDA分析

IDA打开,满满的全是画指令,去除方法如下:

选中下面的画指令的开头,选择菜单Edit -> Patch program -> Assamble,将下面的画指令全部改成nop,之后回到函数的开头,对着函数名按下up,也就是让IDA重新分析此处,其他地方如法炮制,全部修改完成之后就可以正常的f5啦!(有些地方是需要重新选中再进行重新分析的,也就是从它开始到sub_xxx endp的地方)

call    $+5
add     [rsp+18h+var_18], 6
retn

修复完之后,开头就是沙箱,那基本思路就定调了,就是泄露libc->劫持xxx_hooksetcontext+53进行栈迁移,最后再进行orw!(为了阅读方便已经为部分函数重命名了)

__int64 seccomp()
{
  __int64 v1; // [rsp+8h] [rbp-8h]

  v1 = seccomp_init(2147418112LL);
  seccomp_rule_add(v1, 0LL, 59LL, 0LL);
  return seccomp_load(v1);
}

来到下一个函数,此函数询问Do you have grilfriend ?并给出两个选项,先来看看v1 == N里面的函数:

int main_funtion()
{
  int result; // eax
  int v1; // eax
  int i; // [rsp+8h] [rbp-8h]

  puts("============================");
  puts("====welcome to the game=====");
  result = puts("============================");
  for ( i = 0; i <= 1; ++i )
  {
    puts("\n");
    puts("Do you have grilfriend ? ");
    puts("\n");
    v1 = read_0();
    if ( v1 == 'N' )
    {
      puts("what a bad mood++++");
      result = backdoor();
    }
    else
    {
      if ( v1 != 'Y' )
      {
        puts("invaild");
        exit(0);
      }
      chunk_prt = malloc(0x200uLL);
      puts("please buy gifts for her+++++");
      result = heap();
    }
  }
  return result;
}

可以看到里面存在格式化字符串漏洞,之前讲到它开启了FORTIFY来检测,但是还是有办法绕过的!

unsigned __int64 sub_EA6()
{
  char buf[24]; // [rsp+0h] [rbp-20h] BYREF
  unsigned __int64 v2; // [rsp+18h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("why ? reason");
  read(0, buf, 5uLL);
  __printf_chk(1LL, buf, 0LL, 0LL, 0LL, 0LL);
  puts("new a grilfriend");
  heap();
  return __readfsqword(0x28u) ^ v2;
}

先看看FORTIFY是怎么样来保护的:

就是说定义此宏会导致执行一些轻量级检查,以在使用各种字符串和内存操作函数(例如memcpymemsetstpcpystrcpystrncpystrcatstrncatsprintfsnprintfvsprintfvsnprintfgets、 及其宽字符变体)时检测一些缓冲区溢出错误

比如printf就变成了printf_chk,它将可以检查格式化字符串漏洞的特殊字符,就像下面这样

但它真的不能被利用吗?答案肯定是:否!既然不能用%x$p,那%p呢,实验之后确实可以,但它才往buf里面读入5个字节,也就是意味着只能输入%p%p就不能再输入了,尝试之后并不能泄露任何地址(泄露出两个nul

既然它不限制%(),拿其他的试试,直到%a,泄露出了下面的内容,!成功获得libc

之后便进入heap()也就是堆的菜单,除了New Paperexit之外还有两个选项,这个待会再看:

__int64 heap()
{
  __int64 result; // rax

  while ( 1 )
  {
    while ( 1 )
    {
      meun();
      // puts("1. New Paper");
      // puts("2. exit");
      result = (unsigned int)read_0();
      if ( (_DWORD)result == 2 )
        exit(0);
      if ( (int)result > 2 )
        break;
      if ( (_DWORD)result != 1 )
        goto LABEL_11;
      alloc();
    }
    if ( (_DWORD)result == 3 )
      return result;
    if ( (_DWORD)result == 4 )
      show();
    else
LABEL_11:
      puts("invaild");
  }
}

exit就没什么好看的了,看看New Paper,可以获得到的信息是:

  • 只能分配16个堆块,也就是说得再16次以内完成堆叠,实现修改fd指针
  • chunksize受限,不过问题不是很大,但需要注意的是有个!chunk_ptr,按照刚刚的路走过来并没有分配堆块会导致退出:

  • 输入的sizesize + 1,存在人为off-by-one漏洞
__int64 alloc()
{
  __int64 result; // rax
  int size; // [rsp+14h] [rbp-4h]

  result = chunk_num;
  if ( chunk_num <= 16 )    
  {
    puts("size");
    size = read_0();
    if ( size > 768 || size < 0 || !chunk_prt )        
      exit(0);
    *chunk_prt = realloc(*chunk_prt, size);
    if ( *chunk_prt )
    {
      if ( size )
      {
        puts("data");
        read(0, *chunk_prt, size + 1);
      }
    }
    result = ++chunk_num;
  }
  return result;
}

通过上面的信息会发现如果题目在询问Do you have grilfriend ?的时候选择N(78的ascii码)是不能正常进入题目的,出题人还是很好心的

CTF

关闭