abcdefGets

ゲッツ!

Spectre and Meltdownについて

今回CPUに依存する脆弱性が発見されて大きな問題となっている。
そこで正月休みを使ってこの問題の解説を試みる。

Disclaimer

私はセキュリティの専門家ではないので間違えた情報があるかもしれない。
一次情報を載せておくので怪しい場合にはそこにアクセスして自分で確認するように。
この情報を鵜呑みにしておきたいかなる問題にも責任は負いかねる。

今回の脆弱性についてのWEBページ
業務で問題の解決を行う場合にはこのページを参照すること。

Meltdown and Spectre

概要

今回は2つの脆弱性が問題になっている。
1つ目はMeltdownと名付けられており、CPUのout-of-order実行を利用したもの。
2つ目はSpectreというCPUの投機的実行を利用したもの。
今回この問題が深刻なのは、どちらもCPUのパフォーマンス最適化のために存在する機構であり、モダンなほぼすべてのCPUに存在すること、
悪意のあるコードとそうでないコードの見極めが非常に難しいことなどから、解決の難しい問題となっている。

Meltdown

Meltdownから解説していく。
Meltdownと名付けられたこの問題はCPUのout-of-order実行の隙を突いた攻撃方法である。

out-of-order

out-of-orderと言うのはモダンなCPUが搭載しているパフォーマンス最適化手段である。
現在のCPUはアセンブリ命令をμOpsという更に小さなCPUのオペレーションに分割して実行する。
このμOpsに分解された命令はReorder-BufferSchedulerによってデータの依存関係が計算されて、実行順が変更される。
結果としてそれぞれ依存関係から独立した命令は順序に関係なく実行され、依存がある部分は依存先の命令が完了次第即実行される。

以下が今回の問題となる疑似コードだが、

gist4ad97b3858f184ca74efbbfe54e845ef

4行目のmov命令と5行目のshl命令は依存関係があるものの、それ以外はすべて予めμOpsに分解されて実行される。

以下はWikipediaからの引用

OoOでは、命令及び実行結果を一時溜めておく場所を作り、命令の実行を次のように細分化する。

1 命令フェッチ。
2 命令にリオーダ・バッファ(reorder buffer)のエントリを割り当てる。
3 命令を命令待ち行列または命令発行キュー(reservation station, issue queue)に送る(dispatch)。
4 命令待ち行列内の命令は、入力オペランドが得られるまで実行されない。入力オペランドが得られた段階で、待ち行列内にそれより古い命令があっても先に待ち行列から取り除かれ、実行されることになる。
5 命令が適当な実行ユニットに対して発行(issue)され、実行される。
6 実行結果がリオーダ・バッファに格納される。
7 リオーダ・バッファ内の命令のうち、最も古い命令の実行が完了すると、その実行結果はレジスタファイルに書き戻され、命令はリオーダ・バッファから取り除かれる。これを卒業ないしリタイア(graduation, retire)ステージと呼ぶ。命令待ち行列とは異なり、より新しい命令が実行完了状態であっても、それより古い命令がリオーダ・バッファ内にリタイアせずに残っている場合は、その(より新しい)命令がリタイアすることはできない。

アウト・オブ・オーダー実行 - Wikipedia

Preventing reading Karnel Memory from User space

上の例で示した疑似コードは実行するとカーネルの特権メモリをユーザースペースで読み込んでいるため例外を発生させる。
そのため、当然ユーザースペースからカーネルメモリを読み込むことは不可能である。
しかし後に述べるTransient Instructionを経由することでカーネルメモリをCPUキャッシュに載せることが可能になる。

Transient Instruction

ちょっとどう翻訳していいかわからないのだが、これはout-of-order実行によって実行される、
本来の実行パスでは実行されるはずのない命令である。

以下に擬似コードを示す。
このコードは擬似コードであり実際には動作しない。

int main() {
  char arr[1];
  Exception e;
  throw e;
  arr[data * 4096]
}

この擬似コードarr[data * 4096]は本来throw eが例外を投げることによって実行が中断されるため実行されないのだが、
out-of-orderによってarr[data * 4096]のメモリアクセスが実行される。

Reading Privilleged Memory

以上のテクニックを駆使することによって特権がなくても特権が必要なメモリの値をロードすることができる。

コードを再掲する。

gist4ad97b3858f184ca74efbbfe54e845ef

  • まずはline 4mov命令を発行しrcxにあるカーネルメモリの内容を読み込む
  • line 5alに格納した命令にアクセスして依存を生成する
  • line 6は最適化に対する対応なので無視
  • line 7raxつまり、先程カーネルメモリの内容を読み込んだアドレスに対して間接参照をする。

これらの処理がμOpsに分解されout-of-orderによって、line 7line 5/6の命令完了待ち状態になり、
line 5で例外が発生するよりも先にline 7が実行される。
こうすることで例外が発生するよりも先に格納されたメモリにアクセスするすることが可能になる。

Side-Channel-Attack

しかし、上記の方法を利用してもout-of-orderの実行結果は巻き戻されて取得ができない。
ここでSide-Channel-Attackを利用する。
Side-Channel-Attackとは外からハードやCPUの状態を観測することで値を取得するという方法である。
今回のペーパーではFlush+Reloadという方法を利用していた。
Flush+Reloadについては以下

https://eprint.iacr.org/2013/448.pdf

この方法を使うことでline 7でのメモリアクセスを計測し、
キャッシュメモリを調べて、そのメモリアクセスにかかった時間を計測することでどのアドレスがキャッシュされているかを取得できれば、
秘密の値のみに依存したアドレスが取得できるようだ。

また攻撃手法として例外を握りつぶすためにプロセスのforkを行い、その中でこれらの攻撃を行い、
メインプロセスでメモリアドレスを調べる方法が提案されている。

Spectre

こちらはCPUの投機的実行という機能を利用したものでMeltdownと似ているが、より実行しやすい。

投機的実行 (Speculative Execution)

CPUは分岐命令(if文とか)を実行する際に、パフォーマンス最適化のために過去に分岐した履歴から分岐先を予想して、
条件分岐の結果評価が完了する前にthen節やelse節を実行する。
その後条件分岐が確定し、投機的実行した結果の値が必要なくなれば、その結果はそのまま捨てられる。
つまりMeltdownみたいに実行されてほしくないコードが実行されることがありうる。 ちょっと嫌な雰囲気になってきたね。

Side-Channel-Attack

やはりここでもCPUキャッシュの状態を観測することで値を取得する。
以下に擬似コードを示す。

if (x < array1_size)
  y = array2[array1[x] * 256];

以下はペーパーに記載されていたシナリオである。

ここでarray2[array1[x] * 256]x < array1_sizeが確定する前に実行されるとし、xは取得したいアドレスの値とする。
するとarray1[x]がキャッシュから取得され、アドレスkが手に入る。
その後array2[k]にアクセスすることで、キャッシュミスが起こり、その間にx < array1_sizeが確定することで、すべての結果が破棄される。
しかしarray2[k]への投機的実行はすでにCPUキャッシュに変化を起こしてしまっているのでそれを観測することで目的のデータを取得することができる。

Javascirpt

ペーパーによるとjavascriptでも実行できるためそれなりに危険そうである。
V8のデバッグシェルであるd8で実行して成功したようなので結構ダメそう。

Vendors

まとめ

ちょっと内容が複雑なため間違いを含んでいる箇所があったら申し訳ない!
とにかく基本は1次情報にあたってほしい。

より詳しい方、コメント等で修正があったらしていただけると助かります。

もう一度掲載しとく

Meltdown and Spectre