| 
      
      1
      
     
      2
       | 
    
     歌舞为谁演、拨珠为谁算,执戟儿郎为谁站,流血为谁干。
      苏女庆生平,卫商秦问鼎,八千江东子弟兵,明总建大清。
       | 
   
昨天,突然有一个师傅突然提醒我GLIBC-2.37已经发布,就重新看了一下,瞬间感觉背后发凉,largebin没有什么修改,IO修改很大,针对性强的一逼,以前一些利用方式已无法使用,比较庆幸的是还有有一些新的利用方式可以继续挖掘。而且我认为GNU下一步会按2.37的思路不断修改IO,以前很多方法都会渐渐失效,在此我也决定将几个2.37以后失效的攻击链公布出来。本篇的内容主要是开篇,简单介绍一下IO,老手就可以无视。
既然是IO操作,那么就有必要知道计算机到底是怎么读写硬盘或者其他设备的,此处主要以块设备为主。以硬盘为例,要想和硬盘交互,说白了也是和硬盘上的芯片+系统交互,在x86的体系下,其实就是和硬盘上的寄存器进行交互,为了让这个过程变的简单,硬盘上的寄存器被映射成了端口,硬盘的端口说明如下表。而能够交互端口的操作就是汇编指令中的in out,这些指令是特权指令,三环无法使用,像我们的常说的底层read或write函数其实也是靠操作系统执行in,out指令来实现外部设备读写的。具体的执行细节不是我们的重点,不再细写。需要知道的是,对于LBA硬盘来说,读写数据都必须一块一块的读,这也就是常说的块设备。
| I/O地址 | 读(主机从硬盘读数据) | 写(主机数据写入硬盘) | 
|---|---|---|
| 1F0H | 数据寄存器 | 数据寄存器 | 
| 1F1H | 错误寄存器(只读寄存器) | 特征寄存器 | 
| 1F2H | 扇区计数寄存器 | 扇区计数寄存器 | 
| 1F3H | 扇区号寄存器或 LBA 块地址 0~7 | 扇区号或 LBA 块地址 0~7 | 
| 1F4H | 磁道数低 8 位或 LBA 块地址 8~15 | 磁道数低 8 位或 LBA 块地址 8~15 | 
| 1F5H | 磁道数高 8 位或 LBA 块地址 16~23 | 磁道数高 8 位或 LBA 块地址 16~23 | 
| 1F6H | 驱动器/磁头或 LBA 块地址 24~27 | 驱动器/磁头或 LBA 块地址 24~27 | 
| 1F7H | 命令寄存器或状态寄存器 | 命令寄存器 | 
对于LBA硬盘来说,读写数据都必须一块一块的读,如果我们每次执行read,write时都是操作很少的数据,则对系统消耗非常大,因此,C库就想了一个好办法——缓冲区。所以,就比较好理解了,缓冲区是为了减少3坏操作外部硬件时的消耗产生的,一切都是以外部硬件为服务对象。
1.从外部硬件读取时。为了减少消耗,会一次从外部硬件读取一“块”数据,并放入缓冲区,然后当
target需要时,再从头部慢慢读取,只到读完才再次从硬件读取。这个缓冲区叫输入缓冲区。2.向外部硬件写入时。为了减少消耗,不会一有东西就写入,而是先将内容从
source写入缓冲区,当缓冲区满了时候再将内存一起写入硬件。这个缓冲区叫输出缓冲区。
为了更好的定位,对每个操作我们肯定至少要有3个基础数据。首先,以从外部硬件读取为例,我们要有输入缓冲区开始(base)、结尾(end)和当前(ptr)已经用了多少的指针。很明显当ptr == end时,说明输入缓冲区里的东西已经全部读完,需要重新从硬件读入。
同样,对于向外部硬件写入为例,我们要有输出缓冲区开始(base)、结尾(end)和当前(ptr)已经写了多少的指针。很明显当ptr == end时,说明输出缓冲区已经写满,可以向硬件写入了。
上面的内容看似非常清楚,但这里其实有一些比较容易混乱的地方。因为缓冲区内存储的是数据,输入、输出两者数据流动方向不同,但保护主体都一样,都是外部设备,所以有用的数据部分就有所不相同。
- 对于输入缓冲区
 ptr-end是有用的数据,base-ptr为已使用的数据。- 对于输出缓冲区
 base-ptr是要写入硬件的内容(有用数据),ptr-end为空闲区域。- 两者结尾有所不同。
 
