Profile

youngsouk

youngsouk

fopen함수 분석

house_of_orange라는 것도 FSOP라는 공격기법 중 하나입니다. 그래서 이번엔는 house of orange 심화로 FILE과 관련된 구조체, 함수들에 대해 정리를 해보고 익스플로잇(FSOP)을 해보도록 하겠습니다. 

먼저 파일 디스크립터를 다룰때 가장 많이 나오는 foen함수를 보도록 하겠습니다. fopen 함수를 아래처럼 코딩한 후 gdb를 통해 fopen() 함수의 흐름을 보자면

 

#include <stdio.h>

int main(){
        int f = fopen("tmp", "r");
}

 

이런식으로 호출이 된다는 것을 알 수 있습니다. __fopen_internal 부터 보도록합시다. 

typedef struct { int lock; int cnt; void *owner; } _IO_lock_t; //stdio-lock.h에 정의되어있습니다.
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;
};

FILE *
__fopen_internal (const char *filename, const char *mode, int is32)
{
  struct locked_FILE
  {
    struct _IO_FILE_plus fp;
#ifdef _IO_MTSAFE_IO
    _IO_lock_t lock;
#endif
    struct _IO_wide_data wd;
  } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
  //구조체에 대한 설명과 그림은 아래를 참고해주세요.

  if (new_f == NULL)
    return NULL; // 할당받지 못하게 되면 종료한다.
#ifdef _IO_MTSAFE_IO
  new_f->fp.file._lock = &new_f->lock;
#endif
  _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
  _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; // _IO_FILE_plus 구조체의 vtable을 초기화
  _IO_new_file_init_internal (&new_f->fp);
  if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
    return __fopen_maybe_mmap (&new_f->fp.file);

  _IO_un_link (&new_f->fp);
  free (new_f);
  return NULL;
}

위에서 사용되는 구조체를 그림으로 정리하게 되면 이렇게 됩니다.

이 함수가 내부적으로 해주는 것은 구조체의 크기로 malloc을 호출해주는 것밖에 없는것 같습니다.


이제는 _IO_no_init부분을 보겠습니다. 

