tmxklab

stdout flag를 이용한 libc leak 본문

Security/01 System Hacking

stdout flag를 이용한 libc leak

tmxk4221 2020. 8. 26. 03:22

stdout flag값을 변경하여 libc주소를 leak하는 방법을 정리하였다

따로 libc주소를 leak할 방법이 없을 때 유용하게 쓰일 것 같당ㅎㅎ

 

관련 문제 :

 

[HackCTF/Pwnable] ChildHeap

stdout을 이용한 libc주소 leak & fastbin dup 1. 문제 nc ctf.j0n9hyun.xyz 3033 1) mitigation 확인 2) 문제 확인 3) 코드 흐름 확인 3-1) main() int __cdecl __noreturn main(int argc, const char **argv..

rninche01.tistory.com

 


1. 프로세스

_IO_FILE struct의 멤버 변수 _flags를 특정한 값(하위 2byte를 0x1800)으로 변경하고 _flags부터 _IO_write_base의 하위 1byte까지 NULL값으로 채워서 puts와 같이 stdout을 사용하는 함수가 호출됨에 따라 비정상적인 루틴으로 인해 libc주소가 leak이 되도록 한다.

 


2. FILE Structure

FILE이란 파일 스트림이라고 하는 Linux 시스템의 표준 IO 라이브러리에 있는 파일을 설명하는 구조이다. 기본적으로 표준 I/O 라이브러리에서 프로그램 실행시 모두가 잘 아는 표준 스트림 3가지를 제공해준다.

  • stdin : 표준 입력 스트림
  • stdout : 표준 출력 스트림
  • stderr : 표준 에러 스트림

위 3가지 표준 스트림은 _IO_FILE 구조체 형식을 띄며 chain필드를 통해 서로 연결되어 있다. 연결 목록 헤더는 전역 변수_IO _list_all로 연결되어 있는 목록들(stdin, stdout, stderr)을 찾을 수 있다.

  • _IO_list_all을 시작으로 chain필드를 통해 표준 스트림들이 연결
  • 실제 기호
    • stdin : _IO_2_1_stdin
    • stdout : _IO_2_1_stdout
    • stderr : _IO_2_1_stderr

  • 그리고 위 3가지 표준 스트림들은 libc.so의 데이터 세그먼트에 존재한다.

이러한 표준 스트림을 통해 입출력시 임시 버퍼로 사용되어 저장하는 역할을 한다. (예를 들어 puts에서 어떠한 문자열을 출력하는 경우 출력할때 마다 커널 버퍼를 사용하면 매우 비효율적이므로 → 까망눈 연구소님의 글 인용)

예를 들어 puts()를 호출하여 어떠한 문자열을 출력하면 문자열이 stdout에 먼저 저장되고 출력된다.

 

그럼 이제 각 표준 스트림들이 가지는 _IO_FILE 구조체를 살펴보자

 

2.1 _IO_FILE struct

(glibc/libio/bits/types/struct_FILE.h)

/* The tag name of this struct is _IO_FILE to preserve historic
   C++ mangled names for functions taking FILE* arguments.
   That name should not be used in new code.  */
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
};
  • 많은 멤버 변수를 가지고 있는데 여기서 주목해야 할 부분은 첫 번째 멤버 변수인 _flags이다.
  • _flags : 상위 2byte는 0xfbad의 매직 워드를 가지고 있으며 하위 2byte는 플래그 정보를 가지고 있다. (flags 값은 밑에 코드에 정의되어 있다.)
  • _IO_read_ptr부터 _IO_buf_end까지 총 8개의 멤버변수는 stream buffer에 사용된다.
  • _chain : chain필드를 통해 표준 스트림들이 연결

  • _lock : 멀티쓰레딩 환경에서 파일을 읽고 쓸 때 race condition을 방지하기 위해 사용되는 포인터로 write권한이 있는 메모리 영역을 가리키고 있어야 한다.

 

_IO_FILE struct - stdout

  • 좀 차이가 있는 것 같다...(현재 환경 : glibc 2.27)

 