- 对于输入缓冲区,因为从硬盘中读取的数据可能无法填满整个缓冲区的块,所以
 _IO_buf_end != _IO_read_end。输入缓冲区要使用_IO_read_end判断结束。- 对于输出缓冲区,缓冲区的结束就是输出缓冲区结束,
 _IO_buf_end == _IO_write_end。输出缓冲区往往使用_IO_buf_end判断结束。
虽然,输入、输出缓冲区作用不同,但原理上都是一块内存。一块外部设备可能既可以写入也可以读取,为了节省空间,我们可以定义一块缓冲区,需要输入的时候就做输入缓冲区,需要输出就做输出缓冲区。那么我们就有了8个指针。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
       | 
    
     char 
       *
       _IO_buf_base;  
       /
       / 
       缓冲区的基地址
      char 
       *
       _IO_buf_end;   
       /
       / 
       缓冲区的结束地址 
      char 
       *
       _IO_read_base; 
       /
       / 
       输入缓冲区基地址
      char 
       *
       _IO_read_ptr;  
       /
       / 
       输入当前位置 
      char 
       *
       _IO_read_end;  
       /
       / 
       输入缓冲区结尾地址
      char 
       *
       _IO_write_base;
       /
       / 
       输出缓冲区基地址 
      char 
       *
       _IO_write_ptr; 
       /
       / 
       输出当前位置
      char 
       *
       _IO_write_end; 
       /
       / 
       输出缓冲区结尾地址
       | 
   
那么到现在,基本思路理清了,其他就方便了.
从文件中读取 程序是从
fd中读取一批数据到缓冲区中(_IO_buf_base至_IO_buf_end),_IO_read_ptr指向已向target中写完的位置,既_IO_read_ptr至_IO_read_end为还没有写入target中的数据。当_IO_read_ptr == _IO_read_end时,说明输入缓冲区内已经没有可用数据,需要再次从文件中读入数据。向文件输出 程序是先将
source中的数据写入到缓冲区中,_IO_write_ptr指向已从source中写到的位置,既_IO_write_ptr至_IO_write_pend为还剩余的空间。当_IO_write_ptr == _IO_buf_end时,再全部写入fd中。
既然有了数据结构,我们就可以简单定义一些操作来进行操作。
这个逻辑前面已经说的非常清楚,简单逻辑如下。
- 从
 fd中读取一批(一块)数据到输入缓冲区中(_IO_buf_base至_IO_buf_end),同时对_IO_read_base_IO_read_ptr_IO_read_end设置初始值。(_IO_read_ptr == _IO_read_base,当然也可能不同)- 从
 _IO_read_ptr处向需要的内存中复制数据,同时把_IO_read_ptr向后移位。- 当
 _IO_read_ptr == _IO_read_end时,说明缓冲区内已经没有可用数据,需要再次从文件中读入数据。冲入第一步。
同理,操作逻辑如下。
- 先将
 source中的数据复制到输出缓冲区中,_IO_write_ptr指向已写到的位置。- 当
 _IO_write_ptr == _IO_buf_end时,将缓冲区中的内容全部写入fd中,并将_IO_write_ptr设置为_IO_write_base,重复第一步。
上面的操作中,我们还忘了一个基本的问题:缓冲区从哪里来?其实缓冲区就是一块内存,可以在栈上、堆上、libc中,甚至随便mmap一块内存都可以,但不论怎么来,我们都需要这样一块区域,在此,我们借用glibc中在malloc的方法来申请缓冲区。那么我们还需要第三个操作。
这个操作非常简单。
申请一块缓冲区,并设置
_IO_buf_base为开头,_IO_buf_end为结尾。
到此为止,IO的所有基本操作就已经算是完成了。当然,操作中还需要一些安全检测,例如判断缓冲区是否存在、malloc是否成功等内容,这里就不再赘述。下面,我以glibc中_IO_file_jumps为例,梳理一下函数操作的意图。
_IO_file_jumps 函数操作说明顺序根据_IO_file_jumps中的操作顺序来,因为里面的互相调用还是挺多的,就不说具体写过程,主要说明操作的意图。
_IO_new_file_finish这个看名字就非常简单,是文件结束的操作,所以它的操作如下
- 清空所有缓冲区
 - 关闭(close)文件
 
