2018年1月19日金曜日

MeltdownとSpectre

そういえば、いまさらですが、MeltdownとSpectreの話。個人的に調べてFacebookなどに書いていた内容を、ここにも書いておきます。まずは、Meltdownの話。


最近のCPUは、命令の実行を高速化するために、複数の命令を並列実行する機能を持っている。たとえば、ある命令がメモリからのデータ読み込みを必要とするため、待ち時間が生じるような場合、次の命令が前の命令の結果に依存しなければ、それを待ち時間の間に実行するというような処理である。これをアウトオブオーダー実行(順序によらない実行)と言う。最近のプロセッサ命令は、実行過程でよりプリミティブなマイクロコードに分解されるため、こうした前処理は常に先行して行われ、依存関係に直接関わる実行ステージのみが未実行の状態で引き続く命令が待機することになる。これにより、命令列の中の複数の命令が並行して実行されながら、結果を保留した状態で待機していて、先行する命令の完了と同時に結果が反映される。また、こうした命令列の中に分岐(条件分岐)命令が存在した場合は、過去の分岐状況に基づいて予測された分岐結果に基づいて、次にどの命令を先行するか決定して実行する。予測が外れた場合、並行して実行された結果は捨てられ、正しいパスで再度実行される(かえって効率が低下する)ため、こうした方式は投機的実行と呼ばれている。
今回、問題になったのは、並行して実行される命令にOSカーネルのみがアクセス可能なメモリ領域にアクセスするような命令が含まれていた場合、たとえ、プロセス自体に権限がなくても(ユーザプロセスであっても)それが実行されてしまうという点。もちろん、その実行結果が確定された段階では、メモリアクセス例外が発生して処理が中断されるため、直接プログラム側からその値を確認することはできないのだが、実際は、この実行によって、アクセスしたメモリのページがキャッシュに読み込まれるといった副作用が生じる。また、例外発生までに一定のタイムラグが生じるため、先行する命令の結果を受けた後続命令もある程度実行されてしまうため、こうした副作用を、読み込んだデータに依存する形で発生させることができる。この問題は、ソフトウエアへの依存性はなく、こうしたプロセッサのアーキテクチャに存在するため、様々なOSプラットフォームや同種のアーキテクチャを持つCPUで発生する。

あるページがキャッシュされているかどうかは、そのページに対するアクセス速度を調べることで判定ができるが、これを使用してサイドチャネルを作る手法は既にいくつか既知になっている。(たとえば、Flush + Reloadなど)キャッシュを削除しておいて、他のプロセスがどのページを再度キャッシュしたかを調べることができれば、これを悪用してプロセス間でデータ受け渡しが可能になる。(もちろん、そう単純ではないが・・・)
たとえば、先に実行される命令が、カーネルページのアドレスにアクセスして1バイトのデータを取得した後、その値×ページサイズをインデックスにして、後続の命令が実際に実メモリがマッピングされている仮想アドレスにアクセスしたとする。当然、カーネルページへのアクセスでは例外が発生することになるが、この例外発生の前に、カーネルページへのアクセスによるデータが確定して、準備が整ってそれを待っていた後続の命令が実行されてしまう。つまり、ある先頭アドレス+読み出したデータ×ページサイズの位置のページがキャッシュされることになる。もちろん、そのプロセス自身は例外によって読み出したデータを知ることができないが、他のプロセスから、どのページが新たにキャッシュされたかを知ることができれば、読み出したデータの値がわかる。これが、Meltdown攻撃の基本的な原理。サイドチャネル攻撃との組み合わせで効率が悪いように思えるが、それでも平均して500KB/s程度のデータ読み出しが低いエラーレートで可能とのことである。
CPUアーキテクチャの問題であるため、この攻撃を原理的に防ぐ方法は、ハードウエアの改修以外にない。
一方、Linux等のOSでは、ユーザプロセスの仮想メモリ空間に、すべてのカーネルメモリと実メモリがマッピングされている。このことによって、上の攻撃で、最悪の場合、全実メモリの情報やカーネルメモリ内の情報を読み出すことができる。もちろん、それを悪用するためにはカーネルメモリの構造や他のアプリケーションのメモリ構造を知っている必要がある。また、カーネルにおいても、最近ではASLR(KASLR)つまり、アドレス配置のランダム化が行われており、攻撃の難易度は高くなっているが、これも実際のマッピングを推測する手法はいくつか存在する。
Windowsにおいては、Linuxのように全実メモリをプロセス仮想空間にマップするようなことは行っていないが、依然として重要な情報を持つカーネルページがマップされているため、Meltdown攻撃が機能する可能性が高い。
Linuxにおいては、最新の改修で KAISER(KPTI)と呼ばれるパッチが導入された。これにより、当面、全実空間やカーネルページの大部分にアクセスできなくなるが、依然として一部のカーネルページにはアクセス可能である。ただ、KASLRを介してマッピングがランダム化されていることもあって、攻撃難度はかなり上がることになる。当面は、これ(KAISER)が最良の対策となる。ただ、こうしたページに含まれるカーネル空間のポインター値からKASLRのマッピングを推測できる可能性はあり、最終的にはこうしたポインターのすべてをユーザ側からアクセスできなくする必要があるとのこと。
WindowsやMacOS X, iOSにおいても同様のパッチが既に提供されている。思うに、この攻撃自体の難易度は高いものの、影響範囲が様々なOSプラットフォームやハードウエアを越えて非常に広いことから、有効な攻撃コードが出回る可能性も否定できない。仮想環境やコンテナ環境などにおいても、隔離環境からホストOSや他のインスタンスの情報にアクセスできるなど広く影響を与えることから、とりわけクラウドサービス環境における対応は急務と言えるだろう。