2.2 _flags

(glibc/libio/libio.h)

/* 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 struct의 필드인 _flags의 값을 정의한 부분이다.
  • _flags필드는 4byte 크기를 가지며 상위 2byte인 0xfbad0000(_IO_MAGIC)가 정의되어 있다.
  • 나머지 하위 2byte는 _IO_MAGIC_MASK매크로 밑에 매크로들을 조합하여 만든다.
  • libc주소를 leak해야하는 관점에서 주목해야 할 부분은 _IO_CURRENTLY_PUTTING(0x0800)과 _IO_IS_APPENDING(0x1000)이다.

하지만 실제로 IO_FILE구조는 함수 포인터를 가리키는 vtable을 포함한(_IO_jump_t구조체 멤버 변수) _IO_FILE_plus struct로 한 번더 감싸져 있다.

 

2.3 _IO_FILE_plus struct

(glibc/libio/libioP.h)

struct _IO_FILE_plus
{
  _IO_FILE file;
  const struct _IO_jump_t *vtable;
};
  • 기존에 사용되는 _IO_FILE 구조체에서 vtable구조체인 _IO_jump_t멤버 변수가 추가되었다.

 

_IO_FILE_plus - stdout

 

2.4 _IO_jump_t struct

(glibc/libio/libioP.h)

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);
};
  • _IO_FILE_plus구조체에 포함된 vtable관련 구조체

 

_IO_FILE_plus→vtable

  • 오늘 내용에서 주의 깊게 봐야할 부분은 빨간색 박스에 들어있다. ㅎㅎ

 

전체적인 구조)

이제 puts()내부 루틴을 확인해보면서 stdout이 어떻게 사용되는지 확인해보자

 


3. puts() 내부 루틴

puts에 대한 코드는 glibc/libio/iofputs.c 에 있으며 함수 이름은 _IO_puts이다.

 

3.1 _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)
libc_hidden_def (_IO_puts)
  • weak_alias매크로에 의해 puts함수가 _IO_puts로 바인드됨으로써 puts심볼이 _IO_puts로 파싱됨
  • 여기서 중요한 점은 if문에서 _IO_sputn이 호출되는 것이다.
  • _IO_sputn(stdout, str, len)

 

3.2 _IO_sputn() → _IO_XSPUTN()

#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
  • _IO_sputn은 매크로이며 _IO_XSPUTN을 호출한다.
  • _IO_sputn(stdout, str, len) : _IO_XSPUTN(stdout, str, len)

 

이후에 계속 매크로 형식으로 이어져있는 것을 확인할 수 있다.

#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
  • _IO_XSPUTN(stdout, str, len) : JUMP2(__xsputn, stdout, str, len)
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2) 
  • JUMP2(__xsputn, stdout, str, len) : (_IO_JUMPS_FUNC(stdout)→__xsputn)(stdout, str, len)
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)        \
                             + (THIS)->_vtable_offset)))
  • _IO_JUMPS_FUNC(stdout) : (IO_validate_vtable(*(struct _IO_jump_t *) ((void) & _IO_JUMPS_FILE_plus(stdout) + (stdout)→_vtable_offset)))
#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
  • _IO_JUMPS_FIILE_plus(stdout) : _IO_CAST_FIELD_ACCESS((stdout), struct _IO_FILE_plus, vatble)
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
                                       + offsetof(TYPE, MEMBER)))
  • _IO_CAST_FIELD_ACCESS(stdout, struct _IO_FILE_plus, vtable) : (*(_IO_MEMBER_TYPE (struct _IO_FILE_plus, vtable) *)(((char *) (stdout)) + offsetof(struct _IO_FILE_plus, vtable)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)
  • _IO_MEMBER_TYPE(struct _IO_FILE_plus, vtable) : typeof (((struct _IO_FILE_plus){}).vtable)

너무 복잡해지는 것 같은데... 최종적으로

(_IO_FILE_plus)_IO_2_1_stdout→vtable.__xsputn(stdout, str, len)

이 된다.

 

즉, _IO_FILE_plus의 구조체를 가진 _IO_2_1_stdout의 멤버변수인 vtable포인터를 따라가면 __xsputn을 호출하는데 __xsputn에 해당하는 함수인 _IO_new_file_xsputn를 호출하게 된다. (위에서 디버깅 사진 참고)

 

3.3 _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;
}
  • 버퍼의 여유 공간을 가져와서 count에 저장하고 공간이 여유가 있으면 버퍼에 직접 쓰고 나머지는 to_do에 카운트된다.
  • 이후에 to_do가 남아있어 수요가 충족되지 않으면 _IO_OVERFLOW()를 호출한다.
  • 그렇지 않은 경우 _IO_default_xsputn()을 호출하여 데이터를 버퍼에 쓴다.
  • 중점적으로 봐야할 부분은 _IO_OVERFLOW()를 호출하는 부분이다.
  if (to_do + must_flush > 0)
    {
      size_t block_size, do_write;
      /* Next flush the (full) buffer. */
      if (_IO_OVERFLOW (f, EOF) == EOF)
  • to_do(출력할 문자열의 길이)와 must_flush(변수의 의미를 모르겠지만 그 전 if문에서 개행문자가 존재하면 1로 세팅됨)의 합이 0보다 크면 if문안의 로직이 실행된다.
  • 즉, 이전에 출력한 크기 이상으로 출력이 오면 IO_OVERFLOW를 호출
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
  • _IO_OVERFLOW 또한 매크로 형식으로 선언되어 있으며 _IO_2_1_stdout의 vtable의 __overflow에 해당하는 _IO_new_file_overflow()를 호출한다.

 