_IO_new_file_overflow这个函数意图比较简单,主要是处理当输出缓冲区用完时,向硬盘写入数据。当然,其实这个函数内部非常复杂,加入了一些检测。例如,如果缓冲区不存在则要初始化缓冲区。并且,这个函数的参数中有一个标志位。
- 如果
 ch == EOF,则输出f->_IO_write_ptr - f->_IO_write_base的区间。- 如果
 ch != EOF,并且f->_IO_write_ptr == f->_IO_buf_end,则将缓冲区全部输出。- 如果
 ch == '\n',则输出f->_IO_write_ptr - f->_IO_write_base加一个换行符。- 以上都不满足就返回 ch。
 
_IO_new_file_underflow这个函数与_IO_new_file_overflow差不多,主要是用于从硬盘中读取数据,每次读取都是_IO_buf_base 至 _IO_buf_end。为了防止硬盘中没有这么多数据,设置_IO_read_end为读取的总数。如果,缓冲区不存在则要初始化缓冲区。程序返回_IO_read_ptr指针。
__GI__IO_default_uflow(_IO_default_uflow)这个函数就是调用_IO_new_file_underflow,并简单做了一些检测。
__GI__IO_default_pbackfail(_IO_default_pbackfail)设置存储的函数,暂不重要。
_IO_new_file_xsputn这个函数是主要目的是将数据从source放入输出输出缓冲区。显然,放入过程中还有几种情况。
- 如果要写入的数据小于剩余的空间
 _IO_write_ptr - _IO_buf_end,那么就直接将数据写入输出缓冲区即可。- 如果要写入的数据大于剩余的空间
 _IO_write_ptr - _IO_buf_end。
- 先将输出缓冲区填满,再调用
 _IO_new_file_overflow清空输出缓冲区。- 剩余的数据继续调用
 _IO_new_file_xsputn
说明:我们平时的输出函数主要就是调用此函数。
__GI__IO_file_xsgetn(_IO_file_xsgetn)这个函数是主要目的是将数据从输入缓冲区放入target。显然放入过程中还有几种情况。
- 如果要读取的数据小于剩余的数据
 _IO_read_ptr - _IO_read_end,那么就直接将数据读取到target即可。- 如果要读取的数据大于剩余的数据
 _IO_read_ptr - _IO_read_end。
- 先将输入缓冲区全部数据读出,再调用
 _IO_new_file_underflow从硬盘读入一块数据。- 如果需要读取数据特别多,就调用
 __GI__IO_file_read从硬盘直接读取数据。
说明:我们平时的输入函数主要就是调用此函数。
_IO_new_file_seekoff设置偏移函数,就是设置我们所说的ptr指针。
_IO_default_seekpos就是调用_IO_new_file_seekoff。
_IO_new_file_setbuf这个函数也比较简单,看名字就知道是设置缓冲区的,作用就是初始化各个缓冲区
_IO_write_base = _IO_write_ptr = _IO_write_end = _IO_buf_base_IO_read_base = _IO_read_ptr = _IO_read_end = _IO_buf_base(使用_IO_setg宏)
_IO_new_file_sync同步函数,负责与硬盘和缓冲区之间进行同步。
__GI__IO_file_doallocate(_IO_default_doallocate)这个就是申请缓冲区的函数,申请完之后还要把输入、输出缓冲区初始化。
__GI__IO_file_read(_IO_file_read)这个是输入的最终函数,它将syscall_read进行了一定的封装。
_IO_new_file_write这个是输出的最终函数,它将syscall_write进行了一定的封装。
__GI__IO_file_seek(_IO_file_seek)调用__lseek64。
__GI__IO_file_close(_IO_file_close)就和名字一样,关闭文件。
__GI__IO_file_stat(_IO_file_stat)获取文件描述符的状态。调用__fxstat64。
_IO_default_showmanyc此函数没用,返回-1。
_IO_default_imbue此函数没用。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
      
     
      11
      
     
      12
      
     
      13
      
     
      14
      
     
      15
      
     
      16
      
     
      17
      
     
      18
      
     
      19
       | 
    
     #define _IO_MAGIC 0xFBAD0000 /* Magic number */
      #define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio. */
      #define _IO_MAGIC_MASK 0xFFFF0000
      #define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. */
      #define _IO_UNBUFFERED 2
      #define _IO_NO_READS 4 /* Reading not allowed */
      #define _IO_NO_WRITES 8 /* Writing not allowd */
      #define _IO_EOF_SEEN 0x10
      #define _IO_ERR_SEEN 0x20
      #define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. */
      #define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all.*/
      #define _IO_IN_BACKUP 0x100
      #define _IO_LINE_BUF 0x200
      #define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logicly tied. */
      #define _IO_CURRENTLY_PUTTING 0x800
      #define _IO_IS_APPENDING 0x1000
      #define _IO_IS_FILEBUF 0x2000
      #define _IO_BAD_SEEN 0x4000
      #define _IO_USER_LOCK 0x8000
       | 
   
