드림핵 강의를 보고 glibc-2.27을 분석한 글 입니다.
(틀린 내용이 있을 수 있습니다)
파일에 데이터를 쓰기 위한 함수는 대표적으로 fwrite, fputs가 있다.
해당 함수는 라이브러리 내부에서 _IO_sputn 함수를 호출한다.
해당 함수는 _IO_XSPUTN 함수의 매크로이며 실질적으로 _IO_new_file_xsputn 함수를 실행한다.
이 함수에서는 파일 함수로 전달된 인자인 데이터와 길이를 검사하고 _IO_OVERFLOW(_IO_new_file_overflow) 함수를 호출한다.
실제 파일에 내용을 쓰는 과정은 _IO_new_file_overflow를 시작으로 다양한 함수가 호출되면서 이루어진다.
fwrite
fwrite 함수는 _IO_fwrite 함수의 다른 이름이며 iofwrite.c 에 정의되어 있다.
_IO_size_t _IO_fwrite (const void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
{
_IO_size_t request = size * count;
_IO_size_t written = 0;
CHECK_FILE (fp, 0);
if (request == 0)
return 0;
_IO_acquire_lock (fp);
if (_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
written = _IO_sputn (fp, (const char *) buf, request);
_IO_release_lock (fp);
/* We have written all of the input in case the return value indicates
this or EOF is returned. The latter is a special case where we
simply did not manage to flush the buffer. But the data is in the
buffer and therefore written as far as fwrite is concerned. */
if (written == request || written == EOF)
return count;
else
return written / size;
}
weak_alias (_IO_fwrite, fwrite)
인수로 받은 size * count를 request에 저장한다. 0이면 함수가 종료된다.
written은 0으로 초기화 된다.
CHECK_FILE로 fp→_flags를 확인한다. 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가 매직 넘버가 아니면 에러를 설정하는 것으로 보인다.
다음으로 if 문을 보면 _IO_vtable_offset(fp)가 0이 아니거나 _IO_fwide(fp, -1)가 -1이면 _IO_sputn함수가 실행된다.
_IO_vtable_offset는 libioP.h에 정의되어 있다.
#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
...
#if _IO_JUMPS_OFFSET
# define _IO_vtable_offset(THIS) (THIS)->_vtable_offset
#else
# define _IO_vtable_offset(THIS) 0
#endif
_IO_JUMPS_OFFSET이 1이면 _IO_vtable_offset는 fp→_vtable_offset이 되고 아니면 0이 된다.
_IO_fwide는 iofwide.c에 정의되어 있다.
int _IO_fwide (_IO_FILE *fp, int mode)
{
/* Normalize the value. */
mode = mode < 0 ? -1 : (mode == 0 ? 0 : 1);
#if SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_1)
if (__builtin_expect (&_IO_stdin_used == NULL, 0)
&& (fp == _IO_stdin || fp == _IO_stdout || fp == _IO_stderr))
/* This is for a stream in the glibc 2.0 format. */
return -1;
#endif
/* The orientation already has been determined. */
if (fp->_mode != 0
/* Or the caller simply wants to know about the current orientation. */
|| mode == 0)
return fp->_mode;
/* Set the orientation appropriately. */
if (mode > 0)
{
struct _IO_codecvt *cc = fp->_codecvt = &fp->_wide_data->_codecvt;
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_read_end;
fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_base;
/* Get the character conversion functions based on the currently
selected locale for LC_CTYPE. */
{
/* Clear the state. We start all over again. */
memset (&fp->_wide_data->_IO_state, '\0', sizeof (__mbstate_t));
memset (&fp->_wide_data->_IO_last_state, '\0', sizeof (__mbstate_t));
struct gconv_fcts fcts;
__wcsmbs_clone_conv (&fcts);
assert (fcts.towc_nsteps == 1);
assert (fcts.tomb_nsteps == 1);
/* The functions are always the same. */
*cc = __libio_codecvt;
cc->__cd_in.__cd.__nsteps = fcts.towc_nsteps;
cc->__cd_in.__cd.__steps = fcts.towc;
cc->__cd_in.__cd.__data[0].__invocation_counter = 0;
cc->__cd_in.__cd.__data[0].__internal_use = 1;
cc->__cd_in.__cd.__data[0].__flags = __GCONV_IS_LAST;
cc->__cd_in.__cd.__data[0].__statep = &fp->_wide_data->_IO_state;
cc->__cd_out.__cd.__nsteps = fcts.tomb_nsteps;
cc->__cd_out.__cd.__steps = fcts.tomb;
cc->__cd_out.__cd.__data[0].__invocation_counter = 0;
cc->__cd_out.__cd.__data[0].__internal_use = 1;
cc->__cd_out.__cd.__data[0].__flags
= __GCONV_IS_LAST | __GCONV_TRANSLIT;
cc->__cd_out.__cd.__data[0].__statep = &fp->_wide_data->_IO_state;
}
/* From now on use the wide character callback functions. */
_IO_JUMPS_FILE_plus (fp) = fp->_wide_data->_wide_vtable;
}
/* Set the mode now. */
fp->_mode = mode;
return mode;
}
자세히 분석해보진 않았지만 mode를 -1,0,1 중 하나로 정해주는 것 같다.
if 문의 조건을 만족하면 다음 코드를 실행한다.
written = _IO_sputn (fp, (const char *) buf, request);
_IO_sputn 함수는 libioP.h에 있는 코드들을 거쳐서 _IO_file_xsputn를 호출하게 된다.
#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_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)
...
#define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
...
#define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
...
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;
}
아래는 libioP.h의 _IO_jump_t 구조체이다.
typedef _IO_size_t (*_IO_xsputn_t) (_IO_FILE *FP, const void *DATA,
_IO_size_t N);
...
struct _IO_jump_t {
...
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
...
JUMP_FIELD(_IO_xsputn_t, __xsputn);
...
JUMP_FIELD(_IO_doallocate_t, __doallocate);
...
JUMP_FIELD(_IO_write_t, __write);
};
fileops.c의 _IO_file_jumps와 함께 확인해보면
versioned_symbol (libc, _IO_new_file_overflow, _IO_file_overflow, GLIBC_2_1);
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(overflow, _IO_file_overflow),
JUMP_INIT(underflow, _IO_file_underflow),
...
JUMP_INIT(xsputn, _IO_file_xsputn),
...
JUMP_INIT(doallocate, _IO_file_doallocate),
JUMP_INIT(write, _IO_new_file_write),
...
};
__xsputn은 _IO_file_xsputn 이다. 또한 _IO_file_xsputn은 _IO_new_file_xsputn이다.
따라서 _IO_sputn을 호출하면 __xsputn를 거쳐 _IO_new_file_xsputn가 호출된다.
_IO_new_file_xsputn은 fileops.c에 정의되어 있다.
_IO_size_t _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
{
const char *s = (const char *) data;
_IO_size_t to_do = n;
int must_flush = 0;
_IO_size_t count = 0;
if (n <= 0)
return 0;
/* This is an optimized implementation.
If the amount to be written straddles a block boundary
(or the filebuf is unbuffered), use sys_write directly. */
/* First figure out how much space is available in the buffer. */
if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING))
{
count = f->_IO_buf_end - f->_IO_write_ptr;
if (count >= n)
{
const char *p;
for (p = s + n; p > s; )
{
if (*--p == '\n')
{
count = p - s + 1;
must_flush = 1;
break;
}
}
}
}
else if (f->_IO_write_end > f->_IO_write_ptr)
count = f->_IO_write_end - f->_IO_write_ptr; /* Space available. */
/* Then fill the buffer. */
if (count > 0)
{
if (count > to_do)
count = to_do;
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
to_do -= count;
}
if (to_do + must_flush > 0)
{
_IO_size_t block_size, do_write;
/* Next flush the (full) buffer. */
if (_IO_OVERFLOW (f, EOF) == EOF)
/* If nothing else has to be written we must not signal the
caller that everything has been written. */
return to_do == 0 ? EOF : n - to_do;
/* Try to maintain alignment: write a whole number of blocks. */
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
libc_hidden_ver (_IO_new_file_xsputn, _IO_file_xsputn)
먼저 fp→_flags에 _IO_LINE_BUF와 _IO_CURRENTLY_PUTTING가 세팅되어 있을 때와 아닐 때로 나눠서 버퍼에 얼마나 공간이 있는지 확인하고 count 변수에 저장한다.
count > to_do(전달한 인자 n)이면 __mempcpy를 호출해 메모리 복사를 수행한다.
to_do + must_flush > 0일 때는 메모리 공간이 부족한 상태이므로 _IO_OVERFLOW를 호출한다.
_IO_OVERFLOW의 결과가 EOF일 때 to_do가 0이면 EOF를 반환하고 0이 아니면 n - to_do를 반환한다.
_IO_OVERFLOW는 __overflow, _IO_file_underflow를 거쳐 최종적으로 _IO_new_file_overflow를 호출하게 된다. fileops.c에 정의되어 있다.
int _IO_new_file_overflow (_IO_FILE *f, int ch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
/* Allocate a buffer if needed. */
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
/* Otherwise must be currently reading.
If _IO_read_ptr (and hence also _IO_read_end) is at the buffer end,
logically slide the buffer forwards one block (by setting the
read pointers to all point at the beginning of the block). This
makes room for subsequent output.
Otherwise, set the read pointers to _IO_read_end (leaving that
alone, so it can continue to correspond to the external position). */
if (__glibc_unlikely (_IO_in_backup (f)))
{
size_t nbackup = f->_IO_read_end - f->_IO_read_ptr;
_IO_free_backup_area (f);
f->_IO_read_base -= MIN (nbackup,
f->_IO_read_base - f->_IO_buf_base);
f->_IO_read_ptr = f->_IO_read_base;
}
if (f->_IO_read_ptr == f->_IO_buf_end)
f->_IO_read_end = f->_IO_read_ptr = f->_IO_buf_base;
f->_IO_write_ptr = f->_IO_read_ptr;
f->_IO_write_base = f->_IO_write_ptr;
f->_IO_write_end = f->_IO_buf_end;
f->_IO_read_base = f->_IO_read_ptr = f->_IO_read_end;
f->_flags |= _IO_CURRENTLY_PUTTING;
if (f->_mode <= 0 && f->_flags & (_IO_LINE_BUF | _IO_UNBUFFERED))
f->_IO_write_end = f->_IO_write_ptr;
}
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
if (f->_IO_write_ptr == f->_IO_buf_end ) /* Buffer is really full */
if (_IO_do_flush (f) == EOF)
return EOF;
*f->_IO_write_ptr++ = ch;
if ((f->_flags & _IO_UNBUFFERED)
|| ((f->_flags & _IO_LINE_BUF) && ch == '\n'))
if (_IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base) == EOF)
return EOF;
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
이전에 살펴봤던 __underflow와 비슷한 부분도 있다. 버퍼가 없으면 _IO_doallocbuf로 버퍼에 할당을 해준다.
_IO_setg는 libioP.h에 정의되어 있다.
#define _IO_setg(fp, eb, g, eg) ((fp)->_IO_read_base = (eb),\
(fp)->_IO_read_ptr = (g), (fp)->_IO_read_end = (eg))
fp 구조체의 _IO_read_base, _IO_read_ptr, _IO_read_end의 주소를 바꿔준다.
ch는 EOF이므로 _IO_do_write (f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base)의 결과를 리턴한다. _IO_do_write는 _IO_new_do_write의 다른 이름이다. fileops.c에 정의되어 있다.
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를 호출한다. fileops.c에 정의되어 있다.
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;
}
조건 문을 지나 _IO_SYSWRITE가 호출된다. libioP.h를 보면 __write을 호출한다.
#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)
vtable을 확인해보면 __write은 _IO_new_file_write 함수를 호출하게 된다.
_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_nocancel (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;
}
(실직적인 출력을 담당하는 함수로 예상해본다..)
이제 _IO_new_file_xsputn의 남은 실행 코드는 아래와 같다.
...
block_size = f->_IO_buf_end - f->_IO_buf_base;
do_write = to_do - (block_size >= 128 ? to_do % block_size : 0);
if (do_write)
{
count = new_do_write (f, s, do_write);
to_do -= count;
if (count < do_write)
return n - to_do;
}
/* Now write out the remainder. Normally, this will fit in the
buffer, but it's somewhat messier for line-buffered files,
so we let _IO_default_xsputn handle the general case. */
if (to_do)
to_do -= _IO_default_xsputn (f, s+do_write, to_do);
}
return n - to_do;
}
block_size(버퍼 크기) 단위로 new_do_write를 호출해 출력을 수행해주고
남은 출력할 부분은 _IO_default_xsputn를 호출해 출력을 수행해주는 것 같다.
_IO_file_xsputn 실행 후 if(written == request || written == EOF)을 만족하면 count를 반환하고
아니면 written / size를 반환하며 fwrite 함수가 종료된다.
fputs
fputs 함수는 _IO_fputs의 다른 이름으로 iofputs.c에 정의되어 있다.
fwrite와 동일한 부분은 분석을 생략하였다.
int _IO_fputs (const char *str, _IO_FILE *fp)
{
_IO_size_t len = strlen (str);
int result = EOF;
CHECK_FILE (fp, EOF);
_IO_acquire_lock (fp);
if ((_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
&& _IO_sputn (fp, str, len) == len)
result = 1;
_IO_release_lock (fp);
return result;
}
weak_alias (_IO_fputs, fputs)
먼저 str의 길이를 len에 저장하고 result에 EOF를 저장한다.
CHECK_FILE을 거친 뒤 if 문 조건을 만족하면 result = 1로 설정한 뒤 result를 반환한다.
_IO_fputs의 반환 값 result는 if 문 조건을 만족하면 1, 만족하지 못 하면 EOF가 반환된다.
if 문의 조건을 살펴보면 _IO_vtable_offset, _IO_fwide, _IO_sputn을 호출된다.
이제부턴 fwrite 함수와 동일한 흐름으로 넘어 갈 것이다.
if ((_IO_vtable_offset (fp) != 0 || _IO_fwide (fp, -1) == -1)
&& _IO_sputn (fp, str, len) == len)
레퍼런스
https://dreamhack.io/lecture/courses/273
https://youngsouk-hack.tistory.com/67
https://aidencom.tistory.com/503
'background > linux' 카테고리의 다른 글
Frame Pointer Overwirte (One Byte Overflow) (0) | 2023.03.26 |
---|---|
return-to-csu x64 (RTC, JIT ROP) (0) | 2023.01.12 |
[glibc-2.27] fread & fgets (0) | 2023.01.09 |
[glibc-2.27] _IO_FILE & fopen (1) | 2023.01.07 |
SigReturn-Oriented Programming(SROP) (0) | 2022.12.30 |