[Dreamhack] _IO_FILE: 2 - _IO_FILE Arbitrary Address Read - 1

securitykss·2023년 3월 1일
0

Pwnable 강의(dreamhack)

목록 보기
54/58
post-thumbnail

https://dreamhack.io/lecture/courses/273 을 토대로 작성한 글입니다.

1. Introduction

파일을 읽고 쓰는 과정은 라이브러리 함수 내부에서 파일 구조체의 포인터와 값들을 이용한다.

파일을 쓰는 과정에서 파일 구조체를 조작해 임의 메모리 값을 읽는 실습을 해보자

code

// Name: iofile_aar
// gcc -o iofile_aar iofile_aar.c -no-pie 

#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);
}

2. 파일 쓰기 함수 분석

2.1 파일 쓰기 과정

_IO_new_file_xsputn 함수

#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)

파일에 데이터를 쓰기 위한 함수는 대표적으로 fwrite, fputs가 있다.

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

위 코드를 살펴보면, 해당 함수는 _IO_XSPUTN 함수의 매크로이며 실질적으로 _IO_new_file_xsputn 함수를 실행한다.

이 함수에서는 파일 함수로 전달된 인자인 데이터와 길이를 검사하고 _IO_OVERFLOW, 즉 _IO_new_file_overflow 함수를 호출한다.

실제로 파일에 내용을 쓰는 과정은 _IO_new_file_overflow를 시작으로 다양한 함수가 호출되면서 이뤄진다.

그러면 _IO_new_file_overflow 함수 내부에서 어떻게 파일에 데이터를 쓰는지 보자.

2.2 _IO_new_file_overflow

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)

Figure3는 _IO_new_file_overflow 함수로, 코드를 살펴보면,

_IO_new_file_overflow 함수 내부에서는 파일 포인터의 _flags 변수에 쓰기 권한이 부여되어 있는지를 확인하고

해당 함수의 인자로 전달된 ch가 EOF, 즉 -1이라면 _IO_do_write 함수를 호출한다.

앞서 _IO_new_file_overflow를 호출할 때의 인자를 확인해보면 EOF를 전달하므로,

_IO_do_write 함수가 호출되는데, 전달되는 인자가 파일 구조체의 멤버 변수임을 알 수 있다.

_IO_do_write 함수는 내부적으로 new_do_write 함수를 호출하므로,

해당 함수에서 파일 구조체 내 포인터로 어떻게 파일을 쓰는지 알아보자

2.3 new_do_write

new_do_write 함수

#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;

}

new_do_write는 플래그 검사와 _IO_SYSWRITE 호출 한다.

2.3.1 플래그 검사

파일을 쓰기에 앞서 파일 포인터의 _flags 변수에 _IO_IS_APPENDING 플래그가 포함되어 있는지 확인한다.

2.3.2 _IO_SYSWRITE 호출

new_do_write 함수의 인자인 파일 포인터와 data,

그리고 to_do를 인자로 _IO_SYSWRITE 함수를 호출하는데

이는 곧 vtable의 _IO_new_file_write 함수이다.

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

2.4 _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_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);

Reference

https://dreamhack.io/lecture/courses/273

profile
보안 공부를 하는 학생입니다.

0개의 댓글