_IO_do_flush)清空缓冲区,将输出缓冲区清空。
_IO_setg _IO_setp 等等
我认为这是IO里面最让人头疼的地方,它的初始化形式使用大量宏来操作,为了说明问题,我专门找了一个不常用虚表(wfile)。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
      
     
      11
      
     
      12
      
     
      13
      
     
      14
      
     
      15
      
     
      16
      
     
      17
      
     
      18
      
     
      19
      
     
      20
      
     
      21
      
     
      22
      
     
      23
      
     
      24
       | 
    
     const struct _IO_jump_t _IO_wfile_jumps libio_vtable 
       =
      {
        
       JUMP_INIT_DUMMY,
        
       JUMP_INIT(finish, _IO_new_file_finish),
        
       JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
        
       JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
        
       JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
        
       JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
        
       JUMP_INIT(xsputn, _IO_wfile_xsputn),
        
       JUMP_INIT(xsgetn, _IO_file_xsgetn),
        
       JUMP_INIT(seekoff, _IO_wfile_seekoff),
        
       JUMP_INIT(seekpos, _IO_default_seekpos),
        
       JUMP_INIT(setbuf, _IO_new_file_setbuf),
        
       JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
        
       JUMP_INIT(doallocate, _IO_wfile_doallocate),
        
       JUMP_INIT(read, _IO_file_read),
        
       JUMP_INIT(write, _IO_new_file_write),
        
       JUMP_INIT(seek, _IO_file_seek),
        
       JUMP_INIT(close, _IO_file_close),
        
       JUMP_INIT(stat, _IO_file_stat),
        
       JUMP_INIT(showmanyc, _IO_default_showmanyc),
        
       JUMP_INIT(imbue, _IO_default_imbue)
      };
      libc_hidden_data_def (_IO_wfile_jumps)
       | 
   
