【Heap応用編】Use-After-Free・Double Freeで世界を壊す|CTF思考フレームワーク #63
こんにちは、アンペンです!
今回はHeap応用編・Use-After-Free(UAF) と Double Free。実世界のCVEで頻出する2大ヒープバグです。ChromiumやFirefoxのRCE、Linuxカーネル特権昇格の多くがこの2種類に分類されます。tcache poisoningの『仕掛け作り』を引き起こす引き金として、両者の実装ミスを実コードレベルで理解します。
共通の本質は『すでにアロケータが管理を奪い返した領域を、アプリケーションがまだ自分のもののように扱う』こと。アプリとアロケータの『所有権』がズレた瞬間に攻撃が成立します。
ちょっと抽象的なので、たとえで掴んでおきましょう。メモリの“所有権”は、本来アプリとアロケータ(malloc/freeの管理人)の間でキャッチボールされています。freeした瞬間、その領域の所有権は管理人に返る——なのにアプリが『まだ自分のものだ』と勘違いして触り続ける。この“持ち主の食い違い”こそ、UAFもDouble Freeも共通の正体です。要は『返したはずのものを、まだ握っている』ミスなんですね。

『使用後の解放』とか『二重解放』って、そんなに致命的なの?

致命的。UAFは古いポインタが指す領域がC++のオブジェクトとして再利用されると、vtable(仮想関数テーブル)を攻撃者が差し替えて関数呼び出しを乗っ取れる。Double Freeは前回扱ったtcacheリストを循環させて任意mallocに繋げる。どちらもRCE直結だよ。
UAFは『freeしたポインタを使い続ける』バグで、同サイズのアロケートが間に挟まると古いポインタ経由で別オブジェクトを書き換えられます。C++ではvtable書換→任意メソッド呼出という典型経路でRCEに直結。Double Freeは同じチャンクを2回freeすると、tcache連結が同チャンクへの自己循環になり、続くmallocを攻撃者制御下に置けます。glibc 2.29+はtcache_entry.keyでdouble-freeを即abortしますが、UAF経由でkeyフィールドごと潰せば回避可能。守りは『free直後に必ずNULL代入+AddressSanitizer+所有権モデル(Rust)』。
この記事で分かること
- UAFの時系列と『なぜvtableが書き換わるか』の仕組み
- vtable hijackからRCEまでの道筋
- Double Freeで作る自己循環の3ステップ
- glibc 2.29+のkey検出と回避法
- ASan / Rust / MiraclePtr による根本対策
📖 はじめてのWebセキュリティ #63|Heap応用編
UAF・Double Freeで世界を壊す手筋を扱います。 シリーズ一覧を見る →
⚠️ 大事なお約束
本番システム・他者バイナリへのexploitは違法。CTF・自作バイナリ・how2heap等の検証環境のみで確認してください。
UAFの時系列:4ステップで成立する
UAFは下記4ステップで成立します。プログラマが『free後にポインタを使わない』ルールを守れば起きませんが、特にコールバック・参照カウント・グローバル変数経由でポインタが残り、本人すら気づかないことが多いバグです。
- ①malloc(A):オブジェクトAを確保、コードはポインタ
pAを保持 - ②free(A):Aはtcacheに戻されるが、
pAはそのまま残る(dangling pointer) - ③malloc(B):偶然(または攻撃者誘導で)同サイズBを確保→同じアドレスがBに割り当てられ、
pBも同所を指す - ④pA経由で書込or呼出:アプリは『古いA』のつもりで操作、しかし実体はB。Bが関数ポインタ/vtableを持つなら、そこを攻撃者が書いた値で呼び出してRCEへ
この4ステップで一番こわいのは、③と④が“たまたま”起きてしまう点です。free後のポインタが残っているだけなら、運が良ければ何事もなく動いてしまう。でも、攻撃者が『同じサイズの確保』を意図的に挟み込むと、解放済みの場所に自分の用意したデータを“すべり込ませ”られる。つまりUAFは、タイミングを攻撃者が握った瞬間に、ただのバグから一気に乗っ取り手段へと化けるわけです。

賃貸を解約した(=free)のに、コピーした古い鍵をまだ持ち歩いている人がいる。次の入居者(=新しいオブジェクトB)が入った後、その鍵で勝手にドアを開けて部屋に侵入し、家の中の機械(=vtable)の配線を入れ替える。次に入居者が『いつもの便利スイッチ』を押すと、別の場所に通報が飛ぶ──UAFはまさにこの構造。dangling pointer(古い鍵)を残さないことが第一防御です。
ここで覚える用語:vtable hijack
C++クラスのインスタンスは先頭8バイトにvtable(仮想関数テーブル)へのポインタを持ちます。obj->method()を呼ぶと、コンパイラは『vtableを参照してN番目の関数を呼ぶ』コードを生成。UAFで同サイズの『攻撃者制御文字列バッファ』が同アドレスに再アロケートされたら、先頭8バイトに偽のvtableポインタを書く→次のobj->method()呼出が偽vtable内の攻撃者指定関数を呼ぶ、という流れでRCE成立。Chromium/Firefox/JavaScriptエンジン関連のRCEはほぼこの形式。
Double Freeで作る自己循環tcache
Double Freeは同じチャンクを2回freeすると、tcacheに head → X → X → ... という自己循環が出来てしまうバグ。普通に同サイズを連続mallocすると、毎回同じXが返ってくる状態になります。これを使うとtcache poisoningよりさらに少ない手数で任意malloc誘導が可能。
Double Freeの不気味さは、『同じ箱を2回“返却済み”リストに積む』と、リストが自分自身をぐるぐる指す“蛇が尻尾を食う”ような状態になることです。こうなると、次に箱をくれと頼むたびに、毎回同じ箱が返ってくる。同じ箱を2つの用途で同時に使えてしまうので、片方を書き換えればもう片方も化ける。tcache poisoningより少ない手数で、任意の場所をmallocさせる足がかりになるわけです。
- ①X=malloc(0x30); free(X); free(X); tcacheは
head→X→X→… - ②malloc()でXを回収 tcache headはまだX(2回目)
- ③X.fdに偽アドレスtargetを書く tcacheは
head→X→target(=Xを再利用しfdを上書き) - ④もう一度malloc()でtargetが返る 任意書込プリミティブ獲得

