Dirty COW(CVE-2016-5195)
9年潜んだLinuxカーネルの競合バグから、コピーオンライトのTOCTOUレース、読み取り専用ファイルを書き換える権限昇格、そして並行性バグの怖さを一気に学べる。
- LinuxのCOW処理に潜む競合状態。読み取り専用マッピングへの書き込みフォールトとmadvise(MADV_DONTNEED)を並走させ、本来書けないファイルのページキャッシュへ直接書き込む。
- CVSS 7.0のローカル権限昇格で、一般ユーザーが/etc/passwdやsetuidバイナリなどroot所有ファイルを書き換えられる。Linux 2.6.22から約9年間潜伏した。
- 修正は FOLL_COW フラグの導入。書き込み要求つき再フォールトが本物のCOWコピーを経たかを can_follow_write_pte で検証し、コピー済みページにだけ書き込みを許すようにした。
何が起きたか(影響範囲・深刻度)
Dirty COW(CVE-2016-5195)は、2016年10月にPhil Oesterが報告したLinuxカーネルのローカル権限昇格脆弱性です。カーネルのメモリ管理に含まれるコピーオンライト(COW)処理の競合状態が原因で、権限のない一般ユーザーが、自分には書き込み権限のない root 所有の読み取り専用ファイルを書き換えられます。攻撃対象は /etc/passwd や setuid バイナリのように、書き換えれば即座に root を奪えるファイルです。
深刻度はNVDのCVSS v3.1で基本値7.0(AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)。攻撃はローカルかつ低い権限で成立し、機密性・完全性・可用性すべてに高い影響が及びます。攻撃複雑度が「高(AC:H)」なのは、成立にレース窓を踏み抜く必要があるためです(なお実際には成功率が高く、この評点は体感的な容易さより保守的です)。問題のコードはカーネル2.6.22(2007年)から約9年にわたりメインラインに存在し、Linus Torvalds 本人が2005年に一度対処を試みて元に戻していた「古くて厄介な」バグでした。名前とロゴが付いた最初期の脆弱性の一つで、Android を含むほぼ全ての Linux 派生環境が影響を受けました。
脆弱性の原理(なぜ起きるか)
前提を整理します。ファイルを mmap で MAP_PRIVATE かつ読み取り専用にマップすると、その仮想ページは page cache 上の実ページ(ファイルの本体)を読み取り専用で指します。プライベートマッピングへ書き込むと、カーネルはCOWで新しい匿名ページへ内容を複製し、以後の書き込みは複製側にだけ反映されます。ファイル本体は守られる、というのが設計上の保証です。
一般ユーザーが読み取り専用ファイルへ書き込む正規の裏口が /proc/self/mem 経由の書き込み(や ptrace)です。これはカーネル内で get_user_pages() を FOLL_WRITE | FOLL_FORCE 付きで呼び、対象ページを取得してからカーネル空間でコピーします。書き込み要求付きでプライベート読み取り専用ページを触るとCOWフォールトが起き、匿名コピーが作られる—ここまでは正常です。
競合は follow_page の再試行ロジックにありました。おおよそ次の流れです。
1回目: ページを辿る → 書けない読み取り専用ページ → COWフォールトを起こす
→ 匿名コピーを作成し、書き込み可能にする
2回目: 再びページを辿り直す(retry)
→ 今度は書き込み可能なコピーが取れる想定
→ そこへ書き込む
問題は「1回目」と「2回目」の間に別スレッドが割り込める点です。攻撃者はもう一方のスレッドで同じ領域に madvise(addr, len, MADV_DONTNEED) を連打します。MADV_DONTNEED は当該範囲のページテーブルマッピングを破棄し、次アクセス時に元ページを再取得させます。つまりCOWで作ったはずの匿名コピーへの参照が「2回目」の直前に消され、辿り直すと再びファイル本体の page cache ページが返ります。
さらに再試行時、カーネルは無限ループを避けるため書き込み要求(FOLL_WRITE)を実質的に取り下げ、「読み取りとしてなら辿れる」経路でページを取得していました。結果として、書き込み可能なコピーではなく、書き込み不可のはずのファイル本体ページのアドレスが書き込み処理へ渡り、page cache が直接改変されます。これが典型的なTOCTOU(time-of-check to time-of-use)レースです。COWの判定(check)と実際の書き込み(use)の間に、MADV_DONTNEED で状態をすり替える窓が空いていました。
書き込み先は page cache(メモリ上のファイル像)であってディスクの物理ブロックではありません。マッピングは読み取り専用のため dirty フラグは立たず、多くの場合ディスクへは書き戻されません。それでも稼働中カーネルにとっての「そのファイルの中身」はメモリ上の page cache であり、そこを書き換えれば setuid 実行や認証は改変後の内容で処理されます。
エクスプロイトの流れ
横にスクロール
概念レベルの手順は次のとおりです(そのまま動く攻撃コードは示しません)。
| 段階 | 攻撃者の操作 | カーネル側で起きること |
|---|---|---|
| 準備 | root所有の読み取り専用ファイルを MAP_PRIVATE でmmap | page cache の実ページを読み取り専用で参照 |
| 書き込みスレッド | /proc/self/mem 経由で同じオフセットへ改ざん内容を反復書き込み | 毎回 COWフォールト→再試行のループに入る |
| 競合スレッド | 同領域へ madvise(MADV_DONTNEED) を反復 | COWで作った匿名コピーの参照を破棄させる |
| 成立 | 両スレッドの反復でレース窓を踏む | 書き込みが元の page cache ページへ着弾 |
古典的な実証は /etc/passwd の root 行のパスワード欄を書き換えて既知パスワードの root を作る、あるいは SELinux を回避するため pokemon(vDSO を書き換えて任意コード実行に持ち込む)系の手法でした。いずれも本質は同一で、「読み取り専用としてマップしたファイルを、COWレースで本体ごと書き換える」ことに尽きます。攻撃は完全にローカルで、追加の脆弱性を必要とせず、成功率も高い点が実運用上の脅威度を押し上げました。
共有ホストやコンテナでは、低権限プロセスからのホスト側 setuid/共有ライブラリ改変につながり得ます。名前空間はカーネルを共有するため、カーネル自体のこのレースは分離境界を越えます。
修正と対策
メインラインの修正(Linus Torvalds によるコミット 19be0eaffa3a)は、再試行時に書き込み要求を取り下げるという危険な近道をやめ、FOLL_COW フラグを新設しました。要点は「一度COWフォールトを経て本物の匿名コピーが用意できたページに限り、書き込み経路での取得を許す」ことです。判定は can_follow_write_pte() に集約され、PTE が書き込み可能か、または FOLL_COW が立っていて当該ページが確かにCOW済み(pte_dirty 等で検証)である場合だけ書き込みを通します。これにより「読み取りとして辿り直して本体ページを掴む」抜け道が塞がれ、レースに勝っても本体には着弾しなくなりました。
| 観点 | 修正前 | 修正後(FOLL_COW導入) |
|---|---|---|
| 再試行時の書き込み要求 | 無限ループ回避のため実質取り下げ | 取り下げず、COW完了を条件に許可 |
| 書き込み可否の判定 | PTEが書き込み可能かのみ | can_follow_write_pte でCOW済みを厳密検証 |
| レース成立時の帰結 | 本体 page cache に着弾 | 本体には着弾せず匿名コピーに限定 |
運用面の緩和策は、影響を受けるディストリのカーネルへ速やかにパッチ適用することが第一です。即時に再起動できない環境では、SystemTap による一時緩和(ptrace/madvise 経路の監視・遮断)や、/proc/*/mem 書き込みの制限が推奨されました。Android では月次セキュリティパッチで対応されましたが、更新の届かない端末が長く残り、長期的な露出を生みました。なお本件は後にファイルシステム側の類似レース CVE-2022-2590(Dirty COW の亡霊とも呼ばれる shmem/tmpfs 系)を生むなど、同種の並行性バグが尾を引きました。
教訓(一般化できる原則)
Dirty COW は、単純な入力検証では防げない「カーネルの並行性バグ」の代表例です。一般化できる原則を挙げます。
- 状態を確認してから使うまでの窓を疑う: COWの判定と書き込みの間に別経路(
madvise)が状態を変えられる時点で、TOCTOU が成立します。チェックと使用の原子性を保てているかが要です。 - COWは見た目より難しい不変条件を要求する: 「プライベート読み取り専用の書き込みは必ずコピーへ向かう」という保証は、再試行・フォールト・並行アクセスが絡むと容易に破れます。近道(書き込み要求の取り下げ)が不変条件を壊した典型でした。
- 特権の副経路を最小化する:
/proc/self/memやptraceのように「本来書けないものを書ける」機能は、レースの起点になり得ます。存在自体を前提に堅牢化する必要があります。 - 古いコードほど監査を: 9年潜伏した事実は、成熟したコードでも並行性の観点で見直す価値を示します。
投機的実行に起因するSpectre/Meltdownと同様、Dirty COW も「正しく見える最適化・近道」がハードウェアやカーネルの深部で破綻する系譜にあります。カーネルの権限分離や特権昇格の全体像はOS、防御設計や最小権限の考え方はセキュリティの各トピックも参照してください。
脆弱性の解剖の記事ガイド
Dirty COW(CVE-2016-5195)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Linux
比較で見る軸
難易度: advanced / カテゴリ: 脆弱性の解剖 / タグ数: 6
導入後に効く点
CVSS 7.0のローカル権限昇格で、一般ユーザーが/etc/passwdやsetuidバイナリなどroot所有ファイルを書き換えられる。Linux 2.6.22から約9年間潜伏した。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- 脆弱性の解剖
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Linux / カーネル」に近いか確認する。
- 強みである「LinuxのCOW処理に潜む競合状態。読み取り専用マッピングへの書き込みフォールトとmadvise(MADV_DONTNEED)を並走させ、本来書けないファイルのページキャッシュへ直接書き込む。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。