fread함수

dandb3·2023년 5월 30일
0

pwnable

목록 보기
6/17

앞서서 FILE 구조체, fopen함수에 대해서 알아보았다.
이번에는 이어서, 실제로 파일을 읽고 쓰는 함수인 fread, fwrite 중 fread에 대해서 알아보도록 하자.

  • 딱 한 번만 매크로를 쭉 따라가서 어느 함수가 호출되는지 확인해보자.
    굉장히 길 것 같은 느낌...
#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;
}
libc_hidden_def (_IO_fread)
  • 별로 볼 건 없고, 바로 _IO_sgetn으로 넘어가자.
size_t
_IO_sgetn (FILE *fp, void *data, size_t n)
{
  /* FIXME handle putback buffer here! */
  return _IO_XSGETN (fp, data, n);
}
libc_hidden_def (_IO_sgetn)

#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 (_IO_JUMPS_FILE_plus (THIS)))
#define _IO_JUMPS_FILE_plus(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
#define _IO_CAST_FIELD_ACCESS(THIS, TYPE, MEMBER) \
  (*(_IO_MEMBER_TYPE (TYPE, MEMBER) *)(((char *) (THIS)) \
				       + offsetof(TYPE, MEMBER)))
#define _IO_MEMBER_TYPE(TYPE, MEMBER) __typeof__ (((TYPE){}).MEMBER)
...
  • 그만 알아보자.. 정신이 나가버릴 지도..?
  • 함수를 찾아가는 과정에서 vtable이 올바른지 검증하는 과정이 존재한다는 것은 알겠다.
  • 인터넷 뒤적뒤적 해 봤을때 vtable을 덮어쓰는 방법은 막힌지 오래되었다는 것을 봤던 것 같음.
  • 어쨌든 간에 결국 _IO_file_xsgetn이 호출된다.
  • _IO_file_xsgetn
size_t
_IO_file_xsgetn (FILE *fp, void *data, size_t n)
{
  size_t want, have;
  ssize_t count;
  char *s = data;
  want = n;
  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);
    }
  while (want > 0)
    {
      have = fp->_IO_read_end - fp->_IO_read_ptr;
      if (want <= have)
      {
        memcpy (s, fp->_IO_read_ptr, want);
        fp->_IO_read_ptr += want;
        want = 0;
      }
      else
      {
        if (have > 0)
          {
            s = __mempcpy (s, fp->_IO_read_ptr, have);
            want -= have;
            fp->_IO_read_ptr += have;
          }
        /* Check for backup and repeat */
        if (_IO_in_backup (fp))
          {
            _IO_switch_to_main_get_area (fp);
            continue;
          }
        /* 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;
          }
        /* These must be set before the sysread as we might longjmp out
           waiting for input. */
        _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
        _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
        /* Try to maintain alignment: read a whole number of blocks.  */
        count = want;
        if (fp->_IO_buf_base)
          {
            size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
            if (block_size >= 128)
          count -= want % block_size;
          }
        count = _IO_SYSREAD (fp, s, count);
        if (count <= 0)
          {
            if (count == 0)
          fp->_flags |= _IO_EOF_SEEN;
            else
          fp->_flags |= _IO_ERR_SEEN;
            break;
          }
        s += count;
        want -= count;
        if (fp->_offset != _IO_pos_BAD)
          _IO_pos_adjust (fp->_offset, count);
      }
    }
  return n - want;
}
libc_hidden_def (_IO_file_xsgetn)

뭔가 정렬이 이상하게 안 되어있긴 하다. 코드 긁어올 때 뭔가 제대로 복사가 안 된듯.

  • 우선 이 함수의 동작 원리부터 정리하고 가야될 것 같다.

    1. 버퍼를 통한 I/O : 요청이 들어올 때마다 syscall을 호출하는 것이 아닌, 미리 버퍼에 read를 해 놓고 여기서 꺼내쓴다.
    2. 버퍼에서 꺼내 쓰다가 버퍼의 끝에 도달함 -> __underflow를 통해 다시 버퍼에 읽어올 값들을 채워 넣는다.
    3. 읽고 싶은 길이만큼 다 읽기 전까지 반복한다.
  • 하나 조심해야 할 것은, _IO_buf_... 멤버 변수와 _IO_read_... 멤버 변수는 구분해야 한다는 것이다.
    예를 들어보자.

    • 기본 세팅된 버퍼의 크기는 0x1000에 해당한다. 대충 _IO_buf_base = 0x10000이라고 하면, _IO_buf_end = 0x11000이 된다.
    • 읽을 파일의 크기 = 0x500이라고 했을 때, fread가 호출되면 내부적으로 버퍼 크기(0x1000)만큼 버퍼에 read syscall을 통해 채워넣는다. 하지만 파일의 크기는 0x500에 불과하므로, 0x500개 만큼의 데이터만 읽어올 수 있다.
    • 이 때, _IO_read_end값은 '읽어올 수 있는 최대 길이' 에 해당하여 0x10500의 값을 가지게 된다.
    • fread를 통해서 0x300개의 데이터를 읽었다면, _IO_read_ptr의 값은 0x10300이 된다.
  • 예시를 통해 봤었지만, 여기서 각 buffer를 가리키는 포인터들의 역할을 다시 한 번 더 정리해 보자.
    - _IO_buf_base : 할당된 버퍼의 시작지점
    - _IO_buf_end : 할당된 버퍼의 끝지점
    - _IO_read_base : read syscall을 통해서 미리 읽어온 데이터의 시작 지점 (이지만 사실 _IO_buf_base와 동일)
    - _IO_read_end : read syscall을 통해서 미리 읽어온 데이터의 마지막 지점 (_IO_buf_end와 구분!!)
    - _IO_read_ptr : 실제 fread를 통해서 지금까지 읽은 지점

  • 세부적으로 들어가 보자.

    • want : 읽어오고 싶은 문자 개수
    • have : 현재 버퍼에 남아있는 읽지 않은 문자 개수 (_IO_read_end - _IO_read_ptr)
    • while문을 돌면서 진행한다.
      • want <= have
        굳이 syscall을 하지 않고도 버퍼에 남아있는 문자들을 읽어오기만 하면 되므로 그냥 읽어오고 끝.
      • want > have
        일단 버퍼에 남아있는 문자들을 다 읽어온다.
        • want < 전체 버퍼의 크기
          __underflow를 호출해서 syscall을 통해 다시 버퍼에 값을 채워넣고, while문을 다시 돌아 나머지 값들을 읽어온다.
        • want >= 전체 버퍼의 크기
          버퍼의 크기의 배수만큼 통으로 syscall을 하고, 그 후에 남은 문자들은 while문을 돌아서 값을 읽어온다. (이 때, 버퍼의 크기가 128을 기준으로 크거나 같은 경우에만 배수로 syscall을 호출하는데, 아마 최적화 때문에 그런듯.)
  • __underflow

    int
    _IO_new_file_underflow (FILE *fp)
    {
      ssize_t count;
    
      ...(생략)...
    
      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;
    }
    libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

    보면 알겠지만, 포인터들을 모두 _IO_buf_base로 초기화 시켜준 후, read syscall 호출, 마지막으로 read해준 길이만큼 _IO_read_end를 옮겨주는 것을 확인할 수 있다.

  • 이런 식으로 동작하게 된다.

  • 참고 자료

profile
공부 내용 저장소

0개의 댓글