次に、Spectreの話。

Spectre攻撃は、Meltdown同様にキャッシュへの副作用を利用してサイドチャネルから情報を得る方法だが、カーネルメモリや仮想空間にマップされた実メモリページではなく、特定のプロセスの仮想空間内のデータを狙う手法である。従って、Meltdown対策としてのKAISERは有効ではない。攻撃対象となるのは、CPUが持っている分岐予測実行(投機的実行)機能。

分岐予測と分岐先予測

Meltdownの時に書いたように、最近のCPUは、ある命令の待ち時間の間に後続命令の実行準備や先行命令に依存しない命令を実行して結果を保留する、アウトオブオーダー実行の機能を持っているが、これを含めて命令コードを先読みするパイプライン機能は分岐命令によって乱されるため、分岐の多い処理ではパフォーマンスが低下する。これをカバーするために付加されたのが分岐予測の技術である。最近のCPUでは、よく使用される条件分岐命令の仮想アドレスと、その最近の分岐の履歴をテーブルとして保持していて、この情報に基づいて次に先読みすべきコードを決定している。また、間接ジャンプやリターン命令といったダイナミックに飛び先が変わる命令については、よく使用される分岐先アドレスを保持していて、これを用いて分岐先を予測し先読みを行う。分岐予測の結果を高速に利用するため、各命令の仮想アドレスは下位30ビット程度をハッシュテーブル化して保持され(BTB:Branch Target Bufferと呼ばれる)、実行時は分岐命令がデコードされているかどうかにかかわらず、先読みのために使用される。このアドレスは仮想アドレスであるため、たとえば、異なるプロセスであっても、同じ場所に分岐命令があれば(ASLRなどのため、共有ライブラリであってもこの可能性は極めて低いが)同一のものとして扱われる。

分岐予測については、外部からその分岐をコントロールできれば、あらかじめ分岐有無を学習させることが可能である。また、分岐先予測については、対象となる命令の仮想アドレス下位30ビットが等しくなる位置に同じ命令がある攻撃用プロセスを準備できれば、他のプロセスから分岐先予測を攪乱することも可能である。(論文では20ビット程度でもよかったと書かれている)

基本的な攻撃の原理

分岐を伴うコードで、予測結果によって先読みされる部分に、外部から与えたデータで任意の仮想アドレスを参照するような命令を持つ部分を探し、この分岐命令についてそのコードを先読み実行するように学習させた上で、参照したいメモリアドレスを与えて、予測を失敗させる。これにより、もし参照したメモリブロックがキャッシュされていなければキャッシュ要求が発生し、キャッシュされる。あらかじめキャッシュを削除してからサイドチャネルでキャッシュされた場所を調べる手法は、Meltdownと同様。