3.4 _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;
}
  • 먼저 파일이 쓰기 가능한지 확인한다. (첫 번째 if문)
  • 다음에 파일이 쓰기 상태인지 아니면 write_base가 널 값인지 확인하여 write_base가 널 값인 경우 _IO_doallocbuf()를 호출하여 예약된 버퍼를 적용하고 읽기 버퍼에 할당한다.
  • 다음으로 _IO_read_ptr == _IO_buf_end이면 stdout의 필드 값을 세팅해준다.
  • 마지막으로 ch가 EOF이면 (해당 함수를 호출할 때 파라미터로 ch에 EOF로 설정해줬음) _IO_do_write()를 호출하여 기록된 버퍼의 데이터를 파일에 쓴다.
  • 여기서 포인트는 _IO_do_write()를 호출해야 하는 것이다.
  if (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
                         f->_IO_write_ptr - f->_IO_write_base);
  • _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);
  • (해당 함수의 파라미터를 기억해두자, 이따가 다시 언급됨)
  • 위 코드를 실행하기 위해 2개의 if문을 우회해야 한다.

 

1) 첫 번째 if문

먼저 _IO_new_file_overflow()코드의 첫 번째 if문을 보자

  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return EOF;
    }
  • stdout→_flags 값이 _IO_NO_WRITES(0x0008)가 세팅되면 return되므로 세팅되지않도록 하여 return되지 않도록 한다.

 

2) 두 번째 if문

그 다음 2번째 if문을 보자

  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
  • stdout→_flags에서 _IO_CURRENTLY_PUTTING(0x800)가 세팅되지 않았거나 stdout→_IO_write_base의 값이 널 값이면 if문안의 로직이 실행된다.
  • 따라서 stdout→_IO_CURRENTLY_PUTTING(0x800)가 세팅되고 stdout→_IO_write_base가 널 값이 아니도록 세팅하여 불필요한 코드를 우회한다.

 

최종적으로 _IO_new_file_overflow()에서 _IO_do_write()를 호출하게 된다고 가정하고 다음 _IO_new_do_write()코드를 보자(_IO_do_write를 호출하게 되면 _IO_new_do_write()를 호출하게 됨)

 

3.5 _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)
  • return하기전에 new_do_write()를 호출하는 로직이 있는데 저 부분이 포인트이다.

 

