블로그 이전했습니다. https://jeongzero.oopy.io/
fclose 분석
본문 바로가기
보안/Linux

fclose 분석

728x90

1. 개요

전에 풀었던 house of orange 기법에서 사용되었던 File 구조체를 이용한 기법을 FSOP (File Stream Oriented Programming)라고 한다. house of orange도 FSOP 기법 중 하나로, FILE 구조체의 내부 구조를 이용하여 공격을 진행할 수 있는데, 대표적으로 fopen, fread, fclose 등이 있다. 이번에는 fclose 소스코드를 간단하게 살펴보고, 이를 이용해 익스를 진행하는 방법을 살펴보자 

 

 

2. fclose() 함수 분석

int
_IO_new_fclose (FILE *fp)
{
  int status;
  CHECK_FILE(fp, EOF);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif
  /* First unlink the stream.  */
  if (fp->_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
  _IO_acquire_lock (fp);
  if (fp->_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);
  if (fp->_mode > 0)
    {
      /* This stream has a wide orientation.  This means we have to free
         the conversion functions.  */
      struct _IO_codecvt *cc = fp->_codecvt;
      __libc_lock_lock (__gconv_lock);
      __gconv_release_step (cc->__cd_in.__cd.__steps);
      __gconv_release_step (cc->__cd_out.__cd.__steps);
      __libc_lock_unlock (__gconv_lock);
    }
  else
    {
      if (_IO_have_backup (fp))
        _IO_free_backup_area (fp);
    }
  _IO_deallocate_file (fp);
  return status;
}

처음에 fclose 함수를 호출하면 실제로는 _IO_new_fclose() 함수가 호출된다. 위에서부터 차례대로 살펴보자 

 

#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
  /* We desperately try to help programs which are using streams in a
     strange way and mix old and new functions.  Detect old streams
     here.  */
  if (_IO_vtable_offset (fp) != 0)
    return _IO_old_fclose (fp);
#endif

요 부분은 만약 현재 glibc 버전이 예전 버전이면 호환성을 위해 실행되는 것이다. 예전 버전의 glibc가 아니면 넘어가게 된다. 

 

 if (fp->_flags & _IO_IS_FILEBUF)
    _IO_un_link ((struct _IO_FILE_plus *) fp);
  _IO_acquire_lock (fp);
  if (fp->_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
  _IO_release_lock (fp);
  _IO_FINISH (fp);

( fp → flags & _IO_IS_FILEBUF ) 해당 연산을 통해 해당 값이 존재하면, _IO_un_link 함수가 호출된다. 일반적으로 파일 포인터가 fopen으로 열려야지 fclose를 닫는데, fopen으로 열린 fp는 flag가 설정되어있어야 한다. 따라서 파일 스트림을 먼저 unlink 하는 과정을 거치게 된다. _IO_un_link 함수를 살펴보자. 

 

void
_IO_un_link (struct _IO_FILE_plus *fp)
{
  if (fp->file._flags & _IO_LINKED)
    {
      FILE **f;
#ifdef _IO_MTSAFE_IO
      _IO_cleanup_region_start_noarg (flush_cleanup);
      _IO_lock_lock (list_all_lock);
      run_fp = (FILE *) fp;
      _IO_flockfile ((FILE *) fp);
#endif
      if (_IO_list_all == NULL)
        ;
      else if (fp == _IO_list_all)
        _IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
      else
        for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain)
          if (*f == (FILE *) fp)
            {
              *f = fp->file._chain;
              break;
            }
      fp->file._flags &= ~_IO_LINKED;
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}

fp를 _IO_FILE_plus 구조체로 캐스팅하여 진행한다. 해당 구조체에 대한 분석내용을 아래 글을 참고하면 된다. 

House of Orange 분석
우선 ' House of Orange ' 이름에는 큰 의미없다. 그냥 hitcon 2016 문제 이름인데, 여기서 사용된 기법을 저렇게 이름지은거라고 한다. House of Orange는 _int_malloc 함수 내부에서 메모리 손상이 발생했을 때, 에러를 출력하게 되는데, 이 과정을 이용하여 정상적인 에러 메시지 출력이 아닌, 원하는 함수가 동작하게 만드는 공격이다. 그럼 _int_malloc 소스코드를 보면서 어떠한 원리인지 자세하게 파악해보자.
https://wogh8732.tistory.com/205

 

결론적으로 해당 함수는 현재 file stream에 연결되어있는 것들을 전부 해제하는 내용이다. 다시 _IO_new_fclose 함수 코드 설명으로 돌아가자. if (fp->_flags & _IO_IS_FILEBUF) 만약 해당 조건문의 결과가 0이 나오게 되면, _IO_un_link 함수는 호출되지 않는다. 

 

그다음 한번더 동일한 조건을 검사하여 널이 아닌 값이 나오면, _IO_file_close_it 함수가 호출되고 아니면 else로 빠져 삼항연산이 진행된다음, 결과값이 status에 저장된다. _IO_file_close_it 함수를 살펴보자 

 

int
_IO_new_file_close_it (FILE *fp)
{
  int write_status;
  if (!_IO_file_is_open (fp))
    return EOF;
  if ((fp->_flags & _IO_NO_WRITES) == 0
      && (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
    write_status = _IO_do_flush (fp);
  else
    write_status = 0;
  _IO_unsave_markers (fp);
  int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
                      ? _IO_SYSCLOSE (fp) : 0);
  /* Free buffer. */
  if (fp->_mode > 0)
    {
      if (_IO_have_wbackup (fp))
        _IO_free_wbackup_area (fp);
      _IO_wsetb (fp, NULL, NULL, 0);
      _IO_wsetg (fp, NULL, NULL, NULL);
      _IO_wsetp (fp, NULL, NULL);
    }
  _IO_setb (fp, NULL, NULL, 0);
  _IO_setg (fp, NULL, NULL, NULL);
  _IO_setp (fp, NULL, NULL);
  _IO_un_link ((struct _IO_FILE_plus *) fp);
  fp->_flags = _IO_MAGIC|CLOSED_FILEBUF_FLAGS;
  fp->_fileno = -1;
  fp->_offset = _IO_pos_BAD;
  return close_status ? close_status : write_status;
}

위에서 부터 차례대로 봐보자. 

 

  if ((fp->_flags & _IO_NO_WRITES) == 0
      && (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
    write_status = _IO_do_flush (fp);
  else

요 조건문을 만족하게 되면, _IO_do_flush 함수가 호출된다. 이 함수는 버퍼를 비우고 파일 포인터들을 초기화 해주는 함수라고 한다. 자세히는 모르겠다.  

 

  _IO_unsave_markers (fp);
  int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
                      ? _IO_SYSCLOSE (fp) : 0);
  /* Free buffer. */

그다음 _IO_unsave_markers 함수가 호출된다. 그리고 fp->_flags2 & _IO_FLAGS2_NOCLOSE 연산을 진행하여 해당 값이 0이면 _IO_SYSCLOSE(fp) 가 호출된다. 이 함수를 다시 살펴보자 

 

#define _IO_SYSCLOSE(FP) JUMP0 (__close, FP)
...
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
...
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)        \
                             + (THIS)->_vtable_offset)))

