KAISER(KASLR is Dead: Long Live KASLR)のまとめ
始めに
巷で話題の脆弱性の Spectre と Meltdown です。 Meltdown の対策として導入された Kernel Page テーブル Isolation について簡単にまとめます。 この対策技術は、KAISER と呼ばれ、「The KASLR is Dead: Long Live KASLR」というタイトルで ESSoS 2017 で発表されました。 論文中での実装およびパッチは、Linux が対象です。 論文およびパッチは、GitHub から参照できます。
Meltdown
Meltdown は、ユーザー空間のアプリケーションからカーネル空間のメモリアドレスを読み取ることができる脆弱性です。 この攻撃は、本来は権限のないメモリへ読み取り命令を CPU の投機的に実行(プリフェッチ)させ、その結果、キャッシュに残った値を推測するというサイドチャネル攻撃です。 今までも、ユーザー空間からカーネル空間へのサイドチャネル攻撃はいくつか提案されていました。 たとえば、Double Page Fault Attack や intel TSX-based Attack です。 しかし、これらは、ユーザー空間からカーネル空間のメモリアドレスの配置を推測することにとどまっており、メモリの読み取りまでは実現していませんでした。 ここで言う、メモリアドレスの配置を推測というは、kASLR のバイパスにあたります。 Meltdown は、実際に、カーネル空間のメモリを読み出すことができるという点で、今回騒がれたのだと思います。
脆弱性や攻撃の詳細については、以下の記事を読むと理解が進むと思います。
今回の Meltdown は、intel 固有の脆弱性でありますが、根本的な原因は、プロセスのアドレス空間にカーネルとユーザー空間の両方がマップされていることです。
仮想アドレス空間
intel および Linux では、プロセスごとに仮想メモリを持ち、メモリアクセスの際の仮想アドレスから物理アドレスの変換は、ページテーブルを介して行われます。 x86-64 では、4段階のページテーブルを用いてアドレス変換が行われ、最上位のページテーブルは、PML4 と呼ばれます。 この PML4 の物理アドレスを CR3 レジスタにセットすることで、各プロセスのアドレス空間の切り替えを実現しています。
また、システムコールや割り込みによるユーザーモードとカーネルモードの切り替え時には、アドレス空間の切り替え(CR3 のアップデート)は行われません。 これは、プロセスのページテーブルにカーネル空間もマップされており、そのページテーブル(アドレス空間)を用いて処理を継続できるからです。 カーネルモード時にも、プロセスのアドレス空間を用いることは、パフォーマンスの観点で必要です。 プロセスのアドレス空間を切り替える、すなわち、CR3 をアップデートすることは、TLB を Flash することを意味します。 TLB の Flash はパフォーマンスに直結するコストが高い処理です。
前述したサイドチャネル攻撃は、プロセスがもつアドレス空間にカーネル空間もマップされていることが原因です。 KAISER は、ユーザーとカーネルの切り替え時に、アドレス空間(ページテーブル)も切り替える技術です。
ARM (Cortex-A) も intel と似たような機構を持っていますが、少し異なります。 ARM CPU はアドレス変換テーブルの物理アドレスを保持するレジスタが2つあります(TTBR0 and TTBR1)。 1つは、ユーザーアドレス空間をマップするために、もう1つは、カーネルアドレス空間をマップするために使われ、ハードウェア的に、アドレス空間が分離されています。
KAISER の設計
先行研究でも、アドレス空間の分離が提案されている(Stronger Kernel Isolation)。
通常時と、Stronger Kernel Isolation、KAISER の概念図は次のようになる(論文より引用)。
通常時では、ユーザーモードとカーネルモード時に、カーネル空間およびユーザー空間の両方がマップされた1つのアドレス空間を用いる。
Stronger Kernel Isolation は、ユーザーモード時に、カーネル空間がマップされていないアドレス空間を、逆に、カーネルモード時には、ユーザー空間がマップされていないアドレス空間を用いる。
KAISER では、ユーザーモードでは、カーネル空間がマップされていないアドレス空間を、カーネルモード時にはユーザー空間とカーネル空間の両方がマップされているアドレス空間を用いる。
Stronger Kernel Isolation の方が KAISER より強力な分離であるといえるが、論文中では、Stronger Kernel Isolation は、カーネルへの修正が大きく、実用的でないと述べられている。
ユーザー空間とカーネル空間の分離には大きく3つの課題がある。
高度に並列化された今日のシステムでは、コンテキスト切り替え時にスレッドがページテーブル構造を変更すると、同じプロセスのすべてのスレッドに影響を与える。
現在の x86 プロセッサは、コンテキストスイッチ中にユーザー空間とカーネルス空間の両方に有効な複数の領域を必要とする。 さらに、コンテキストスイッチ中に、core-local storage のようなセグメントメモリアクセスが必要である。 カーネル空間のマップされていない部分を再マッピングすることなく、セグメントを見つけて復元できなければならない。 先行研究では、カーネルの大部分を書き換える必要があった。
アドレス空間の切り替えは、暗黙的な full TLB flush を、アドレス空間の修正は、部分的なの TLB flush を招く。 近年の OS は、暗黙の Full TLB flush を削減するために、高度に最適化されており、パフォーマンスへの影響が大きい。
実装
への対処
ユーザー空間のみがマッピングされている Shadow アドレス空間と、カーネルと SMAP および SMEP で保護されているユーザー空間がマッピングされている2つのアドレス空間を用意する。 ユーザーモードとカーネルモードの切り替え時には、対応する PML4 の値である CR3 レジスタを更新する。 アドレス空間の切り替えは、カーネルとユーザー空間の両方にマッピングされているカーネルの一部のみにアクセスして行われる(一部両方のアドレス空間に共通してカーネルページをマッピングする必要がある)。
通常の PML4 は、8kB にアライメントされた物理メモリブロックとして割り当て、+ 4kB の位置に Shadow PML4 を割り当てる。 これにより、メモリ検索を必要とせずに、アドレス空間の切り替えに、スクラッチレジスタを1つ用意すればよい。
Shadow アドレス空間の概念図(論文より引用)。への対処
先行研究では、割り込みディスパッチャコードのごくわずかな部分だけが両方のアドレス空間にマッピングされる必要があることが示唆されたが、実際にはより多くの領域をマップする必要がある。 x86 と Linux はコンテキストスイッチの割り込みを使用して構築されているため、割り込み記述子テーブル(IDT)と割り込みエントリと終了の.text セクションをマップする必要がある。 また、マルチスレッドアプリケーションを異なるコアで実行できるようにするには、CPU 単位のメモリ領域を識別し、それらを Shadow アドレス空間にマップする必要がある。への対処
Linux カーネルは、暗黙的な full TLB flush を最小限にするようにしている(e.g.カーネルモードからユーザーモード間で CR3 を更新しない)。 KAISER は、2 つのアドレス空間が異なる CR3 レジスタ値を有するので、各コンテキストスイッチ毎に CR3 更新する必要がある。 PTE のグローバルビットを設定したページは、暗黙の TLBFlash から除外されることを利用し。暗黙の TLBFlash の影響が軽減される。
評価
論文中でのパフォーマンスオーバーヘッドは最大 0.68%.
メモリのオーバーヘッドはシステムで 1M 程度。