3.6 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;
}
  • offset을 찾아 _IO_SYSWRITE()를 호출한다.
  • 위 코드에서 포인트는 _IO_SYSWRITE()를 호출하는 부분이다.
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
  • _IO_SYSWRITE() 또한 매크로로 정의되어 있으며 stdout vtable의 __write에 해당하는 _IO_new_file_write()를 호출하게 된다. _IO_new_file_write()는 실제로 len만큼 버퍼에 있는 데이터를 화면에 출력해주는 역할을 한다.
  • 최종적으로 _IO_SYSWRITE()를 호출하게 되면서 leak이 된다.
  • _IO_SYSWRITE() 실행하기에 앞서 문제가 발생하는 new_do_write()로직의 else if문을 우회해야 한다.

 

1) 첫 번째 if문

 if (fp->_flags & _IO_IS_APPENDING)
  • stdout→_IO_IS_APPENDING(0x1000)을 세팅해주면 else if문의 우회한다.

 

3.7 puts() 내부 루틴 정리

 

 

3.8 결론

다시 돌아와서 우리의 목표는 libc주소를 leak을 하는 것이고 leak을 하기 위해 위와 같은 루틴이 되어야 한다.

그리고 위와 같은 루틴대로 흘러가기 위해서 아까 얘기한 불필요한 코드 및 _IO_SYSWRITE를 실행하기 위해 특정 조건문을 우회해야 하는 것이다.

 

따로 올려보기 귀찮아서 다시 우회해야 하는 부분을 정리해보았다.

 

1) _IO_new_filw_overflow()

첫 번째 if문

  • stdout→_flags값이 _IO_NO_WRITES(0x0008)로 세팅되지 않도록 하기
    • 보통 해당 0x0008로 세팅되지 않으므로 신경 ㄴㄴ

두 번째 if문

  • stdout→_flags값이 _IO_CURRENTLY_PUTTING(0x0800)로 세팅하거나
  • stdout→_IO_write_base가 널 값이 아니도록 세팅
    • 보통 _IO_write_base에 주소 값 있어서 걍 _IO_CURRENTLY_PUTTING(0x0800)로 세팅하는거 하나만 해도됨

2) new_do_write()

첫 번째 if문

  • stdout→_flags값이 _IO_IS_APPENDING(0x1000)로 세팅하면 됨
    • 걍 0x1000으로 세팅하면됨

 

결론)

stdout→_flags값을 _IO_CURRENTLY_PUTTING(0x0800)과 _IO_IS_APPENDING(0x1000)로 세팅하면 되므로 하위 2byte를 0x1800으로 세팅

 

그리고 가장 궁금한 부분이 있을 것이다. 그럼 data가 출력되지 않고 어떠한 값이 출력되는 것일까?

아까 전에 _IO_new_file_overflow()에서 어떤 함수의 파라미터를 기억해두라고 한 부분이 있을 것이다. 그 부분부터 로직을 살펴보자

그렇다. 두둥... 최종적으로 _IO_SYSWRITE(fp, data, to_do)가

_IO_SYSWRITE(f, f→_IO_write_base, f→_IO_write_ptr - f→_IO_write_base)

이렇게 호출된다.

 

그리고 결국 출력되는 값은 stdout→_IO_write_base이며 출력 길이는 stdout→_IO_write_ptr - stdout→_IO_write_base 이다.

 


4. Debugging

그럼 위의 내용을 바탕으로 stdout→_flags의 하위 2byte를 0x1800로 변경하고 puts를 호출하여 확인해보자

 

1) 소스코드

[ stdout_leak.c ]

#include<stdio.h>