其中,带default的都是共用的函数,大都在genops.c里面;new_file和file的大都在fileops.c里面;wdefault是宽字符共用的函数,大都在wgenops.c里面;只有wfile的才是自己单独定义的函数,在wfileops.c里面。从上面可以看出wfile单独定义的操作只有5个。
IO_FILE结构体通过源码可以看出,_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
      
     
      43
      
     
      44
      
     
      45
      
     
      46
      
     
      47
      
     
      48
      
     
      49
      
     
      50
      
     
      51
      
     
      52
      
     
      53
      
     
      54
      
     
      55
      
     
      56
      
     
      57
       | 
    
     struct _IO_FILE
      {
        
       int 
       _flags;        
       /
       * 
       High
       -
       order word 
       is 
       _IO_MAGIC; rest 
       is 
       flags. 
       *
       /
        
       /
       * 
       The following pointers correspond to the C
       +
       + 
       streambuf protocol. 
       *
       /
        
       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;
        
       int 
       _flags2;
        
       __off_t _old_offset; 
       /
       * 
       This used to be _offset but it's too small.  
       *
       /
        
       /
       * 
       1
       +
       column number of pbase(); 
       0 
       is 
       unknown. 
       *
       /
        
       unsigned short _cur_column;
        
       signed char _vtable_offset;
        
       char _shortbuf[
       1
       ];
        
       _IO_lock_t 
       *
       _lock;
      #ifdef _IO_USE_OLD_IO_FILE  // 可以看出如果使用旧的 _IO_FILE ,那我们经常说的IO就是  _IO_FILE_complete
      };
      struct _IO_FILE_complete
      {
        
       struct _IO_FILE _file;
      #endif
        
       __off64_t _offset;
        
       /
       * 
       Wide character stream stuff.  
       *
       /
        
       struct _IO_codecvt 
       *
       _codecvt;
        
       struct _IO_wide_data 
       *
       _wide_data;
        
       struct _IO_FILE 
       *
       _freeres_list;
        
       void 
       *
       _freeres_buf;
        
       size_t __pad5;
        
       int 
       _mode;
        
       /
       * 
       Make sure we don't get into trouble again.  
       *
       /
        
       char _unused2[
       15 
       * 
       sizeof (
       int
       ) 
       - 
       4 
       * 
       sizeof (void 
       *
       ) 
       - 
       sizeof (size_t)];
      };
      struct _IO_FILE_complete_plus
      {
        
       struct _IO_FILE_complete 
       file
       ;
        
       const struct _IO_jump_t 
       *
       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
      
     
      25
      
     
      26
      
     
      27
      
     
      28
      
     
      29
      
     
      30
      
     
      31
      
     
      32
       | 
    
     pwndbg> ptype stdout
      type 
       = 
       struct _IO_FILE {
          
       int 
       _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;
          
       int 
       _flags2;
          
       __off_t _old_offset;
          
       unsigned short _cur_column;
          
       signed char _vtable_offset;
          
       char _shortbuf[
       1
       ];
          
       _IO_lock_t 
       *
       _lock;
          
       __off64_t _offset;
          
       struct _IO_codecvt 
       *
       _codecvt;
          
       struct _IO_wide_data 
       *
       _wide_data;
          
       struct _IO_FILE 
       *
       _freeres_list;
          
       void 
       *
       _freeres_buf;
          
       size_t __pad5;
          
       int 
       _mode;
          
       char _unused2[
       20
       ];
      } 
       *
       | 
   
fflush)| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
      
     
      11
      
     
      12
      
     
      13
      
     
      14
      
     
      15
      
     
      16
      
     
      17
       | 
    
     # define fflush(s) _IO_fflush (s)  //  /assert/assert.c
      /
       /  
       /
       libio
       /
       iofflush.c
      int 
       _IO_fflush (
       FILE 
       *
       fp)
      {
        
       if 
       (fp 
       =
       = 
       NULL)
          
       return 
       _IO_flush_all ();
        
       else
          
       {
            
       int 
       result;
            
       CHECK_FILE (fp, EOF);
            
       _IO_acquire_lock (fp);
            
       result 
       = 
       _IO_SYNC (fp) ? EOF : 
       0
       ;
            
       _IO_release_lock (fp);
            
       return 
       result;
          
       }
      }
      libc_hidden_def (_IO_fflush)
       | 
   
可以看出 fflush函数在参数为空时,清空(_IO_flush_all_lockp => _IO_OVERFLOW)全部文件;不为空时,同步(sync)指定文件,两种情况执行步骤不同。
FSOP执行是靠_IO_flush_all_lockp,该函数的功能是刷新所有FILE结构体的输出缓冲区,执行这个程序的时候会沿着fp->chain执行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
       | 
    
     int 
       _IO_flush_all_lockp (
       int 
       do_lock)
      {
        
       int 
       result 
       = 
       0
       ;
        
       struct _IO_FILE 
       *
       fp;
        
       int 
       last_stamp;
        
       fp 
       = 
       (_IO_FILE 
       *
       ) _IO_list_all;
        
       while 
       (fp !
       = 
       NULL)
          
       {
              
       ...
            
       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;
          
       fp 
       = 
       fp
       -
       >_chain; 
       /
       /
       遍历链表
          
       }
      ...
      }
       | 
   
_IO_flush_all_lockp调用函数的时机包括:
- 执行
 abort函数时。(2.27之后不再刷新)__malloc_assert(仅刷新stderr,2.36后不再刷新)- 执行
 exit函数时。- 从
 main函数返回时。(也是执行exit)
首先是abort函数的流程,利用的double free漏洞触发,栈回溯为:
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
       | 
    
     _IO_flush_all_lockp (do_lock
       =
       do_lock@entry
       =
       0x0
       )
      __GI_abort ()
      __libc_message (do_abort
       =
       do_abort@entry
       =
       0x2
       , fmt
       =
       fmt@entry
       =
       0x7ffff7ba0d58 
       "*** Error in `%s': %s: 0x%s ***\n"
       )
      malloc_printerr (action
       =
       0x3
       , 
       str
       =
       0x7ffff7ba0e90 
       "double free or corruption (top)"
       , ptr
       =
       <optimized out>, ar_ptr
       =
       <optimized out>)
      _int_free (av
       =
       0x7ffff7dd4b20 
       <main_arena>, p
       =
       <optimized out>,have_lock
       =
       0x0
       )
      main ()
      __libc_start_main (main
       =
       0x400566 
       <main>, argc
       =
       0x1
       , argv
       =
       0x7fffffffe578
       , init
       =
       <optimized out>, fini
       =
       <optimized out>, rtld_fini
       =
       <optimized out>, stack_end
       =
       0x7fffffffe568
       )
      _start ()
       | 
   
