드림핵 강의를 보고 glibc-2.27을 분석한 글 입니다.
(틀린 내용이 있을 수 있습니다)
파일의 내용을 읽기 위한 함수는 대표적으로 fread, fgets가 있다.
해당 함수는 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출한다.
실제 파일의 내용을 읽는 과정은 _IO_new_file_underflow를 시작으로 다양한 함수가 호출되면서 이루어진다.
fread
fread 함수는 _IO_fread의 별칭으로 iofread.c 에 정의되어 있다.
_IO_size_t _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t bytes_requested = size * count;
_IO_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)
weak_alias (_IO_fread, fread)
포인터 변수 buf, _IO_size_t 타입 변수 size와 count, 포인터 변수 fp를 받고 시작한다.
libio.h에 _IO_size_t는 size_t라고 정의되어 있다.
#define _IO_size_t size_t
fread 함수의 실행문을 살펴보자.
bytes_requested에 size*count를 저장한다. 0이면 return 0를 만나게 된다.
CHECK_FILE 메크로 함수는 libioP.h에 정의되어 있다.
#ifdef IO_DEBUG
# define CHECK_FILE(FILE, RET) do { \
if ((FILE) == NULL \
|| ((FILE)->_flags & _IO_MAGIC_MASK) != _IO_MAGIC) \
{ \
__set_errno (EINVAL); \
return RET; \
} \
} while (0)
#else
# define CHECK_FILE(FILE, RET) do { } while (0)
#endif
fp가 null이거나 fp의 _flags가 매직 넘버가 아니면 에러를 설정하는 것으로 보인다.
이후 _IO_acquire_lock 함수를 만나는데 stdio-lock.h에 정의되어 있다.
#if defined _LIBC && IS_IN (libc)
# ifdef __EXCEPTIONS
# define _IO_acquire_lock(_fp) \
do { \
FILE *_IO_acquire_lock_file \
__attribute__((cleanup (_IO_acquire_lock_fct))) \
= (_fp); \
_IO_flockfile (_IO_acquire_lock_file);
# else
# define _IO_acquire_lock(_fp) _IO_acquire_lock_needs_exceptions_enabled
# endif
# define _IO_release_lock(_fp) ; } while (0)
#endif
중요해보이진 않는다.. 함수 이름에서 유추해보건데 수정을 못하도록 잠그는 것 같다.
(이후 _IO_release_lock 함수로 _IO_acquire_lock에서 잠갔던 것을 해제하지 않을까?)
이제 _IO_sgetn 함수를 만나는데 여기가 중요하다. genops.c에 정의되어 있다.
_IO_sgetn은 _IO_XSGETN을 호출한다.
size_t _IO_sgetn (FILE *fp, void *data, size_t n){
/* FIXME handle putback buffer here! */
return _IO_XSGETN (fp, data, n);
}
아래는 libioP.h의 코드 중 일부다. _IO_XSGETN를 보자.
#if (!defined _IO_USE_OLD_IO_FILE \
&& (!defined _G_IO_NO_BACKWARD_COMPAT || _G_IO_NO_BACKWARD_COMPAT == 0))
# define _IO_JUMPS_OFFSET 1
#else
# define _IO_JUMPS_OFFSET 0
#endif
...
#define _IO_JUMPS_FILE_plus(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE_plus, vtable)
...
#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
(IO_validate_vtable \
(*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset)))
# define _IO_JUMPS_FUNC_UPDATE(THIS, VTABLE) \
(*(const struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
+ (THIS)->_vtable_offset) = (VTABLE))
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
...
#define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
...
/* The 'xsgetn' hook reads upto N characters into buffer DATA.
Returns the number of character actually read.
It matches the streambuf::xsgetn virtual function. */
typedef _IO_size_t (*_IO_xsgetn_t) (_IO_FILE *FP, void *DATA, _IO_size_t N);
#define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)
_IO_XSGETN는 JUMP2 함수이며 JUMP2로 가면 인자들을 활용해 __xsgetn 함수를 호출한다.
JUMP2는 (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)이다.
_IO_JUMPS_OFFSET이 0일 때를 기준으로 _IO_JUMPS_FUNC(THIS)는 IO_validate_vtable(_IO_JUMPS_FILE_plus (THIS))이다.
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
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;
}
IO_validate_vtable(_IO_JUMPS_FILE_plus (THIS))의 결과로 vtable을 리턴한다.
(_IO_JUMPS_FUNC(FP)->__xsgetn) (FP, DATA, N)이 (vtable->__xsgetn) (FP, DATA, N)이 되는 것이다.
typedef _IO_size_t (*_IO_xsputn_t) (_IO_FILE *FP, const void *DATA,
_IO_size_t N);
...
struct _IO_jump_t {
...
JUMP_FIELD(_IO_underflow_t, __underflow);
...
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
...
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
...
};
fileops.c의 _IO_file_jumps와 함께 확인해보면
versioned_symbol (libc, _IO_new_file_underflow, _IO_file_underflow, GLIBC_2_1);
...
const struct _IO_jump_t _IO_file_jumps libio_vtable =
{
...
JUMP_INIT(underflow, _IO_file_underflow),
...
JUMP_INIT(xsgetn, _IO_file_xsgetn),
...
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(read, _IO_file_read),
...
};
__xsgetn은 _IO_file_xsgetn 함수임을 알 수 있다.
따라서 _IO_sgetn을 호출하면 __xsgetn를 거쳐 _IO_file_xsgetn가 호출된다.
fileops.c에 _IO_file_xsgetn가 정의되어 있다.
_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 (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)
{
_IO_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;
}
_IO_doallocbuf 함수로 버퍼가 없으면 버퍼를 할당해주나 보다. _IO_doallocbuf은 genops.c에 정의되어 있다.
void _IO_doallocbuf (_IO_FILE *fp)
{
if (fp->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED) || fp->_mode > 0)
if (_IO_DOALLOCATE (fp) != EOF)
return;
_IO_setb (fp, fp->_shortbuf, fp->_shortbuf+1, 0);
}
_IO_DOALLOCATE는 libioP.h에 정의되어 있다.
#define _IO_DOALLOCATE(FP) JUMP0 (__doallocate, FP)
vtable을 확인해보면 __doallocate는 _IO_file_doallocate이다. filedoalloc.c에 정의되어 있다.
/* Allocate a file buffer, or switch to unbuffered I/O. Streams for
TTY devices default to line buffered. */
int _IO_file_doallocate (_IO_FILE *fp)
{
_IO_size_t size;
char *p;
struct stat64 st;
size = _IO_BUFSIZ;
if (fp->_fileno >= 0 && __builtin_expect (_IO_SYSSTAT (fp, &st), 0) >= 0)
{
if (S_ISCHR (st.st_mode))
{
/* Possibly a tty. */
if (
#ifdef DEV_TTY_P
DEV_TTY_P (&st) ||
#endif
local_isatty (fp->_fileno))
fp->_flags |= _IO_LINE_BUF;
}
#if _IO_HAVE_ST_BLKSIZE
if (st.st_blksize > 0 && st.st_blksize < _IO_BUFSIZ)
size = st.st_blksize;
#endif
}
p = malloc (size);
if (__glibc_unlikely (p == NULL))
return EOF;
_IO_setb (fp, p, p + size, 1);
return 1;
}
malloc으로 버퍼를 동적 할당해주나 보다.
인자로 전달한 n(want) ≤ _IO_read_end - _IO_read_ptr 일 때는 __mempcpy 를 호출한다.
__mempcpy는 string/mempcpy.c에 정의되어 있다.
void * MEMPCPY (void *dest, const void *src, size_t len)
{
return memcpy (dest, src, len) + len;
}
libc_hidden_def (__mempcpy)
인자로 전달한 n(want) ≤ _IO_buf_end - _IO_buf_base 일 때는 __underflow (_IO_new_file_underflow) 함수를 호출한다.
__underflow는 _IO_file_underflow인데 _IO_file_underflow는 _IO_new_file_underflow이다.
즉, __underflow를 호출하면 _IO_new_file_underflow가 호출된다.
int _IO_new_file_underflow (_IO_FILE *fp)
{
_IO_ssize_t count;
#if 0
/* SysV does not make this test; take it out for compatibility */
if (fp->_flags & _IO_EOF_SEEN)
return (EOF);
#endif
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);
}
/* Flush all line buffered files before reading. */
/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
#if 0
_IO_flush_all_linebuffered ();
#else
/* 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 (_IO_stdout);
if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (_IO_stdout, EOF);
_IO_release_lock (_IO_stdout);
#endif
}
_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;
}
플래그에 읽기 권한 등을 비롯해 여러 가지 검사를 수행한 뒤 _IO_SYSREAD를 호출한다.
_IO_SYSREAD는 libioP.h에 정의되어 있다.
#define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)
위에서 __xsgetn처럼 찾아보면, 결론적으로 read함수를 호출한다.
read는 _IO_file_read이며 fileops.c에 정의되어 있다.
_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));
}
_IO_file_read 함수 내부에서는 read 시스템 콜을 사용해 파일의 데이터를 읽는다.
시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno, _IO_buf_base인 buf, 그리고 _IO_buf_end - _IO_buf_base로 연산된 size 변수가 전달된다.
read(f->_fileno, _IO_buf_base, _IO_buf_end - _IO_buf_base);
이렇게 계속 파일을 읽어들이며 다 읽은 뒤 마지막 리턴 문에서 bytes_requested == bytes_read이면 count를 리턴하고 아니면 bytes_read / size를 리턴하며 fread 함수는 종료된다.
fgets
fgets 함수는 _IO_fgets의 다른 이름이며 iofgets.c에 정의되어 있다.
fread 함수와 구조가 비슷한 듯 하니 분석이 수월할 듯 하다.
fread와 동일한 부분은 분석을 생략하였다.
#include "libioP.h"
#include <stdio.h>
char * _IO_fgets (char *buf, int n, _IO_FILE *fp)
{
_IO_size_t count;
char *result;
int old_error;
CHECK_FILE (fp, NULL);
if (n <= 0)
return NULL;
if (__glibc_unlikely (n == 1))
{
/* Another irregular case: since we have to store a NUL byte and
there is only room for exactly one byte, we don't have to
read anything. */
buf[0] = '\0';
return buf;
}
_IO_acquire_lock (fp);
/* This is very tricky since a file descriptor may be in the
non-blocking mode. The error flag doesn't mean much in this
case. We return an error only when there is a new error. */
old_error = fp->_IO_file_flags & _IO_ERR_SEEN;
fp->_IO_file_flags &= ~_IO_ERR_SEEN;
count = _IO_getline (fp, buf, n - 1, '\n', 1);
/* If we read in some bytes and errno is EAGAIN, that error will
be reported for next read. */
if (count == 0 || ((fp->_IO_file_flags & _IO_ERR_SEEN)
&& errno != EAGAIN))
result = NULL;
else
{
buf[count] = '\0';
result = buf;
}
fp->_IO_file_flags |= old_error;
_IO_release_lock (fp);
return result;
}
weak_alias (_IO_fgets, fgets)
_IO_getline이 호출된다. iogetline.c에 정의되어 있다.
밑에 있는 함수인 _IO_getline_info가 호출된다.
_IO_size_t _IO_getline (_IO_FILE *fp, char *buf, _IO_size_t n, int delim,
int extract_delim)
{
return _IO_getline_info (fp, buf, n, delim, extract_delim, (int *) 0);
}
_IO_size_t _IO_getline_info (_IO_FILE *fp, char *buf, _IO_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)
{
_IO_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 ((_IO_size_t) len >= n)
len = n;
t = (char *) memchr ((void *) fp->_IO_read_ptr, delim, len);
if (t != NULL)
{
_IO_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;
}
libc_hidden_def (_IO_getline_info)
_IO_read_end - _IO_read_ptr ≤ 0 이면 __uflow가 호출된다.
vtable에서 __uflow가 _IO_default_uflow임을 찾을 수 있다.
_IO_default_uflow는 genops.c에 정의되어 있다.
int _IO_default_uflow (_IO_FILE *fp)
{
int ch = _IO_UNDERFLOW (fp);
if (ch == EOF)
return EOF;
return *(unsigned char *) fp->_IO_read_ptr++;
}
_IO_UNDERFLOW를 호출하는데 libioP.h를 보면 __underflow를 호출하게 됨을 알 수 있다.
#define JUMP0(FUNC, THIS) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS)
...
#define _IO_UNDERFLOW(FP) JUMP0 (__underflow, FP)
__underflow 함수, 즉, _IO_new_file_underflow를 호출하는 순간부턴 fread와 같아진다.
레퍼런스
https://dreamhack.io/lecture/courses/274
https://aidencom.tistory.com/504
https://youngsouk-hack.tistory.com/66
'background > linux' 카테고리의 다른 글
return-to-csu x64 (RTC, JIT ROP) (0) | 2023.01.12 |
---|---|
[glibc-2.27] fwrite & fputs (1) | 2023.01.11 |
[glibc-2.27] _IO_FILE & fopen (1) | 2023.01.07 |
SigReturn-Oriented Programming(SROP) (0) | 2022.12.30 |
Master Canary (0) | 2022.12.08 |