void
_IO_no_init (FILE *fp, int flags, int orientation,
	     struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
{
  _IO_old_init (fp, flags);
  fp->_mode = orientation;
  if (orientation >= 0)
    {
      fp->_wide_data = wd;
      fp->_wide_data->_IO_buf_base = NULL;
      fp->_wide_data->_IO_buf_end = NULL;
      fp->_wide_data->_IO_read_base = NULL;
      fp->_wide_data->_IO_read_ptr = NULL;
      fp->_wide_data->_IO_read_end = NULL;
      fp->_wide_data->_IO_write_base = NULL;
      fp->_wide_data->_IO_write_ptr = NULL;
      fp->_wide_data->_IO_write_end = NULL;
      fp->_wide_data->_IO_save_base = NULL;
      fp->_wide_data->_IO_backup_base = NULL;
      fp->_wide_data->_IO_save_end = NULL;

      fp->_wide_data->_wide_vtable = jmp;
    }
  else
    /* wide functino이 바이트 스트림에서 호출되었을 때 예상가능한 충돌이 발생한다. */
    fp->_wide_data = (struct _IO_wide_data *) -1L;
  fp->_freeres_list = NULL;
}

old_init을 호출한 뒤에 wide_data 구조체의 내용을 초기 셋팅을 해주는 것을 볼 수 있습니다. 또 충돌이 일어나게 되면 -1과 NULL을 이용해 값을 셋팅하게 됩니다.

이제는 old_init함수를 보겠습니다. 

void
_IO_old_init (FILE *fp, int flags)
{
  fp->_flags = _IO_MAGIC|flags;
  fp->_flags2 = 0;
  if (stdio_needs_locking)
    fp->_flags2 |= _IO_FLAGS2_NEED_LOCK;
  fp->_IO_buf_base = NULL;
  fp->_IO_buf_end = NULL;
  fp->_IO_read_base = NULL;
  fp->_IO_read_ptr = NULL;
  fp->_IO_read_end = NULL;
  fp->_IO_write_base = NULL;
  fp->_IO_write_ptr = NULL;
  fp->_IO_write_end = NULL;
  fp->_chain = NULL; /* 꼭 필요하지는 않다. */

  fp->_IO_save_base = NULL;
  fp->_IO_backup_base = NULL;
  fp->_IO_save_end = NULL;
  fp->_markers = NULL;
  fp->_cur_column = 0;
#if _IO_JUMPS_OFFSET
  fp->_vtable_offset = 0;
#endif
#ifdef _IO_MTSAFE_IO
  if (fp->_lock != NULL)
    _IO_lock_init (*fp->_lock);
#endif
}

여기에서는 _IO_FILE_plus구조체의 초기셋팅을 해주고 있는 것을 볼 수 있습니다.

즉 _IO_old_init부분은 _IO_FILE_plus 구조체 초기화를, _IO_no_init은 _wide_data 구조체 초기화를 담당하고 있는것입니다.

----------------------------------------------------------------------------------------------------------------------------

다음으로 볼 함수는 이 초기화를 한뒤에 실행되는 _IO_new_file_init_internal 함수입니다. 

void
_IO_new_file_init_internal (struct _IO_FILE_plus *fp)
{
  /* 또다른 파일 헨들이 우리의 파일 디스크립터를 변경하도록 허락한다. 
  	 그러므로, 우리는 우리가 첫번째로 fseek을 하기 전(그리고 따라오는 fflush전)까지는 실제 위치는 모른다.
  */
  fp->file._offset = _IO_pos_BAD; // _IO_pos_BAD은 오류가 났을 때 보통 셋팅된다.
  				 // 여기서 이게 셋팅되는 이유는 _IO_SYSSEEK함수가 오류가 났는지 확인하기 위해서이다.
  fp->file._flags |= CLOSED_FILEBUF_FLAGS; //버퍼에 출력하거나 입력하거나 등을 할 것이 없을 때 셋팅된다.
					//이것도 위의 변수와 마찬가지로 오류가 났는지 판단하기 위해서 셋팅된다.
  _IO_link_in (fp);
  fp->file._fileno = -1; //아직 file이 안열렸다는 표시로 -1을 셋팅해준다.
}

변수를 셋팅하고 _IO_link_in을 호출해주게 됩니다. 

void
_IO_link_in (struct _IO_FILE_plus *fp)
{
  if ((fp->file._flags & _IO_LINKED) == 0) // link가 안되어있으면 참이된다.
    {
      fp->file._flags |= _IO_LINKED;
#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
      fp->file._chain = (FILE *) _IO_list_all; // chain을 _IO_list_all 변수로 바꾸게 된다.
      _IO_list_all = fp; //_IO_list_all에는 fp를 저장하게 되는데 이것과 위의 명령을 통해 
      					 //single linked list를 통해 구조체를 관리한다는 것을 알 수 있다.
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
libc_hidden_def (_IO_link_in)

_IO_link_in 함수는 _IO_list_all과 chain을 통해 single linked list를 구성하는 함수임을 알 수 있습니다.


이제는 _IO_file_fopen(_IO_new_file_fopen의 줄임말)함수를 봐야합니다. 

#define _IO_NO_READS          0x0004 /* read가 허용되지 않는다.  */
#define _IO_NO_WRITES         0x0008 /* write가 허용되지 않는다.  */
#define _IO_IS_APPENDING      0x1000

#define	O_RDONLY	0x0000		/* 읽기만 가능하게 연다 */
#define	O_WRONLY	0x0001		/* 쓰기만 가능하게 연다 */
#define	O_RDWR		0x0002		/* 읽기 쓰기 둘 다 가능하게 연다 */
#define	O_CREAT		0x0200		/* 만약 존재하지 않는다면 만든다. */
#define	O_TRUNC		0x0400		/* 길이가 0이되도록 줄인다. */
#define	O_EXCL		0x0800		/* 만약 이미 존재하면 에러가 발생한다. */

#define _IO_FLAGS2_MMAP 1
#define _IO_FLAGS2_NOTCANCEL 2
#define _IO_FLAGS2_CLOEXEC 64

FILE *
_IO_new_file_fopen (FILE *fp, const char *filename, const char *mode,
		    int is32not64)
{
  int oflags = 0, omode;
  int read_write;
  int oprot = 0666;
  int i;
  FILE *result;
  const char *cs;
  const char *last_recognized;

  if (_IO_file_is_open (fp)) //fp->_fileno가 -1인지 확인한다. -1이면 아직 open이 안되어있다는 증거이다.
    return 0;
  switch (*mode) // 우리가여는 mode의 첫번째 글자에 따라 omode와 read_write 권한을 셋팅해준다.
    {
    case 'r':
      omode = O_RDONLY;
      read_write = _IO_NO_WRITES;
      break;
    case 'w':
      omode = O_WRONLY;
      oflags = O_CREAT|O_TRUNC;
      read_write = _IO_NO_READS;
      break;
    case 'a':
      omode = O_WRONLY;
      oflags = O_CREAT|O_APPEND;
      read_write = _IO_NO_READS|_IO_IS_APPENDING;
      break;
    default:
      __set_errno (EINVAL);
      return NULL;
    }
  last_recognized = mode; //가장 마지막의 mode를 기억해논다.
  for (i = 1; i < 7; ++i)
    {
      switch (*++mode) // 다음 글자를 뽑아온다.
	{					//다음 글자에 따라 omode와 read_write를 갱신해준다.
	case '\0':
	  break;
	case '+':
	  omode = O_RDWR;
	  read_write &= _IO_IS_APPENDING;
	  last_recognized = mode;
	  continue;
	case 'x':
	  oflags |= O_EXCL;
	  last_recognized = mode;
	  continue;
	case 'b':
	  last_recognized = mode;
	  continue;
	case 'm':
	  fp->_flags2 |= _IO_FLAGS2_MMAP;
	  continue;
	case 'c':
	  fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
	  continue;
	case 'e':
	  oflags |= O_CLOEXEC;
	  fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
	  continue;
	default:
	  /* 무시한다.  */
	  continue;
	}
      break;
    }

  result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
			  is32not64);

 ----------------중도생략---------------------

이해를 더 쉽게 하기 위해 _IO_file_open함수를 본뒤에 _IO_file_fopen(_IO_new_file_fopen의 줄임말)함수의 나머지를 보겠습니다.

#define _IO_mask_flags(fp, f, mask) \
       ((fp)->_flags = ((fp)->_flags & ~(mask)) | ((f) & (mask)))

FILE *
_IO_file_open (FILE *fp, const char *filename, int posix_mode, int prot,
	       int read_write, int is32not64)
{
  int fdesc;
  if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
    fdesc = __open_nocancel (filename,
			     posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); //syscall을 통해 file open
  else
    fdesc = __open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot); //syscall을 통해 file open
  if (fdesc < 0) // 파일열기에 실패하면 참이된다.
    return NULL;
  fp->_fileno = fdesc; // _fileno를 셋팅해준다. (ex : 3, 4 등)
  _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING); //_flags에 read_write 권한을 셋팅해준다.
  /* append 모드일 때, 파일 오프셋을 파일의 끝으로 보낸다. 오프셋을 캐쉬를 통해 업데이트하지 마십시오.
     왜냐하면 파일 헨들이 작동하지 않습니다.*/
  if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS)) // append 모드일 때
      == (_IO_IS_APPENDING | _IO_NO_READS))
    {
      off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end); //파일 오프셋을 파일의 끝으로 보낸다.
      if (new_pos == _IO_pos_BAD && errno != ESPIPE) // 파일 오프셋이 잘못 이동되었을 때
	{
	  __close_nocancel (fdesc);
	  return NULL;
	}
    }
  _IO_link_in ((struct _IO_FILE_plus *) fp); // _IO_list_all과 다시 링크시켜준다.
  return fp;
}
libc_hidden_def (_IO_file_open)

