포스트

_IO_FILE

_IO_FILE

_IO_FILE 구조체

  • _IO_FILE 구조체는 리눅스 표준 라이브러리에서 파일 스트림을 나타내기 위한 구조체이며, 파일을 열기 위한 fopen 함수를 호출했을 때 힙 영역에 할당된다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    struct _IO_FILE_plus
    {
      FILE file;
      const struct _IO_jump_t *vtable;
    };
    
    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
    };
    
    • _flags: 파일에 대한 각종 권한. 매직 값 0xfbad0000을 사용하며, 하위 2바이트를 비트 플래그로 사용한다.
    • _IO_read_ptr: 파일 읽기 버퍼에 대한 포인터
    • _IO_read_end: 파일 읽기 버퍼 주소의 끝을 가리키는 포인터
    • _IO_read_base: 파일 읽기 버퍼 주소의 시작을 가리키는 포인터
    • _IO_write_ptr: 파일 쓰기 버퍼에 대한 포인터
    • _IO_write_base: 파일 쓰기 버퍼 주소의 시작을 가리키는 포인터
    • _IO_write_end: 파일 쓰기 버퍼 주소의 끝을 나타내는 포인터
    • _chain: 프로세스의 _IO_FILE 구조체는 이 필드를 통해 연결 리스트를 만든다. 연결 리스트의 헤더는 라이브러리의 _IO_list_all에 저장된다.
    • _fileno: 파일 디스크립터 값
    • _IO_jump_t *vtable: 파일 관련 작업을 수행하는 가상 함수 테이블
  • _flags 멤버변수는 fopen 함수로 파일을 열 때 전달한 모드에 따라 값이 설정된다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    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;
          ...
      }
    
    • fopen 함수의 두 번째 인자인 mode 변수가 ‘r’, ‘w’, ‘a’ 문자인지 확인하고, 각 권한에 해당하는 비트가 할당된다.
    • read_write 뿐만 아니라 omode 변수에 O_RDONLY, O_WRONLY 등이 저장되는데, fopen 함수가 결국 open 시스템 콜을 호출해 파일을 열어야 하기 때문이다.
  • _IO_FILE_plus 구조체를 살펴보면 vtable 포인터가 있다. 파일 구조체는 각 파일마다 함수 테이블을 가지고 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    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);
    };
    
    • 디버깅을 통해 vtable의 모습을 볼 수 있다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      
      pwndbg> p *(struct _IO_jump_t *)0x7ffff7dca2a0
      $5 = {
        __dummy = 0,
        __dummy2 = 0,
        __finish = 0x7ffff7a6e2d0 <_IO_new_file_finish>,
        __overflow = 0x7ffff7a6f2b0 <_IO_new_file_overflow>,
        __underflow = 0x7ffff7a6efd0 <_IO_new_file_underflow>,
        __uflow = 0x7ffff7a70370 <__GI__IO_default_uflow>,
        __pbackfail = 0x7ffff7a71c00 <__GI__IO_default_pbackfail>,
        __xsputn = 0x7ffff7a6d8d0 <_IO_new_file_xsputn>,
        __xsgetn = 0x7ffff7a6d530 <__GI__IO_file_xsgetn>,
        __seekoff = 0x7ffff7a6cb30 <_IO_new_file_seekoff>,
        __seekpos = 0x7ffff7a70940 <_IO_default_seekpos>,
        __setbuf = 0x7ffff7a6c7f0 <_IO_new_file_setbuf>,
        __sync = 0x7ffff7a6c670 <_IO_new_file_sync>,
        __doallocate = 0x7ffff7a600b0 <__GI__IO_file_doallocate>,
        __read = 0x7ffff7a6d8b0 <__GI__IO_file_read>,
        __write = 0x7ffff7a6d130 <_IO_new_file_write>,
        __seek = 0x7ffff7a6c8b0 <__GI__IO_file_seek>,
        __close = 0x7ffff7a6c7e0 <__GI__IO_file_close>,
        __stat = 0x7ffff7a6d120 <__GI__IO_file_stat>,
        __showmanyc = 0x7ffff7a71d80 <_IO_default_showmanyc>,
        __imbue = 0x7ffff7a71d90 <_IO_default_imbue>
      }
      

