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;
}
C
복사
처음에 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
C
복사
요 부분은 만약 현재 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);
C
복사
( 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
}
}
C
복사
fp를 _IO_FILE_plus 구조체로 캐스팅하여 진행한다. 해당 구조체에 대한 분석내용을 아래 글을 참고하면 된다.
결론적으로 해당 함수는 현재 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;
}
C
복사
위에서 부터 차례대로 봐보자.
if ((fp->_flags & _IO_NO_WRITES) == 0
&& (fp->_flags & _IO_CURRENTLY_PUTTING) != 0)
write_status = _IO_do_flush (fp);
else
C
복사
요 조건문을 만족하게 되면, _IO_do_flush 함수가 호출된다. 이 함수는 버퍼를 비우고 파일 포인터들을 초기화 해주는 함수라고 한다. 자세히는 모르겠다.
_IO_unsave_markers (fp);
int close_status = ((fp->_flags2 & _IO_FLAGS2_NOCLOSE) == 0
? _IO_SYSCLOSE (fp) : 0);
/* Free buffer. */
C
복사
그다음 _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)))
Markdown
복사
_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);
C
복사
위 코드에서 절취선 위까지이다. _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
}
C
복사
_IO_new_file_finish 함수에서 _IO_do_flush 를 호출해서 버퍼를 다시 비운다. 그리고 최종적으로 _IO_new_file_finish 함수를 호출하여 할당했던 버퍼를 free시킨다.
최종적으로 fclose함수가 호출되었을때의 과정을 잘 표현한 그림이 있다.