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 |