int main(){

	setbuf(stdout, NULL);
	stdout->_flags = 0xfbad0000 | 0x1000 | 0x800;
	stdout->_IO_write_base -= 8;

	puts("hi");

	return 0;
}
  • stdout스트림에 버퍼가 다 채워지지 않으면 출력되지 않으므로 setbuf(stdout, NULL)코드를 추가하여 stdout stream을 unbuffered stream으로 만들어 버퍼가 채워지지 않아도 강제로 출력하게 만들었다.
  • _flags에 매직넘버인 0xfbad0000과 하위 2byte에는 0x1800을 넣었다.
  • 추가로 stdout→_IO_write_base에 8을 빼는 이유는 출력 길이를 늘리기 위해서이다.
  • 위에서 _IO_SYSWRITE 를 호출할 때 출력 길이를 f→_IO_write_ptr - f→_IO_write_base 로 정해졌는데 8을 빼지 않으면 둘 다 동일한 값이어서 길이가 0이므로 아무것도 출력되지 않는다. _IO_OVERFLOW 로직이 종료되고 _IO_new_file_xsputn()로직으로 돌아와 "hi"를 출력하게 된다.

인자로 "hi"가 담겨있는 buffer와 출력할 길이 2byte를 받고 vtable에 의해 _IO_new_file_write를 호출한다.

 

2) stdout

  • puts()호출 직전 stdout의 멤버변수 세팅된 결과이다.
  • _flags : 0xfbad1800
  • _IO_write_base : 0x7ffff7dd07db
  • _IO_write_ptr : 0x7ffff7dd07e3
    • _IO_write_ptr(0x7ffff7dd07e3)에서 _IO_write_base(0x7ffff7dd07db)를 빼면 8이므로 8bytes만큼 출력될 것이다.
    • _IO_SYSWRITE(f, f→_IO_write_base, 8)

 

3) _IO_new_file_overflow()

  • 현재 _IO_new_file_overflow에 들어온 상황이다.
  • $bt(backtrace)를 통해 확인한 결과 _IO_puts() → _IO_new_file_xsputn() → _IO_new_file_overflow()흐름으로 들어온 것을 확인할 수 있다.
  • 그리고 첫 번째 if문인 if (f->_flags & _IO_NO_WRITES) 을 실행하기 위해 test cl, 0x8 (_IO_NO_WRITES → 0x0008)을 하고 jne인스트럭션이 실행되는 것을 확인할 수 있다. (cl에는 stdout→_flags값이 들어있음)

  • 두 번째 if문인 if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) 을 검사하기 위해 먼저 _IO_CURRENTLY_PUTTING(0x0800)이 stdout→flags에 세팅되었는지 test ch, 0x8인스트럭션을 실행한다.
  • 세팅 했으므로 AND연산을 통해 (f->_flags & _IO_CURRENTLY_PUTTING)가 1이 되므로 ZF는 설정되지 않을 것이다.

  • ZF가 설정되지 않았으므로 다음 test rsi, rsi가 실행된다.

  • rsi에는 _IO_write_base의 주소 값이 들어있으므로 f->_IO_write_base == NULL를 통과할 수 있다.

  • 최종적으로 if문을 우회하고 _IO_new_do_write()를 호출하게 된다.

 

4) _IO_new_do_write()

  • _IO_new_do_write()가 호출되고 첫 번째 if문인 if(fp->_flags & _IO_IS_APPENDING)을 검증하기 위해 test DWORD PTR[rdi], 0x1000을 실행하였다. 아까 _IO_IS_APPENDING(0x1000)을 세팅해줬기 때문에 참이되며 else if문을 피해 if문 안의 로직을 실행한다.

  • 최종적으로 _IO_SYSWRITE()를 호출하기 위해 인자 값을 stdout, stdout→_IO_write_base, 8을 받고 있다.
  • 참고로 _IO_SYSWRITE()는 _IO_new_file_write() 를 호출한다. 위에 참고

 

5) 최종 - _IO_new_file_write()

  • libc_write호출

 

출력 값)

  • 아까 stdout→_IO_write_base에 저장된 0xffffffffff000000 총 8bytes가 출력된다.

ㅋ......

 

근데 이거 가지고 무슨 libc 주소가 leak되냐고 의아할 것이다.

맞다 아직 한 가지 안했당. ㅋㅋㅋㅋ

 

처음 프로세스 설명에서 _flags를 특정한 값(하위 2byte를 0x1800)으로 변경하고 _flags부터 _IO_write_base의 하위 1byte까지 NULL값으로 채운다고 했다.

그렇다 이 부분을 안했다.... ㅈㅅ...

 

