ghkdtlwns987
flose() 분석 본문
이번엔 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
+추가로 이번에 내가 정리할 때 정말 많은 글을 보았는데, 전적으로 밑의 블로그를 주로 참조했으니 설명이 조금 미흡했다고 생각하면 이 블로그를 한번 들어가보자.
'시스템' 카테고리의 다른 글
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 |