利用_IO_2_1_stdout_泄露libc前置知识stdin/stdout/stderr在 Linux 系统中“标准流”Standard Streams是程序与外部环境进行数据交互的核心机制标准流主要包括以下三种类型标准输入stdin这是程序或shell接收数据的通道。通常情况下当你在命令行中输入内容时就是通过标准输入将这些内容传递给程序的。例如当你输入ls命令来列出目录内容时这个命令就是通过标准输入传递给系统的。标准输出stdout这是用于输出程序运行的正常结果的通道。继续以ls命令为例该命令执行后列出的目录内容就是通过标准输出显示在你终端上的。标准错误stderr这也是一种输出通道不过它专门用于输出错误信息或者诊断信息。比如你用 ls 去访问一个不存在的路径终端会提示 “No such file or directory没有那个文件或目录”这类错误信息就是通过标准错误通道输出的。_IO_2_1_stdout_glibc 内部定义的全局变量存储stdout流的完整状态以及很多函数指针在标准IO函数中会调用这些函数指针,因此当程序没有输出功能时可以劫_IO_2_1_stdout_泄露libcFILE结构_IO_FILE_plusstruct_IO_FILE_plus{_IO_FILE file;conststruct_IO_jump_t*vtable;};然后我们分别看下他的两个组成部分_IO_FILEFILE结构在程序执行fopen函数时会自动进行创建并分配在堆中。FILE结构定义在glibc/libio/libio.h中结构源码如下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 putbackget 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;#if0int_blksize;#elseint_flags2;#endif_IO_off_t _old_offset;/* This used to be _offset but its too small. */#define__HAVE_COLUMN/* temporary *//* 1column number of pbase(); 0 is unknown. */unsignedshort_cur_column;signedchar_vtable_offset;char_shortbuf[1];/* char* _save_gptr; char* _save_egptr; */_IO_lock_t*_lock;#ifdef_IO_USE_OLD_IO_FILE};进程中的FILE结构会通过_chain域彼此连接形成一个链表链表头部用全局变量_IO_list_all表示通过这个值可以遍历所有的FILE结构大致的链表结构如下图在标准I/O库中每个程序启动时有三个文件流是自动打开的stdin、stdout、stderr。因为会自动打开所以在初始状态下_IO_list_all指向了一个有这些文件流构成的链表但是需要注意的是这三个文件流位于的是libc.so的数据段_IO_jump_tstruct_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);/* showmany */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);};vtable即虚表内部存储着很多函数指针当用户调用标准IO函数时就会查看虚表然后找对应的函数指针完成输入或输出。_IO_2_1_stdout_在了解完结构体后我们再深入理解下_IO_2_1_stdout_的类型以及和他相关联的结构体struct_IO_FILE_plus;externstruct_IO_FILE_plus_IO_2_1_stdin_;externstruct_IO_FILE_plus_IO_2_1_stdout_;externstruct_IO_FILE_plus_IO_2_1_stderr_;_flags规则通过上述对就结构体的了解我们将深入讲解下第一个成员变量_flags这个变量在利用_IO_2_1_stdout_泄露libc有至关重要的作用。先简单介绍一下_flag的规则_flag的高两位字节是由libc固定的不同的libc可能存在差异但是基本上都一样0xfbad0000。高两位字节其实就是作为一个标识标志这是一个什么文件。而低两位字节的位数规则决定了程序的执行状态低两位的规则如下#define_IO_MAGIC0xFBAD0000/* Magic number */#define_OLD_STDIO_MAGIC0xFABC0000/* Emulate old stdio. */#define_IO_MAGIC_MASK0xFFFF0000#define_IO_USER_BUF1/* User owns buffer; dont delete it on close. */#define_IO_UNBUFFERED2#define_IO_NO_READS4/* Reading not allowed */#define_IO_NO_WRITES8/* Writing not allowd */#define_IO_EOF_SEEN0x10#define_IO_ERR_SEEN0x20#define_IO_DELETE_DONT_CLOSE0x40/* Dont call close(_fileno) on cleanup. */#define_IO_LINKED0x80/* Set if linked (using _chain) to streambuf::_list_all.*/#define_IO_IN_BACKUP0x100#define_IO_LINE_BUF0x200#define_IO_TIED_PUT_GET0x400/* Set if put and get pointer logicly tied. */#define_IO_CURRENTLY_PUTTING0x800#define_IO_IS_APPENDING0x1000#define_IO_IS_FILEBUF0x2000#define_IO_BAD_SEEN0x4000#define_IO_USER_LOCK0x8000#define_IO_FLAGS2_MMAP1#define_IO_FLAGS2_NOTCANCEL2一般在执行流程中会将_flag和定义常量进行按位与运算并根据与运算的结构进行判断如何执行。puts()函数执行流程接下来我们以put()函数为例看下函数的实际执行逻辑是怎样的。总览_IO_puts -- _IO_new_file_xsputn-- _IO_do_write -- new_do_write-- _IO_SYSWRITE_IO_puts -- _IO_new_file_xsputnput()函数在glibc源码中表现形式为_IO_putsint_IO_puts(constchar*str){intresultEOF;_IO_size_t lenstrlen(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)resultMIN(INT_MAX,len1);_IO_release_lock(_IO_stdout);returnresult;}#define_IO_sputn(__fp,__s,__n)_IO_XSPUTN(__fp,__s,__n)这里可以看到_IO_puts在过程当中调用了一个叫做_IO_sputn函数_IO_fwrite也会调用这个_IO_sputn其实是一个宏定义在glibc/libio/libioP.h,它的作用就是调用_IO_2_1_stdout_中的vtable所指向的_xsputn也就是_IO_new_file_xsputn函数_IO_new_file_xsputn -- _IO_OVERFLOW简单的描述一下_IO_new_file_xsputn函数的执行过程在关键部分展示代码._介绍之前先说明三个指针的意义_IO_write_base --缓冲区起始地址 _IO_write_ptr --下一个写入的地址 _IO_write_end--缓冲区结尾地址首先进入函数之后判断输出缓冲区还有多少空间这里是由_IO_write_end - _IO_write_base得来的接下来如果缓冲区有空间则先把数据载入输出缓冲区,然后判断是否有数据还需要写入或者必须刷新缓冲区。如果条件满足代码首先尝试刷新缓冲区。通过_IO_OVERFLOW(f, EOF)尝试刷新缓冲区。_IO_OVERFLOW就是vtable中的__overflow。if(_IO_OVERFLOW(f,EOF)EOF)/* If nothing else has to be written we must not signal the caller that everything has been written. */returnto_do0?EOF:n-to_do;_IO_new_file_overflow -- _IO_do_writeint_IO_new_file_overflow(_IO_FILE*f,intch){//绕过条件一if(f-_flags_IO_NO_WRITES)/* SET ERROR */{f-_flags|_IO_ERR_SEEN;__set_errno(EBADF);returnEOF;}//绕过条件二/* If currently reading or no buffer allocated. */if((f-_flags_IO_CURRENTLY_PUTTING)0||f-_IO_write_baseNULL){/* Allocate a buffer if needed. */if(f-_IO_write_baseNULL){_IO_doallocbuf(f);_IO_setg(f,f-_IO_buf_base,f-_IO_buf_base,f-_IO_buf_base);}/* Otherwise must be currently reading. If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end, logically slide the buffer forwards one block (by setting the read pointers to all point at the beginning of the block). This makes room for subsequent output. Otherwise, set the read pointers to _IO_read_end (leaving that alone, so it can continue to correspond to the external position). */if(__glibc_unlikely(_IO_in_backup(f))){size_tnbackupf-_IO_read_end-f-_IO_read_ptr;_IO_free_backup_area(f);f-_IO_read_base-MIN(nbackup,f-_IO_read_base-f-_IO_buf_base);f-_IO_read_ptrf-_IO_read_base;}if(f-_IO_read_ptrf-_IO_buf_end)f-_IO_read_endf-_IO_read_ptrf-_IO_buf_base;f-_IO_write_ptrf-_IO_read_ptr;f-_IO_write_basef-_IO_write_ptr;f-_IO_write_endf-_IO_buf_end;f-_IO_read_basef-_IO_read_ptrf-_IO_read_end;f-_flags|_IO_CURRENTLY_PUTTING;if(f-_mode0f-_flags(_IO_LINE_BUF|_IO_UNBUFFERED))f-_IO_write_endf-_IO_write_ptr;}if(chEOF)return_IO_do_write(f,f-_IO_write_base,f-_IO_write_ptr-f-_IO_write_base);//目标位置if(f-_IO_write_ptrf-_IO_buf_end)/* Buffer is really full */if(_IO_do_flush(f)EOF)returnEOF;*f-_IO_write_ptrch;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)returnEOF;return(unsignedchar)ch;}上面即是_IO_new_file_overflow函数的部分代码我们想要利用的就是最后红色框中的_IO_do_write (f, f-_IO_write_base,f-_IO_write_ptr - f-_IO_write_base)_IO_do_write就是我们需要执行的目标函数这个函数执行后会调用系统调用write输出缓冲区传入_IO_do_write函数的参数为stdout结构体、_IO_write_base和size_IO_write_end - _IO_write_base计算得来如果我们事先在stdout的_IO_write_base的位置部署要输出的起始地址那么在去利用_IO_do_write函数即可打印部分内存地址打印出来的内容就包含我们所需要泄露的libc但是如果我们要利用_IO_do_write需要绕过两个检查if(f-_flags_IO_NO_WRITES)这里判断_flags的标志位是否包含_IO_NO_WRITES将_flags和_IO_NO_WRITES进行一个按位与的操作我们可以向前翻一下_flag规则的章节_flag与_IO_NO_WRITES各自定义的常量为#define_IO_MAGIC0xFBAD0000/* 魔数 */#define_IO_NO_WRITES8/* 不可写 */可以看到_flag魔数的常量为0xfbad0000_IO_NO_WRITES不可写标志位的常量为8我们返回上图的程序中如果进行按位与操作之后的结果为真则返回为错误。一旦返回的是错误那么后续我们想要利用的_IO_do_write函数就不会再被执行了所以我们要将此处的与运算为假#define_IO_MAGIC0xFBAD0000#define_IO_NO_WRITES8_flags_IO_NO_WRITES0_flags0xfbad0000然后我们看第二个条件if((f-_flags_IO_CURRENTLY_PUTTING)0||f-_IO_write_baseNULL)第二个判断是为了检查输出缓冲区是否为空如果为空则进行分配空间并且会初始化指针。一旦进行初始化操作那么就会覆盖掉我们事先在stdout的_IO_write_base的数据这样一来我们无法改写_IO_write_base。所以这个判断条件分支尽可能的也不进入。因此绕过#define_IO_MAGIC0xFBAD0000#define_IO_CURRENTLY_PUTTING0x800f-_flags_IO_CURRENTLY_PUTTING1_flags0xfbad0800而第二个条件因为我们会部署_IO_write_base因此很好绕过_IO_new_do_write -- new_do_writeint_IO_new_do_write(_IO_FILE*fp,constchar*data,_IO_size_t to_do){return(to_do0||(_IO_size_t)new_do_write(fp,data,to_do)to_do)?0:EOF;}可以看到_IO_new_do_write并没有做太多的操作就调用了new_do_write函数并且new_do_write函数的参数和传入的参数是一样的第一个参数是stdout结构体第二个参数是输出缓冲区起始地址第三个参数是输出长度new_do_write -- _IO_SYSWRITEstaticsize_tnew_do_write(FILE*fp,constchar*data,size_tto_do){size_tcount;if(fp-_flags_IO_IS_APPENDING)/* On a system without a proper O_APPEND implementation, you would need to sys_seek(0, SEEK_END) here, but is not needed nor desirable for Unix- or Posix-like systems. Instead, just indicate that offset (before and after) is unpredictable. */fp-_offset_IO_pos_BAD;elseif(fp-_IO_read_end!fp-_IO_write_base){off64_tnew_pos_IO_SYSSEEK(fp,fp-_IO_write_base-fp-_IO_read_end,1);if(new_pos_IO_pos_BAD)return0;fp-_offsetnew_pos;}count_IO_SYSWRITE(fp,data,to_do);if(fp-_cur_columncount)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_basefp-_IO_write_ptrfp-_IO_buf_base;fp-_IO_write_end(fp-_mode0(fp-_flags(_IO_LINE_BUF|_IO_UNBUFFERED))?fp-_IO_buf_base:fp-_IO_buf_end);returncount;}这里有一个if和else if条件绕过两个条件都绕过难度较高可以利用性也不强我们可以通过满足一个条件而绕过另一个条件这里我们通过构造_flag满足if条件从而绕过else if这么做的原因是一般在做这种题的时候都会伴随着随机化保护的开启进行攻击的时候我们一般采用的都是覆盖末位字节的方式造成偏移因为即使随机化偏移也会存在0x1000对齐。但是这时候就会遇到一个很尴尬的情况_IO_read_end和_IO_write_base存放的地址是由末位字节和其他高字节共同组成的其他高字节由于随机化的缘故无法确定基本上这个地方很难绕过。而一旦进行到else if 内那随后执行的_IO_SYSSEEK函数会因为fp-_IO_write_base - fp-_IO_read_end难以控制导致执行失败。而if分支相对来说造成的影响就比较小了内部仅仅将偏移设置为标准值不会影响后续的输出流程。并且if判断的条件也很容易满足我们只需要将fp-_flags _IO_IS_APPENDING ! 0即可:#define_IO_MAGIC0xFBAD0000#define_IO_IS_APPENDING0x1000fp-_flags_IO_IS_APPENDING1_flags0xfbad1000总结在实际操作中我们只需要满足_flag为0xFBAD1800将之后24个字节置零即_IO_read_ptr,_IO_read_end,_IO_read_base这三个8字节的地址置为0。之后的_IO_write_base与_IO_write_ptr分别为我们想要泄露的地址的起止点之后遇到puts或printf就会将_IO_write_base指向的内容打印出来。。参考文章件也很容易满足我们只需要将fp-_flags _IO_IS_APPENDING ! 0即可:#define_IO_MAGIC0xFBAD0000#define_IO_IS_APPENDING0x1000fp-_flags_IO_IS_APPENDING1_flags0xfbad1000总结在实际操作中我们只需要满足_flag为0xFBAD1800将之后24个字节置零即_IO_read_ptr,_IO_read_end,_IO_read_base这三个8字节的地址置为0。之后的_IO_write_base与_IO_write_ptr分别为我们想要泄露的地址的起止点之后遇到puts或printf就会将_IO_write_base指向的内容打印出来。。参考文章好好说话之IO_FILE利用1利用_IO_2_1_stdout泄露libc_libc泄露方式-CSDN博客