exit 函数,栈回溯为:
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
       | 
    
     _IO_flush_all_lockp (do_lock
       =
       do_lock@entry
       =
       0x0
       )
      _IO_cleanup ()
      __run_exit_handlers (status
       =
       0x0
       , listp
       =
       <optimized out>, run_list_atexit
       =
       run_list_atexit@entry
       =
       0x1
       )
      __GI_exit (status
       =
       <optimized out>)
      main ()
      __libc_start_main (main
       =
       0x400566 
       <main>, argc
       =
       0x1
       , argv
       =
       0x7fffffffe578
       , init
       =
       <optimized out>, fini
       =
       <optimized out>, rtld_fini
       =
       <optimized out>, stack_end
       =
       0x7fffffffe568
       )
      _start ()
       | 
   
程序正常退出,栈回溯为:
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
       | 
    
     _IO_flush_all_lockp (do_lock
       =
       do_lock@entry
       =
       0x0
       )
      _IO_cleanup ()
      __run_exit_handlers (status
       =
       0x0
       , listp
       =
       <optimized out>, run_list_atexit
       =
       run_list_atexit@entry
       =
       0x1
       )
      __GI_exit (status
       =
       <optimized out>)
      __libc_start_main (main
       =
       0x400526 
       <main>, argc
       =
       0x1
       , argv
       =
       0x7fffffffe578
       , init
       =
       <optimized out>, fini
       =
       <optimized out>, rtld_fini
       =
       <optimized out>, stack_end
       =
       0x7fffffffe568
       )
      _start ()
       | 
   
从上面可以看出,很多函数并没有用,但为什么还要设置这些呢?以下是我的猜想。
glibc的文件操作经历了很多版本迭代,为了兼容之前的版本,保留了很多没有用的操作。- 跳表种类很多,我们目前看到的是
 file操作,还有字符操作(str)、宽字符(wdate)操作、帮助文档操作(help)等等,有一些操作是独有的,类似于_IO_new_file_xsputn。有一些是通用的操作,类似于_IO_default_uflow。- 现将框架搭起来,如果以后有需要的时候可以方便进行扩展。
 
对这些清楚了之后,我们就可以看看其他的house到底是干什么的了。
虚表检测是2.24之后加入的内容,IO_validate_vtable检测如果虚表超出范围就进入_IO_vtable_check函数。各路大神找到的house很多都不是打file的跳表,而是其他处理跳表,但都差不太多。简要梳理如下。
- 2.23 的没有任何限制,可以将
 vtable劫持在堆上并修改其内容,然后触发FSOP,- 2.24 引入了
 vtable check,使得将vtable整体劫持到堆上已不可能,大佬发现可以使用内部的vtable中_IO_str_jumps或_IO_wstr_jumps来进行利用。- 2.31 中将
 _IO_str_finish函数中强制执行free函数,导致无法使用上述问题,因而催生出其他调用链。
虚表位置判断主要在IO_validate_vtable函数,2.37以前判断区间为_IO_helper_jumps - _IO_str_jumps之间的区域 0xd60,里面有以下虚表。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
      
     
      11
      
     
      12
      
     
      13
      
     
      14
      
     
      15
      
     
      16
      
     
      17
      
     
      18
       | 
    
     _IO_helper_jumps
      _IO_helper_jumps
      _IO_cookie_jumps
      _IO_proc_jumps
      _IO_str_chk_jumps
      _IO_wstrn_jumps
      _IO_wstr_jumps
      _IO_wfile_jumps_maybe_mmap
      _IO_wfile_jumps_mmap
      __GI__IO_wfile_jumps
      _IO_wmem_jumps
      _IO_mem_jumps
      _IO_strn_jumps
      _IO_obstack_jumps
      _IO_file_jumps_maybe_mmap
      _IO_file_jumps_mmap
      __GI__IO_file_jumps
      _IO_str_jumps
       | 
   
