【Heap入門編】tcache・fastbinから読み解くglibcヒープ|CTF思考フレームワーク #62
こんにちは、アンペンです!
今回はHeap入門編。スタックBoFはスタック上の戻りアドレスを書き換える単純さでしたが、ヒープ攻撃はそうはいきません。glibc mallocの内部構造(tcache・fastbin・unsorted bin等)を理解した上で、『アロケータが内部で持つリンクリストの先頭ポインタを書き換える』という間接的な手段で、次回のmallocを攻撃者制御下に置くのがゴールです。
名前は色々(tcache poisoning / fastbin dup / unsorted bin attack…)ありますが、本質は共通で『freeリストの行き先を改竄→次のmallocが攻撃者指定アドレスを返す』に集約されます。
ここまでのスタック攻撃は『戻りアドレスを直接書き換える』という、いわば正面からの一撃でした。ヒープ攻撃は、そこから一歩奥に進みます。狙うのは、mallocやfreeが“裏で使っているメモ書き”。それをこっそり書き換えて、『次にメモリをくださいと頼んだら、攻撃者の狙った場所が返ってくる』状態を作るんです。直接ではなく“仕掛けて待つ”——少し頭を使う、でも理屈が分かると面白い領域ですよ。

mallocとfreeに脆弱性なんてあるの?

malloc/free自体じゃなく『アロケータが内部で使うリンクリスト』の管理データを攻撃者が書き換えられる、というのが穴。BoFやUAFでチャンク内のfdポインタを書き換えれば、次のmallocが攻撃者指定アドレスを返す。
glibc mallocは高速化のため、freeしたチャンクをサイズ別の再利用リスト(tcache / fastbin / unsorted bin / small bin / large bin)に保管します。最頻出がtcache(thread-localの先入れ後出しリスト、0x20〜0x410サイズの64クラス×7チャンク)。チャンクのデータ部先頭8バイトに『次のfreeチャンクへのポインタ(fd)』が入る性質を悪用して、fdを攻撃者指定アドレスに書き換え→次回mallocで任意アドレスが返るのが tcache poisoning。守りはglibc 2.32+のSafe-Linking(fdをヒープ上位ビットでXOR保護)で大幅に困難化。
この記事で分かること
- glibcヒープの5層構造とサイズ別の流れ
- チャンクヘッダの構造(prev_size, size, fd/bk)
- tcache poisoningの3ステップ
- Safe-Linking と XOR解除の式
- how2heapで動作確認する手順
📖 はじめてのWebセキュリティ #62|Heap入門編
tcache・fastbinから読み解くglibcヒープ。 シリーズ一覧を見る →
⚠️ 大事なお約束
本番バイナリへのexploitは違法。CTF・自作バイナリ・how2heap等の検証環境のみで確認してください。
glibc mallocの心臓部:5層のbin
glibc(Linuxの標準C library)のmallocは『free済みチャンクをサイズに応じて違うリストに保管し、次回mallocで使い回す』仕組みです。完全に新規確保するより、すでにOSから貰った領域の中で繋ぎ直す方が桁違いに速いため。リストの種類は以下の5層。
- tcache(2017〜): thread-localの単方向リスト。サイズ0x20〜0x410で64クラス、各クラス7チャンクまで。最も高速で、最も狙われる
- fastbin: 共有の単方向リスト。0x20〜0xb0等の小サイズ用
- unsorted bin: 双方向リスト。大きめfreeチャンクの仮置き場
- small bin: サイズ別の双方向リスト(0x20〜0x3f0)
- large bin: 大きいサイズの双方向リスト+サイズ範囲別
『なぜ5種類も棚があるの?』と思いますよね。理由はただ一つ、速さのためです。使い終わったメモリを毎回OSに返していては遅いので、mallocは“サイズ別の再利用棚”に積んでおいて、同じサイズの注文が来たらサッと出す。tcacheはその中でも最速の“手元の棚”です。よく使われるぶん、攻撃者にいちばん狙われる棚でもあります。まずは『tcacheという速い棚が、いちばんの主戦場』とだけ覚えておけば十分です。