이렇게 _IO_file_open함수는 실제로 파일을 syscall을 통해 open시켜주고, flag에 권한을 셋팅한 뒤에 반환 시켜준다는 것을 알 수 있습니다. 

이제는 중간에 끊어졌던 _IO_file_fopen(_IO_new_file_fopen의 줄임말)함수를 보아야합니다.

  if (result != NULL)
    {
      /* mode 문자열이 ccs를 통해 인코딩 방법을 명시하고 있는지 검사한다. */
      cs = strstr (last_recognized + 1, ",ccs=");
      if (cs != NULL) // ccs라는 문자열이 있으면 
	{
	  /* 적절한 전환을 하고, 방향을 wide로 설정한다. */
	  struct gconv_fcts fcts;
	  struct _IO_codecvt *cc;
	  char *endp = __strchrnul (cs + 5, ',');
	  char *ccs = malloc (endp - (cs + 5) + 3);

	  if (ccs == NULL)
	    {
	      int malloc_err = errno;  /* malloc이 실패하든지간에 셋팅한다.  */
	      (void) _IO_file_close_it (fp);
	      __set_errno (malloc_err);
	      return NULL;
	    }

	  *((char *) __mempcpy (ccs, cs + 5, endp - (cs + 5))) = '\0';
	  strip (ccs, ccs);

	  if (__wcsmbs_named_conv (&fcts, ccs[2] == '\0'
				   ? upstr (ccs, cs + 5) : ccs) != 0)
	    {
	      /* 무엇인가 잘못되어서, 우리가 변환 모듈을 불러올 수 없다.
		 이것은 우리가 사용자가 분명히 이것을을 위해서 말했기 때문에 처리할 수가 없다는 것을 의미한다.
		*/
	      (void) _IO_file_close_it (fp);
	      free (ccs);
	      __set_errno (EINVAL);
	      return NULL;
	    }

	  free (ccs);

	  assert (fcts.towc_nsteps == 1);
	  assert (fcts.tomb_nsteps == 1);

	  fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
	  fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_base;

	  /* 상태를 초기화시킨다. 우리는 다시 한번 시작할 것이다.  */
	  memset (&fp->_wide_data->_IO_state, '\0', sizeof (__mbstate_t));
	  memset (&fp->_wide_data->_IO_last_state, '\0', sizeof (__mbstate_t));

	  cc = fp->_codecvt = &fp->_wide_data->_codecvt;

	  /* 이 함수들은 항상 기능이 같다.  */
	  *cc = __libio_codecvt;

	  cc->__cd_in.__cd.__nsteps = fcts.towc_nsteps;
	  cc->__cd_in.__cd.__steps = fcts.towc;

	  cc->__cd_in.__cd.__data[0].__invocation_counter = 0;
	  cc->__cd_in.__cd.__data[0].__internal_use = 1;
	  cc->__cd_in.__cd.__data[0].__flags = __GCONV_IS_LAST;
	  cc->__cd_in.__cd.__data[0].__statep = &result->_wide_data->_IO_state;

	  cc->__cd_out.__cd.__nsteps = fcts.tomb_nsteps;
	  cc->__cd_out.__cd.__steps = fcts.tomb;

	  cc->__cd_out.__cd.__data[0].__invocation_counter = 0;
	  cc->__cd_out.__cd.__data[0].__internal_use = 1;
	  cc->__cd_out.__cd.__data[0].__flags
	    = __GCONV_IS_LAST | __GCONV_TRANSLIT;
	  cc->__cd_out.__cd.__data[0].__statep =
	    &result->_wide_data->_IO_state;

	  /* 지금부터는 wide character callback 함수를 사용한다. */
	  _IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;

	  /* mode를 즉시 설정한다.  */
	  result->_mode = 1;
	}
    }

  return result;
}
libc_hidden_ver (_IO_new_file_fopen, _IO_file_fopen)

