File Structure 基础 && PUTS 流程分析
2023-1-30 11:46:0 Author: www.freebuf.com(查看原文) 阅读量:8 收藏

为了减少I/O时,系统调用syscall的数量,Glibc会预设一定大小的缓冲区buffer,当进行输入/输出时,会先将内容放进缓冲区,根据模式的不同,由操作系统决定什么时候系统调用处理缓冲区里的内容

所以在pwn题中,为了让I/O单纯一些,会设置:

setvbuf(stdout,0,_IONBF,0);无缓冲模式

为了探究

stdinstdoutstderr的本源,首先从源码下手,看其定义:

FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
FILE *stderr = (FILE *) &_IO_2_1_stderr_;

而再深究则发现,其都为_IO_FILE_plus结构体

extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

_IO_FILE_plus中包含了_IO_FILE结构 和 一个vtable指针

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

深入_IO_FILE结构体

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

flags

其中对于int _flags;有以下几种:

/* Magic number and bits for the _flags field.  The magic number is
   mostly vestigial, but preserved for compatibility.  It occupies the
   high 16 bits of _flags; the low 16 bits are actual flag bits.  */
#define _IO_MAGIC         0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK    0xFFFF0000
#define _IO_USER_BUF          0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED        0x0002
#define _IO_NO_READS          0x0004 /* Reading not allowed.  */
#define _IO_NO_WRITES         0x0008 /* Writing not allowed.  */
#define _IO_EOF_SEEN          0x0010
#define _IO_ERR_SEEN          0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close.  */
#define _IO_LINKED            0x0080 /* In the list of all open files.  */
#define _IO_IN_BACKUP         0x0100
#define _IO_LINE_BUF          0x0200
#define _IO_TIED_PUT_GET      0x0400 /* Put and get pointer move in unison.  */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING      0x1000
#define _IO_IS_FILEBUF        0x2000
                           /* 0x4000  No longer used, reserved for compat.  */
#define _IO_USER_LOCK         0x8000

该字段用于表明当前_IO_FILE的属性,比如:

  • _IO_NO_READS:当前_IO_FILE不允许读
  • _IO_NO_WRITES:当前_IO_FILE不允许写

buffer 的指定

再往后的字段,用于指定当前_IO_FILE所使用缓冲区的位置:

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. */

基本分为三种buffer

  • read buffer
    • _IO_read_baseread buffer的开头
    • _IO_read_endread buffer的结尾
    • _IO_read_ptr:当前read buffer用到的地方
  • write buffer
  • reserve buffer保留buffer

struct _IO_FILE *_chain;

_chain用于将所有的_IO_FILE串起来,串成一个链,前面曾提到的:stdin 0stdout 1stderr 2就是串在一个链上的

int _fileno;

  • stdin_fileno为 0
  • stdout_fileno为 1
  • stderr_fileno为 2

vtable

其中存放了一些函数的指针,标准I/O函数中可能会调用这些函数的指针

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

下面简单说说一些C函数对_IO_jump_t虚表里面函数的调用情况

  • printf/puts最终会调用_IO_file_xsputn
  • fclose最终会调用_IO_FILE_FINISH
  • fwrite最终会调用_IO_file_xsputn
  • fread最终会调用_IO_fiel_xsgetn
  • scanf/gets最终会调用_IO_file_xsgetn

对于stdinstdoutstderr的定义与初始化

DEF_SEDFILE

#ifdef _IO_MTSAFE_IO
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
  static _IO_lock_t _IO_stdfile_##FD##_lock = _IO_lock_initializer; \\
  struct _IO_FILE_plus NAME \\
    = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};
#else
#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
  struct _IO_FILE_plus NAME \\
    = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};
#endif

DEF_STDFILE这个宏用于初始化_IO_FILE结构体

此处分析,不带线程安全版DEF_SEDFILE的源码:

#define DEF_STDFILE(NAME, FD, CHAIN, FLAGS) \\
  struct _IO_FILE_plus NAME \\
    = {FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL), &_IO_old_file_jumps};

