Tech/heap

stdout을 이용한 leak

hackctf childheap 문제로 처음 접한 기법이다. 최근 heap문제에서 자주 사용되기도 하고 한번 정리해두고 싶어 글을 쓰게 되었다. (모자란 부분은 조금씩 채워 나갈 예정)

Source Code

source code는 puts의 code를 간단하게 분석하면서 최종적으로 _IO_SYSWRITE가 어떻게 호출이 되는지 알아보자.

먼저 호출되는 puts함수부터 살펴보면 다음과 같다.

_IO_puts

#include "libioP.h"
#include <string.h>
#include <limits.h>
​
int _IO_puts (const char *str)
{
  int result = EOF;
  size_t len = strlen (str);
  _IO_acquire_lock (_IO_stdout);
​
  if ((_IO_vtable_offset (_IO_stdout) != 0
       || _IO_fwide (_IO_stdout, -1) == -1) //스트림 지향 판별                                                                         //(return > 0 : wide | return < 0 byte)
      && _IO_sputn (_IO_stdout, str, len) == len
      && _IO_putc_unlocked ('\n', _IO_stdout) != EOF)
    result = MIN (INT_MAX, len + 1);
​
  _IO_release_lock (_IO_stdout);
  return result;
}
weak_alias (_IO_puts, puts)
libc_hidden_def (_IO_puts)

여기서 가장 중요한 것은 _IO_sputn함수이며, _IO_sputn 함수는 다음과 같은 매크로를 거쳐 최종적으로 _IO_new_file_xsputn함수가 호출된다.

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)#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 _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
#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)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)

 

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

우리가 이 함수에서 원하는것은 _IO_OVERFLOW를 호출하는 것이며, 호출을 하기 위해서는 조건문을 우회할 필요가 있다.

조건문

  • if (n<=0)
    • n은 문자열 길이에 해당한다. 보통은 특별히 신경 안 써도 되는 부분
  • if ((f->_flags & _IO_LINE_BUF) && (f-> _flags & _IO_CURRENTLY_PUTTING))
    • flags에 두 flag가 set 되어야 조건문을 들어가지만 이 중 _IO_CURRENTLY_PUTTING만 사용하니 상관이 없다
  • else if (f->_IO_write_end -> f-> _IO_write_ptr)
    • file pointer의 write_end와 write_ptr 부분을 비교하지만 크게 신경 쓸 필요가 없다
  • if (count > 0)
    • 다른 조건문을 우회했다면 count는 0이다
  • if (to_do + must_flush > 0)
    • to_do는 n의 값을 가지고 있어 조건문을 들어가 _IO_OVERFLOW를 호출할 수 있다.

 

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

_IO_new_file_overflow 함수에서는 _IO_do_write 함수를 호출해야 하며, 이전 함수와 같이 조건문을 우회해야 한다.

조건문

  • if (f->_flags & _IO_NO_WRITES)
    • flags에 _IO_NO_WRITES를 set가 되어야 성립하므로 set를 되지 않도록 한다.
  • if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || (f-> _IO_write_base) == NULL)
    • flasg만 신경을 쓰면 되며, flags에 _IO_CURRENTY_PUIING을 set 해주면 0이 아니기 때문에 우회할 수 있다.
  • if (ch == EOF)
    • 이전 함수(_IO_new_file_xsputn)에서 인자로 EOF를 넘겨주었기에 조건문의 성립되어 _IO_do_write 함수를 호출할 수 있다.

 

_IO_do_write, _IO_new_do_write

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;
}
libc_hidden_ver (_IO_new_do_write, _IO_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_new_do_write를 호출하게 되면, _IO_new_do_write의 인자를 그대로 가지고 new_do_write 함수를 호출하게 된다.

new_do_write 함수에서는 _IO_SYSWRITE 함수를 호출하여 leak 하게 되는데 그러기 위해서는 처음 조건문을 만족하여 else if문을 우회하여 _IO_STSWRITE 함수를 호출한다.

  • if (fp->_flags & _IO_IS_APPENDING)
    • IO_IS_APPENDING을 set 해주어 조건문에 만족하게 한다.

 

최종 flags

_flags = 0xfbad0000   
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000  _flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800  _flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

 

Reference