Pwnable/Tech

_IO_FILE AAR

Kon4 2023. 2. 18. 19:17

_IO_FILE AAR

파일 쓰기 함수는 대표적으로 fwrite, fputs가 있다.

해당 함수들은 라이브러리 내부에서 _IO_sputn 함수를 호출한다.

_IO_sputn 은 _IO_XSPUTN 의 매크로이며 실질적으로 _IO_new_file_xsputn 함수를 실행한다.

_IO_new_file_xsputn code

#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_OVERFLOW, 즉 _IO_new_overflow 함수를 호출한다.

이떄 “_IO_new_overflow” 에서 실질적으로 파일에 내용을 쓰는 과정이 시작된다.

_IO_new_file_overflow code & new_do_write code

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 (ch == EOF)
    return _IO_do_write (f, f->_IO_write_base,
			 f->_IO_write_ptr - f->_IO_write_base);
}
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)

_IO_new_file_overflow 함수를 살펴보면, 함수 내부에서 파일 포인터의 flags 변수에 쓰기 권한이 부여되어 있는지 확인하고 해당 함수가의 인자로 ch(기본값 = 전달값 EOF)가 EOF, 즉 -1이라면 _IO_do_write 함수를 호출한다. 이때 인자로 파일 구조체의 멤버 변수들이 들어감을 알 수 있습니다.

_IO_do_write 함수의 내부를 보면 new_do_write 함수를 호출하는 것을 알 수 있습니다.

new_do_write code

#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 플래그가 포함되어 있는지 확인한다.

 if (fp->_flags & _IO_IS_APPENDING)

_IO_SYSWRITE 함수를 호출한다. 이는 vtable의 _IO_new_file_write 함수이다.

#define _IO_SYSWRITE(FP, DATA, LEN) JUMP2 (__write, FP, DATA, LEN)

vtable: _IO_new_file_write(_IO_SYSWRITE) code

_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;
}

_IO_new_file_write 함수 내부에서는 write 시스템콜을 사용해 파일에 데이터를 작성한다.

이때 인자로는 파일 구조체의 파일 디스립터를 나타내는 _fileno, _IO_write_base인 data, _IO_write_ptr - _IO_write_base로 연산된 to_do 변수가 전달된다.

write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);

익스 시나리오

#include <stdio.h>
#include <unistd.h>
#include <string.h>
char account_buf[1024];
FILE *fp;
void init() {
  setvbuf(stdin, 0, 2, 0);
  setvbuf(stdout, 0, 2, 0);
}
int read_account() {
	FILE *fp;
	fp = fopen("/etc/passwd", "r");
	fread(account_buf, sizeof(char), sizeof(account_buf), fp);
	fclose(fp);
}
int main() {
  const char *data = "TEST FILE!";
  init();
  read_account();
  fp = fopen("testfile", "w");
  printf("Data: ");
  read(0, fp, 300);
  fwrite(data, sizeof(char), sizeof(account_buf), fp);
  fclose(fp);
}
  1. 파일 구조체 조작
    1. read함수로 fp, _IO_FILE 구조체를 조작 할 수 있으므로 이를 통해 다음을 조작한다.
    2. _flags 변수를 _IO_IS_APPENDING 으로 조작해야한다. 방법은 다음과 같다.
    _flags의 매직 값: 0xfbad0000 _IO_IS_APPENDING : 0x800_IO_write_base 를 account_buf의 주소로 조작 한다.new do write 함수 내 아래와 같은 코드가 있기떄문에
    else if (fp->_IO_read_end != fp->_IO_write_base)
    
    d. 또한 파일 디스크립터인 fileno가 현재 입력인 0(stdin)으로 설정 되어 있기떄문에 이를 1로 바꿔 줌으로써 stdout 표준 출력을 한다.
  2. _fileno = 1 (stdout)
  3. _IO_read_end = account_buf 를 해준다.
  4. _IO_write_ptr 을 account_buf + 1024 주소로 조작한다.
  5. —> 최종 _flags = 0xfbad080 3. flags 조건을 만족 했다면, _IO_write_prt 과 _IO_write_base를 조작한다.

최종 익스코드

 드림핵 문제이므로 상상에 맡기도록 하겠습니다 ^^7