分岐先予測を使った攻撃ではもう少し自由度が上がる。メモリ参照を行うコードは分岐命令とは全く違う場所にあってもよい。この場合は、攻撃プロセスを使って、分岐先予測を攪乱し、メモリ参照を行うコードのアドレスをBTBに押し込んでしまえばいい。これは、バッファオーバフロー攻撃時のROP(Return Oriented Programing)と類似の手法である。もちろん、実行結果は最終的に破棄されることになるが、キャッシュは残る。

以下、具体的な攻撃コンセプトの例。

A) 条件分岐を攻撃する方法(分岐予測への攻撃)例

1) 標的とするプロセスのプログラム内で、外部から与えられる値によって分岐条件が変わり、投機的実行によって、その値に依存する位置のメモリブロックをキャッシュする副作用を発生するようなコードを見つける・・たとえば、指標値による二重のテーブルルックアップ

if (x < size_a1)
  y = a2[a1[x] * 256];

のようなコード。これは機械命令に落ちる際に if 条件を満たさなければ分岐するようなコードになるが、この分岐予測が「分岐しない」となる場合は、xによってa1 + xのメモリにある値が参照され、それに対して256(キャッシュブロックのサイズの倍数の値)を掛けた値だけa2に加えた位置へのアクセス要求が発生し、そのブロックがキャッシュされる。

2) この分岐に対して、いくつか正常な(size_a1より小さな)値を与えて学習させた後に、xに不正な(size_a1より大きな)値を与えると、分岐予測によって、不正なxを用いた計算が先行して(投機的に)行われる。たとえば、事前にキャッシュをあふれさせたりフラッシュしてしまうことで、条件評価の際にsize_a1をメモリから取得しなければいけないなどの時間がかかるようにしておく必要がある。これにより、a2 + (a1 + x) * 256 の位置のブロックがキャッシュされる。

3) サイドチャネルを使って、新たにキャッシュされたブロックのアドレスを調べると、a1 + xにある値が推定できるので、この作業をxを変えながら行うことで、標的プロセスの仮想メモリ空間内にある値を読み出すことが可能になる。

B) 間接分岐やリターン命令を攻撃する方法(分岐先予測への攻撃)例

1) 標的プロセス内で、2つのレジスタ(R1, R2)に外部から操作可能な値を持った状態で実行される間接分岐(メモリ間接のジャンプ)命令やリターン命令などを見つける。

2) 同じく標的プロセスのコードや使われている共有ライブラリ(DLL, SOなど)内で、まず、R1をオフセットとしてメモリをDWORD参照し、その値をR2に加える(32bit演算)ようなコードと、その次にR2を使ってメモリを間接参照するなんらかの命令を持つ部分を見つける。たとえば、以下のような例(実際のWindowsのコードの一部)が書かれている。ここで、EDIとEBXが外部から制御可能。edxの値は既知である前提。

adc edi,dword ptr [ebx+edx+13BE13BDh]
adc dl,byte ptr [edi]

3) 1)の間接分岐もしくはリターン命令について分岐先予測ロジックを攻撃し、2) のコードを分岐先と予測するように学習させる。

4) R1を参照したいアドレス(CPUによってバイトオーダーを考える必要あり)にし、R2をメモリ参照によってアクセスされるアドレスの先頭に設定するような値を標的プロセスに外部から与えて 1)のコードを実行させる。これにより、間接分岐やリターン命令がメモリアクセスを待っている間に分岐先予測によって2)が投機的に実行され R2 + [R1]<<24 + [R1+1]<<16 + [R1+2] << 8 + [R1+3} の位置のメモリ(バイトオーダーに注意)がキャッシュされる。

5) サイドチャネルを使用して新たにキャッシュされたブロックのアドレスを調べることで、4)のアドレスがわかる。A)ほど単純ではないが、こうした手法をR1の値を変えながら繰り返せば(メモリアクセスバウンダリーの問題は考慮が必要だが)標的プロセスの任意のメモリの値を取得する事は原理的には可能である。

C) JavaScriptを使用したブラウザ攻撃の可能性

JavaScript実装の多くが、JIT(実行時コンパイル)を行っているため、分岐命令やそれに伴った予測実行コードを意図的に生成することができると考えられる。サイドチャネルを作るためのキャッシュの破棄やキャッシュされたかどうかの判断は、巨大な配列を使用することで間接的に可能になる可能性がある。これによって、ブラウザに実装されているサンドボックス機能等のプロテクションが回避される可能性がある。

とりあえず、参考までに・・・・

0 件のコメント:

コメントを投稿