首页
社区
课程
招聘
[原创]看雪CTF.TSRC 2018 团队赛 第十四题
发表于: 2018-12-29 11:13 6349

[原创]看雪CTF.TSRC 2018 团队赛 第十四题

2018-12-29 11:13
6349

一、初探

       我们看如果我们能够构造下面的条件就可以执行:

(*((__int64 (__fastcall **)(unsigned __int64))a1 + 28))(2 * v6 + 100);

     这里的a1,就为我们控制内存的起始地址。但是这里存在一个问题: 要求 ((2 * v6 + 100指向一个“/bin/sh"字符串,我们可以让其指向 libc中的‘/bin/sh’位置,但是这要有个前提  这个地址是由 2 * v6 + 100算出来的,因此需要 =‘/bin/sh’必须是偶数,不幸的是,本题敲好位于奇数位置:

     .rodata:000000000018CD57 2F 62 69 6E 2F 73 68 00             aBinSh          db '/bin/sh',0    

     因此不能使用 _IO_str_overflow,我们再看 _IO_str_finish函数汇编:

   

1、查看开启保护,  可以看到got表可写,其他的保护全开。
           
2、反编译程序
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  signed int v3; // ebx
  unsigned int v4; // eax
  void *pbuf; // rbp

  v3 = 5;
  sub_558ED32B1AB0();
  puts("echo from your heart");
  do
  {
    __printf_chk(1LL, "lens of your word: ");
    v4 = GetBufLenFromStdin();
    if ( v4 > 0x1000 )
    {
      puts("too long");
      exit(1);
    }
    pbuf = malloc(v4);
    __printf_chk(1LL, "word: ");
    gets(pbuf);
    __printf_chk(1LL, "echo: ");
    __printf_chk(1LL, pbuf);
    putchar(10);
    --v3;
  }
  while ( v3 );
  return 0LL;
}
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  signed int v3; // ebx
  unsigned int v4; // eax
  void *pbuf; // rbp

  v3 = 5;
  sub_558ED32B1AB0();
  puts("echo from your heart");
  do
  {
    __printf_chk(1LL, "lens of your word: ");
    v4 = GetBufLenFromStdin();
    if ( v4 > 0x1000 )
    {
      puts("too long");
      exit(1);
    }
    pbuf = malloc(v4);
    __printf_chk(1LL, "word: ");
    gets(pbuf);
    __printf_chk(1LL, "echo: ");
    __printf_chk(1LL, pbuf);
    putchar(10);
    --v3;
  }
  while ( v3 );
  return 0LL;
}
程序逻辑非常清晰:
a) 调用GetBufLenFromStdin函数获得要malloc的size;
程序逻辑非常清晰:
a) 调用GetBufLenFromStdin函数获得要malloc的size;
b)如果size超过0x1000,则退出程序;
c)调用gets函数从终端获取数据;
d)调用__printf_chk打印gets获取数据;
e)整个流程执行5次后退出。
发现存在2个漏洞:
1、调用gets函数获取用户数据没有对输入的范围做限定,造成可以写任意数据到堆中,包括改写top trunk的size;
2、存在格式化字符漏洞,可以泄露主程序和libc的基址。
三、hourse of orange
这是一个典型的hourse of orange 漏洞。这个漏洞的具体介绍就不细说了。但是本题需要考虑以下问题:
1、 一般情况 hourse of orange需要获得libc基址以及堆基址。我们通过字符串格式化漏洞可以获得libc基址,但是无法拿到堆基址。虽然 hourse of orange也能泄露 libc基址以及堆基址,但是gets函数遇到回车符后会将回车清0,从而让字符串断链,无法将后面的相应地址读出来。也就无法获得堆地址。
2、 hourse of orange要求当将我们能对已经变成 "unsort bin"的内存控制权。

为了能拿到shell,我们就需要程序在某一时刻能够执行类似于 call system("/bin/sh") 。或者执行类似于 call [rax+0xxx]的代码,而 call [rax+0xxx ]指向system,
 hourse of orange 主要的目的是通过某种构造使程序执行 call [rax+0xxx ]。
发现存在2个漏洞:
1、调用gets函数获取用户数据没有对输入的范围做限定,造成可以写任意数据到堆中,包括改写top trunk的size;
2、存在格式化字符漏洞,可以泄露主程序和libc的基址。
这是一个典型的hourse of orange 漏洞。这个漏洞的具体介绍就不细说了。但是本题需要考虑以下问题:
1、 一般情况 hourse of orange需要获得libc基址以及堆基址。我们通过字符串格式化漏洞可以获得libc基址,但是无法拿到堆基址。虽然 hourse of orange也能泄露 libc基址以及堆基址,但是gets函数遇到回车符后会将回车清0,从而让字符串断链,无法将后面的相应地址读出来。也就无法获得堆地址。
2、 hourse of orange要求当将我们能对已经变成 "unsort bin"的内存控制权。

