Mais conteúdo relacionado
Semelhante a 本当にわかる Spectre と Meltdown (20)
Mais de Hirotaka Kawata (10)
本当にわかる Spectre と Meltdown
- 2. 自己紹介
@hktechno 川田 裕貴 (かわた ひろたか)
● LINE 株式会社 開発1センター LINE 開発1室
○ メッセンジャープラットフォームの開発
○ 主に、スタンプ・絵文字・着せかえ、たまに IoT
● 自作 CPU 開発にハマっていた
○ アウト・オブ・オーダー実行できる自作 CPU
○ 主にコンパイラを作っていた
○ http://open-arch.org/
○ http://amzn.asia/gtThKbh
2
- 4. 本日の進め方
● CPU の基本的な仕組み: 60分
○ パイプライン、キャッシュ、投機的実行、分岐予測
○ Spectre / Meltdown の理解に必要な知識の説明
● Spectre / Meltdown の説明: 30分
● Spectre Variant 1 PoC を作ってみよう: 90分
○ Spectre Variant 1 の対策を考えてみよう
○ Spectre Variant 1 の考えた対策を実装してみよう
● Meltdown の PoC デモとか: 10分
● Spectre Variant 4 の説明: 10分
○ Foreshadow の説明も...?
4
- 6. Spectre と Meltdown について
CPU を対象としたサイドチャネル攻撃に対する脆弱性
● 2018年1月に Google の Project Zero などが共同で公開した
論文が発端
○ https://meltdownattack.com/
● オリジナルの手法は大きく分けて3つ (Variant 1,2,3)
○ Variant 3 のことを特に Meltdown と呼ぶ
● 現代の CPU アーキテクチャを巧妙に操ることにより、
サイドチャネル攻撃を可能に
○ 具体的には、メモリ内容の吸い取り
6
- 7. Spectre / Meltdown の手法の概要
公開当初、大きく3つの手法が公表された
https://googleprojectzero.blogspot.com/2018/01/reading-privileged-memory-with-side.html
Code Name 通称 CVE
Spectre
Variant 1 Bounds Check Bypass CVE-2017-5753
Variant 2 Branch Target Injection CVE-2017-5715
Meltdown
Variant 3 Rouge Cache Data Load CVE-2017-5754
7
- 8. Spectre / Meltdown その後
その後、同じような脆弱性がいくつも発見されている...
Code Name 通称 CVE
Meltdown Variant 3a Rouge System Register Read CVE-2018-3640
SpectreNG Variant 4 Speculative Store Bypass CVE-2018-3639
Foreshadow (-NG) L1 Terminal Fault
CVE-2018-3615
CVE-2018-3620
CVE-2018-3646
8
- 9. Spectre / Meltdown の手法の概要
条件分岐予測の悪用
分岐予測をトレーニングし、
メモリアクセスの境界値
チェックをバイパスして実行
例えば...
本来は読み取りが行われ
ないはずのメモリ領域の
データを、吸い出すことが
可能。
Spectre Variant 2
間接分岐予測の悪用
間接分岐予測テーブルの
挙動を解析し、分岐予測を
同じキーになるアドレスでト
レーニングした上で狙った
コードを実行
例えば...
本来は読み取りが行われ
ないはずのメモリ領域の
データを、吸い出すことが
可能。
Meltdown (Variant 3)
Out of order 実行の悪用
特権チェックバイパス
本来は例外が発生し実行さ
れないコードを、特権レベ
ルのチェックをバイパスして
実行
例えば...
本来は読み取ることができ
ないメモリ領域のデータを、
吸い出すことが可能。
Spectre Variant 1
9
- 11. Meltdown と Spectre の難しい点
よく CPU アーキテクチャを理解していないと、理解困難
● アセンブリが書けるだけでは、知識が足りない
● 繊細な CPU の気持ちを、よくわかっている必要がある
ハードウェア由来の脆弱性、ソフトウェアでの対策が困難
● 不可能ではないが、パフォーマンスの低下を伴う
● Spectre Variant 2 については、一部のアーキテクチャでは
ハードウェア支援がないと対策がとても難しい
11
- 12. サイドチャネル攻撃 - Flush + Reload
メモリアクセスのレイテンシの差を利用した攻撃
1. キャッシュをクリア (特に、以下の array2 の領域)
○ キャッシュを追い出す (Eviction) させてもいい
2. メモリを間接的にアクセスさせる
○ y = array2[array[x] * 4096]
○ array[x] のメモリ内容を基に array2 にアクセスする
○ 4096 をかけるのは、プリフェッチを防ぐため
3. array2 のアクセスレイテンシを調べる
○ キャッシュに乗っていればレイテンシ低⇓
○ 過去にアクセスされた可能性大 => 値がわかる
12
- 14. サイドチャネル攻撃 - Flush + Reload
array2
攻撃対象
data[x] = 0xa
array1
CPU
時間のかかる何か
if (Array 境界値チェック)
array1[array2[x]]
その先の何か
まだ終わってない
x は確定していた!
array[0xa] もアクセスできる!
キャッシュに乗る!
準備完了
先に実行だ!
14
- 15. サイドチャネル攻撃 - Flush + Reload
array2
攻撃対象
data[x] = 0xa
array1
CPU
array1 を Flush
時間のかかる何か
if (Array 境界値チェック)
array1[array2[x]]
その先の何か
array1 を Reload
array1 のキャッシュを消す
array1 のアクセスレイテンシをチェック
過去にアクセスしたことがあれば速い。
レイテンシの差で、array2 のデータがわかる...
15
- 19. 外の世界から見える CPU
CPU の違いってなんだろう?
● 命令セットの違い
○ ARM vs x86
○ 命令が増えた (e.g. AVX-512 とか...)
● クロック (2.4GHz, 3GHz, 4GHz…)
○ クロック数が変わってないから速度が変わらない?
● プロセス (14nm, 10nm, Tri-gate transistor…)
○ プロセスが細かくなったら何が良いの?
CPU って中身どうなってるの???
19
- 20. 基本的な CPU パイプライン (MIST32 - In order)
● 大学の授業で出てきそうな構成?
○ Instruction Fetch
○ Instruction Buffer
○ Instruction Decode
○ Dispatch
○ Execution
IB
20
- 21. Intel Skylake の内部
これが現実 ⇨
● 現代の CPU はとても複雑
○ これでも単純化済
● 命令の流れは一緒?
○ いろんなバッファ
○ 並列化
● 日々進化する内部構造
/(^o^)\ よくわからない
わかると楽しい\(^o^)/
21
- 23. Intel x86 の内部的な命令: uOps (Micro-Ops)
よく見る x86 の命令は、内部的な実行命令とは違う
● x86 アセンブラ1命令 => 複数の uOps
○ 最近は、逆の場合もある (Macro Fusion)
● なぜ命令を変換するのか?
○ x86 は CISC (命令がいろいろできるが複雑)
○ 時代・効率・回路規模は RISC
○ 設計が古いが、“互換性が重要”
○ 命令セットの設計に依存しない実行回路に
● Pentium III (P6) 以降に採用
23
- 24. Intel x86 のフロントエンド
デコーダが複雑
だが強い RISC に負けないように...
● 命令セットがクソ 設計が大変古い
=> uOps で解決
○ デコーダが大変複雑
○ 消費電力、回路規模に悪影響...
● デコーダをなるべく動かさない設計
○ 1度デコードしたところは、再度デコードしない
○ uOp Cache (以前は Trace cache)
○ Loop stream detector
24
- 25. Intel x86 における、マイクロコード (Microcode)
ある1命令を、プログラム可能な uOps に変換する仕組み
● 利用目的
○ ハードウェアの不具合 (エラッタ) の修正
○ 複雑な命令の
● ハードウェアは基本的に修正パッチを当てられない
○ 出荷した後は不具合が修正不可能
○ マイクロコードなら後からパッチを当てられる
● 利用例
○ Haswell における TSX 無効化
○ Spectre / Meltdown 対策における命令追加
25
- 26. Intel x86 の実行ユニット
1コアの中に、複数の並列実行可能な実行ポート
● スーパースカラ + 投機的実行
○ 投機的実行の詳細は後ほど詳しく
● Skylake では、実行ポートは8ポート
○ Haswell 以降増えた
https://www.anandtech.com/show/6355/intels-haswell-architecture/8
● ポートごとにできることが違う
○ ALU, FPU
○ Branch
○ Load / Store
26
- 27. Intel x86 のキャッシュとメモリアクセス
● L1, L2, LLC (L3) の 3-Layer キャッシュ
○ L1 は Instruction, Data の2つのキャッシュ
● キャッシュラインは 64 byte
○ L1, L2: 8-way セットアソシアティブキャッシュ
○ 基本的にこの単位で、領域がキャッシュされる
● ページサイズは、基本的に 4096 byte
○ ページ境界を超えてプリフェッチはされない
27
- 29. Meltdown / Spectre がなぜ起きるのか
現代の CPU が命令を実行する際に行う、高度なハードウェアレ
ベルでの最適化が原因
● 特に、”投機的実行 (Speculative Execution)” と呼ばれる、命
令を先読みして高速化する手法が問題
CPU 内で行われる投機的実行の例
● 命令のプリフェッチ
● ☆ アウト・オブ・オーダー実行 (順序を守らない実行)
● ☆ 分岐予測器を組み合わせた投機的実行
29
- 32. アウト・オブ・オーダー実行
(Out of Order Execution, OoO)
先読みした命令の依存関係を解析して、実行できる命令から実際
の記述順序を無視して先に実行する
● 命令間のレジスタ依存関係を動的に解析する
○ 例えば、即値ロード命令は他の命令と依存関係がない
○ 即値ロード命令の例: mov eax, 0x1
● CPU は、複数の命令実行ポートを持っている
○ すべての命令が同時に実行完了するとは限らない
● 依存しているレジスタの結果が準備されたら、先に実行
○ レジスタマッピングや、書き戻しのバッファリングにより整
合性を保つ
32
- 34. アウト・オブ・オーダー実行の例
mov eax, [eax]
xor ebx, ebx
add ebx, eax
inc ecx
add eax, ecx
1
1
2
1
2
⇓実行順
命令の順番を入れ替えても構わない
しかも、開いてる実行ポートで並列に実行できる
Load (遅い)
↑の命令とは依存がない
先に実行が可能
↑の命令とは依存がない
先に実行が可能
34
- 35. OoO に関する疑問・破壊する常識
● あるレジスタ (eax や ebx) は CPU 内に1つじゃないの?
○ レジスタ・リネーミングを利用しています
○ 実はすべてただのエイリアスです
● CPU 内部で ”同時” に実行できる命令は1つじゃないの?
○ 複数の命令実行ポートが並列に並べられています
○ 例えば、Haswell 以降なら8実行ポート
● メモリアクセスを行う命令も並列に実行するの?
○ Intel の最近の CPU はメモリアクセスも OoO です
● なんでコンパイラでやらないの?
○ 成功していたら Itanium は死んでいない (VLIW)
35
- 36. アウト・オブ・オーダー実行を支える技術 - 1
レジスタ・リネーミング (Register Renaming)
● 物理レジスタを、内部の仮想レジスタにリネームする
● 命令の依存をより少なくできる
mov eax, [eax]
inc eax
mov [eax], eax
mov eax, ebx
mov eax, [eax]
同じ eax レジスタ
しかし、依存はない
実は先に実行可能
36
- 37. アウト・オブ・オーダー実行を支える技術 - 1
レジスタ・リネーミング (Register Renaming)
● 内部的なレジスタファイル内の物理レジスタと、論理レジスタ
(eax, ebx…) との対応を持つ
○ 論理レジスタの数倍の物理レジスタを持っている
mov r1, [r1]
inc r1
mov [r1], r1
mov r3, r2
mov r3, [r3]
レジスタ依存はない
実は先に実行可能
37
- 38. レジスタ・リネーミング (Register Renaming)
まとめると...
● Intel x86 のレジスタ少なすぎない?
○ どうせリネーミングするからあまり問題にならない
● 内部的なレジスタファイルの大きさ (Skylake)
○ Integer 180個
○ Vector 168個
● リネーミング時に依存性の排除を行う
○ 0 や 1 の即値やクリアは特殊な扱いをする場合も
○ Zeroing Idioms, Ones Idioms
38
- 39. アウト・オブ・オーダー実行を支える技術 - 2
Tomasulo のアルゴリズム
● Reservation Station
○ 命令の依存関係を待つところ
○ ソースレジスタが利用可能かを監視して、 実行可能であれ
ば実行を開始する
○ Unified な方式、実行ポートごとに持たせる方式
● Re-order Buffer
○ 命令のコミットを制御するバッファ
○ 演算の結果は一度ここへ書かれる
※ 実際の Intel の CPU がこの通り実装されているわけではない
39
- 41. Reorder Buffer を使ったデータの流れ
Reorder Buffer Stage
1. 割り当て
Writeback Stage
2. 結果の書き込み
Retire / Commit
Reorder Buffer
Register File
3. コミット
※ 実際の Intel の CPU がこの通り実装されているわけではない 41
- 43. Reservation Station & Reorder Buffer
まとめると...
● CPU 内部では Reorder Buffer と Reservation Station の仕
組みにより、命令の実行順序が入れ替わる
● メモリアクセスも順番が入れ替わることがある
Skylake における実装
● Reorder Buffer 224 エントリ
● Reservation Station 97 エントリ
43
- 46. 分岐予測の例 - 簡単な分岐命令
mov eax, [eax]
cmp eax, 0
je is_zero
...
...
is_zero:
...
...
実行完了するまで
Jump するかわからない
Load (遅い)
Block 1
Block 2
Block1 or Block2
どちらかを投機的に実行したいが、高
確率で正解を選びたい
基本的な if 文であれば PC 相対ジャンプであることが多い
46
- 48. 分岐予測器 (Branch Predictor)
古典的な実装
● 分岐しない (or する) 方向で常に予測しておく
○ x86 だと Pentium 以前の CPU はこれ (i486 とか)
● 飽和カウンター (Bi-modal Counter)
○ 4つのステートを持って、1度ミスしただけでは予測方向が
変わらないような方式
○ ループの分岐予測には有効
48
- 49. 分岐予測器 - 最近の CPU の実装
いくつかの方式の組み合わせで、あらゆるパターンでも予測
● 局所的分岐予測
○ 同じアドレスの命令の、過去の分岐履歴を利用
○ 同じアドレスで、同じパターンを繰り返す場合有用
● グローバル分岐予測
○ 過去のコア全体の分岐履歴のパターンをもとに、次の分岐
の挙動を予測
● 上記の組み合わせを bit 演算で行い、予測テーブルを引く
○ PC + 局所履歴 + グローバル履歴
49
- 50. 分岐予測の例 - 絶対アドレス間接ジャンプ
(Indirect Branch)
レジスタに格納されたアドレスにジャンプする場合
● 例えばjXX [eax], call [eax]
● レジスタに格納されたアドレスが確定するまで、投機的実行が
できない
○ これまでの手法は、PC 相対 + 即値ジャンプの場合
○ 間接ジャンプで、特にLoad が噛むととても遅い
● 間接ジャンプ先も、実は予測可能では?
○ 同じ PC のジャンプは、よく同じアドレスにジャンプするので
はないか?
※ PC (Program Counter): Intel では IP (Instruction Pointer)
実行中命令のアドレスのこと
50
- 52. Branch Target Buffer (BTB)
ブランチ命令のジャンプ先アドレスを記憶しておくバッファ
● 同じ PC のジャンプは、よく同じ場所に飛ぶことが多い
○ 間接的 (Indirect branch) であっても同じ
○ これをキャッシュしておけば、投機的実行を実際のアドレス
確定前に行うことが可能
● PC => ブランチ先 PC のバッファを持つ
○ 最近の Intel x86 CPU の場合...
○ PC を XOR で圧縮して、BTB のキーとする
○ キー内の一部の bit は他の bit と共有される
○ つまり、エントリを共有する場合がある
52
- 53. BTB の構造 - Intel x86 Direct Branch Prediction
下位 31bit + XOR Folding でビット演算をして Lookup
● 下位 13 bit はそのまま
● 残りの 18bit に対して謎の XOR Folding を行う
○ 下記表参照 ⇨
○ 規則性...?
● エントリの内容
○ 下位32bitのアドレス
○ 4GB (2^32) を超えてジャンプ
するかどうか
○ 上位 32bit のアドレス
bit A bit B
0x40.0000 0x2000
0x80.0000 0x4000
0x100.0000 0x8000
0x200.0000 0x1.0000
0x400.0000 0x2.0000
0x800.0000 0x4.0000
0x2000.0000 0x10.0000
0x4000.0000 0x20.0000
53
- 54. 余談: なぜ BTB は頑張ってビット幅を減らすのか
連想メモリ (CAM) は回路規模が厳しいから (多分)
● 大きな bit 列を比較 == 大きな規模のコンパレーター
○ エントリ数 * bit 幅に比例して大きくなる
○ 半分のビット幅にできたら、エントリが沢山入る
○ ただ、比較をするための回路でも馬鹿にできない
● ハードウェア上に大きな連想メモリを置くのは難しい
○ キャッシュ, TLB, BTB…
○ いろいろなものが、CAM の制限を受けている
○ 回路を作ってみるとわかるかも?
54
- 56. BTB の構造 - Intel x86 - Indirect Branch
Prediction
複数ある分岐予測先を予測するために...
● Branch History Buffer (BHB) を使う
○ 分岐元下位ビットを Branch buffer state に利用
○ Branch buffer state は、右シフトしつつ新しい情報が加え
られる
● 過去の分岐履歴が変わると、BTB エントリが別になる
○ 分岐履歴により、複数の分岐予測先を持てる
56
- 57. Branch History Buffer の挙動 (予測)
void bhb_update(uint58_t *bhb_state, unsigned long src,
unsigned long dst) {
*bhb_state <<= 2;
*bhb_state ^= (dst & 0x3f);
*bhb_state ^= (src & 0xc0) >> 6;
*bhb_state ^= (src & 0xc00) >> (10 - 2);
*bhb_state ^= (src & 0xc000) >> (14 - 4);
*bhb_state ^= (src & 0x30) << (6 - 4);
*bhb_state ^= (src & 0x300) << (8 - 8);
*bhb_state ^= (src & 0x3000) >> (12 - 10);
*bhb_state ^= (src & 0x30000) >> (16 - 12);
*bhb_state ^= (src & 0xc0000) >> (18 - 14);
}
57
- 58. 余談: Return Stack Buffer (RSB)
call, ret 命令に特化したスタックバッファ
● call や ret も間接分岐命令だが...
● ret 命令は、直前の call の次の命令へジャンプ
○ call, ret は常にペアになっていることが多い
○ スタックへのアクセスは Load なのでとても遅い
● call 時に、アドレスをどこかに記憶しておけば...
○ => Return Stack Buffer
○ ret 命令後の投機的実行が高速に可能
○ 複数の return address をスタックでキャッシュする
58
- 59. 余談: 現代の CPU の分岐予測器の進化
分岐予測器の進化は、最近でも続いている (というかアツい)
● 電力をそれほど消費せずに性能向上が可能
○ 分岐予測のミスは大きなペナルティ
○ ブランチ命令の先のパイプラインが止まってしまう
● ニューラルネットワーク分岐予測機
○ AMD の CPU? Intel も?
● 各社詳しい仕様は公開していない
○ 改良されたという事実のみが記載されている
○ 外側から観察して挙動を推測するのみ
59
- 63. Spectre Variant 1 - 大まかな流れ
cache_flush_array2(); // flush
for (N) read(training_x); // training
read(x + malicious_x); // attack
check_array2_latency(); // reload
---
void read(x) {
if (x < array1_size)
y = array2[array1[x] * 4096];
}
63
- 64. Spectre Variant 1 を利用した攻撃の影響範囲
実行中の権限で読めるすべてのデータを読み取り可能
● Intel 以外の CPU でも多く発生
● ページテーブルに乗っていれば何でも読める
○ マップされてない領域は読めない
○ 特権昇格は不可能
● コードを流し込めればいいので、うまくいくと...?
○ 仕組みが単純なので、色んな所に流し込める
○ カーネル空間に対して、eBPF という抜け道
○ JavaScript 上で Spectre できたら?
64
- 67. Spectre Variant 2
BTB と分岐履歴をコントロールして、上手に特定のアドレスの命
令を投機的実行させる
● BTB は、コア内で共通である
● 同じコア内の違うプロセス・スレッドで、うまいこと BTB と分岐
履歴を訓練することができる
○ 例えば、SMT (HT) で動作している場合など
○ キーは、アドレス下位ビットが XOR 圧縮される
● その中でアクセスしたデータはキャッシュに残る
○ もしかしたらデータを抜き出せるかもしれない
○ もちろん、レジスタは破棄される
67
- 68. Spectre Variant 2 - BTB Injection
BTB・BHB をうまく騙して、共有させる
● まずは BTB・BHB を訓練
○ 投機的実行を騙して、実行させたいフローを再現
● BTB のエントリは、共有される
○ 簡単にコントロールすることができる
● 例えば Direct branch prediction BTB の場合
○ e.g. 0x100.0000 と 0x140.2000 はエントリが一緒
■ ※ BTB の解説ページ参照
■ 0x40.0000 と 0x2000 は XOR される
○ しかし、Direct branch BTB injection は成功率が低い?
68
- 69. Spectre Variant 2
うまいこと BHB を訓練させ、BTB を引く
● BHB は、過去の分岐履歴を保持する
● 分岐履歴を訓練させることは可能
○ また、BHB はうまくやると 0 の状態を作れる
■ BHB の挙動スライドを参照
■ このビット演算は XOR とシフトなので、反転してシフトしたものを流し込む
と 0 にすることが可能
○ その状態から、ジャンプ先アドレスを推測することも可能
● 訓練は、他のプロセスから行える
○ うまく攻撃対象のコードへ誘導できるかも
69
- 71. Meltdown - Spectre Variant 3
ユーザー権限で任意のメモリ領域の内容を推測可能な脆弱性
● このシリーズの中で最もリスクが高い (個人的な感想)
● カーネル空間に物理メモリがそのままマップされている
○ Linux (x86_64) の場合 0xffff880000000000
○ 通常は、ユーザーモードから読むことは不可能
● 投機的実行と CPU の特権レベルの関わりが影響
○ 特権レベルを無視して CPU が投機的実行を行う
○ 投機的実行を行ったあとに、特権違反により例外発生
○ キャッシュの内容は...?
○ Intel の CPU のみで発生
71
- 72. Meltdown の裏側 - 投機的実行
コードでは3行で説明できるぐらい簡単
● 裏で何が起きているのかを説明するのが難しい
...
raise_exception(); // 実際はここまで
// 以下の行には到達しない, でも OoO で実行されてる
access(probe_array[data * 4096]);
CPU の投機的実行は何を行うのだろう?
72
- 73. Meltdown の裏側 - 投機的実行
kernel_adder の内容を Flush+Reload で推測する例
raise_exception();
access(probe_array[*kernel_addr * 4096]);
投機的実行により、kernel_addr と proble_array の load が
raise_exception() より先に行われる
● 特権レベルの確認より先にメモリの読み取りが行われる
● proble_array を Flush + Reload で *kernel_addr を吸い出し
73
- 74. Meltdown の裏側 - 投機的実行
Meltdown のアセンブリの例
; rcx = kernel address
; rbx = probe array
retry:
mov al, byte [rcx] ; 実際はここで例外
shl rax, 0xc
jz retry
mov rbx, qword [rbx + rax]
74
- 75. 仮想アドレス空間のメモリマップ - Linux x86_64
物理メモリすべての領域が 0xffff880000000000 からマップ
● ユーザー領域は仮想アドレス空間の先頭に配置
● 後半部分に、物理メモリすべてがストレートマップ
○ つまり、物理メモリに乗っていれば、すべての
ユーザー空間・カーネル空間の領域にアクセス可能
75
- 78. Spectre / Meltdown まとめ
高度で複雑な CPU 高速化によって、思いがけない問題が
● 一般的なコンピューター全てで動作するという怖さ
○ 成功率・実現性などを考えたとしても、できる可能性がある
ということを、見逃してはいけない
● CPU を作っている人たちはどう思っていただろう?
○ 問題として捉えていたのかどうか
○ 自分だったら、見逃してしまっていたかも...
○ とはいえ、Meltdown は明らかに問題がある
● この後も、様々な変種が発見されていく...?
○ セキュリティ技術者にとってはチャンス?
78
- 80. Spectre Variant 1 の PoC を作ってみよう
同じプログラム内の配列を直接読み出さずに Spectre する
● これまでの仕組みを利用して、”CPU を騙してみよう”
○ 理解してしまったら単純な仕組みだが...
○ 書いてみたらちょっと難しい
○ PoC を書いてみたからわかることも
● 何個か実装にあたって気をつけない点が
○ CPU の気持ちがわからないといけない
○ 投機的実行のデバッグはほぼ不可能
80
- 81. 実装のコツ - Flush + Reload
● 必要な配列
char *secret; // 攻撃対象の文字列
uint8_t array1[適当な長さ]; // ダミー配列
uint8_t array2[256 * ???]; // Flush+Reload用
● 気をつけること: キャッシュライン・プリフェッチ
○ array1 と array2 の配置
○ array2 の長さ: char == 8bit == 256
■ それぞれの要素がキャッシュに乗る乗らないを判別するには?
81
- 82. 実装のコツ - Flush
● Flush には Intrinsic を使う
○ 特殊なアセンブリを実行する組み込み関数
● その他利用すると便利な Intrinsics
○ #include <x86intrin.h>
○ _mm_clflush(addr)
■ 特定アドレスのキャッシュをフラッシュ
○ __rdtscp(&junk);
■ クロックサイクル単位の高精度タイムスタンプカウンタを読む
■ Reload で利用
○ _mm_mfence();
■ メモリフェンス命令
■ メモリアクセスが、必ずこの命令のあとで実行される
82
- 83. 実装のコツ - Reload
メモリアクセスのレイテンシを読み取る例
int junk = 0;
uint64_t time1, time2;
time1 = __rdtscp(&junk);
junk = *addr;
time2 = __rdtscp(&junk) - time1;
● junk が最適化でいなくならないように
○ どこかでなにか使ってあげてください
● addr は、必ず volatile をつけること
○ addr のメモリを必ず読んで、レイテンシを測るため
83
- 84. 実装のコツ - 分岐予測
if (x < array1_size)
y = array2[array1[x] * 4096];
● 正しい分岐を何度か成功させ学習させる
○ array1_size 以下の x を利用して “何度か” 実行
○ array1_size 以上の “非正規な” x を利用して実行
● コンパイラの最適化で消されないように
○ y は何も操作しない && ローカル変数では
○ y はメモリをいじるように volatile
○ なにか演算をさせたほうが良い
84
- 85. 実装のコツ - 分岐予測
if (x < array1_size)
y = array2[array1[x] * 4096];
● 分岐予測 + 投機的なメモリアクセスが成功する条件
○ 分岐予測後メモリアクセスが完了するまでに、
条件分岐が実際に確定してはいけない
○ if の前に何らかの時間稼ぎが必要
● 時間稼ぎに使えそうなもの
○ 適当なメモリアクセス (条件を確定させないためには?
○ 適当な膨大な量の計算
○ OoO で Reorder・LOAD されることも考慮にいれて...
85
- 86. 実装のコツ - 分岐予測
学習ループ内に条件分岐を生やすとうまく行かないことも
● 例えば、実際に攻撃対象の非正規 x を指定するときなど
● ループから出るのも条件が変わるので微妙
● 以下のような技も必要かも
// if (j % 6 == 0) x = malicious_x;
// else x = training_x;
x = ((j % 6) - 1) & ~0xFFFF;
x = (x | (x >> 16));
x = training_x ^ (x & (malicious_x ^ training_x));
86
- 87. Spectre Variant 1 の PoC を作ってみよう
すぐに終わって先に進みたい猛者たちに...
● コンパイラの最適化を有効にしてみよう
○ -O1 多分そのまま動く/ -O0 より成功率高い?
○ -O2, -O3 ちょっと考えないと難しい
● パラメーターをいじったりループ回数を変更して、投機的実行
の結果・CPU の動作を観察してみよう
○ 分岐予測の訓練回数を減らす・増やす
○ メモリアクセスを減らす・増やす
○ array2 のオフセットを減らす・増やす
87
- 88. Spectre / Meltdown 対策を考えてみよう
コンパイラ・CPU・カーネル... どんな対策でも OK
● Variant 1 ★☆☆
○ 境界値チェックが投機的実行されても安全な方法は?
○ パッチを当てて、確かめてみよう
● Variant 2 ★★★
○ 間接ブランチ命令を使わないでジャンプ...?
● Meltdown / Variant 3 ★★☆
○ ユーザー空間とカーネル空間の分離...?
※ 答えを知ってる人は、それを自分なりに説明してみよう
88
- 90. Spectre Variant 4 (Spectre NG)
Speculative Store Bypass (投機的ストアバイパス)
https://blogs.technet.microsoft.com/srd/2018/05/21/analysis-and-mitigation-of-speculative-store-bypass-cve-2018-3639/
● LOAD 命令は投機的に実行される
● 手前にある STORE のアドレスが確定していなくても、LOAD
は先読みされる
mov [rdi+rcx],al
movzx r8,byte [rsi+rcx]
shl r8,byte 0xc
mov eax,[rdx+r8]
90
⇦ これはどうなる?
- 91. Spectre Variant 4 (Spectre NG)
STORE のアドレスが確定したあと...
● 以下の条件の場合、movzx が先に実行されてしまう
○ ”rdi” が未確定、“rsi” が確定済み
○ しかし、rsi と rdi が最終的に同じだったら?
mov [rdi+rcx],al
movzx r8,byte [rsi+rcx]
shl r8,byte 0xc
mov eax,[rdx+r8]
91
- 92. Foreshadow, Foreshadow-NG NEW!
L1 Terminal Fault を利用したメモリ内容の抜き取り
● ページテーブルエントリ (PTE) は P bit (present) のような、有
効か無効かという情報を保持している
○ もし、PTE が無効なら Terminal fault が発生
● Intel の CPU はこのような Fault が発生するとしても、先に L1
へアクセスしに行く
○ 無効な PTE を使うかも
○ 高速化のため
○ 結果は捨てるが...
○ Page walk の時間内に...
92
- 93. 本当にわかる
Spectre と Meltdown 対策編
川田裕貴 @hktechno
93
セキュリティ・キャンプ全国大会2018
講義資料 URL: https://goo.gl/uTwX2c
PoC コード: https://goo.gl/Uozhy6
- 95. Spectre Variant 1 対策
array の index のビットをマスクする
● array の外側にアクセスできないようにすればいい
○ ブランチ命令はいくらでも学習可能なので使えない
○ 投機的実行が起きても問題ないように
● ビット演算のみで行う
○ パフォーマンス低下もそれほどではない
○ cmp size,index; sbb mask,mask; => x と AND
● Linux Kernel : array_index_nospec
○ https://github.com/torvalds/linux/blob/v4.17/include/linux/nospec.h
○ https://github.com/torvalds/linux/blob/v4.17/arch/x86/include/asm/barrier.h
95
- 96. Spectre Variant 1 対策
既存のもの (CPU, ソフトウェア) に対する対策はほぼ不可能
● CPU の microcode だけで治せるような問題ではない
● Intel 以外の CPU でも存在
○ 高速化のためにみんなやっていること
○ 問題があることにどのベンダーも気づいていなかった
○ 将来的にはどうなるのかまだわからない...
● ソフトウェア側では対応可能だが...
○ コンパイル済みのものは不可能
○ コンパイラが、コンパイル時に行うのも難しい
○ 手でコードを入れるしかない
96
- 97. Spectre Variant 1 対策
JavaScript や eBPF に対する対策
● 根本的な対策ではない
● JavaScript
○ JavaScript 上でも、仕組み的には Spectre できる
○ Timer の精度を下げると Spectre しづらくなる
● eBPF
○ 無効にする
○ Spectre に使われそうな部分に、きちんと対策をしておく
97
- 99. Spectre Variant 2 対策
Indirect branch が BTB を共有するのが問題だった
● ハードウェアによる対策 (microcode update)
○ 命令の追加: IBRS, IBPB, STIPB
○ IBRS: BTB の利用の抑制する命令
● Indirect branch を使わなければ良い
99
- 100. Spectre Variant 2 対策 - Retpoline
ROP のような手法を使って、投機的実行を騙す
ret は RSB (Return Stack Buffer) を利用して投機的実行する
call retpoline_call_target
2: /* 投機的実行はこっちに行く */
lfence /* stop speculation */
jmp 2b
retpoline_call_target:
lea 8(%rsp), %rsp /* スタック操作 */
ret /* 2: には戻らない */
100
- 101. Spectre Variant 2 対策 - Retpoline
しかし、Retpoline にも問題があることがわかった
● Return Stack Buffer (RSB) が枯渇したとき
○ BTB を引きにいく場合がある
○ どうも、Skylake 以降でこの修正が入った?
● うまいこと RSB を枯渇させられると、死んでしまう
101
- 103. Meltdown 対策
Kernel Page Table Isolation : KPTI
● カーネルページテーブルの分離
○ ユーザープロセスのページテーブルから、カーネル領域が
見えないように
○ Linux では KAISER という仕組みを実装
● パフォーマンス低下の問題
○ 最近の Intel CPU (Haswell 以降) では、緩和される
■ PCID のサポートの有無
○ 新しい Kernel + CPU を使っていれば問題ない
○ 古い CPU では、パフォーマンス低下が大きめ
103
- 104. TLB (Translation Lookaside Buffer) と
ページテーブルの関係
ページテーブルをキャッシュするものが TLB
● ページテーブルを引く (page walk) はとても遅い
● よくアクセスする領域は TLB にキャッシュする
○ ページテーブル切替時に、TLB はフラッシュされる
● KPTI を行うことで、カーネルを呼ぶたび TLB フラッシュ
○ とても遅くなる
104
これまで KPTI
User -> Kernel いらない TLB フラッシュ必要
プロセス切替 TLB フラッシュ必要 TLB フラッシュ必要
- 105. KPTI と PCID
PCID (Process Contex Identifers) により性能低下を抑制
● KPTI の問題: TLB が syscall でフラッシュされること
● プロセスごとに PCID を設定
● PCID を TLB にタグ付け、指定したタグだけフラッシュ
○ INVPCID 命令
○ PCID, INVPCID は、比較的最近 x86 に追加された機能
105
仮想アドレス タグ (PCID) 物理アドレス
0x10000000 100 0x00001000
0x10000000 200 0x00002000
0x20000000 100 0x00003000
- 106. PCID のメリット
PCID を導入すると、部分的に TLB フラッシュが可能
106
これまで KPTI KPTI + PCID
User -> Kernel いらない 全フラッシュ 一部フラッシュ
プロセス切替 全フラッシュ 全フラッシュ 一部フラッシュ