_IO_SYSCLOSE(fp) 는 매크로로 설정되어있는 함수이다. JUMP0가 호출되고, 이는 _IO_JUMPS_FUNC 를 또 호출한다. 결론적으로 house of orange에서 처럼 vtable→__close 부분을 호출하는데 이는 _IO_jump_t 구조체 멤버변수에서 +0x88 위치에 해당하는 값이다. 

 

결론적으로 자기 자신 즉, _IO_new_fclose 을 내부적으로 다시 호출하게 된다. 

 

이제 다시 원래의 함수인 _IO_new_fclose 함수를 살펴보자. 지금까지 진행된 상황은,  

  if (fp->_flags & _IO_IS_FILEBUF)
    status = _IO_file_close_it (fp);
  else
    status = fp->_flags & _IO_ERR_SEEN ? -1 : 0;
=======================여기 위 까지 진행된 상태. 이제 아래 부분 설명할꺼임================
  _IO_release_lock (fp);
  _IO_FINISH (fp);

 

위 코드에서 절취선 위까지이다. _IO_FINISH 함수역시 매크로로 설정되어 있고, vtable→__IO_file_finish 를 호출하게 되는데 이는 실제로 __IO_new_file_finish 함수를 호출하게 된다. 이 함수를 또 살펴보자 

 

void
_IO_new_file_finish (FILE *fp, int dummy)
{
  if (_IO_file_is_open (fp))
    {
      _IO_do_flush (fp); 
      if (!(fp->_flags & _IO_DELETE_DONT_CLOSE))
        _IO_SYSCLOSE (fp);
    }
  _IO_default_finish (fp, 0);
}
....
=====================

void
_IO_default_finish (FILE *fp, int dummy)
{
  struct _IO_marker *mark;
  if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
    {
      free (fp->_IO_buf_base);
      fp->_IO_buf_base = fp->_IO_buf_end = NULL;
    }
  for (mark = fp->_markers; mark != NULL; mark = mark->_next)
    mark->_sbuf = NULL;
  if (fp->_IO_save_base)
    {
      free (fp->_IO_save_base);
      fp->_IO_save_base = NULL;
    }
  _IO_un_link ((struct _IO_FILE_plus *) fp);
#ifdef _IO_MTSAFE_IO
  if (fp->_lock != NULL)
    _IO_lock_fini (*fp->_lock);
#endif
}

_IO_new_file_finish 함수에서 _IO_do_flush 를 호출해서 버퍼를 다시 비운다. 그리고 최종적으로 _IO_new_file_finish 함수를 호출하여 할당했던 버퍼를 free시킨다. 

 

최종적으로 fclose함수가 호출되었을때의 과정을 잘 표현한 그림이 있다. 

 

출처 : https://youngsouk-hack.tistory.com/68?category=850224 

 

728x90

'보안 > Linux' 카테고리의 다른 글

seccomp 간단 정리  (0) 2020.07.08
main 함수가 호출, 종료되는 과정  (0) 2020.05.14
House of Orange 분석  (0) 2020.04.29
House of Force 분석  (0) 2020.04.20
Unsafe Unlink 분석  (6) 2020.04.20