可以看到有四个参数:

  • NAME: 直接用该参数作为名称定义一个_IO_FILE_plus结构体

    struct _IO_FILE_plus
    {
        FILE file;
        const struct _IO_jump_t *vtable
    };
    
  • FILEBUF_LITERAL(CHAIN, FLAGS, FD, NULL)FILE file部分

  • &_IO_old_file_jumpsvtable部分

_IO_old_file_jumps

const struct _IO_jump_t _IO_old_file_jumps libio_vtable =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_old_file_finish),
  JUMP_INIT(overflow, _IO_old_file_overflow),
  JUMP_INIT(underflow, _IO_old_file_underflow),
  JUMP_INIT(uflow, _IO_default_uflow),
  JUMP_INIT(pbackfail, _IO_default_pbackfail),
  JUMP_INIT(xsputn, _IO_old_file_xsputn),
  JUMP_INIT(xsgetn, _IO_default_xsgetn),
  JUMP_INIT(seekoff, _IO_old_file_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_old_file_setbuf),
  JUMP_INIT(sync, _IO_old_file_sync),
  JUMP_INIT(doallocate, _IO_file_doallocate),
  JUMP_INIT(read, _IO_file_read),
  JUMP_INIT(write, _IO_old_file_write),
  JUMP_INIT(seek, _IO_file_seek),
  JUMP_INIT(close, _IO_file_close),
  JUMP_INIT(stat, _IO_file_stat)
};

其中第一个参数对应第二个参数的函数,明确初始化vtable中对应的函数

STDFILE 的初始化

DEF_STDFILE(_IO_stdin_, 0, 0, _IO_NO_WRITES);
DEF_STDFILE(_IO_stdout_, 1, &_IO_stdin_, _IO_NO_READS);
DEF_STDFILE(_IO_stderr_, 2, &_IO_stdout_, _IO_NO_READS+_IO_UNBUFFERED);
  • 第二个参数:初始化其对应的_fileno
    • stdin 0
    • stdout 1
    • stderr 2
  • 第三个参数:将_IO_FILE结构体串到一个_chain
    • 0:表示_IO_stdin_位于链表头,前面没有其他元素
    • &_IO_stdin_:表示_IO_stdout_在链表中位于_IO_stdin_的后面
    • &_IO_stdout_:表示_IO_stderr_在链表中位于_IO_stdout的后面
  • 第四个参数:flags部分

PUTS 流程

当我们使用 puts 函数时,实际上调用的是_IO_puts

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_putc_unlocked ('\\n', stdout) != EOF)
    result = MIN (INT_MAX, len + 1);
  _IO_release_lock (stdout);
  return result;
}

weak_alias (_IO_puts, puts) // puts 就是 _IO_puts
libc_hidden_def (_IO_puts)

主要关注_IO_sputn (stdout, str, len) == len

其中_IO_sputn是一个宏,定义为:

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

位于:glibc/libio/libioP.h

接下来_IO_XSPUTN又会对应一些列的展开:

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus(THIS)))

IO_validate_vtable基本作用是为了验证当前vtable的位置是否正确合理

#define _IO_JUMPS_FILE_plus(THIS)  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \\
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \\
                       + offsetof(TYPE, MEMBER)))

算出当前TYPEMEMBERoffset在哪边,并将其加到THIS

简化总结其流程就是:

stdout->vtable->__xsputn(stdout,str,len)

但其实其调用的应该是_IO_file_xsputn,该函数是_IO_new_file_xsputn的别名

libc_hidden_ver (_IO_new_file_xsputn,_IO_file_xsputn)

_IO_new_file_xsputn才是最终将内容输出出来的函数

若可以修改stdout内部的某个参数,则可能实现任意读

poc:

#include <stdio.h>

int main()
{
    _IO_FILE *p;
		// FILE *p;
    char buf[] = "Programmer: You can't see me\\n";

    printf("Let's Demo a arbitrary read\\n");

    p = stdout;
    p->_IO_read_end = buf;
    p->_IO_write_base = buf;
    p->_IO_write_ptr = buf + strlen(buf);
    p->_IO_buf_end = buf + strlen(buf);

    puts("Hacker: uhhh,but I can\\n");

}

该程序通过修改stdout内部指针指向,修改了其默认所指缓冲区的位置,达到任意读的目的

