FSOP

dandb3·2024년 5월 3일
0

pwnable

목록 보기
18/22

glibc 2.34버전을 기준으로 한다.

glibc 최신 버전에서 쓸 수 있는 기법.

_IO_wide_data에서는 IO_validate_vtable이 사용되지 않는다는 점을 이용한다.

배경지식 - IO_validate_vtable

file struct에서는 파일 입출력과 관련된 여러 함수들을 호출할 때 vtable을 참조하여 호출한다.

간단하게 확인해 보자.

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

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 구조체 바로 뒤에 vtable이 존재하며, 여기에는 여러 함수포인터들이 위치해 있다.
즉, 특정 함수를 호출할 때 vtable로부터의 offset을 통해 값을 읽어들여 함수를 실행한다.

여기서 기본적으로 생각해 볼 수 있는 취약점은, vtable의 값을 임의의 값으로 바꿀 수 있다면, 메모리에 적혀있는 주소로 jump할 수 있다는 것이다.

이러한 취약점을 막기 위해 IO_validate_vtable이라는 함수가 등장하였다.

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

함수를 보면 알 수 있듯이, 인자로 들어온 vtable의 값이 __start___libc_IO_vtables__stop___libc_IO_vtables 사이에 존재하는지 확인한다.

vtable이 '유효한 범위'에 존재하지 않는다면, _IO_vtable_check() 함수가 실행되어 memory corruption을 감지하게 된다.
_IO_vtable_check() 우회에 대해서는 나중에 알아볼 예정.

하지만 '유효한 범위' 내부에는 다양한 vtable이 존재하므로, 꽤 쓸만한 함수들이 많이 있다.

그 중 하나인 _IO_wfile_overflow에 대해서 알아볼 예정이다.

_IO_wide_data

우선 _IO_wide_data에 대해 알고 갈 필요가 있다.

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

struct _IO_FILE을 보면, 마지막 부분에 struct _IO_wide_data *_wide_data가 존재하는 것을 알 수 있다.

struct _IO_wide_data
{
  wchar_t *_IO_read_ptr;	/* Current read pointer */
  wchar_t *_IO_read_end;	/* End of get area. */
  wchar_t *_IO_read_base;	/* Start of putback+get area. */
  wchar_t *_IO_write_base;	/* Start of put area. */
  wchar_t *_IO_write_ptr;	/* Current put pointer. */
  wchar_t *_IO_write_end;	/* End of put area. */
  wchar_t *_IO_buf_base;	/* Start of reserve area. */
  wchar_t *_IO_buf_end;		/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
  wchar_t *_IO_backup_base;	/* Pointer to first valid character of
				   backup area */
  wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */

  __mbstate_t _IO_state;
  __mbstate_t _IO_last_state;
  struct _IO_codecvt _codecvt;

  wchar_t _shortbuf[1];

  const struct _IO_jump_t *_wide_vtable;
};

_IO_wide_data의 경우, 멀티 바이트 문자를 위해 쓰이는 구조체이다. (이 정도로만 하고 넘어가자.)
내부를 보면 _IO_FILE과도 굉장히 유사한데, 특히 마지막에 const struct _IO_jump_t *_wide_vtable 이 존재한다는 것을 알 수 있다.

여기서 주목할 점은, _IO_FILE의 vtable의 경우 IO_validate_vtable 함수를 통해서 유효한 범위에 속해 있는지 판단을 했었는데, _IO_wide_data의 경우 따로 체크를 하지 않는다는 점이다.

즉, _IO_wide_data의 vtable을 원하는 값으로 바꾸어 놓고 _IO_wide_data에 속해있는 함수를 실행시키면 원하는 흐름으로 바꿀 수 있을 것이다.

_IO_wfile_overflow

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);
	  _IO_free_wbackup_area (f);
	  _IO_wsetg (f, f->_wide_data->_IO_buf_base,
		     f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);

	  if (f->_IO_write_base == NULL)
	    {
	      _IO_doallocbuf (f);
	      _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
	    }
	}
    
    ...
    
}

우리의 목표는 이 함수에서 _IO_wdoallocbuf를 호출하는 것이다.

void
_IO_wdoallocbuf (FILE *fp)
{
  if (fp->_wide_data->_IO_buf_base)
    return;
  if (!(fp->_flags & _IO_UNBUFFERED))
    if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
      return;
  _IO_wsetb (fp, fp->_wide_data->_shortbuf,
		     fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

보면 알 수 있듯이, 조건만 잘 만족시킨다면 함수의 흐름이 _IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE 로 이어지게 된다.

여기서 _IO_WDOALLOCATE는 매크로로, _IO_wide_data의 vtable을 이용해서 참조하는 함수이다.
이 때, vtable값이 조작되어 있다면 익스플로잇을 할 수 있는 것이다.

예제와 함께 보자.

FSisOP


IDA를 통해 분석해 본 결과이다.

코드 자체는 간단하다.
printf를 통해 받은 데이터를 통해서 libc주소를 leak할 수 있고, 그 이후에 read를 통해 stdout 구조체를 원하는 데이터로 덮을 수 있다.
그 후 puts함수가 실행된다.

여기서 알아야 할 점은, 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;
}

소스코드를 보면, _IO_sputn, 즉 _IO_new_file_xsputn을 호출하게 된다.

다시 문제의 코드로 돌아가 보면, stdout의 vtable 포인터를 잘 조작해서 _IO_new_file_xsputn이 아닌 _IO_wfile_overflow가 호출되도록 하면 된다.
이 때 _IO_wfile_overflow는 유효한 범위에 속해있는 값으로, vtable을 조작하여도 IO_validate_vtable 검사에 걸리지 않게 된다.

그렇게 되면 결국 _IO_WDOALLOCATE 함수가 _IO_wide_data 포인터를 통해서 호출되게 된다.
처음에 값을 덮어씌울 때 _IO_wide_data 포인터를 잘 조작한 후, vtable 변수가 위치하는 곳에 적절한 주소를 써 넣고, 그 vtable에서 _IO_wfile_doallocate이 가리키는 위치에 one_gadget을 써 넣으면 익스플로잇을 할 수 있다.

귀찮아서 마지막은 대충 씀. 그림으로 그리면 아래와 같다.


진짜 대충그림. 어쨌든 이런 느낌이다. offset이 정확하진 않을 수 있음.

profile
공부 내용 저장소

0개의 댓글