So, here comes my favorite topic, File Stream Oriented Programming. This topic can be a little bit complex for people new to Heap Exploitation. This awesome technique was developed by Angelboy.

First thing’s first, let’s discuss about the FILE structure.

A stream is a logical entity that represents a file or device, that can accept input or output. FILE is nothing but a typedef’d strucutre present in the standard IO library.

FILE is a structure for describing files in a standard IO library of a Linux system, called a file stream.

You may run ptype \o FILE or dt FILE to view the _IO_FILE

struct _IO_FILE {

  int _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



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


A stream is linked to a file using an open operation and unlinked using a close operation. _flags denotes the mode of a file stream such as read-only, open for reading and writing, etc.Thus, the stream buffer can be divided into three parts: read buffer (consists of _IO_read_ptr ,_IO_read_end and _IO_read_base ), write buffer (_IO_write_ptr, _IO_write_end, _IO_write_base) and reserve buffer(_IO_buf_base, _IO_buf_end). i/o operations . The pointers ending with _base point to the start of their respective buffers. The pointers ending with _ptr point to the current position in the buffer. Similarly, the pointers ending with _end point to the end of the buffer. The _mode field can be used to disable the file stream

extern _IO_FILE *_IO_stdin attribute_hidden;
extern _IO_FILE *_IO_stdout attribute_hidden;
extern _IO_FILE *_IO_stderr attribute_hidden;

Two FILE strucutres are connected to each other by the _chain field. Thus, the FILE structures form a linked list whose head is represented by the global variable _IO_list_all. _fileno is a file descriptor which is set by the sys_open syscall.

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

vtable is the virtual function table

#define JUMP_FIELD(TYPE, NAME) TYPE NAME

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);
#if 0
    get_column;
    set_column;
#endif
};

The _IO_FILE_plus struct is an extension of the FILE structure. Standard input, output and error streams also use this structure.

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

stdin, stdout and stderr streams reside in the libc’s .data section

Virtual Functions

A virtual function is a member function of the base class that can be redefined in the derived classes. If we create a virtual function in the base class and it is being overridden in the derived class, we do not to redeclare it as virtual. It will automatically be considered as a virtual function.

vtables (Virtual Tables)

A vtable is a lookup table of functions used to resolve function calls in a dynamic manner. It stores function pointers of the virtual functions that can be called by objects of that class. Every class that uses virtual functions (or is derived from a class that uses virtual functions) is given its own vtable as a hidden data member (setup by the compiler at compile time). Every vtable has a vptr associated with it. A vptr points to a vtable, and is used to access functions present in the vtable. The addresses stored in the vtable or the vptr itself can be overwritten to perform code execution!!

Now, let’s see the working of fopen, fread , fwrite and fclose as explained by Angelboy.

fopen

Whenever fopen is called, the glibc allocates some memory for the FILE structure.

*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));

// Initializing the vtable

_IO_JUMPS (&new_f->fp) = &_IO_file_jumps;
_IO_file_init (&new_f->fp);


_IO_link_in is used to called to link the newly allocated FILE struct into the linked list of FILE structures

void _IO_link_in (fp)     struct _IO_FILE_plus *fp;

{

    if ((fp->file._flags & _IO_LINKED) == 0)

    {

      fp->file._flags |= _IO_LINKED;

fp-> file._chain = (_IO_FILE *) _IO_list_all;
      _IO_list_all = fp;

      ++_IO_list_all_stamp;

    }

}

After that, _IO_file_fopen is called

if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL)

    return __fopen_maybe_mmap (&new_f->fp.file);

Summary: Whenever fopen is called, the glibc allocates some memory for the FILE structure. , some fields of the FILE structure are initialized followed by the insertion of FILE structure into the linked list of FILE stream after which a sys_open syscall is made.

fread

If the file stream is not created, _IO_file_doallocate is used to allocate a new buffer for the file stream.

int _IO_file_doallocate (_IO_FILE *fp)
{
  _IO_size_t size;
  char *p;
  struct stat64 st;

......................................
  p = malloc (size);
  if (__glibc_unlikely (p == NULL))
    return EOF;
  _IO_setb (fp, p, p + size, 1);
  return 1;
}

After that, _IO_fread is called which internally calls _IO_sgetn (present in the vtable as _IO_XSGETN).

size_t _IO_fread (void *buf, size_t size, size_t count, FILE *fp)
{
  size_t bytes_requested = size * count;
  size_t bytes_read;
  CHECK_FILE (fp, 0);
  if (bytes_requested == 0)
    return 0;
  _IO_acquire_lock (fp);
  bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
  _IO_release_lock (fp);
  return bytes_requested == bytes_read ? count : bytes_read / size;
}