为了能拿到shell,我们就需要程序在某一时刻能够执行类似于 call system("/bin/sh") 。或者执行类似于 call [rax+0xxx]的代码,而 call [rax+0xxx ]指向system,
        整个漏洞利用包括如下过程:
        1、先申请一块空间,然后输入 '%016lx %016lx %016lx %016lx %016lx %016lx %016lx'  拿到libc基址;
        2、再申请一块空间,同时输入超过空间大小的数据造成堆溢出,并将top trunk的size改写,使其小于0x1000;
        3、再次申请大于top trunk大小的空间,此时会将 top trunk 加入到unsort bin中,变为old trunk;
        4、再次申请小于 old trunk 的空间,此时申请到的空间会从 unsort bin中拿掉,但剩余的部分仍然变成 unsort bin,注意在此时我们对新申请的空间写入的数据超过其大小,就会覆盖剩余的 unsort bin ,从而获得 已经变成 "unsort bin"的内存控制权 。
        5、当我们拿到 "unsort bin"的内存控制权 后,就可以精心构造相应数据,来达到我们的效果。
        6、当下次再进行内存分配时(一般大于此时 "unsort bin"的大小),此时系统会将其从unsort bin拆除,并根据其大小插入到相应的bins中。我们通过步骤5构造的数据,如果能够让malloc发现数据非法,从而产生如下调用
        malloc_printerr->__libc_message -> abort() ->_IO_flush_all_lockp,而在 _IO_flush_all_lockp中会遍历_IO_list_all全局变量指向的地址(为_IO_FILE_plus类型结构,如果 _IO_list_all的数据满足某些条件是,会调用本结构中的某个虚函数表的某个函数。如果能够改写 _IO_list_all地址内容,让其变成 unsort bin 链表地址,当发现异常时, _IO_flush_all_lockp就会将 unsort bin内存当做_IO_FILE_plus结构进行遍历,如果能够满足条件就可以达到 call [rax+0xxx ]这样的条件,让 [rax+0xxx ] = system地址,同时使其的第一个参数执行字符串"/bin/sh"。
        7、条件构造:   
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
	{
	  /* Something was added to the list.  Start all over again.  */
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}
int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  struct _IO_FILE *fp;
  int last_stamp;

#ifdef _IO_MTSAFE_IO
  __libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
  if (do_lock)
    _IO_lock_lock (list_all_lock);
#endif

  last_stamp = _IO_list_all_stamp;
  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;

      if (last_stamp != _IO_list_all_stamp)
	{
	  /* Something was added to the list.  Start all over again.  */
	  fp = (_IO_FILE *) _IO_list_all;
	  last_stamp = _IO_list_all_stamp;
	}
      else
	fp = fp->_chain;
    }

#ifdef _IO_MTSAFE_IO
  if (do_lock)
    _IO_lock_unlock (list_all_lock);
  __libc_cleanup_region_end (0);
#endif

  return result;
}
 从上面的源码中可以看到,其中如果能够让流程走的IO_OVERFLOW (fp, EOF) == EOF)这里,就初步达到目的
 从上面的源码中可以看到,其中如果能够让流程走的IO_OVERFLOW (fp, EOF) == EOF)这里,就初步达到目的
  if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
  if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
  if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
#endif
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
我们看反汇编代码会更明显:   
    if ( *((_DWORD *)v4 + 48) <= 0 )
      {
        if ( v4[5] <= v4[4] )
          goto LABEL_29;
      }
      else if ( *(_QWORD *)(v4[20] + 0x20LL) <= *(_QWORD *)(v4[20] + 24LL) )
      {
        goto LABEL_29;
      }
      if ( (*(unsigned int (__fastcall **)(_QWORD *, signed __int64))(v4[27] + 24LL))(v4, 0xFFFFFFFFLL) == -1 )
        v7 = -1;
             
         我们看其会执行v4[27] + 24 指向的地址,其中v4为我们 unsort bin(已被加入到smallbin[4]位置)的地址。如果我们将 v4[27]的内容执行一片我们能写的区域,然后将其_IO_str_jumps位置写入system地址,而此时输入的参数仍然是v4,我们将v4的起始地址写入'/bin/sh',从而就达到我们目的拿到shell。但是不幸的我们只能控制堆中的数据,而堆的地址却无法获取到。在这种情况下我们需要找一个代理函数来达到这个目的。libc中存在这样的虚函数表结构

const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_str_finish),
  JUMP_INIT(overflow, _IO_str_overflow),
  JUMP_INIT(underflow, _IO_str_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_str_pbackfail),
  JUMP_INIT(xsputn, _IO_default_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_str_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_default_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};
    if ( *((_DWORD *)v4 + 48) <= 0 )
      {
        if ( v4[5] <= v4[4] )
          goto LABEL_29;
      }
      else if ( *(_QWORD *)(v4[20] + 0x20LL) <= *(_QWORD *)(v4[20] + 24LL) )
      {
        goto LABEL_29;
      }
      if ( (*(unsigned int (__fastcall **)(_QWORD *, signed __int64))(v4[27] + 24LL))(v4, 0xFFFFFFFFLL) == -1 )
        v7 = -1;
             
         我们看其会执行v4[27] + 24 指向的地址,其中v4为我们 unsort bin(已被加入到smallbin[4]位置)的地址。如果我们将 v4[27]的内容执行一片我们能写的区域,然后将其_IO_str_jumps位置写入system地址,而此时输入的参数仍然是v4,我们将v4的起始地址写入'/bin/sh',从而就达到我们目的拿到shell。但是不幸的我们只能控制堆中的数据,而堆的地址却无法获取到。在这种情况下我们需要找一个代理函数来达到这个目的。libc中存在这样的虚函数表结构
最后于 2019-1-11 19:15 被kanxue编辑 ,原因:
收藏
免费 5
支持
分享
最新回复 (2)
雪    币: 213
活跃值: (5184)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
wsc
2
非常赞
2018-12-29 15:56
0
雪    币: 11716
活跃值: (133)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
3
 #寻宝大战#祝看雪19岁快乐!
2019-1-11 20:05
0
游客
登录 | 注册 方可回帖
返回
//