無査読の論文投稿サイトのArXiveに「SafeSpec:Banishing the Spectre of a Meltdown with Leakage-Free Speculation」という論文が発表された。筆頭著者のカリフォルニア大学リバーサイド校のKhaled Khasawneh氏をはじめ6人の名前が並んでいる。指導教官は著者の一人となっているリバーサイド校のNael Abu-Ghazaleh教授である。

投機実行と攻撃方法

最近のプロセサは性能を上げるために「投機実行(Speculative Execution)」を使っている。例えば、ifの条件が成立するか、しないかが、if文を実行するタイミングで判明していなくても、過去の履歴から、成立、不成立を予測して、その予測に基づいてその先の文の命令を実行していく。

予測が正しければ、条件の成立、不成立が判明するまで待つよりも処理性能を高めることができる。一方、予測が間違っていれば、間違って実行した文以降の命令の実行結果を取り消し、正しい分岐方向の文の命令の実行をやり直す必要があり、ロスがでる。

しかし、ループの繰り返しを判定するif文などは、繰り返しの最中は同じ方向に分岐し、最後の回だけ、ループを抜ける方向に分岐する。このような場合は、常にループを回る方に分岐すると予測すれば、大部分のケースは予測が当たる。そして、最近のプロセサでは過去の分岐方向の履歴を使うより高度な分岐予測が使われ、90%以上の高い精度で正しい予測が行える。このため、分岐予測に基づいて投機的に命令を実行していくという方法を使って性能を上げるという設計が一般的である。

また、メモリを読むロード命令は、特にキャッシュミスの場合は、長い時間が掛かる。このため、できるだけ早い時点で、条件分岐の先にあるロード命令の実行を投機的に開始するということも行われる。この場合も、そのロード命令は本当は実行されない命令であったことが判明すると、そのロード命令は取り消され、ロード命令の結果のデータを格納したレジスタの値は、元に戻される。

これでプログラムの実行は、投機実行を行わず、命令を順次実行した場合と(実行時間が短くなることを除いて)完全に同じ結果となる。これでメデタシメデタシであったのであるが、今年、Googleのチームなどが「Spectre」と「Meltdown」という投機実行の隙を突く攻撃ができることを明らかにした。

メモリをアクセスするロード命令を実行する場合は、そのプログラムが使用できるメモリ範囲内のアドレスであることを「MMU(Memory Management Unit)」がチェックして、範囲外のアドレスはアクセスできないようになっているが、投機実行を行う場合は、メモリアクセスとアドレスのチェックが並行して行われるという設計が一般的である。

メモリアクセスは行われてしまうが、許容範囲外のアドレスに対するアクセスであることが判明した場合は、ロード命令の実行を取り消してしまえば、問題は無い。

しかし、メインメモリを読み、キャッシュメモリに格納されたデータは消されずに残ったままとなる。つまり、攻撃者が、本来はアクセスが禁止されているパスワードなどの秘密情報を含むメモリ領域を読み出す命令を投機的に実行すれば、秘密情報がキャッシュに格納された状態になってしまう。

なお、この場合は、許容範囲外のメモリアクセスであるので、通常はアクセス違反が検出されて、それがOSに通知されてアクセスを行ったプログラムは実行が打ち切られてしまう。したがって、このような範囲外アドレスのデータを盗み出すためには、アクセス違反の通知を止めるなどの攻撃と組み合わせる必要がある。

これで狙った秘密情報がキャッシュメモリに入った状態になるのであるが、その後に実行するコードでそのデータを読み出しても、ロード命令が取り消されるときに、キャッシュから秘密情報を読み出す命令の実行も取り消されてしまい、秘密情報を読み出すことはできない。

このため、攻撃には、Flush+Reloadなどの秘密の抜け道(covert channel)が使われる。Flush+Reloadでは、まず、キャッシュ全体をフラッシュして空にしておく。次に、秘密情報を含む領域を読み出すコードを投機的に実行させる。このコードは

unsigned char secret;
dummy = array[secret * 64];

のようなコードで、secretの値は読み出せないが、secretの値でデータが読み込まれるキャッシュラインが変わるようになっている。

この後、Arrayをキャッシュラインサイズである64バイト飛びに読んでいく。キャッシュは空になっているので、大部分の読み出しではキャッシュミスとなり、メモリからデータを読むので長い時間が掛かる。しかし、投機的にデータが読み込まれたキャッシュラインだけは、キャッシュヒットとなり短時間でアクセスが終わる。このことから、秘密のsecretの値がいくつであったが推測できる。

このように、秘密データを盗むのは手間が掛かるが、この作業を繰り返せば、広いメモリ領域全体のデータを盗むことも可能になる。

なお、Spectre V1、V2とMeltdownでは、秘密情報を持つデータをキャッシュラインに入れる方法が異なる。

Spectre V1では

if(offset < array1\_size)
y = array2[array1[offset] * 64];

のようなコードを使う。

If文でoffsetがarray1_sizeより大きい場合は次のy=は実行されないコードであるが、条件が成立するoffsetで何回もif文を実行させると、分岐予測機構は次も条件成立と予測するようになる。さらに、array1_sizeをキャッシュからフラッシュして、その次に、array1_sizeより大きなoffsetを与えても、y=の文を投機実行してしまう。この時、array1_sizeはキャッシュから消えているので、メモリから読んでくる必要があり、条件が不成立であることが判明するのに時間が掛かり、細工がしやすくなる。

Spectre V2は間接分岐の分岐先を予測するBTB(Branch Target Buffer)を細工する。本来のプログラムと攻撃プログラムの分岐先予測がBTBの同じエントリを使うように、分岐命令のアドレスを調整しておき、攻撃プログラムを多数回実行することで、投機的な分岐先を変えてしまう。

この状態で、本来のプログラムで投機的に間接分岐を行うと、攻撃プログラムの分岐先に分岐が行われ、秘密情報をアクセスしてキャッシュに読み出すコードを実行させる。これもチェックが追いついてくると、分岐先が誤っていたことが判明し、誤った間接分岐とそれ以降の命令は取り消されるが、キャッシュに読み込まれたデータは残る。

Spectre V1では分岐予測外れを起こさせて秘密情報を読むコードを投機実行させるが、Meltdownでは例外を発生させる命令の次に置いた秘密情報を読み出すコードを実行させる。例外が発生すると、例外ハンドラの入り口にジャンプするので、本来は、その直後のコードは実行されないのであるが、投機実行により、例外発生命令に続く命令も実行されてしまう。もちろん、それらの投機実行された命令の影響は後に取り消されてしまうのであるが、それらの命令で読み込まれたキャッシュの内容は残り、flush+reloadなどでoffsetの値を推定することが可能になる。

例外発生命令が実行されているので、プロセサはスーパバイザ状態になっており、OSのカーネル領域のデータも読めるという点で、Meltdown攻撃は強力である。

投機実行メカニズムの隙を突くSpectreやMeltdownによる攻撃が可能であることが示され、プロセサ各社は攻撃を防御する手段を検討し、ソフトウェアのパッチやマイクロコードのパッチなどをユーザに配布している。しかし、その多くは、かなりのプロセサの性能低下が起こったりするものであり、また、その後も別の攻撃法が発見されるなど、対応は後手に回っている。また、Intelは最近のプロセサに対してはパッチなどを提供しているが、ちょっと古いプロセサでは防御不能とあきらめてしまったものもあるという状況で、対策は極めて不十分な状況である。

(次回は6月27日に掲載します)