1675049643_63d73aab0655ae91b3a9f.png!small?1675049643067

注意:若编译失败,提示找不到_IO_FILE结构体,可能是因为已将其重命名去掉_IO_前缀的原因,使用FILE 即可

根据刚刚的分析下,最后输出数据的是_IO_new_file_xsputn这个函数,所以就从该函数开始跟踪

_IO_new_file_xsputn

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;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)

该函中一进来首先会检查_flags标志位,检查其是否启用:

  • _IO_LINE_BUF
  • _IO_CURRENTLY_PUTTING

1675050003_63d73c139148792bdb37d.png!small?1675050003376

若两个标志位都有启用设置(实际stdout这两个标志都会被设置),就会计算count值(_IO_buf_end_IO_write_ptr的距离:当前缓冲区还剩多少空余空间),count = _IO_buf_end - _IO_write_ptr

若让此处的count = 0_IO_buf_end = _IO_write_ptr)则后面的利用会更加容易

count != 0则后续还会涉及到应对措施

1675050020_63d73c24311e2484dab9e.png!small?1675050020417

count = 0则不会进入以上的if而是接着向下执行,又因为to_do是一个> 0的数(size_t to_do = n;),所以to_do + must_flush一定> 0则会进入接下来的if语句中

1675050032_63d73c30d27c62f030a20.png!small?1675050033115

在该if中,会调用_IO_OVERFLOW(f,EOF)_IO_OVERFLOW也是一个宏,其展开过程与上面类似,最终调用的是_IO_new_file_overflow,所以接着跟入该函数

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;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)

该函数中一开始要检查_flags中,是否设置了_IO_NO_WRITES标志位

1675050048_63d73c401148783b1443c.png!small?1675050047978

stdout本身就没有设置该标志位,所以此处无需绕过该if

之后会检测:

  • 是否没设定_IO_CURRENTLY_PUTTING最初就有设定,无需刻意绕
  • _IO_write_base是否为NULL一般不为NULL,无需刻意绕

只要满足任何一点,就会进入,该if中会进行安全检查,妨碍利用代码

1675050062_63d73c4e8c44ff5ee4662.png!small?1675050062376

若没有进入该if,则继续向下执行,再次if判断ch == EOF,由于当初调用_IO_new_file_overflow时,传递的参数ch = EOF,所以此处判断成立

1675050074_63d73c5ad02917324bf44.png!small?1675050074665

则会调用_IO_do_write函数,传递的参数为:

  • f->_IO_write_base

    写缓冲区指针

  • f->_IO_write_ptr - f->_IO_write_base

    由于_IO_write_ptr指向了当前已经写入数据的最后位置,所以此处就是算出当前写缓冲区中写入了多少数据,方便接下来输出

    也就是输出_IO_write_base_IO_write_ptr的数据

其中_IO_do_write_IO_new_do_write的别名,而_IO_new_do_write又会调用new_do_write

1675050089_63d73c69ed24952e77ea8.png!small?1675050089974

所以最终跟进new_do_write来看

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

开始会检查是否不具有_IO_IS_APPENDING标志位,此处无需担心,本身该标志位就没有被设定,所以不会进入该if,无需特意绕过

1675050104_63d73c782ab2091174711.png!small?1675050104377

所以会进入下一个else if进行判断

1675050115_63d73c834b38e032f9982.png!small?1675050114963

若进入该else if则会进行相关安全性检查,会破坏利用,所以应尽量绕过该else if

所以要让_IO_read_end = _IO_write_base

绕过该if就会进入_IO_SYSWRITE此时就无需再往后追啦,基本已经到达最低层

_IO_SYSWRITE (fp, data, to_do);

fp输出to_dodata

  • data_IO_write_base
  • to_do_IO_write_ptr - _IO_write_base

结论

所以最后得出只要满足一下几个条件,便可以绕过安全检查:

  • _IO_buf_end = _IO_write_ptr
  • _IO_read_end = _IO_write_base

这样呼叫puts就会额外输出_IO_write_base_IO_write_ptr的内容

代码查看

https://codebrowser.dev/glibc/glibc/libio/


文章来源: https://www.freebuf.com/articles/system/355987.html
如有侵权请联系:admin#unsafe.sh