Summary:At the beginning of fread, if the stream is not created, _IO_file_doallocate is used to allocate a new buffer for the file stream. After that, data is read from the file into the stream buffer and then it is copied from the stream buffer to the destination.

fwrite

_IO_fwrite calls _IO_sputn (present in the vtable as _IO_XSPUTN).

_IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
  _IO_size_t request = size * count;
  _IO_size_t written = 0;
  CHECK_FILE (fp, 0);
  if (request == 0)
    return 0;
  _IO_acquire_lock (fp);
  if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
    written = _IO_sputn (fp, (const char *) buf, request);
  _IO_release_lock (fp);
  /* We have written all of the input in case the return value indicates
     this or EOF is returned.  The latter is a special case where we
     simply did not manage to flush the buffer.  But the data is in the
     buffer and therefore written as far as fwrite is concerned.  */
  if (written == request || written == EOF)
    return count;
  else
    return written / size;
}

Further, _IO_XSPUTN calls _IO_OVERFLOW (or _IO_new_file_overflow)

...
if (_IO_OVERFLOW (f, EOF) == EOF)
.....

Internally, _IO_OVERFLOW calls the system interface for write and checks if a flushes the stream.

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;

Summary: If the stream buffer is not created, it allocates a stream buffer. After that, the user data is copied into the stream buffer and then data is written from the stream buffer to a file.

fclose

_IO_unlink_it is called which unlinks a FILE structure from the linked list.

if (fp->_IO_file_flags & _IO_IS_FILEBUF)

_IO_unlink_it ((struct _IO_FILE_plus *) fp);

After that, _IO_file_close_it is called which closes the file

if (fp->_IO_file_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);

Finally, _IO_file_finish is called which releases the file structure

_IO_FINISH (fp);

Summary: It removes a FILE structure from the linked list of the file stream and then flushes the stream buffer, to make sure that everything is written to the file. Finally, it closes the file and releases the memory.

Attacking the vtable

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

The vtable pointer can be used to gain control of execution flow in a program. This post provides a good demonstration of attack on vtables. Vtable is one of the best targets to attack in file structure exploitation.

_IO_acquire_lock : a lock structure pointer in the file structure that must be pointing to a writable memory. It is used to prevent race conditions in a multithreaded environment. FSOP is triggered via _IO_flush_all_lockp. This function iterates through the file pointer linked list in _IO_list_all, which is equivalent to calling fflush for each FILE, and subsequently calling _IO_overflow in _IO_FILE_plus.vtable. In short, this function is used to flush the file streams in the linked list when the program terminates.

int _IO_flush_all_lockp (int do_lock)
{
  .........

  fp = (_IO_FILE *) _IO_list_all;
  while (fp != NULL)
    {
      run_fp = fp;
      if (do_lock)
    _IO_flockfile (fp);
 
      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;
 
      if (do_lock)
    _IO_funlockfile (fp);
      run_fp = NULL;
 
      if (last_stamp != _IO_list_all_stamp)
    {
      /* Something was added to the list.  Start all over again.  */
      fp = (_IO_FILE *) _IO_list_all;
      last_stamp = _IO_list_all_stamp;
    }
      else
    fp = fp->_chain;
    }
 
....
}

vtable verification

A vtable protection was added in libc2.24 which checks the address of a virtual function before calling it. Two functions, IO_validate_vtable and _IO_vtable_check were added to prevent vtable tampering. Read this for more information on bypassing the vtable pointer check.

static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  uintptr_t ptr = (uintptr_t) vtable;
  uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}

The function checks whether the vtable pointer lies in the __libc_IO_vtables section or not.

void attribute_hidden _IO_vtable_check (void)
{
#ifdef SHARED
  void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
  PTR_DEMANGLE (flag);
#endif
  if (flag == &_IO_vtable_check)
    return;
  {
    Dl_info di;
    struct link_map *l;
    if (_dl_open_hook != NULL
       || (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
            && l->l_ns != LM_ID_BASE))
      return;
  }
#else /* !SHARED */
  if (__dlopen != NULL)
    return;
#endif
  __libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}

To bypass this check, you need to make the FILE’s vtable point to some other place, which is already present inside the __libc_IO_vtables section. The _IO_str_jumps section contains a pointer to the function _IO_str_overflow.

_IO_str_overflow (_IO_FILE *fp, int c)
{
  int flush_only = c == EOF;
  _IO_size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
        return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf
        = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);