_IO_FILE vtable 호출 과정

  • fread 함수는 _IO_fread 함수와 동일하게 정의되어 있으며, 해당 함수 내부에서 _IO_sgetn 함수를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    #define fread(p, m, n, s) _IO_fread (p, m, n, s)
    size_t
    _IO_fread (void *buf, size_t size, size_t count, FILE *fp)
    {
      size_t bytes_requested = size * count;
      size_t bytes_read;
      CHECK_FILE (fp, 0);
      if (bytes_requested == 0)
        return 0;
      _IO_acquire_lock (fp);
      bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
      _IO_release_lock (fp);
      return bytes_requested == bytes_read ? count : bytes_read / size;
    }
    
  • _IO_sgetn 함수는 _IO_XSGETN 매크로를 호출하며, 이를 순차적으로 확인해보면 vtable 변수를 참조하는 것을 확인할 수 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
    #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
    #define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable ( (THIS)))
    size_t
    _IO_sgetn (FILE *fp, void *data, size_t n)
    {
      /* FIXME handle putback buffer here! */
      return _IO_XSGETN (fp, data, n);
    }
    
  • fread를 포함한 모든 파일 함수는 가상 함수 테이블을 참조하여 파일 작업을 시도한다. 그러나 해당 함수 테이블은 동적으로 할당되는 영역으로 쓰기 권한이 있기 때문에 공격에 악용될 수 있다.

_IO_FILE AAW

파일 읽기 과정

  • fread, fgets 등 파일 읽기 함수는 내부에서 _IO_FILE_xsgetn 함수를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    _IO_size_t
    _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
    {
      _IO_size_t want, have;
      _IO_ssize_t count;
      _char *s = data;
      want = n;
        ...
        /* If we now want less than a buffer, underflow and repeat
          the copy.  Otherwise, _IO_SYSREAD directly to
          the user buffer. */
        if (fp->_IO_buf_base
            && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
          {
            if (__underflow (fp) == EOF)
        break;
    
            continue;
          }
      ...
    }
    
    • 이 함수에서는 인자로 전달된 n_IO_buf_end - _IO_buf_base보다 작은지 검사하고, _IO_new_file_underflow 함수를 호출한다.
    • 실제로 파일을 읽는 과정은 _IO_new_file_underflow를 시작으로 다양한 함수가 호출되면서 이뤄진다.

_IO_new_FILE_underflow

  • 해당 함수 내부에서는 파일 포인터의 _flags 변수에 읽기 권한이 부여되어 있는지 확인한다. 이후 _IO_SYSREAD 함수의 인자로 파일 포인터와 파일 구조체의 멤버 변수를 연산한 값이 전달된다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    #define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)
    int _IO_new_file_underflow (FILE *fp)
    {
      ssize_t count;
      if (fp->_flags & _IO_NO_READS)           
        {
          fp->_flags |= _IO_ERR_SEEN;
          __set_errno (EBADF);
          return EOF;
        }
      ...
      count = _IO_SYSREAD (fp, fp->_IO_buf_base,     
      fp->_IO_buf_end - fp->_IO_buf_base);
    }
    
    • 이때 _IO_SYSREAD 함수는 vtable의 _IO_file_read 함수이다.

_IO_FILE_read

  • _IO_FILE_read 함수 내부에서는 read 시스템 콜을 사용해 파일의 데이터를 받는다.
    1
    2
    3
    4
    5
    6
    7
    
    _IO_ssize_t
    _IO_file_read (_IO_FILE *fp, void *buf, _IO_ssize_t size)
    {
    return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
        ? __read_nocancel (fp->_fileno, buf, size)
        : __read (fp->_fileno, buf, size));
    }
    
    • 시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno, _IO_buf_basebuf, 그리고 _IO_buf_base로 연산된 size 변수가 전달된다.

How to AAW?

  • 임의 주소에 값을 쓰기 위해서는 _IO_buf_end_IO_buf_base를 조작한다. 파일의 내용을 읽을 때 아래와 같은 코드가 실행된다.
    1
    
    read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);
    
    • _IO_buf_base원하는 주소로 조작하고, _IO_buf_end원하는 주소에 원하는 크기보다 큰 수를 더한 값으로 조작한다.
    • 원하는 크기보다 큰 수를 더하는 이유는, _IO_new_file_underflow 코드 내에서 _IO_buf_end - _IO_buf_base 값이 fread 함수의 인자로 전달된 읽을 크기보다 커야 하는 조건이 있기 때문이다.
    • 위 조건을 만족했다면, 파일에 데이터를 쓰는 것이 아닌 프로그램에서 표준 입력을 통해 값을 쓰기 위해 filenostdin을 나타내는 0으로 조작한다.

