Use-After-Free とヒープ悪用の原理
解放したはずのメモリがなぜ乗っ取りに化けるのか。割当器の自由リスト再利用からヒープグルーミング、tcache/fastbin の悪用、MTE による根治までを図で押さえられる。
- 1.Use-After-Free(UAF)は、解放済みオブジェクトを指すダングリングポインタ経由でメモリを読み書きする攻撃。割当器が同じ領域を別オブジェクトに再利用した後に古いポインタを使うと、型混同や関数ポインタ・vtable の書き換えで制御を奪える。
- 2.成立には「解放後にその領域を攻撃者が制御するデータで埋める」必要があり、ヒープグルーミング(Heap Feng Shui)で割当器の自由リストを誘導する。glibc では tcache/fastbin の単方向リストが格好の標的で、ダブルフリーから任意アドレス確保へつなぐ。
- 3.緩和は確率的な GWP-ASan やクォランティン(解放後の再利用を遅らせる)と、決定的な Arm MTE(割当ごとのタグ照合で解放後アクセスを検出)。根治はメモリ安全な言語または所有権モデルへの移行。
なぜ「解放済み」を使うと乗っ取りになるのか
Use-After-Free(UAF, CWE-416)の核心は、メモリの所有権が free で割当器に返った後も、それを指すポインタがコード側に生き残る点にあります。C/C++ には「解放した瞬間にそのポインタを無効化する」仕組みがありません。free(p) は領域を割当器の自由リストに戻すだけで、変数 p の値(アドレス)はそのまま残ります。この宙ぶらりんのポインタをダングリングポインタと呼びます。
問題は、解放された領域がいつまでも空き地のままではないことです。割当器は効率のため、解放された領域を次の malloc 要求に優先して再利用します。つまり解放から再利用までの間に、まったく別のオブジェクトが同じアドレスに置かれ得ます。ここでダングリングポインタを使うと、ポインタが思っている型(A)と、実際にそこにあるオブジェクト(B)が食い違う――これが型混同(type confusion)です。
解説は教育・防御目的です。攻撃手順そのものではなく、なぜ成立し、どの緩和がどこを塞ぐかという原理に絞ります。検証は許可された環境でのみ行うべきで、無断の実行はペネトレーションテストで触れたとおり法的責任を伴います。本記事はメモリ破壊攻撃の原理で扱ったスタック系の攻撃に対する、ヒープ側の対応物です。
C++ では、オブジェクトの先頭に仮想関数テーブル(vtable)へのポインタが置かれることが多く、これが UAF を強力にします。解放済みオブジェクトのメソッドを呼ぶコードが残っていると、攻撃者が再利用領域に偽の vtable ポインタを仕込めば、仮想呼び出しの瞬間に攻撃者の指定したアドレスへ制御が飛びます。
時系列で見る UAF
1. p = new Widget(); // p は Widget を指す。先頭に vtable ポインタ
2. free(p); // 領域は自由リストへ。だが p の値は残る(ダングリング)
3. q = malloc(同サイズ); // 割当器が同じ領域を再利用 → q == p
攻撃者が q に偽 vtable ポインタを書き込む
4. p->method(); // p が指す先は今や攻撃者制御 → 偽 vtable 経由で乗っ取り
割当器の自由リストという攻撃面
UAF を制御に変えるには「解放後、その領域を攻撃者が選んだ内容で埋める」必要があり、そのためには割当器がどの順序で領域を再利用するかを理解しなければなりません。Linux の標準である glibc の ptmalloc を例に取ります。
ptmalloc は解放されたチャンクをサイズ帯ごとの**ビン(bin)**に分類して管理します。とりわけ小さく、頻繁に使われる帯が攻撃者に好まれます。
| ビン | 構造 | 再利用順 | 攻撃上の特徴 |
|---|---|---|---|
| tcache | スレッドローカルの単方向リスト(既定で各サイズ7個まで) | LIFO(後入れ先出し) | ロックを取らず最優先で再利用。next ポインタがチャンク内に平文で並ぶ最重要標的 |
| fastbin | グローバルな単方向リスト(小サイズ) | LIFO | 隣接チャンクと併合されないため解放状態を保ちやすい。tcache 満杯時にここへ |
| unsorted / small / large | 双方向リスト | 概ね FIFO・サイズ整合 | fd/bk の2ポインタ。Unlink 時の整合性チェックが厳しい |
tcache と fastbin が標的になる理由は、自由リストの「次」を指すポインタ(fd / next)がチャンク本体の中に直接置かれることです。解放されたチャンクは、ユーザーデータ領域の先頭8バイトに「次の空きチャンクのアドレス」を書き込みます。もし攻撃者が UAF や溢れでこの8バイトを書き換えられれば、割当器が次に返すアドレスを偽装できます。
tcache の単方向リスト(LIFO)。head から next をたどる
head ──▶ [ chunk C | next=B ] ──▶ [ chunk B | next=A ] ──▶ [ chunk A | next=0 ]
▲ ここを攻撃者が書き換えると…
head ──▶ [ chunk C | next=偽アドレス ] ──▶ (攻撃者の選んだ任意番地)
→ 次々の malloc が「偽アドレス」をチャンクとして返す(任意アドレス確保)
ヒープグルーミング:割当器を意図どおりに並べる
UAF が成立しても、解放された直後に攻撃者の狙ったデータでその領域が埋まる保証はありません。再利用のタイミングと内容を攻撃者が支配するために行うのがヒープグルーミング(別名 Heap Feng Shui/ヒープフェンシュイ)です。割当器の決定的な振る舞いを逆手に取り、ヒープのレイアウトを「整地」します。
典型的な手順は次の発想で組み立てます。
- スプレー(heap spray):攻撃者が制御するデータ(偽 vtable やシェルコード相当)を大量に
mallocしてヒープを敷き詰め、どこへ着地しても攻撃者データに当たる確率を上げる。 - 穴あけ(grooming):狙ったサイズのオブジェクトを多数確保してから一つおきに解放し、自由リストに「型 A 用の穴」を意図的に作る。次に別の型 B を同サイズで確保すると、割当器の LIFO 特性によりちょうどその穴に B が入る。
- 再利用の誘導:解放したオブジェクトと同じサイズ帯のオブジェクトを確保することが鍵。割当器はサイズ帯ごとにビンを分けるため、サイズが違えば同じ領域は返ってこない。攻撃者は標的構造体と同サイズになる「都合のよいオブジェクト」(長さ可変の文字列やバッファなど、内容を攻撃者が決められるもの)を探す。
グルーミングで「型混同」を仕込む
確保: A A A A A A (標的と同サイズのオブジェクトで埋める)
解放: A . A . A . (一つおきに解放 → 自由リストに穴が並ぶ)
確保: A B A B A B (内容操作可能な B を確保 → LIFO で穴に B が入る)
→ ダングリング A* が指す先には B のデータ。A としてアクセスすれば型混同
ブラウザのような複雑なソフトでは、JavaScript から ArrayBuffer や文字列の確保・解放を呼べるため、攻撃者はスクリプト経由でヒープを精密に操作できます。グルーミングが「フェンシュイ(風水)」と呼ばれるのは、まさにオブジェクトの配置を整える術だからです。
ダブルフリーから任意アドレス確保へ
UAF と並ぶヒープ系の代表がダブルフリー(double free, CWE-415)――同じポインタを2回 free する欠陥です。これは UAF と表裏一体で、解放済み領域を「もう一度解放対象として扱う」ことで自由リストの構造を壊します。
単方向リストでダブルフリーが致命的になる流れを、tcache を例に追います。素朴な実装では free(p); free(p); とすると、同じチャンクが自由リストに2回現れ、リストが自己ループを作ります。すると malloc で同じアドレスが2回返り、1つの領域を2つの型として同時に保持できる――UAF と同じ型混同が、より能動的に実現します。
ダブルフリーで自由リストを汚染(概念)
free(p) ×2 → head ─▶ [ p | next=head_old ] ─▶ ... ─▶ [ p ] (p がリストに二重登録)
malloc() → p を返す。だが p はまだリスト上にも残る
返ってきた p の next 欄に「偽の目標アドレス」を書く
malloc() ×2 → 2回目で「偽の目標アドレス」がチャンクとして返る
→ GOT エントリや関数ポインタの番地を確保し、書き換えて制御奪取へ
近年の glibc は反撃を入れています。tcache の各チャンクには鍵(key)フィールドが置かれ、解放時に「既にこの tcache に入っているか」を照合して素朴なダブルフリーを検知します。fastbin にも「先頭が自分自身か」の二重解放チェックがあり、確保時には返すチャンクのサイズ帯がビンと一致するかを検証します。さらに glibc 2.32 以降は Safe-Linking:next ポインタを「(リスト位置のアドレス >> 12) XOR 本来の値」で難読化し、ヒープのアドレスを知らないと有効な偽ポインタを書けなくしました。いずれも攻撃を高コスト化しますが、ヒープアドレスの情報漏洩や、key を回避する解放順序の工夫で迂回され得ます。割当器のチェックは根治ではなく障害物です。
これらの操作が最終的に目指すのは、**任意アドレスへの書き込み(write-what-where)**です。割当器に「攻撃者の選んだ番地をチャンクとして返させる」ことができれば、そこを通常の malloc 戻り値として書き込めます。標的は GOT エントリ(解決済み関数のアドレス表)、__free_hook のような関数ポインタ、あるいは別オブジェクトの vtable ポインタです。書き換えた関数ポインタが呼ばれた瞬間、制御は攻撃者の手に渡ります。発想はメモリ破壊攻撃の戻りアドレス書き換えと同じ「制御フローを決める値を奪う」ことであり、舞台がスタックからヒープと割当器メタデータへ移っただけです。
UAF の緩和:確率的検出と決定的タグ付け
緩和は大きく「解放後の再利用を遅らせる/検出する」アプローチと、「ポインタとメモリにタグを付けて照合する」アプローチに分かれます。
クォランティン(quarantine)と隔離アロケータ。 解放されたチャンクをすぐ自由リストに戻さず、一定量たまるまで**隔離(quarantine)**して再利用を遅延させます。これにより「解放直後にグルーミングで埋める」窓が狭まります。さらに進んだ設計が type-isolating allocator(PartitionAlloc など、各型・サイズを別アリーナに隔離)で、異なる型を同じ領域に重ねさせないことで型混同そのものを起こりにくくします。確率的・統計的な防御である点に注意が必要です。
GWP-ASan。 AddressSanitizer(ASan)はメモリエラーを確実に捕らえますが、シャドウメモリと重い計装で本番環境では遅すぎます。GWP-ASan は、ごく一部の割当だけをガードページ付きの専用ページに載せ、解放後はそのページをアクセス不能(PROT_NONE)にマップします。ダングリングポインタでそこへ触れると即座にセグメンテーション違反が起き、UAF を現場で検出できます。サンプリング率を低く保つためオーバーヘッドはごく小さく、多数のユーザーに配ることで統計的に UAF を炙り出す――個々の実行ではなく母集団で効かせる発想です。
Arm MTE(Memory Tagging Extension)。 最も決定的なハード緩和です。Arm v8.5 以降の MTE は、メモリを16バイト粒度の「タグ付きグラニュール」に分け、ポインタの上位ビットにも4ビットのタグを埋め込みます。割当時にメモリ領域とポインタへ同じタグを振り、アクセスのたびにポインタのタグとメモリのタグを照合します。
MTE の照合(概念)
malloc → 領域に tag=5 を塗り、返すポインタにも tag=5 を埋める
free → その領域を別タグ(例 tag=9)で塗り替える(再タグ付け)
古いポインタ(tag=5 のまま)で解放後アクセス
→ メモリは tag=9、ポインタは tag=5 → 不一致 → ハードがフォルト
→ Use-After-Free を「メモリの色違い」として決定的に検出
タグは4ビット(16通り)なので、偶然タグが一致してすり抜ける確率は約1/16残ります。完全な確定検出ではありませんが、確率的緩和より桁違いに強く、しかもソフト計装より遥かに軽量(ハード支援)です。同様の隣接バッファ溢れ(線形オーバーフロー)も、隣接領域が別タグになるため捕捉できます。
| 緩和 | 種別 | 塞ぐもの | 限界 |
|---|---|---|---|
| 割当器の整合性チェック(tcache key 等) | 決定的(限定的) | 素朴なダブルフリー、サイズ不整合 | 情報漏洩や解放順序の工夫で迂回 |
| Safe-Linking | 決定的(限定的) | ヒープアドレス未知での next 偽装 | ヒープアドレスのリークで失効 |
| クォランティン / 型隔離アロケータ | 確率的・統計的 | 解放直後の即時再利用・型混同 | 窓を狭めるだけで根絶しない |
| GWP-ASan | 確率的(サンプリング) | 本番での UAF / 溢れの検出 | 低サンプリング率ゆえ単発は見逃す |
| Arm MTE | ほぼ決定的(ハード) | 解放後アクセス・隣接溢れ全般 | タグ一致で約1/16の取りこぼし、対応 CPU 必須 |
(1) UAF の本質は解放後も生きるダングリングポインタによる型混同で、vtable や関数ポインタ書き換えで制御を奪う。(2) 成立には解放領域を攻撃者データで再利用させる必要があり、glibc では tcache/fastbin の単方向リストが標的。(3) ダブルフリーは自由リストを汚染して**任意アドレス確保(write-what-where)**へつなぐ。(4) glibc の tcache key・Safe-Linking は障害物だが情報漏洩で迂回され得る。(5) 緩和は確率的な GWP-ASan・クォランティンと、ほぼ決定的な Arm MTE(タグ照合、約1/16で取りこぼし)。根治はメモリ安全な実装。
実務での向き合い方:根治は所有権モデル
割当器のチェックも MTE も「攻撃を起こりにくく・検出しやすく」するものであり、UAF が発生し得る言語を使い続ける限り、可能性そのものはゼロになりません。根治の方向は、解放と参照の関係をコンパイル時に管理することです。
- メモリ安全な言語へ移す:Rust の所有権と借用検査は、解放後に生きる参照をコンパイル時に拒否します。GC を持つ言語(Go, Java 等)はそもそも解放を手動で行わないため UAF が原理的に起きません。新規・刷新部分から段階的に置き換えるのが現実的です。
- C++ では生ポインタ所有を排す:所有権を
unique_ptr/shared_ptrで表現し、weak_ptrで「他者が解放したら無効化される」参照を作る。生ポインタを所有者として使わないだけで UAF の温床が大きく減ります。 - 開発段階で計装検出を回す:AddressSanitizer と fuzzing を CI に組み込み、UAF・ダブルフリーを実行時に捕らえる。本番では GWP-ASan を低サンプリングで常時稼働させ、対応プラットフォームでは MTE を有効化します。
最後に権限設計です。万一 UAF から制御を奪われても、プロセスを最小権限の原則で動かしていれば攻撃者が得る能力を狭められます。サンドボックス(プロセス分離)も、ヒープ悪用が成立した後の被害を封じ込める一線として有効です。UAF が教えるのは、「いつ解放したか」と「誰がまだ参照しているか」を人手で正しく追い切るのは破綻しやすいということ――所有権を言語に管理させることが、最も確実な防御です。
セキュリティ Article
Use-After-Free とヒープ悪用の原理を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Use-After-Free
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 6
導入後に効く点
成立には「解放後にその領域を攻撃者が制御するデータで埋める」必要があり、ヒープグルーミング(Heap Feng Shui)で割当器の自由リストを誘導する。glibc では tcache/fastbin の単方向リストが格好の標的で、ダブルフリーから任意アドレス確保へつなぐ。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Use-After-Free / ヒープ」に近いか確認する。
- 強みである「Use-After-Free(UAF)は、解放済みオブジェクトを指すダングリングポインタ経由でメモリを読み書きする攻撃。割当器が同じ領域を別オブジェクトに再利用した後に古いポインタを使うと、型混同や関数ポインタ・vtable の書き換えで制御を奪える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。