_IO_vtable_check在IO_validate_vtable函数检查如果虚表超出范围,会进入_IO_vtable_check函数,
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
       | 
    
     void attribute_hidden _IO_vtable_check (void)
      {
      #ifdef SHARED
        
       /
       * 
       Honor the compatibility flag.  
       *
       /
        
       void (
       *
       flag) (void) 
       = 
       atomic_load_relaxed (&IO_accept_foreign_vtables);
      #ifdef PTR_DEMANGLE
        
       PTR_DEMANGLE (flag);
      #endif
        
       if 
       (flag 
       =
       = 
       &_IO_vtable_check) 
       /
       /
       检查是否是外部重构的vtable
          
       return
       ;
       | 
   
这里就很有意思,也就是说GNU其实也同意你能够外部重构vtable,只是要满足一定条件。那么我们还是可以绕过虚表检测的。
- 泄露
 ptr_guard,反算IO_accept_foreign_vtables然后修改。- 因为
 IO_accept_foreign_vtables中基本都是0,直接将ptr_guard修改为&_IO_vtable_check也可以。
但无论如何我们都需要有ld文件。
check_stdfiles_vtables函数是设置外置虚表的函数,如果能执行这个函数,也可以绕过虚表检测。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
       | 
    
     static void  check_stdfiles_vtables (void)
      {
        
       if 
       (_IO_2_1_stdin_.vtable !
       = 
       &_IO_file_jumps
            
       || _IO_2_1_stdout_.vtable !
       = 
       &_IO_file_jumps
            
       || _IO_2_1_stderr_.vtable !
       = 
       &_IO_file_jumps)
          
       IO_set_accept_foreign_vtables (&_IO_vtable_check);
      }
       | 
   
将宽字符函数调用单独拿出来主要是因为,目前(2.36及以前,2.37也没有修订)宽字符跳表的引用没有加入保护,house_of_apple house_of_cat都是利用这一点。
以2.36为例,目前,涉及到宽字符跳转的函数一共有19个,也就是说跳表中的都定义了。
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
      
     
      5
      
     
      6
      
     
      7
      
     
      8
      
     
      9
      
     
      10
      
     
      11
      
     
      12
      
     
      13
      
     
      14
      
     
      15
      
     
      16
      
     
      17
      
     
      18
      
     
      19
       | 
    
     #define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
      #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
      #define _IO_WUNDERFLOW(FP) WJUMP0 (__underflow, FP)
      #define _IO_WUFLOW(FP) WJUMP0 (__uflow, FP)
      #define _IO_WPBACKFAIL(FP, CH) WJUMP1 (__pbackfail, FP, CH)
      #define _IO_WXSPUTN(FP, DATA, N) WJUMP2 (__xsputn, FP, DATA, N)
      #define _IO_WXSGETN(FP, DATA, N) WJUMP2 (__xsgetn, FP, DATA, N)
      #define _IO_WSEEKOFF(FP, OFF, DIR, MODE) WJUMP3 (__seekoff, FP, OFF, DIR, MODE)
      #define _IO_WSEEKPOS(FP, POS, FLAGS) WJUMP2 (__seekpos, FP, POS, FLAGS)
      #define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH)
      #define _IO_WSYNC(FP) WJUMP0 (__sync, FP)
      #define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
      #define _IO_WSYSREAD(FP, DATA, LEN) WJUMP2 (__read, FP, DATA, LEN)
      #define _IO_WSYSWRITE(FP, DATA, LEN) WJUMP2 (__write, FP, DATA, LEN)
      #define _IO_WSYSSEEK(FP, OFFSET, MODE) WJUMP2 (__seek, FP, OFFSET, MODE)
      #define _IO_WSYSCLOSE(FP) WJUMP0 (__close, FP)
      #define _IO_WSYSSTAT(FP, BUF) WJUMP1 (__stat, FP, BUF)
      #define _IO_WSHOWMANYC(FP) WJUMP0 (__showmanyc, FP)
      #define _IO_WIMBUE(FP, LOCALE) WJUMP1 (__imbue, FP, LOCALE)
       | 
   
但实际上有引用的仅为以下4个
| 
      
      1
      
     
      2
      
     
      3
      
     
      4
       | 
    
     #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
      #define _IO_WUFLOW(FP) WJUMP0 (__uflow, FP)
      #define _IO_WSETBUF(FP, BUFFER, LENGTH) WJUMP2 (__setbuf, FP, BUFFER, LENGTH)
      #define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
       | 
   
其中,_IO_WSETBUF仅用在_IO_setbuffer中,也就是我们经常用的setbuf
更多【无路远征——GLIBC2.37后时代的IO攻击之道(零)】相关视频教程:www.yxfzedu.com