(自分メモ)Meltdown・SpectreのGoogle Project Zeroの報告内容について

Meltdown・Spectreについて報告されている

の内容について、自分なりに理解した自分メモを記録として残しておきます。


以下、基本的に1次情報であるProjectZeroの報告の自分なりの和訳&要約です。


脆弱性の番号と個別報告資料(PDF)

Spectre

Variant 1: bounds check bypass (CVE-2017-5753)

分岐予測はIntelのデータシートで以下のように説明されている

Branch prediction predicts the branch target and enables the processor to begin executing instructions long before the branch true execution path is known.

→ 分岐予測は、分岐ターゲットを予測し、プロセッサが、分岐真の実行パスが分かるずっと前の命令の実行を開始することを可能にする。


要するに、分岐予測では、条件分岐の判断が行われ、実際の実行パスが分かるずっと前から条件分岐後のコードを予め実行しておける、ということ


また、L1 DCache の説明には、

  • ロードは以下が行える
    • 先行した分岐が解決される前にロードを実施されることができる
    • キャッシュミスをアウトオブオーダー、あるいはインオーダー(Overlapped manner?)で取ることができる


また、intel P6以降のプロセッサには「暗黙的キャッシング」がある

暗黙のキャッシングは、通常のフォンノイマンシーケンスでアクセスされたこ
とはないが、メモリ要素が潜在的にキャッシュ可能にされたときに発生する。
積極的なプリフェッチ、分岐予測、およびTLBミス処理のために、暗黙のキャッ
シングがP6以降のプロセッサフ​​ァミリで発生します。 暗黙的キャッシングは、
既存のIntel386、Intel486、およびPentiumプロセッサー・システムの動作を
拡張したものです。これらのプロセッサー・ファミリーで実行されるソフトウェ
アも、命令プリフェッチの動作を確定的に予測できませんでした。

要するに、実際の命令実行結果にかかわらず、積極的にデータキャッシングを行うということ
→ 実際には、事前の条件分岐でデータアクセスを行う当該命令には至らないにもかかわらず、先読みでデータ読み出しを行ってしまう


以下コードを考えた時、

struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = ...;
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 ...
}

実際には untrusted_offset_from_caller >= arr1->length だとしても、
arr1->length がキャッシュされていない場合、プロセッサはこの分岐を解決する前に、
arr1->data[untrusted_offset_from_caller]を投機的にロードすることができる。


ただ、これだけの場合、実際に条件分岐が解決された際にプロセッサが実行状態をロールバックするため、
問題にはならない。


しかし、以下のコードには問題がある。

struct array {
 unsigned long length;
 unsigned char data[];
};
struct array *arr1 = ...; /* small array */
struct array *arr2 = ...; /* array of size 0x400 */
/* >0x400 (OUT OF BOUNDS!) */
unsigned long untrusted_offset_from_caller = ...;
if (untrusted_offset_from_caller < arr1->length) {
 unsigned char value = arr1->data[untrusted_offset_from_caller];
 unsigned long index2 = ((value&1)*0x100)+0x200;
 if (index2 < arr2->length) {
   unsigned char value2 = arr2->data[index2];
 }
}


プロセッサが「untrusted_offset_from_caller < arr1->length」を真と予測した場合、
arr1->data[untrusted_offset_from_caller] が投機的にキャッシュへロードされ、
((value&1)*0x100)+0x200 の計算結果に応じて arr2->dataは、arr2->data[0x200] あるいは arr2->data[0x300] いずれかのオフセットからロードを開始し、
対応するキャッシュラインをL1キャッシュにロードする


プロセッサがuntrusted_offset_from_caller >= arr1->lengthだと気づくと、
実行が非推測パスへ戻されるが、arr2->data[index2] を含むキャッシュラインはL1キャッシュにとどまる(これがプロセッサバグ?)
そのため、この後に arr2->data[0x200] と arr2->data[0x300] のロードにかかる時間を計測すると、
いずれか一方はキャッシュにのっているためアクセスが早く、もう片方はキャッシュにのっていないためアクセスが遅い
となり、攻撃者は投機実行中のindex2を推測でき、arr1->data[untrusted_offset_from_caller]の最下位ビットが0か1かを推測できる。


実際にこれを攻撃に転用するには、攻撃対象の環境で上記の様なコードを実行させる必要がある。
JITコンパイラなどが考えられる。
PoCではeBPFインタプリタまたはeBPF JITエンジンを使用した。これはカーネルに組み込まれており通常のユーザーからアクセス可能。

Variant 2: branch target injection (CVE-2017-5715)