チャンクの構造とfdポインタ
mallocが返すアドレスは『チャンクのデータ部』ですが、その8バイト前にヘッダがあります。x64では:
prev_size(8B): 前の物理隣接チャンクが空きの時のみ意味を持つsize(8B): 自分のサイズ+下位3bit(PREV_INUSE/IS_MMAPPED/NON_MAIN_ARENA)(データ部開始)← free中は先頭8BがfdポインタとしてLink先を保存
つまりfree済みチャンクのデータ先頭8B = 次のfreeチャンクへのリンク。UAFやBoFでここを書き換えると、bin(リスト)の頭が攻撃者指定アドレスを指すようになり、後続のmallocがそれを『正規のfreeチャンク』だと信じてユーザに返してしまう、というのが攻撃の幹です。
ここがヒープ攻撃のいちばんの“ひらめきポイント”です。使用中のチャンクには普通のデータが入っていますが、free(解放)した瞬間、その先頭8バイトが『次の空き箱はどこ?』という“住所メモ”に化けます。空いている箱なので、もう中身は要らない——だからアロケータは、その場所を再利用してリンクを書くわけです。攻撃者はこの“住所メモ”を上書きして、アロケータに嘘の場所を教え込む。これが、すべてのヒープ攻撃に共通する幹なんです。
会社の備品庫に『再利用ボックス』があって、使い終わった同じサイズの箱を積んでおくと次の人が手早く取り出せる仕組み。各箱には『次の箱はこの棚の3段目』とラベルが貼ってあります(=fdポインタ)。攻撃者がそのラベルを書き換えて『次は隣の社長室の金庫』にすると、次に来た人は社長室の金庫を『再利用ボックスの箱』だと信じて取り出し、自由に書き換えてしまう。tcache poisoningはまさにこの構造です。
ここで覚える用語:tcache poisoning / Safe-Linking
tcache poisoningは『同サイズチャンクをmalloc→free→UAFでfdを攻撃者指定アドレスに書換→2回mallocで2回目に任意アドレスが返る』典型手筋。__free_hookや_IO_2_1_stdout_を狙えばRCE。Safe-Linking(glibc 2.32+)はfd_obfuscated = (heap_addr >> 12) XOR fd_realとXOR保護する仕組みで、ヒープベースをリークしないとfdの正しい書込ができなくなり、tcache poisoning単独での攻略を大幅に困難化させます。
tcache poisoning 3ステップ
- ①同サイズ2チャンクをmalloc→free:
A=malloc(0x30); B=malloc(0x30); free(B); free(A);tcacheにhead→A→B→NULLという連結が出来る - ②UAFやheap BoFでA.fdを書換:
*(void**)A = target_addrにする。glibc 2.32+ならXOR保護があるためtarget ^ (heap_base >> 12)と仕込む必要 - ③malloc 2回:1回目で
A、2回目でtarget_addrがデータ領域として返る。target_addrが__free_hookなら、そこにsystemを書いて任意プログラムをfreeすればシェル起動
glibc 2.29+はtcache_entry構造体にkeyフィールドが追加されtcache double-freeを検出するため、同じチャンクを2回freeで自己循環を作る古典は失敗します。古いglibcバージョンのバイナリだと通る場合があるので、まずバージョン確認が定石。
ここで実戦的なコツを一つ。同じヒープ攻撃でも、『相手のglibcが何バージョンか』で通る・通らないが大きく変わります。新しいバージョンほど、double-free検出やSafe-Linkingといった“見張り”が増えているからです。だからCTFでヒープ問題に出会ったら、まず ldd --version やバイナリ同梱のlibcでバージョンを確認するのが鉄則。『どの時代の防御まで備わっているか』を知ってから、使う手筋を選ぶわけです。


CTFでやってみよう:how2heapで実機体験
shellphish/how2heapで自分のglibcに合うサンプルを実行
目的は『tcacheのfd書換で次のmallocが嘘の場所に行く』を実際にprintf出力で確認することです。
git clone https://github.com/shellphish/how2heap.git- 自分のglibcバージョンを確認:
ldd --version→ 例) 2.35 - 対応するフォルダ(
glibc_2.35/)に移動 make tcache_poisoningでビルド、./tcache_poisoningで実行- 出力で『stack上の変数 stack_var をmallocが返してきた!』というメッセージを確認
- gdb+pwndbgで
heap chunks/binsコマンドを実行し、tcacheの中身を眺める - 余裕があれば
house_of_einherjar / safe_link等の最新版も触る
守る側:ヒープ攻撃の現代的防御
- glibcは2.32以上を使う(Safe-Linkingでfd改竄に強い)
- glibc 2.29+ のtcache double-free検出を活かす(古いバージョン継続使用を避ける)
- glibc 2.34+ は
__free_hook/__malloc_hookが削除済み→hook経由のRCE経路が消える - 開発・CIでAddressSanitizer(
-fsanitize=address)を回し、UAF/Double Free/Heap-BoFを即検出 - libFuzzer / AFL++でカバレッジ誘導型Fuzzingを回し、未知のヒープバグを早期発見
- 本番ではhardened allocator(
Scudo/jemallocのセキュア設定 /mimalloc-secure)を採用 - 新規プロジェクトはRust/Goで原理的にUAF/Double Freeを排除

『リサイクルボックスのラベル書換』ってよくできてるね。

次回はUAF・Double Free。具体的に世界を壊す技術を扱うよ。
まとめ:『freeリストのfdを書き換えると世界が変わる』
- glibcヒープはtcache/fastbin/unsorted/small/largeの5層構造
- free済みチャンクのデータ先頭8B=fdポインタ(次のfreeチャンクへのリンク)
- fd書換→次のmallocで攻撃者指定アドレスが返る=tcache poisoning
- glibc 2.32+はSafe-Linking(XOR)で対策、2.34+は
__free_hook削除 - 守りは新glibc+ASan+Fuzz+hardened allocator+メモリ安全言語
今日の持ち帰りは『ヒープ攻撃は“箱そのもの”ではなく“箱の管理メモ”を狙う』。free済みチャンクのfdポインタという一点を書き換えるだけで、その後のmallocを丸ごと攻撃者の手中に置けてしまう。スタックより一段抽象的ですが、“リサイクルボックスのラベル書換”と思えば腑に落ちるはずです。守りは『新しいglibc+メモリ安全言語』が王道になります。
次回はHeap応用編・UAF/Double Free。具体的な実装ミスから攻撃を組み立て、vtable hijackでRCEに繋げる経路を扱います。