_IO_FILE_AAR

  • fwrite, fputs 등 파일 쓰기 함수는 내부에서 _IO_sputn 함수를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
    
    _IO_size_t
    _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
    {
      ...
      if (to_do + must_flush > 0)
        {
          _IO_size_t block_size, do_write;
          /* Next flush the (full) buffer. */
          if (_IO_OVERFLOW (f, EOF) == EOF)
    
    • _IO_sputn_IO_XSPUTN 함수의 매크로이며, 실질적으로 _IO_new_file_xsputn 함수를 호출한다.
    • 이 함수에서는 파일 함수로 전달된 인자인 데이터와 길이를 검사하고 _IO_new_file_overflow 함수를 호출한다.

_IO_new_file_overflow

  • 파일 포인터의 _flags 변수에 쓰기 권한이 있는지 확인한다.
    1
    2
    3
    4
    5
    6
    
    if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
      {
        f->_flags |= _IO_ERR_SEEN;
        __set_errno (EBADF);
        return EOF;
      }
    
  • _flags_IO_CURRENTLY_PUTTING이 설정되어 있지 않으면, 파일 구조체의 _IO_write_ptr, _IO_write_base, _IO_write_end 필드 등을 다른 값으로 바꾼다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
    if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
      {
        /* 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;
      }
    
  • 해당 함수의 인자로 전달된 ch가 EOF 즉 -1이라면, _IO_do_write 함수를 호출한다.
    1
    2
    3
    4
    
    if (ch == EOF)
      return _IO_do_write (f, f->_IO_write_base,
               f->_IO_write_ptr - f->_IO_write_base);
    ...
    
    • 앞서 _IO_new_file_overflow를 호출할 때의 인자를 확인해보면 EOF를 전달하므로 _IO_do_write 함수가 호출되는데, 전달되는 인자가 파일 구조체의 멤버변수임을 알 수 있다.
  • _IO_do_write 함수는 내부적으로 new_do_write 함수를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    
    int
    _IO_new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
    {
    return (to_do == 0
        || (_IO_size_t) new_do_write (fp, data, to_do) == to_do) ? 0 : EOF;
    }
    libc_hidden_ver (_IO_new_do_write, _IO_do_write)
    

new_do_write

  • new_do_write 함수 내부에서는 플래그 검사와 _IO_SYSWRITE 함수 호출이 이루어진다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
    #define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
    static
    _IO_size_t
    new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
    {
      _IO_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)
        {
          _IO_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;
    }
    
    • 파일 포인터의 _flags 변수에 _IO_IS_APPENDING 플래그가 포함되어 있지 않다면, _IO_read_end_IO_write_base가 다를 경우 lseek 시스템 콜을 호출한다.
    • new_do_write 함수의 인자인 파일 포인터와 data, to_do를 인자로 _IO_SYSWRITE 함수를 호출한다. 이는 매크로이며, vtable의 _IO_new_file_write가 호출된다.

_IO_new_file_write

  • IO_new_file_write 함수 내부에서는 write 시스템 콜을 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    
    _IO_ssize_t
    _IO_new_file_write (_IO_FILE *f, const void *data, _IO_ssize_t n)
    {
      _IO_ssize_t to_do = n;
      while (to_do > 0)
        {
          _IO_ssize_t count = (__builtin_expect (f->_flags2
                  & _IO_FLAGS2_NOTCANCEL, 0)
            ? write_not_cancel (f->_fileno, data, to_do)
            : write (f->_fileno, data, to_do));
          if (count < 0)
      {
        f->_flags |= _IO_ERR_SEEN;
        break;
      }
          to_do -= count;
          data = (void *) ((char *) data + count);
        }
      n -= to_do;
      if (f->_offset >= 0)
        f->_offset += n;
      return n;
    }
    
    • 시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno, _IO_write_basedata, _IO_write_ptr - _IO_write_base로 연산된 to_do 변수가 전달된다.

How to AAR?

  • 임의 주소 값을 읽기 위해서 _flags 변수를 매직 값인 0xfbad0000IO_CURRENTLY_PUTTING을 포함한 값으로 변경한다.
    • IO_CURRENTLY_PUTTING이 포함되어 있지 않으면 _IO_new_file_overflow 함수 내부에서 _IO_write_base_IO_write_ptr을 다른 값으로 바꿔버리기 때문이다.
  • _IO_write_base원하는 주소로 조작하고, _IO_write_ptr원하는 주소에서 원하는 크기만큼 더한 주소로 조작한다.
    • new_do_write 함수에서 lseek 시스템 콜 호출을 방지하기 위해 _IO_read_end 역시 원하는 주소로 조작한다.
  • fileno 역시 원하는 file descriptor로 조작한다. (stdout: 1)

vtable validation

  • 파일 함수가 호출될 때에는 전달된 파일 포인터의 vtable 주소를 참조한다.

IO_validate_vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
  /* Fast path: The vtable pointer is within the __libc_IO_vtables
     section.  */
  uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
  const char *ptr = (const char *) vtable;
  uintptr_t offset = ptr - __start___libc_IO_vtables;
  if (__glibc_unlikely (offset >= section_length))
    /* The vtable pointer is not in the expected section.  Use the
       slow path, which will terminate the process if necessary.  */
    _IO_vtable_check ();
  return vtable;
}
  • vtable은 __libc_IO_vtables 섹션에 할당되며, 코드에서는 해당 섹션의 시작 주소와 끝 주소를 뺄셈하여 섹션의 크기를 알아내고, 호출하려는 vtable의 주소가 섹션의 크기를 벗어나는 값이라면 _IO_vtable_check 함수를 호출해 에러를 발생시킨다.
  • 이전 버전에서는 파일 포인터의 vtable 포인터를 덮을 수 있는 상황이라면 파일 함수가 참조하는 함수 포인터의 주소를 덮어써서 쉽게 공격할 수 있었으나, IO_validate_vtable 함수가 추가되면서 더 이상 같은 기법으로 공격할 수 없다.
  • 해당 검사를 우회하기 위해 __libc_IO_vtables 섹션에 존재하는 함수들 중 공격에 사용될 수 있는 함수를 찾아야 한다.

_IO_str_overflow

  • 해당 함수는 __libc_IO_vtables 섹션에 존재하는 함수이다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    int
    _IO_str_overflow (_IO_FILE *fp, int c)
    {
    int flush_only = c == EOF;
    _IO_size_t pos;
    if (fp->_flags & _IO_NO_WRITES)
        return flush_only ? 0 : EOF;
    if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
      {
        fp->_flags |= _IO_CURRENTLY_PUTTING;
        fp->_IO_write_ptr = fp->_IO_read_ptr;
        fp->_IO_read_ptr = fp->_IO_read_end;
      }
    pos = fp->_IO_write_ptr - fp->_IO_write_base;
    if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
      {
        if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
      return EOF;
        else
      {
        char *new_buf;
        char *old_buf = fp->_IO_buf_base;
        size_t old_blen = _IO_blen (fp);
        _IO_size_t new_size = 2 * old_blen + 100;
        if (new_size < old_blen)
          return EOF;
        new_buf
          = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
    
  • 코드 하단에서 아래와 같이 파일 포인터 내 _s._allocate_buffer라는 함수 포인터를 호출한다.
    1
    
    new_buf	    = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
    
    • 함수 포인터의 인자로 new_size를 전달한다. 이는 아래와 같이 결정된다.
      1
      2
      3
      4
      5
      
      #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
      size_t old_blen = _IO_blen (fp);
      _IO_size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)   
      return EOF;
      
      • _IO_FILE 구조체 변수인 _IO_buf_end_IO_buf_base 변수의 연산 값을 사용한다.
      • 파일 구조체를 조작할 수 있는 상황이라면 new_size 변수를 조작할 수 있다.
    • 함수 포인터를 호출하기 위해서는 조건문을 통과해야 한다.
      1
      2
      3
      4
      
      int flush_only = c == EOF;
      _IO_size_t pos;
      pos = fp->_IO_write_ptr - fp->_IO_write_base;  
      if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
      
      • _IO_FILE 구조체 변수인 _IO_write_ptr_IO_write_base 변수의 연산 값을 사용한다.
      • 비교 구문에서 사용되는 flush_only 변수는 기본값이 0이므로 pos >= _IO_blen(fp)가 된다.
      • 따라서 _IO_write_base를 0으로 초기화하면 _IO_write_ptr 값이 곧 pos 변수의 값이 되므로 쉽게 해당 비교 구문을 통과할 수 있고, 함수 포인터를 호출할 수 있다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

"Linux Userland" 카테고리의 게시물

📄 _IO_FILE
📄 environ
📄 FSOP