바로 디버깅하면서 확인해보자

 

참고로 _flags부터 _IO_write_base의 하위 1byte까지 25byte이며 굳이 _IO_write_base의 하위 1byte까지 널 값으로 덮는 이유는 한 번 생각해보길 바란다... ㅎㅎㅎ

 

1) stdout flag변경 및 null 25bytes 삽입

gdb-peda$ set {int}0x7ffff7dd0760=4222425088 // 0xfbad1800
gdb-peda$ set {double}0x7ffff7dd2628=0
gdb-peda$ set {double}0x7ffff7dd2630=0
gdb-peda$ set {double}0x7ffff7dd2638=0
gdb-peda$ set {char}0x7ffff7dd2640=0 // total 25byte null 추가
  • 코딩하려고 했는데 까망눈 연구소님의 블로그에서 set명령어로 하는 방법을 보고 이마를 탁 쳤다.

  • ㅎㅎㅎ아무튼 위에서 세팅한대로 값이 들어가 있다.

자 그럼 생각을 해보자 flags에 하위 2byte를 0x1800 그대로 설정해주었고 _IO_read_ptr부터 _IO_write_base의 하위 1byte까지 널 값으로 채워줬다.

 

그럼 아까 로직대로 최종적으로 _IO_SYSWRITE()를 호출하게 되는데 이 때, 파라미터를 확인해보면

_IO_SYSWRITE(stdout, _IO_write_base, _IO_write_ptr - _IO_write_base)이다.

 

즉 출력할 수 있는 길이가 늘어났다.

_IO_write_ptr(0x7ffff7dd07e3) - _IO_write_base(0x7ffff7dd0700) = 0xe3(227)

 

따라서, _IO_write_base부터 총 227byte 출력하게 되는데(항상 227byte는 아님, 기본적으로 세팅된 주소 값에 따라 다름)이 때, 밑에 세팅된 stream buffer의 주소 값이 릭될 것이다.

 

2) libc leak

  • write(1, _IO_write_base, 0xe3);

 

+) 추가

근데 항상 _flags부터 25byte 널 값을 붙이지 않아도 된다.

요점은 length를 늘려주기 위해 _IO_write_base의 하위 1byte를 널 값으로 변경한 것

 

다음 디버깅을 통해 확인해보자

  • _flags의 하위 2byte와 _IO_write_base의 하위 1byte만 변경하였다.

  • 이렇게 해도 결국 똑같이 write(1, _IO_write_base, 0xe3)이 되면서 leak이 가능하다.

 

즉, 굳이 25byte 널 값을 넣을 필요는 없는데

워 게임 문제를 풀 때 libc주소를 찾았고 libc주소를 통해 stdout에 임의의 값을 쓸 수 있을 때 _flags값부터 세팅해주고 또 굳이 _IO_write_base주소를 찾아서 하위 1byte를 널 값으로 채워줄 필요없이 걍 _flags부터 _IO_write_base주소까지 한번에 채워주는게 편하므로 그냥 _flags다음으로 25byte만큼 널 값을 채워주는 것 같다.

 

 

진짜 최종 결론)

  • libc주소를 마땅히 leak할 만한 함수가 없고 stdout을 이용하여 출력하는 함수가 보일 때( ex) puts(), printf() )유용한 방법이다.
  • stdout의 주소에 값을 쓸 수 있는 상황이여야 한다.
  • stdout의 _flags에 0xfbad1800으로 세팅하고 _flags다음 _IO_read_ptr부터 _IO_write_base의 하위 1byte까지 널 값으로 세팅한다.
  • 세팅이 완료되면 예를 들어 puts("이얏호응");를 호출하게 되면 최종적으로 write(1, _IO_write_base, 0xe3); (항상 0xe3은 아님)호출돼서 stdout의 stream buffer의 주소를 leak할 수 있게 된다.
  • 참고로, stdout은 libc의 데이터세그먼트에 존재하므로 libc base address를 구할 수 있다.

 

leak)

 

 


5. 참고 자료

Comments