ghkdtlwns987

flose() 분석 본문

시스템

flose() 분석

2020/03/31 2020. 10. 10. 21:11

이번엔 fclose() 함수를 분석해보도록 하겠다. 

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

여기서 유심히 봐야 할 것을 강조해서 보도록 하겠다.

 

  /* 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->_flags & _IO_IS_FILEBUF)

다음은 flags & FILEBUF 가 서로 같다면 _IO_un_link() 함수가 호출 된다. 

소스코드를 보면 _IO_unlink 함수를 덮어 Exlpoit 함수 있을 것이다. 

여기서 헷갈리면 안되는게, 파일 포인터가 fopen으로 열려있어야 fclose를 닫는데, fopen으로 열린 fp는 flag가 설정되어있어야 한다. 

=> flags 에 0xffffffff 을 주게 되면? -> _IO_un_link() 함수가 호출된다.

=> flags 에 0을 조게 되면? -> _IO_un_link() 함수가 호출되지 않고, _IO_acuire_lock() 가 호출된다.

+ 여기서 fd+0x88 부분에 위치해 있는 _lock 변수가 있는데, write 가능한 영억이 들어가도록 조작한다.

    다음은 _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
        }
    }

     

    만약

      if (fp->file._flags & _IO_LINKED)

    위 조건이 충족되지 않는다면 _IO_un_link() 함수는 호출되지 않는다. 그리고 다음 조건문을 지나는데,

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

    만약 NULL 이 나오지 않으면 위의 조건문 내부로 지나게 되고, _IO_file_close_it() 함수가 호출이 되는데, 

    이 결과값으로 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;
    }

     

    보기 편하게 조금씩 나눠서 분석해보자.

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

    다음을 보면 fp 가 열려있는지 확인하고, fp가 닫혀있으면 _IO_do_flush() 함수를 호출한다. 

    do_flush() 함수는 버퍼를 비워주는 함수이다.(fllush() 와 비슷하다고는 하는데, 나도 자세히는 잘 모른다.)

     else
        write_status = 0;
      _IO_unsave_markers (fp);
      int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
                          ? _IO_SYSCLOSE (fp) : 0);

    다음부분을 분석해보자. 

    위 코드에선 _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 를 또 호출한다.    이는 _IO_jump_t 구조체 멤버변수에서 +0x88 위치에 해당하는 값이다.

     

    =>  _IO_new_fclose 을 내부적으로 다시 호출하게 된다. 

     

     

    이제 다음부분이다.

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

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

     

     

    다음으로 flose() 의 남은 부분이다.

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

    다음을 보면 _IO_release_lock(fp); 를 호출하고

    _IO_FINISH(fp); 를 호출한다.

    이는 곧 vtable→__IO_file_finish 를 호출하게 되는데 이는 실제로 __IO_new_file_finish 함수를 호출하게 된다. 

    그다음 _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
    }

     

    앞서 내가 빨간색과 파란색으로 굵은 글씨로 표현한게 있는데, 이는 flose() 를 이용한 Exploit 방법이다.

    기왕하는거 보기좋게 한눈에 정리하겠다.

    1. _IO_un_link() Exploit
    	1. fp→_flags = 0xffffffff 
    	2. IO_un_link() 함수가 호출 => IO_file_close_it 를 호출
    	3. 이떄의 *vtable + 0x88 값을 overwrite (vtable→__close)
    
     
    
    2. _IO_acuire_lock() Exploit
    	1. fp→flags = 0
    	2. acqurie() 함수 호출, 머시기가 호출되는데, 
        	여기서 fd+0x88에 위치해 있는 _lock 변수를 체크하는로직이 있기 때문에, 
        	여기에 write 가능한 영역의 주소를 넣어줌
    	3. 그다음 _IO_FINISH 함수 내부에서 호출되는 걸 이용 (vtable→__IO_file_finish)
    
    

     

    다음과 관련된 Exploit 방법은 pwnable.xyz 문제의 fclose() 를 풀면서 적용해볼 수 있다. 

    난 아직 pwnable.xyz 문제를 풀어보지는 않았지만, 다음에 풀면 그때 다시 언급하도록 하겠다.

           

     

    다음은 분석한 것을 그림으로 표현한건데 상당히 보기 좋아서 가져와봤다.

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

     

    +추가로 이번에 내가 정리할 때 정말 많은 글을 보았는데, 전적으로 밑의 블로그를 주로 참조했으니 설명이 조금 미흡했다고 생각하면 이 블로그를 한번 들어가보자.

    wogh8732.tistory.com/207

     

    fclose 분석

    1. 개요 전에 풀었던 house of orange 기법에서 사용되었던 File 구조체를 이용한 기법을 FSOP (File Stream Oriented Programming)라고 한다. house of orange도 FSOP 기법 중 하나로, FILE 구조체의 내부 구조를..

    wogh8732.tistory.com

     

    '시스템' 카테고리의 다른 글

    stdout 으로 libc leak  (3) 2020.11.10
    FSOB(_IO_flush_all_lockp )  (0) 2020.10.10
    FSOB(File Stream Oriented Programming) 1  (0) 2020.10.10
    _IO_FILE_vtable_check  (0) 2020.10.10
    _IO_FILE_vtable 2  (0) 2020.10.10
    Comments