FSOP
Introduction
FSOP(File Stream Oriented Programming) 는 FILE 구조를 이용해서 익스를 하는 프로그래밍 기법이다. 이것은 libc 2.23까지는 보안이 약했으나, 2.24부터 보안 장치가 들어오더니 2.29부터는 상당히 보안이 강화되었다. 그럼에도 여전히 익스가 가능한 취약점이 존재하고, 다른 방식으로 RCE가 불가능할 때 주로 사용한다.
해당 내용을 다루기 위해서 필자는 glibc 2,23 부터 2.35 버전을 살펴보았으며, 일반적인 개론 이후 각 버전에 어떠한 보안 장치가 있는지, 그리고 어떻게 우회할지 하나씩 다룰 것이다.
Overview in FILE structure
C에서 쓰이는 FILE
구조체는 glibc 내부적으로 _IO_FILE
로 typedef 되어 있으며, 이 구조체는 파일 스트림을 관리하는데 사용된다. 이 구조체는 다양한 필드를 포함하고 있으며, 파일의 상태, 버퍼링 정보, 읽기/쓰기 위치 등을 저장한다.
Note: FSOP에서 주로 다루는 함수들은
libioP.h
에 정의되어 있다.
// glibc 2.23
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain; // 다른 FILE 스트림과 연결된 체인
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE // compatibility를 위해 존재한다.
// }; // 일반적으로 compil되지 않으므로 무시한다.
//
// struct _IO_FILE_complete
// {
// struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data; // 익스에 활용 가능
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
상당히 길기도 하고, 어차피 자주 쓰이는 부분만 살펴보면 된다. 아래 표의 형태로 정리했다. (설명은 verbose하게 썼는데, 핵심만 읽어도 된다.)
필드명 | 오프셋 | 핵심 | 설명 |
---|---|---|---|
_flags | 0x00 | 파일 권한 설정 | 파일 스트림의 상태를 나타내는 플래 그. 파일이 열려 있는지, 읽기/쓰기 모드인지 등을 나타낸다. |
_IO_read_ptr | 0x08 | fread 시 포인터 | 현재 읽기 위치를 가리키는 포인터. 파일에서 읽을 다음 바이트의 위치를 나타낸다. |
_IO_read_end | 0x10 | fread 시 읽는 파일의 끝 | 읽기 영역의 끝을 가리키는 포인터. 읽기 작업이 끝나는 위치를 나타낸다. |
_IO_read_base | 0x18 | fread 시 읽는 파일의 시작 | 읽기 영역의 시작을 가리키는 포인터. 파일에서 읽기 작업을 시작하는 위치를 나타낸다. |
_IO_write_base | 0x20 | fwrite 시 쓰는 파일의 시작 | 쓰기 영역의 시작을 가리키는 포인터. 파일에 쓰기 작업을 시작하는 위치를 나타낸다 |
_IO_write_ptr | 0x28 | fwrite 시 포인터 | 현재 쓰기 위치를 가리키는 포인터. 파일에 쓸 다음 바이트의 위치를 나타낸다. |
_IO_write_end | 0x30 | fwrite 시 쓰는 파일의 끝 | 쓰기 영역의 끝을 가리키는 포인터. 쓰기 작업이 끝나는 위치를 나타낸다. |
_IO_buf_base | 0x38 | fread에서 버퍼를 사용시 버퍼의 시작 | 버퍼 영역의 시작을 가리키는 포인터. 파일 스트림의 버퍼링 정보를 나타낸다. |
_IO_buf_end | 0x40 | fread에서 버퍼를 사용시 버퍼의 끝 | 버퍼 영역의 끝을 가리키는 포인터. 버퍼의 끝을 나타낸다. |
_IO_save_base | 0x48 | fread에서 버퍼를 사용시 버퍼 포인터 | 비활성화된 읽기 영역의 시작을 가리키는 포인터. 이전 읽기 작업의 상태를 저장한다. |
note | - | buffer에 대한 추가 설명 | _IO_read_end - _IO_read_ptr < 1 일시 내부적으로 read 호출 후 버퍼가 사용된다. 이를 이용해 익스 가능. |
_IO_backup_base | 0x50 | - | 백업 영역의 시작을 가리키는 포인터. 읽기 작업을 되돌릴 때 사용된다. |
_IO_save_end | 0x58 | - | 비활성화된 읽기 영역의 끝을 가리키는 포인터. 이전 읽기 작업의 끝을 저장한다. |
_markers | 0x60 | - | 파일 스트림의 마커를 가리키는 포인터. 파일 스트림에서 특정 위치를 표시하는 데 사용된다. |
_chain | 0x68 | - | 파일 스트림의 체인을 가리키는 포인터. 여러 파일 스트림을 연결할 때 사용된다. |
_fileno | 0x70 | - | 파일 디스크립터를 나타내는 정수. 파일 스트림이 연결된 파일의 디스크립터를 나타낸다. |
_flags2 | 0x74 | - | 추가적인 파일 스트림 플래그를 나타내는 정수. 파일 스트림의 추가적인 상태 정보를 나타낸다. |
_old_offset | 0x78 | - | 이전 오프셋을 나타내는 정수. 파일 스트림의 이전 위치를 저장한다. |
_cur_column | 0x80 | - | 현재 열(column) 번호를 나타내는 정수. 파일 스트림의 현재 열 위치를 나타낸다. |
_vtable_offset | 0x82 | - | 가상 함수 테이블의 오프셋을 나타내는 정수. C++ 스트림에서 가상 함수 호출을 지원한다. |
_shortbuf | 0x84 | - | 짧은 버퍼를 나타내는 문자 배열. 파일 스트림의 짧은 버퍼를 저장한다. |
_lock | 0x88 | _lock가 풀려 있어야 파일관련 함수 실행 가능. | 파일 스트림의 잠금을 나타내는 포인터. 멀티스레딩 환경에서 파일 스트림의 동기화를 지원한다.libc + libc_e.bss() + 0x1000 처럼 write 권한이 있는 영역을 가리키도록 하면 된다. |
_offset | 0x90 | - | 파일 스트림의 오프셋을 나타내는 정수. 파일 스트림의 현재 위치를 나타낸다. |
_codecvt | 0x98 | - | 와이드 문자 인코딩 변환을 위한 구조체 포인터. 와이드 문자 스트림에서 인코딩 변환을 지원한다. |
_wide_data | 0xa0 | - | 와이드 문자 스트림 데이터를 나타내는 구조체 포인터. 와이드 문자 스트림의 데이터를 저장한다. |
_freeres_list | 0xa8 | - | 파일 스트림의 자유 자원 목록을 나타내는 포인터. 파일 스트림이 해제될 때 사용되는 자원 목록을 저장한다. |
_lock
에 대한 추가설명을 하자면_lock
가 가리키는 값은 쓰기 영역이고 해당 값이 0인 주소여야 한다.
평소에 C를 사용할 때는 보이지 않지만, 이러한 구조체를 이용해서 파일 스트림을 관리한다.
그렇치만, 아직 끝난 것이 아니다.
_IO_FILE
은 실제로 _IO_FILE_plus
라는 구조체로 확장된다!
// glibc 2.23
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
이렇게 말이다. 따라서 _IO_FILE
을 넘어 offset 0xD8
에 접근하게 되면 _IO_jump_t
구조체(정확히는 그 포인터)에 접근하게 된다.
Note:
_IO_FILE
의 크기인0xD8
는 운영체제 및 glibc 버전에 다를 수 있다. 다만 일반적으로는0xD8
의 offset을 가진다.
_IO_jump_t
에는 재미있는 것이 있다.
// glibc 2.23
#define JUMP_FIELD(TYPE, NAME) TYPE NAME
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
바로 FILE 스트림의 함수 포인터들을 가지고 있다. 만약 이 값들을 오버라이트 할 수만 있다면 원하는 함수를 호출할 수 있다. 아쉽게도 vtable은 읽기 전용 영역이라 오버라이트는 못하지만, vtable 포인터 자체를 오버라이트 할 수 있다. 바로 아래에서 살펴볼 vtable overwrite이다. libc 2.23이하에서 가능하다.
아래는 대략적으로 어떤 포인터가 어떤 함수인지 정리한 것이다.
필드명 | offset | 핵심 함수 | 설명 |
---|---|---|---|
__finish | 0x10 | TODO | 파일 스트림을 닫을 때 호출되는 함수. |
__overflow | 0x18 | TODO | 버퍼 오버플로우 시 호출되는 함수. |
__underflow | 0x20 | TODO | 버퍼 언더플로우 시 호출되는 함수. |
__uflow | 0x28 | TODO | 언더플로우 시 호출되는 함수. |
__pbackfail | 0x30 | TODO | 백업 실패 시 호출되는 함수. |
__xsputn | 0x38 | fprintf | 문자열을 출력할 때 호출되는 함수. |
__xsgetn | 0x40 | TODO | 문자열을 입력할 때 호출되는 함수. |
__seekoff | 0x48 | TODO | 파일 오프셋을 이동할 때 호출되는 함수. |
__seekpos | 0x50 | TODO | 파일 위치를 이동할 때 호출되는 함수. |
__setbuf | 0x58 | TODO | 버퍼를 설정할 때 호출되는 함수. |
__sync | 0x60 | TODO | 파일 스트림을 동기화할 때 호출되는 함수. |
__doallocate | 0x68 | TODO | 파일 스트림을 할당할 때 호출되는 함수. |
__read | 0x70 | TODO | 파일 스트림에서 읽을 때 호출되는 함수. |
__write | 0x78 | TODO | 파일 스트림에 쓸 때 호출되는 함수. |
__seek | 0x80 | TODO | 파일 스트림의 위치를 이동할 때 호출되는 함수. |
__close | 0x88 | TODO | 파일 스트림을 닫을 때 호출되는 함수. |
__stat | 0x90 | TODO | 파일 스트림의 상태를 가져올 때 호출되는 함수. |
__showmanyc | 0x98 | TODO | 파일 스트림에서 많은 문자를 읽을 때 호출되는 함수. |
__imbue | 0x100 | TODO | 파일 스트림의 로케일을 설정할 때 호출되는 함수. |
마지막으로 stdin
, stdout
, stderr
는 각각 _IO_2_1_stdin_
, _IO_2_1_stdout_
, _IO_2_1_stderr_
로 정의되어 있다.
// stdio.h
/* Standard streams. */
extern struct _IO_FILE *stdin; /* Standard input stream. */
extern struct _IO_FILE *stdout; /* Standard output stream. */
extern struct _IO_FILE *stderr; /* Standard error output stream. */
/* C89/C99 say they're macros. Make them happy. */
#define stdin stdin
#define stdout stdout
#define stderr stderr
// stdio.c
_IO_FILE *stdin = (FILE *) &_IO_2_1_stdin_;
_IO_FILE *stdout = (FILE *) &_IO_2_1_stdout_;
_IO_FILE *stderr = (FILE *) &_IO_2_1_stderr_;
flags도 익스에 필요하기에 같이 첨부한다.
#define _IO_MAGIC 0xFBAD0000 /* Magic number */
#define _IO_MAGIC_MASK 0xFFFF0000
#define _IO_USER_BUF 0x0001 /* Don't deallocate buffer on close. */
#define _IO_UNBUFFERED 0x0002
#define _IO_NO_READS 0x0004 /* Reading not allowed. */
#define _IO_NO_WRITES 0x0008 /* Writing not allowed. */
#define _IO_EOF_SEEN 0x0010
#define _IO_ERR_SEEN 0x0020
#define _IO_DELETE_DONT_CLOSE 0x0040 /* Don't call close(_fileno) on close. */
#define _IO_LINKED 0x0080 /* In the list of all open files. */
#define _IO_IN_BACKUP 0x0100
#define _IO_LINE_BUF 0x0200
#define _IO_TIED_PUT_GET 0x0400 /* Put and get pointer move in unison. */
#define _IO_CURRENTLY_PUTTING 0x0800
#define _IO_IS_APPENDING 0x1000
#define _IO_IS_FILEBUF 0x2000
/* 0x4000 No longer used, reserved for compat. */
#define _IO_USER_LOCK 0x8000
// 보통 0xFBAD2080으로 설정한 후 상황에 맟추어 바꾼다.
Libc < 2.23
vtable overwrite
libc 2.23에서는 위에서 살펴본 _IO_jump_t
구조체의 함수 포인터들을 오버라이트 할 수 있다.
왜 가능할까?
확인하기 위해서 fprintf
의 구현을 살펴보자.
/* The 'finish' function does any final cleaning up of an _IO_FILE object.
It does not delete (free) it, but does everything else to finalize it.
It matches the streambuf::~streambuf virtual destructor. */
typedef void (*_IO_finish_t) (_IO_FILE *, int); /* typedef for finish function */
#define _IO_FINISH(FP) JUMP1 (__finish, FP, 0)
#define _IO_WFINISH(FP) WJUMP1 (__finish, FP, 0)
예를 들어 fclose
를 호출하면 내부적으로 호출되는 _IO_new_fclose
같은 함수에서 __IO_FINISH
매크로를 통해 __finish
함수 포인터를 호출한다.
그런데 이 과정에서 별도의 체크가 없기에 vtable을 오버라이드할 수 있다.
실습
Dreamhack의 IO_FILE vtable 문제를 같이 풀면서 공부했다. 별도의 글에 작성했다.
libc < 2.29
Bypass IO_validate_vtables
libc2.24부터는 vtable 호출시 추가적인 검사가 추가되었다. 확인을 위해 libc2.27을 살펴보자.
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
...
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_IO_vtable_check ();
return vtable;
}
위 함수는 JUMP
매크로를 실행할 때 함께 호출된다.
validation을 어떻게 하는지 분석하자.
- section_length = stop_libc_IO_vtables - start_libc_IO_vtables;
->
__libc_IO_vtables
섹션의 길이를 계산한다. - ptr = (const char *) vtable;
-> vtable 포인터를
char
포인터로 변환한다. - offset = ptr - start_libc_IO_vtables;
-> vtable 포인터가
__libc_IO_vtables
섹션의 시작 주소에서 얼마나 떨어져 있는지 계산한다. - if (__glibc_unlikely (offset >= section_length)) -> 만약 offset이 섹션 길이보다 크거나 같다면,
- _IO_vtable_check ();
->
_IO_vtable_check()
함수를 호출한다. 이 함수는 vtable이 올바른 섹션에 있는지 확인하고, 그렇지 않으면 프로세스를 종료시킨다.
따라서 위 validation은 vtable을 가리키는 포인터가 변조되지 않도록 하는 역할을 한다.
추가적으로 _IO_vtable_check()
를 살펴보자.
void attribute_hidden
_IO_vtable_check (void)
{
#ifdef SHARED
/* Honor the compatibility flag. */
void (*flag) (void) = atomic_load_relaxed (&IO_accept_foreign_vtables);
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (flag);
#endif
if (flag == &_IO_vtable_check)
return;
/* In case this libc copy is in a non-default namespace, we always
need to accept foreign vtables because there is always a
possibility that FILE * objects are passed across the linking
boundary. */
{
Dl_info di;
struct link_map *l;
if (!rtld_active ()
|| (_dl_addr (_IO_vtable_check, &di, &l, NULL) != 0
&& l->l_ns != LM_ID_BASE))
return;
}
#else /* !SHARED */
/* We cannot perform vtable validation in the static dlopen case
because FILE * handles might be passed back and forth across the
boundary. Therefore, we disable checking in this case. */
if (__dlopen != NULL)
return;
#endif
__libc_fatal ("Fatal error: glibc detected an invalid stdio handle\n");
}
IO_accept_foreign_vtables
는 외부에 정의된 vtables를 허용할지 여부를 결정하는 플래그이다.
만약 iofile_vtable 문제처럼 vtable에 정의된 영역이 아닌 곳으로 vtable을 사용하면 IO_accept_foreign_vtables
의 허가 없이는 vtable을 정상적으로 사용할 수 없다.
물론 IO_accept_foreign_vtables
를 true
로 설정하면 외부 vtable을 허용할 수 있지만, 손이 많이 간다는 문제가 있다.
PTR_MANGLE 과정도 있기에 fs레지스터가 가리키는 tcbhead_t를
이용해서 pointer guard를 유출하거나 오버라이트 해야한다.
Note: 그렇다고 해서 불가능 한 것을 아니다...!
따라서 일반적으로 libc<2.29에서는 vtable 내부의 함수를 활용한다.
_IO_str_overflow
// glibc 2.29
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow); // HERE!
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};
__overflow
는 일반적으로 _IO_file_overflow
로 매핑된다.
하지만, vtable을 오버라이트해서 vtable 내에 있는 다른 취약한 함수를 사용한다면 어떻게 될까?
아래 _IO_str_overflow
를 살펴보자.
// glibc 2.29, strops.c
int _IO_str_overflow (_IO_FILE *fp, int c)
{
int flush_only = c == EOF;
_IO_size_t pos;
if (fp->_flags & _IO_NO_WRITES)
return flush_only ? 0 : EOF;
if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
{
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_IO_write_ptr = fp->_IO_read_ptr;
fp->_IO_read_ptr = fp->_IO_read_end;
}
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
{
if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
return EOF;
else
{
char *new_buf;
char *old_buf = fp->_IO_buf_base;
size_t old_blen = _IO_blen (fp);
_IO_size_t new_size = 2 * old_blen + 100;
if (new_size < old_blen)
return EOF;
new_buf
= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
if (new_buf == NULL)
{
/* __ferror(fp) = 1; */
return EOF;
}
if (old_buf)
{
memcpy (new_buf, old_buf, old_blen);
(*((_IO_strfile *) fp)->_s._free_buffer) (old_buf);
/* Make sure _IO_setb won't try to delete _IO_buf_base. */
fp->_IO_buf_base = NULL;
}
memset (new_buf + old_blen, '\0', new_size - old_blen);
_IO_setb (fp, new_buf, new_buf + new_size, 1);
fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
fp->_IO_write_base = new_buf;
fp->_IO_write_end = fp->_IO_buf_end;
}
}
if (!flush_only)
*fp->_IO_write_ptr++ = (unsigned char) c;
if (fp->_IO_write_ptr > fp->_IO_read_end)
fp->_IO_read_end = fp->_IO_write_ptr;
return c;
}
이중 new_buf= (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
부분이 공격에 활용된다.
만약 _allocate_buffer
와 new_size
를 조작할 수 있다면...system("/bin/sh");
처럼 실행시킬 수 있을것이다.
가능한지 확인하자.
그러기 위해 _allocate_buffer
가 어디서 나왔는지 알아야 한다.
// glibc 2.29
typedef struct _IO_strfile_
{
struct _IO_streambuf _sbf;
struct _IO_str_fields _s;
} _IO_strfile;
struct _IO_streambuf
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
};
struct _IO_str_fields
{
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
};
즉, _IO_strfile_
의 메모리상 구조는
_IO_strfile_
{
struct _IO_FILE _f;
const struct _IO_jump_t *vtable;
_IO_alloc_type _allocate_buffer;
_IO_free_type _free_buffer;
}
위와 같다.
갑자기 _IO_strfile_
가 등장해서 당황스러울 것이다.
위 구조체들이 등장하는 이유는 "정상적인" 함수 호출이라면 위 구조체를 통해서 _IO_str_overflow
가 호출되기 때문이고, 우리는 이에 맞추어 해당 위치에 system
함수를 오버라이드하고 싶은 상황이다.
즉, vtable포인터를 넘어 다음 것이 _allocate_buffer
이므로 이곳을 system
으로 오버라이트 해야 한다는 것이다.
한편 new_size
는 2*old_blen+100
로 계산되는데, old_blen
은 _IO_blen(fp)
라는 매크로에서 온 값이다.
그리고 _IO_blen(fp)
는 fp->_IO_buf_end - fp->_IO_buf_base
로 계산된다.
이를 종합하자.
"/bin/sh"의 메모리 주소를 m이라 하면 m=2*(_IO_buf_end-_IO_buf_base)+100
.
_IO_buf_base = 0
_IO_buf_end = (m - 100)/2
위처럼 주소를 설정하면 된다.
마지막으로 이제 실행 조건만 맞추어 _IO_str_overflow
를 호출하면 된다.
이 내용은 securitykss.log님의 블로그를 참고해서 작성하였다.
_IO_str_overflow
는 메모리를 통해 발생한 스트림에서 쓰이는 함수이기에 일반적으로 호출되지 않는다.
그렇기에 vtable의 주소를 변경 해서 _IO_str_overflow
가 호출되도록 해야 한다.
이렇게 되면 여전히 vtable 내부의 함수를 사용하는 것이므로 앞서 살펴본 검사를 통과할 수 있다.
다음으로는 _IO_str_overflow
의 if문을 통과해야 한다.
pos = fp->_IO_write_ptr - fp->_IO_write_base;
if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
이 조건을 통과하기 위해서 fp->_IO_write_base=0
, fp->_IO_write_ptr=(m-100)/2
으로 설정한다.
이렇게 vtable을 조작하고 _IO_str_overflow
를 호출하면 _allocate_buffer
가 system
으로 오버라이트되고, /bin/sh
가 실행된다.
Note: 복잡하지만, 이 방법 또한 최신 libc에서는 막혔다.
new_buf
에서 함수를 실행해주는 부분이 삭제되었다.
_IO_str_finish
이 함수 또한 비슷한 방식으로 익스가 가능하다.
실습
Dreamhack의 Bypass IO_validate_vtable, iofile_vtable_check을 통해 연습이 가능하다.
libc > 2.29
_wdoallocate
를 이용한다. 자세한 내용은 관련 문제 풀이에서 확인하자.
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;
};