what is _IO_FILE Arbitrary Address Write

https://dreamhack.io/wargame/challenges/367

해당 문제를 풀면서 배운 점을 다룬다.

위글을 읽고 오면 편하다.

문제를 풀면서 배운점이다.

C
int main() {
  FILE *fp;

  char file_buf[1024];

  init();

  fp = fopen("/etc/issue", "r");

  printf("Data: ");

  read(0, fp, 300);

  fread(file_buf, 1, sizeof(file_buf)-1, fp);

  printf("%s", file_buf);

  if( overwrite_me == 0xDEADBEEF) 
    read_flag();

  fclose(fp);
}

여기서 보면 read(0, fp, 300); 으로 파일 구조체를 덮어쓸 수 있고

fread(file_buf, 1, sizeof(file_buf)-1, fp); 코드로 해당 파일을 사용하게 된다.

fread 는 vtable을 사용하는 대표적인 함수이다. 따라서 vtable overwrite도 가능하겠지만, 이번에는 문제의 의도답게 vtable 내에 함수에서 일어날 수 있는 취약점을 분석해보겠다.

C
pwndbg> bt
#0  __GI__IO_file_read (fp=0x602260, buf=0x602490, size=4096) at fileops.c:1153
#1  0x00007ffff7a6f148 in _IO_new_file_underflow (fp=0x602260) at fileops.c:531
#2  0x00007ffff7a6d748 in __GI__IO_file_xsgetn (fp=0x602260, data=<optimized out>, n=1023) at fileops.c:1344
#3  0x00007ffff7a613c1 in __GI__IO_fread (buf=<optimized out>, size=1, count=1023, fp=0x602260) at iofread.c:38
#4  0x00000000004008b6 in main ()
#5  0x00007ffff7a03b97 in __libc_start_main (main=0x40082b <main>, argc=1, argv=0x7fffffffe668, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe658) at ../csu/libc-start.c:310
#6  0x00000000004006ca in _start ()
pwndbg> 

main → fread → __GI__IO_fread → __GI__IO_file_xsgetn 으로 vtable 함수인 __GI__IO_file_xsgetn 이 실행되었음을 알 수 있다.

__GI__IO_file_xsgetn 코드에서

C
	  if (fp->_IO_buf_base
	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
	    {
	      if (__underflow (fp) == EOF)
		break;

	      continue;
	    }

다음과 같이 내부 버퍼가 비면 새로운 데이터를 읽어 오기 위해 _IO_new_file_underflow 을 vtable을 통해 실행하게 된다.

_IO_new_file_underflow 함수에는

C
 count = _IO_SYSREAD (fp, fp->_IO_buf_base,
		       fp->_IO_buf_end - fp->_IO_buf_base);

다음과 같은 부분이 있는데

여기서 이미 정리한것처럼 read함수를 실행한다. 즉, 여기에서 내부적으로

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

다음과 같은 함수를 실행하게 된다. 어 근데 인자가 fp->_IO_buf_base.가 buf로

fp->_IO_buf_end - fp->_IO_buf_base 가 size 으로 들어가게 된다. 즉, 내가 _IO_FILE 구조체를 덮어써서 해당 구조체 필드 값들을 조작하면 buf와 size를 조작해서 특정 위치에 내가 원하는 값을 쓰는

Arditary Writable 공격이 가능하게 된다.

‣https://elixir.bootlin.com/glibc/glibc-2.27/source/libio/bits/libio.h#L245

참고로 덮어써야할 _IO_FILE 구조체는

C
struct _IO_FILE {
  int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;	/* Current read pointer */
  char* _IO_read_end;	/* End of get area. */
  char* _IO_read_base;	/* Start of putback+get area. */
  char* _IO_write_base;	/* Start of put area. */
  char* _IO_write_ptr;	/* Current put pointer. */
  char* _IO_write_end;	/* End of put area. */
  char* _IO_buf_base;	/* Start of reserve area. */
  char* _IO_buf_end;	/* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;

..

다음과 같다 저 자세한 건 위에 링크

참고로

추가 내용 ) 문제를 풀다보니 fileops.c:1313

에 있는

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

while 문에 무한으로 갇히는 문제가 있었다. 해당 문제를 해결하기 위해 억지로 overwrite 크기를 want 보다 크게 해서. if (want <= have) 을 만족 시켜 want = 0; 코드를 실행시켜야 한다.