Search

fclose 분석

Tag
fsop
Create time
2020/05/01

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함수가 호출되었을때의 과정을 잘 표현한 그림이 있다.