glibc 2.29+ は tcache_entry 構造体にkeyフィールド(tcache_perthread_structへのポインタ)が追加され、freeする時に『すでに同じkeyが設定されていればabort』で双重解放を即検出します。回避するにはUAF経由でkeyを別値で潰す手順を挟む必要があります。
ここで守りの基本が効いてきます。実は『freeしたら、すぐそのポインタにNULLを入れる』——たったこれだけで、Double Freeの多くは無害化できるんです。NULLは何回freeしても何も起きないからです。古い鍵を持ち歩かず、その場で捨てる。地味ですが、これがUAF・Double Free対策の“いの一番”。だからこそ、後半の対策リストでも真っ先に挙がっています。

CTFでやってみよう:UAFでvtable hijack
関数ポインタ付き構造体を再利用してシェル奪取
目的は『UAFが起きた瞬間、攻撃者が次のmallocサイズを制御できれば任意関数呼出に化ける』の流れを実機で見ることです。
- C言語で
struct Obj { void (*action)(); char name[24]; };を持つメニュー型バイナリを作成(create/free/use の3操作) checksecでnoPIE / Partial RELROを確認(検証を簡単にするため)- obj=create() でAを生成→free(obj)でtcacheへ→obj_ptrはdanglingのまま
- 同サイズ(0x20)で
name_buf=create_string()を呼び、先頭8バイトに p64(system_addr) を書く文字列を投入 - 古いobj_ptr経由で
obj->action()を呼ぶ → 実体はname_buf先頭のsystem関数アドレスが呼ばれる - 引数RDIに事前に”/bin/sh”のアドレスを仕込んでおく(ROPと同様)→シェル獲得
- 余裕があれば、how2heapの
tcache_house_of_spirit等の現代版に挑戦
ここまで攻撃の形を見てきましたが、UAFもDouble Freeも、根っこは同じ『所有権のズレ』でした。ということは、守り方も自ずと決まります——“返したものは二度と触らない”を、仕組みで強制すればいい。次の道具立ては、その強制をどの層で効かせるか、という視点で並んでいます。
守る側:UAF/Double Freeを仕留める道具立て
- すべての
free直後にNULL代入を必須化(free(p); p = NULL;) ──二重freeをNULL書込で無害化 - 所有権モデルがある言語へ移行:Rust(コンパイラがUAF/Double Freeを構文的に拒否)・Swift(ARC)
- 開発・CIでAddressSanitizer(
-fsanitize=address)を全実行に適用。UAF/double-free検出率は実質100% - libFuzzer / AFL++でカバレッジ誘導Fuzzingを24h以上回し、未知のヒープバグを早期発見
- 大規模C++ではMiraclePtr / BackupRefPtr(Chromium)等のスコープド・スマートポインタ
- 本番にはHardened allocator(
Scudo/mimalloc-secure/GWP-ASan)を採用、確率的検出 - glibcは2.29以上を維持。
__free_hook/__malloc_hookを必要としない設計に

『退去後の鍵を持ち続けない=free後にNULL』、基本中の基本だね。

そう。次回はHouseシリーズ。House of Force / Spirit など、ヒープ攻撃の流派を整理するよ。
まとめ:『古いポインタは捨てる、二重解放は厳禁』
- UAFは4ステップ(malloc→free→再malloc→旧ポインタ使用)で成立
- C++ではvtable書換でRCEに直結。実CVEで超頻出
- Double Freeはtcache自己循環→3〜4回mallocで任意アドレス
- glibc 2.29+の
key検出は強力だが、UAFと組合せれば回避可能 - 守りはfree後NULL+ASan+Rust+Hardened allocatorの多層
今日の持ち帰りは『返したメモリは、もう他人のもの』。freeした瞬間に所有権は手を離れている——その一線を越えて触り続けることが、最悪RCEにまで化けるんです。だからこそ“free後はNULL、できればRustで所有権を言語に任せる”。攻撃の派手さに対して、守りの一手はとてもシンプルです。
次回はHouseシリーズ編。House of Force / Spirit / Lore / Orange など、glibc仕様の隙を突く古典的なヒープ乗っ取り技を整理します。