fopen을 호출할 때 인코딩 방법을 명시하기 위해 ccs를 인자로서 함께 줄 때가 있습니다. 이 함수는 바로 이때를 위해 값을 셋팅하고 전달해준 인코딩 방식이 잘못되었다면 free시키는 역할을 하고 있습니다. 


이제 볼 함수는 __fopen_maybe_mmap이라는 함수인데 이함수는 성공적으로 파일이 열렸을때 호출이 되는 함수입니다.

FILE *
__fopen_maybe_mmap (FILE *fp)
{
#if _G_HAVE_MMAP
  if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES))
    {
      /* 이것은 읽을 수만 있기 때문에, 우리는 내용을 바로 mmap할 수 있다.
      	 우리는 mmap이나 바닐라 파일 명령 그리고 jump table을 그에 맞추어 초기화하는 함수들을
         포함하는 jump table을 주면서 첫번째 읽기 시도까지 결정을 미루게 된다.
      */
      if (fp->_mode <= 0) 
        _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_maybe_mmap;
      else
        _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_maybe_mmap;
      fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap;
    }
#endif
  return fp;
}

이렇게 읽기만 가능할 때, _mode의 값에 따라 vtable의 값을 설정해주는 것을 볼 수 있습니다.

정상적으로 할당이 되게 된다면 __fopen_maybe_mmap함수에 의해 fp를 반환하면서 fopen()함수는 종료가 되게 됩니다.


마지막으로 정상적으로 할당이 안되어있을 때 실행되는 _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) // single_linked list에 제일 최근에 들어왔을때(제일 마지막에 있을 때)이다.
	_IO_list_all = (struct _IO_FILE_plus *) _IO_list_all->file._chain;
      else
	for (f = &_IO_list_all->file._chain; *f; f = &(*f)->_chain) // single_linked list의 중간에 있을 때이다.
	  if (*f == (FILE *) fp)
	    {
	      *f = fp->file._chain; // 중간에 있는 것이 하나 빠졌으니 빠진 양옆의 구조체를 연결시켜주어야 한다.
	      break;
	    }
      fp->file._flags &= ~_IO_LINKED; // link되지 않았다는 표시를 남긴다.
#ifdef _IO_MTSAFE_IO
      _IO_funlockfile ((FILE *) fp);
      run_fp = NULL;
      _IO_lock_unlock (list_all_lock);
      _IO_cleanup_region_end (0);
#endif
    }
}
libc_hidden_def (_IO_un_link)

이렇게 fopen()함수 분석은 전부 끝이 나게 되었습니다. 최종적으로 fopen()에서 쓰인 함수들의 이름과 기능을 그림으로 표현하자면

이런식이 됩니다.

'FSOP' 카테고리의 다른 글

setvbuf 함수 분석  (0) 2019.09.25
FSOP - seethefile(pwnable.tw)  (0) 2019.09.20
fclose 분석  (0) 2019.09.13
fwrite 분석  (0) 2019.09.13
fread 분석  (0) 2019.09.13