variant 2のための我々のPoCの理論背景を説明する。
variant 2は、Intel Haswell Xeon CPU上のvirt-managerを使用して作成されたKVMゲストの中でroot権限で動作中で、
ホスト側は特定のバージョンのDebianカーネルが動作しているとき、
約1500bytes/secondでホストのカーネルモリーを読むことができる、というもの


先行研究[*1]で、別々のセキュリティコンテキスト内のコードがお互いの分岐予測に影響を与える可能性があることがわかっている。
この問題はこれまで、コードがどこにあるかを推測するためにのみ使用されていた。(つまり、被害者から攻撃者への干渉を引き起こす)
しかし、この攻撃の変形での基本的な仮説は、犠牲者のコンテキストでコードの実行をリダイレクトできるということ
(言い換えれば、攻撃者から犠牲者への干渉を作成するために、他の方法で使用することもできるということ)


[*1] ASLR leak
これ?
http://inaz2.hatenablog.com/entry/2014/05/02/231433


この攻撃の基本的な考え方は、ターゲットアドレスがメモリからロードされ、
ターゲットアドレスを含むキャッシュラインをメインメモリにフラッシュする間接分岐を含むターゲットコード
をターゲットにすること。
CPUが間接分岐に達すると、ジャンプの真の宛先がわからず、キャッシュラインのCPUへのロードが終了するまで真の宛先を計算できなくなる(その間、数百サイクル)。
したがって、典型的には100サイクルを超える時間ウィンドウがあり、CPUは分岐予測に基づいて命令を投機的に実行する。

Meltdown

Variant 3: rogue data cache load (CVE-2017-5754)

詳細は以下を参照
https://cyber.wtf/2017/07/28/negative-result-reading-kernel-memory-from-user-mode/


要約すると、この問題のこの亜種を使用した攻撃は、カーネルコードの制御フ
ローを誤って指示することなく、ユーザー空間からカーネルメモリを読み込も
うとします。 これは、以前のバリアントで使用されたが、ユーザー空間で使
用されたコードパターンを使用することによって機能します。 基本的な考え
方は、アドレスへのアクセスチェックが、メモリからレジスタへのデータ読み
取りのクリティカルパス上にない可能性があります。この場合、パーミッショ
ンチェックがパフォーマンスに大きな影響を与える可能性があります。 その
代わりに、メモリ読取りによって、読取り結果が後続の命令ですぐに使用可能
になり、許可検査が非同期的に実行され、許可検査が失敗した場合に例外が発
生するようにするリオーダ・バッファにフラグを設定する。


上記URLによると、結局はVariant 1と同じ


理論としては、Intel CPUでは命令はマイクロコードへ分割され、
マイクロコード単位で扱うことで内部的にはRISC的に振る舞えるようになっている。
その際、マイクロコードはリオーダーバッファというバッファへ格納され、
その中で命令間の依存関係を崩さない範囲で、高速化を目的とした命令順の入れ替えが行われる。


ここでこのVariantのミソとなるのが、
「リオーダーバッファ内で発生した例外は、一連のリオーダーバッファの実行が完了したあとで処理されるのではないか」
という仮説である。


その場合、以下の命令列を考えた時、

Mov rax, [somekerneladdress]
And rax, 1
Mov rbx,[rax+Someusermodeaddress]

1つ目の命令で例外が発生するのだが、リオーダーバッファ内で3つ目の命令まで実行された後に、
ロールバックするというVariant 1と同様の挙動が考えられる。
その場合、Variant 1同様にL1キャッシュにはデータが残っているため、
ロールバックした後であっても、データアクセスにかかる時間を比較することで上記命令列でカーネルアドレスからロードしたraxの最下位ビットは推測できてしまう。


上記記事の筆者はより確度を上げるために以下の取り組みをしている。

prefetcht0命令を使用して、アドレスがL1にロードされる確率を向上させます。
Grussら[4]は、プリフェッチ命令がアクセス権を持たないにもかかわらずキャッシュをロードする可能性があると結論付けた。

ntellのTomasuloアルゴリズム(リオーダーバッファ内の実行のアルゴリズム)の実装について
私が作ったいくつかの前提があります。
1)割り込みフラグにもかかわらず投機的実行が継続する
2)私は投機的実行とリタイア(例外を検知しリオーダーバッファを捨てる)の間の競争状態に勝つことができる
3)投機的な実行中にキャッシュをロードできます
4)割り込みフラグにもかかわらずデータが提供される


筆者の実験は成功する場合と失敗する場合があったとのことで、
「確実に読み取りができる」というたぐいのものではないが、
「Tomosuloのアルゴリズムが、実際には例外/割り込みが発生しているにもかかわらず投機実行を続けている」ことは証明できており、
「サイドチャネルに弱い」ことは示されてしまった(投機的実行をしてしまっている以上、実行した命令がメモリアクセスを含むならキャッシュへのロードも行われてしまっている)。