- Linux 6.10の mseal は、実行中の仮想メモリ領域を封印し、コード実行権限を得た攻撃者がその後に VMA の権限や配置を変更しにくくする、エクスプロイト緩和向けの syscall
mseal(start, len, flags) はページ整列された VMA 範囲に VM_SEALED を付与し、カーネルは mprotect、munmap、mmap(MAP_FIXED)、mremap、一部の破壊的な madvise 経路で変更を拒否する
- 既存の
memfd_create や memfd_secret が RAM ベースの匿名ファイルや秘密メモリアクセス制御に近いのに対し、mseal はリモート攻撃者によるコード実行後の権限変更・アンマップ・再マップの防止に焦点を当てている
- 代表的な防御対象は、
mprotect(PROT_EXEC) で NX を回避してシェルコードを実行する攻撃と、munmap/再マップでメモリに穴を開けた後に攻撃者データで埋める データ専用エクスプロイト
- glibc 2.41 以降で統合される可能性があるが、スタックとヒープは実行時の拡張・縮小のため自動封印の対象ではなく、適用時点と範囲は開発者が慎重に決める必要がある
mseal が提供する保護
- メモリ封印(memory sealing) は、実行中の VMA 範囲を後からの危険な変更に対して、ほぼ不変にする機能
- 攻撃者がコード実行プリミティブを確保しても、封印済み VMA では仮想メモリ操作で権限を変えたり、配置を都合よく操作したりすることが難しくなる
- Chrome Security チームは、Linux ベースの ChromeOS で V8 CFI 戦略 を支えるためにこの syscall を導入した
- 複数回の議論と書き直しを経て Linux カーネルに取り込まれ、glibc 統合 によってブラウザ外にも利用範囲が広がる可能性がある
memfd 系との違い
memfd_create と memfd_secret は ファイル封印(file sealing) 系に近い
- RAM ベースの匿名ファイルを作成できる
memfd_secret は、そのファイルディスクリプタを持つプロセスだけが対象メモリ領域にアクセスできるようにする
- 機密なインメモリデータを守る「secure enclave」スタイルのユーザー空間マッピングに活用できる
mseal は、機密情報の漏えいを狙うローカル攻撃者よりも、コード実行を狙う リモート攻撃者 に対するエクスプロイト緩和を目的とした syscall
カーネル内部の動作
- syscall シグネチャは
int mseal(unsigned long start, size_t len, unsigned long flags)
start と len は、封印する有効な VMA の開始位置と長さを表す
len は正しくページ整列されていなければならない
- 現時点で
flags は未使用で、0 である必要がある
- Linux 6.12 時点の実装では
do_mseal を呼び出す
current->mm は、呼び出し元プロセスの仮想メモリアドレス空間全体を表す mm_struct を指す
mm_struct の mmap は、mmap によって作られた連続メモリ領域である vm_area_struct の一覧を保持する
- スタックや VDSO のような領域も 1 つの VMA として表現されることがある
do_mseal は範囲終端アドレスを計算した後、mmap_write_lock_killable でメモリ領域をロックする
check_mm_seal は対象範囲の各 VMA を走査し、まず 境界の妥当性 を確認する
- 範囲の途中に未割り当てメモリがある場合は
-ENOMEM を返す
- この一次検査は、エラー時に一部の VMA だけが封印される状況を避けるための段階
apply_mm_seal は同じ範囲を再度走査し、mseal_fixup を通じて対象 VMA に VM_SEALED フラグを追加する
封印後に禁止されるメモリ操作
- カーネルのパッチセットは
VM_SEALED の確認を mm/madvise.c、mm/mmap.c、mm/mprotect.c、mm/mremap.c、mm/mseal.c に追加した
mprotect と pkey_mprotect は内部で mprotect_fixup から can_modify_vma を呼び出し、VMA が封印済みなら -EPERM を返す
- 封印済み VMA では次の操作が許可されない
mprotect、pkey_mprotect による 権限ビットの変更
munmap によるアンマップ
mmap(MAP_FIXED) による、封印済みマッピングの可変・未封印マッピングへの置き換え
mremap によるサイズ拡張または縮小
mremap(MREMAP_MAYMOVE | MREMAP_FIXED) による新しい宛先への移動
- 一部の破壊的フラグを使った
madvise
mremap による移動では、ソースと宛先の VMA の両方に対して封印チェックが適用される
MREMAP_DONTUNMAP がない場合、ソース VMA はアンマップされ、このときも munmap の封印チェックを通過しなければならない
- Linux 6.10 以降では、直接 syscall を呼ぶことで
mseal を利用でき、サンプルラッパーは syscall 番号 462 と flags=0 を使っている
NX 強化によるシェルコード実行の阻止
- 攻撃者は ROP のようなコード再利用手法があっても、シェルコード実行 を好むことがある
- 実行不可のスタックやヒープにシェルコードを配置する
- 脆弱性を使って初期 ROP チェーンを実行する
mprotect(PROT_EXEC) でシェルコードのある領域の NX ビットを無効化する
- その領域へジャンプしてシェルコードを実行する
- CVE-2018-7445 を利用した MikroTik RouterOS SMB デーモンへの攻撃は、この流れの一例
- ソケットベースのシェルコードを実行不可ヒープに配置する
- スタックオーバーフローで作成した ROP チェーンがヒープメモリ権限を変更する
- その後シェルコードを実行する
mseal でその VMA を封印すれば、mprotect 呼び出し時の can_modify_vma チェックが権限変更を阻止する
- サンプルコードでは、スタック上のシェルコードページを封印しない場合、
mprotect(PROT_READ|PROT_WRITE|PROT_EXEC) の後に実行可能になる
- 同じページを
mseal で封印すると、mprotect は実際の権限変更に失敗し、シェルコード実行時にセグメンテーション違反が発生する
スタック・ヒープ適用の制約
- glibc 2.41 以降で
mseal が導入されると、動的ローダーは事前に決まった VMA 集合に封印を適用する予定
- 現在の計画では スタックとヒープは自動封印されない
- スタックとヒープは実行時に拡張される可能性があり、自動封印はアプリケーション動作を壊しかねない
- ヒープアロケータが空き領域を回収するために
brk syscall を呼ぶことがある
- この経路では
arch_unmap と do_vmi_unmap を経て縮小が行われることがある
- 封印状態ではこの種のアンマップが許可されず、動的メモリ割り当てが壊れるおそれがある
- 開発者は、アプリケーション文脈に応じて、どの時点でどの領域に封印を適用してよいかを判断する必要がある
- サンプルマクロでは、信頼できないデータが存在しうる選択されたスタックフレームのページを繰り返し封印する
- すでに封印済みの VMA に再度
mseal を呼んでも no-op として扱われ、エラーにはならない
- 自動スタック拡張や stack splitting のような機能には別途注意が必要
- 攻撃者がどうしてもシェルコード実行にこだわるなら、新しい実行可能領域を
mmap し、読み取り可能な領域からペイロードをコピーすることもできるが、手順はより煩雑になる
アンマップベースのデータ専用エクスプロイト緩和
mprotect の遮断は、封印された領域が書き込み可能に変更されることも防ぐため、改変されるとエクスプロイトプリミティブを強化しうるデータ変数の保護にも役立つ
- Chrome のメンテナーは、破損したポインタをアンマップ/再マップ syscall に渡してメモリに 穴を開け、攻撃者制御データで再充填する手法を想定している
- この方式は、スタックのリターンアドレスや関数ポインタのような制御フロー遷移を直接書き換えないため、forward-edge と backward-edge の CFI 保証を回避できる
- JIT コンパイラを実装するブラウザでは、この手法が特に有効になりうる
- V8 の Turbofan は RW と RX を切り替える領域を作れる
- 攻撃者は、JIT コンパイル過程で hot-path JavaScript によって実行コードを生成させることができる
- アンマップした領域を埋めて重要データを上書きし、その結果コード実行につながる変更を作り出せる
- これは、直接的な制御フロー乗っ取りやポインタリークを必要とせず、メモリ内の特定データを操作して制御フローに影響を与える データ専用エクスプロイト
mseal はアンマップと再マップを阻止することで、この種の hole-punching シナリオを防げる
House of Muney の事例
- House of Muney は、ユーザー空間ヒープエクスプロイトで類似のアンマップベース手法を用いている
- Qualys は、古い Qmail バグに対する 実証エクスプロイト でこの手法を使用した
- この手法は、大きな割り当てチャンクが
M_MAP_THRESHOLD を超える場合、malloc と free がそれぞれ直接 mmap と munmap を呼ぶ性質に依存している
- 大きなチャンクには中間の freelist キャッシュがなく、エクスプロイト手順が単純になる
- 割り当てチャンク先頭のサイズメタデータを別のページサイズに改ざんしてから
free すると、チャンク隣接メモリ領域に対して munmap が発生しうる
- Dulin の例では、任意の
munmap で .gnu.hash と .dynsym 領域を狙う
- その後、より大きい
mmap チャンクで再び埋める
- まだ解決されていない PLT エントリを 1 つ上書きできるようにする
- GOT overwrite 型攻撃を復活させる
- 短縮版 PoC では、2 つの大きなチャンクを確保し、
top[-1] の size フィールドを操作した後、free(top) で隣接領域までアンマップし、さらに大きな割り当てで X データを再充填する
- この手法は CFI があっても成立しうえ、事前の ASLR リークを必要としない
- glibc の
mseal 統合で予定される VMA 集合の封印は、マップ済みバイナリコードや動的ライブラリをアンマップ/再マップのトリックから保護し、この攻撃を自動的に緩和すると期待される
- さらに強化したい開発者は、プログラムの寿命中に拡張もアンマップもされない
mmap 割り当てを選択的に封印できる
今後の利用範囲
mseal は Linux カーネルの比較的新しい緩和機能であり、まだ十分に検討されていない利用例がほかにもあるかもしれない
- glibc 統合が完了して成熟すれば、syscall の要件に合わせた改善が引き続き行われる可能性がある
- 現在は未使用の
flags パラメータにも、将来的に具体的な用途が定義されるかもしれない
- 実際のソフトウェアへ適用する際は、封印するメモリの寿命と変更の必要性をあわせて評価する必要がある
1件のコメント
Hacker News のコメント
カーネルのメーリングリストでの激しい議論で、どんな反対や懸念があったのか、内情を知る人に要約してほしい
メーリングリストそのものは辛辣すぎることがあるので避けがちだし、すでに頭痛は十分ひどい
仕組み自体は合理的に見えるが、こういう機能がカーネルにまだなかったという点は意外
提案されたフラグの中にはシール(seal)を無視できるようにするものがあり、Linusは「ある場所では
munmapできないようにするのに、別の場所ではシールを無視するというのか」といった反応をしたその後は「いったんシールされたらシールされたもの」なので、「ある場所ではシールを守り、任意の別の場所では守らない」というやり方は認められないと、さらに強い口調で述べた
後には、シールは無視できず交渉対象でもなく、フラグでシールを無視しようという提案をさらにするなら ignore list に入れるとまで言っていた
Theo de Raadt が Jeff Xu に送ったメールで、
mimmutable()とmseal()のアプローチは異なるが、どちらも攻撃者からメモリをシールして Linux アプリケーションをより安全にすることを目標としている、と述べたことに対し、「あなたは Chrome のためだけに mseal を作っているように見える」と返したアプリケーション全般にはあまり合わないだろうと見ており、理由として複雑すぎること、そして
mimmutable()の経験上、アプリケーションが直接扱うのではなく、execve()、libc の初期化、ld.soが担当するようになる点を挙げたTheo と Linus に同意し、
mimmutable()の基本アイデアは良いが、それを分割して実装したやり方はひどいと見ていたこのシステムコールをどう使うのか気になる
Chrome はこれを望んでいるが、攻撃者が別のフラグで再マップできてしまうので、シールされたページはアンマップできない
だとすると、ランタイムに割り当てられたページには、プロセスの全寿命にわたって保持するつもりでない限り、実質的に使えないのではないかと思う
そうなら、JS サンドボックスメモリのような非常に魅力的な標的には適用しにくいのではないか?
この種の処理は別プロセスで実行し、メモリをシールした後、終わったらプロセスを kill する、という形で解決するのか気になる
Chrome のメモリ管理やプロセス管理のやり方をよく知らないので、なぜ問題にならないのか確信が持てず、この機能が大半のプログラムには有用ではないとよく説明される理由もそのためなのか気になる
Chrome はこれを広範に使っていると理解しているし、名前空間による隔離のような理由でも別プロセスはいずれにせよ必要なので、その方向なのかもしれない
mseal()後の議論、2023年10月20日: https://lwn.net/Articles/948129/mseal()が近づく、2024年1月19日: https://lwn.net/Articles/958438/GNU C Library のためのメモリシーリング、2024年6月12日: https://lwn.net/Articles/978010/
現代の x86_64 アーキテクチャには安全なプログラミングとコンピューティングを助ける機能が多いにもかかわらず、OS がこうした呼び出しを実装しなければならない点は残念だ
古い遺産と思考様式、現在の世界や知識に合わないパラダイムの上に築かれた古いシステムを修繕して使い続けようとする態度が、コンピューティングの発展を遅らせ、文字どおり数十億人を危険にさらしていると思う
もちろん、こうした変更が正しい方向への一歩ではないと言いたいわけではないが、OS はこう動作すべきだという従来の理想を手放し、現在のシステムと知識、人々がシステムに求めるものを考慮すれば、今日の開発者とユーザーに課されている負担とリスクから解放されたシステムを想像できる
アーキテクチャ上のバグが存在するのは確かだが、ソフトウェアが現在の機能を十分に活用できてもいないので、アーキテクチャ上のバグを持ち出して反論するのは核心を外している
揺らいだ土台の上に作られるなら、より安価な侵害手段は常に存在するだろう
なぜ
msealが正当化されないと考えるのか、代わりに何がより良いのかも気になるLD_PRELOADのトリックでmsealシステムコールを上書きしたり無効化したりできるだろうか?msealは Linux の従来のメモリ保護方式とは異なり、メモリ内の機密情報を抜き取ろうとするローカル攻撃者よりも、リモートコード実行攻撃の緩和に焦点を当てたシステムコールであるリモート攻撃者がローカル環境を変更できるなら、すでにシステムに侵入されている状態と見るべきである
LD_PRELOADでは無理だろう効果を持たせるにはインポートされた関数である必要があるが、生のシステムコールはそのような形では横取りできない
Linux システムコールの横取りに関する議論: https://stackoverflow.com/questions/69859/how-could-i-interc...
msealが動作しているふりをするパッチ済みカーネルを自分で作るのが、この機能を「無効化」する最も簡単な方法かもしれないただし、
msealを使うプログラムは実際に機能しているか検証できるため、改ざんされたカーネルでは、適用後にアプリの検査を妨げるためにmsealをこっそり無効化する方法まで必要になる例えば実行ファイルを
ptraceで追跡してシステムコールを監視し、mseal(2)をスキップさせることができるこのシステムコールは「攻撃者がプロセス初期化前にすでにアクセス権を持っていた」という脅威モデルのためのものではない
mseal呼び出しのラッパーは上書きできるが、システムコール自体は上書きできない調べたところ、プリロード方式によるシステムコールの上書きはいずれもラッパーを差し替える方式で、直接システムコールを行う場合は上書きできなさそうだ
技術的には
syscall関数自体を上書きできるかもしれないメタ: 記事中の
mseal()のプロトタイプは編集が必要である1つ目の引数が
unsigned start addrと表示されているが、おそらくunsigned long start_addrが正しいはずであるint mseal(unsigned long start, size_t len, unsigned long flags)“Memory Sealing ‘Mseal’ System Call Merged for Linux 6.10” (2024) https://news.ycombinator.com/item?id=40474510#40474551 では、CPython が
mseal()システムコールをどのようにサポートすべきかがすでに議論されていたOpenBSD にはかなり前からあった機能である https://man.openbsd.org/mimmutable.2
これほど当然の機能が、なぜ今になって Linux に入るのか気になる
mimmutableは 2023年4月10日にリリースされた OpenBSD 7.3 で導入されたものなので、「かなり前から」ではない一方で Linux と FreeBSD には
memfd_createが以前からあり、OpenBSD には匿名ファイルがないためshm_openに依存している