図解でわかるSpectreとMeltdown

 図解でわかるSpectreとMeltdown

A brief explanation of spectre(variant 1) and Meltdown(variant 3)

842515eaf8fbb2dfcc75197e7797dc15?s=128

Satoru Takeuchi

February 17, 2018
Tweet

Transcript

  1. 図解でわかる SpectreとMeltdown Feb. 17. 2018 Satoru Takeuchi Twitter ID: satoru_takeuchi

    1
  2. はじめに • 脆弱性の種類と、この資料の説明範囲 • キャッシュを使ったサイドチャネルアタックによる不正なデータ読み出し • ソフトウェアではなくCPUの高速化機能にまつわる脆弱性 ◦ それぞれ脆弱性のある CPUのリストが公開されている

    2 コードネーム 別名 しくみ 対策 Spectre Variant 1: Bounds Check Bypass ✔ Variant 2: Branch Target Injection Meltdown Variant 3: Rogue data cache load ✔ ✔
  3. もくじ • 前提知識 • Variant 1のしくみ • Variant 3のしくみ •

    Variant 3の対策 3
  4. 前提知識 • サイドチャネルアタック • Flush+Reload攻撃 • アウトオブオーダー実行 • 分岐予測 4

  5. サイドチャネルアタック • 暗号化されたデータを当該データ処理時に発生する物理的な現象の観測によって 読み出す攻撃方法 ◦ 処理時間の違い ◦ 消費電力の違い ◦ 電磁波の違い

    • Variant 1-3によって読み出すのは暗号化されたデータではなく、コード上は論理的 にアクセスできないデータ(例: 攻撃対象のメモリ、カーネルのメモリ) 5
  6. Flush+Reload攻撃 • キャッシュメモリを使ったサイドチャネルアタック a. 初期化 b. 抜き取り c. 復元 6

  7. Flush+Reload攻撃: 初期化と抜き取り 7 #define PAGE_SIZE 4096 char probe[256 * PAGE_SIZE];

    # データ抜き取り用領域 Char init_and_snoop(char *p) { Probe[]のキャッシュをフラッシュ (*1) # 初期化 Dont_use = probe[(*p) * PAGE_SIZE]; # 抜き取り } *1) clflush命令などを使う
  8. Flush+Reload攻撃の実行例: 初期化 • (*p) == 2とすると… 8 #define PAGE_SIZE 4096

    char probe[256 * PAGE_SIZE]; Char init_and_snoop(char *p) { probe[]のキャッシュをフラッシュ Dont_use = probe[(*p) * PAGE_SIZE]; }
  9. Flush+Reload攻撃: 初期化直後のprobe[] 9 PAGE_SIZE … … 0 * PAGE_SIZE 1*

    PAGE_SIZE … … 2* PAGE_SIZE 255 * PAGE_SIZE … … On cache Not on cache … 全部 not on cache
  10. Flush+Reload攻撃の実行例: 抜き取り • (*p) = 2とすると… 10 #define PAGE_SIZE 4096

    char probe[256 * PAGE_SIZE]; Char init_and_snoop(char *p) { probe[]のキャッシュをフラッシュ Dont_use = probe[(*p) * PAGE_SIZE]; }
  11. Flush+Reload攻撃: 抜き取り直後のprobe[] • 仮定: (*p) == 2 11 PAGE_SIZE …

    … 0 * PAGE_SIZE 1* PAGE_SIZE … … 2* PAGE_SIZE 255 * PAGE_SIZE … … On cache Not on cache … 1つだけがon cache
  12. Flush+Reload攻撃: 復元コード • “On cache”と”not on cache”のアクセスレイテンシの差を利用して(*a)の値を復元 12 int restore()

    { For (int i = 0; i < 256; i++) { Begin = 現在時刻測定(*1) Dont_use = probe[i * PAGE_SIZE]; End = 現在時刻測定 If (end - begin < 閾値) # trueならon cache, falseならnot on cache Return i; } Return -1; # 失敗 } *1) rdtsc命令などを使う
  13. アクセスレイテンシの概念図 13 アクセスレイテンシ probe[]のインデックス 0 (255 * PAGE_SIZE) (2 *

    PAGE_SIZE) 閾値
  14. probe[]のサイズが256ではない理由 • あるデータのキャッシュへのロードが隣のデータも影響を与える • CPUはキャッシュライン(サイズは64バイト, 128バイトなど)単位でメモリを読み出す ◦ Probe[0]をキャッシュに入れると probe[1]もキャッシュに乗る •

    キャッシュラインにシーケンシャルアクセスすると次のラインをプリフェッチすること がある ◦ プリフェッチはページをまたがない • probe[256 * PAGE_SIZE]のほうがデータを抜ける可能性が高い 14
  15. アウトオブオーダー実行 • CPUの各命令の所要時間には大きな違いがある ◦ 例: キャッシュに乗っていないデータのロードは非常に遅い (演算処理の数十倍) • 連続する複数の処理間に依存性が無いことがある •

    依存関係が無い命令列の並列実行によって高速化可能 • 命令の実行順序が入れ替わりうるからOut-of-Order(あるいはOoO)実行と呼ぶ 15
  16. OoO実行の流れ: 初期状態 16 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ *) 実装詳細が気になる人のための検索ワード : 「リオーダバッファ」「レジスタリネーミング」 命令実行 ユニット RBX: 1
  17. OoO実行の流れ: 命令読み出し 17 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1
  18. OoO実行の流れ: 作業領域を割り当て 18 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1
  19. OoO実行の流れ: 実行開始 19 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1 $(100)のデータほ しい $(200)のデータ ほしい
  20. OoO実行の流れ: 後の命令が先に完了 20 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) Mov RBX $(200) RBX: 1 3 $(100)のデータま だ? $(200)のデータ 来た
  21. OoO実行の流れ: 先の命令が後に完了 21 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU 2 RAX: 0 作業領域 レジスタ 命令実行 ユニット Mov rax $(100) RBX: 1 3 $(100)のデータ 来た
  22. OoO実行の流れ: 前から順番に反映(リタイア) 22 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU 2 RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 1 3
  23. OoO実行の流れ: 前から順番に反映(リタイア) 23 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3 3
  24. OoO実行の流れ: 完了 24 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3
  25. 分岐予測 • 分岐命令発生時、分岐先を予測して投機的に実行 ◦ 予測成功: 投機実行の結果をそのまま利用。高速 ◦ 予測失敗: 作業領域の結果を捨てて正しい分岐先からやり直し。低速 •

    予測方法の例 ◦ 過去数回の分岐結果をもとに、最頻ルートを予想 25
  26. 分岐予測の流れ: 初期状態 26 Char a[100]; Int len = 100; Void

    foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; … (b) Return ret; } コード • 内部的には以下2つの流れを並列実行 a. Lenをロード -> (x < len)の評価 -> 分岐先決定 b. A[x]をロード -> retにa[x]をストア # 投機的実行 CPUはx < lenをtrueと予測
  27. • foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 27 Char a[100]; Int len =

    100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード
  28. • foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 28 Char a[100]; Int len =

    100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード完了 2. 流れaが完了。分岐予測成功 a. Lenをロード完了 -> “x(=10) < len(=100)”がtrue b. retにa[x]をストア 3. a, bの順にリタイア
  29. • foo(x)を呼ぶと… 分岐予測の流れ: 分岐予測成功の場合 29 Char a[100]; Int len =

    100; Void foo(x) { Int ret = -1; If (x < len) … (a) ret = a[x]; .. (b) Return ret; } コード 1. 流れbが先行 a. len(not on cache)をロード中… b. A[x](on cache)をロード完了 2. 流れaが完了。分岐予測成功 a. Lenをロード完了 -> “x(=10) < len(=100)”がtrue b. retにa[x]をストア完了 3. a, bの順にリタイアしてから先に進む
  30. 分岐予測が効く典型的なコード 30 For (i = 0; i < 1000; i++)

    Sum += i; • 1000回の評価中999回はtrue • i < 1000 == trueという予測はほぼ成功
  31. • X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 31 Char a[100]; Int

    len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード
  32. • X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 32 Char a[100]; Int

    len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード完了 2. 流れaが完了。分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”がfalse b. retにa[x]をストア中…
  33. • X = 1000とすると… 分岐予測の流れ: 分岐予測失敗の場合 33 Char a[100]; Int

    len = 100; Void foo(x) { Int ret = -1; If (x < len) ... (a) ret = a[x]; … (b) Return ret; } コード 1. 流れbが先行 a. lenをロード中… b. A[x]をロード完了 2. 流れaが完了。分岐予測失敗を検出 a. Lenをロード完了 -> “x(=1000) < len(=100)”がfalse b. retにa[x]をストア中… 3. 流れbの実行を捨てて先に進む
  34. Variant 1のしくみ • 脆弱性 • 攻撃方法 34

  35. 問題のあるコード 35 Char a[100]; Int len = 100; Void foo(x)

    { Int ret = -1; If (x < len) … (a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } • 以下2つの流れが並列実行 a. Lenをロード -> (x < len)の評価 -> 分 岐 b. A[x]をロード -> probe[a[x] * PAGE_SIZE]をロード -> retに probe[...]をストア CPUはx < len == trueと予想
  36. • foo(1000)を呼ぶと… 分岐予測が外れた場合 36 Char a[100]; Int len = 100;

    Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE]をロード
  37. • foo(1000)を呼ぶと… 分岐予測が外れた場合 37 Char a[100]; Int len = 100;

    Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中…
  38. • foo(1000)を呼ぶと… 分岐予測が外れた場合 38 Char a[100]; Int len = 100;

    Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中… 3. 流れaをリタイア。流れbの実行結果は捨てる
  39. • foo(1000)を呼ぶと… 分岐予測が外れた場合 39 Char a[100]; Int len = 100;

    Void foo(x) { Int ret = -1; If (x < len) …(a) ret = probe[a[x] * PAGE_SIZE]; …(b) Return ret; } 1. 流れbが先行 a. Lenをロード中… b. A[x]をロード -> Probe[a[x] * PAGE_SIZE] をロード 2. 流れaにおいて分岐予測失敗を検出 a. Lenをロード -> “x(=1000) < len(=100)”は false b. ret にprobe[...]をストア中… 3. 流れaをリタイア。流れbの実行結果は捨てる 論理的に読んでないはずの A[1000]をprobe[]から復元可能
  40. Variant 1を利用したFlush+Reload攻撃 • できること a. Fooの引数変更によって、攻撃対象プロセスがアクセス可能な任意のメモリを読み出せる b. カーネル内で実行すればカーネル内のメモリも読み出せる • 処理の流れ

    a. 初期化: Probe[]をキャッシュフラッシュ b. 教育: foo()を何度も呼び出して if文におけるCPUの分岐予測先をtrueにする c. 抜き取り: foo()を実行してif文の分岐予測を失敗させる -> 取りたいデータをprobe[]に抜く d. 復元 40
  41. 現実的な攻撃方法 • 攻撃対象バイナリ上に元々存在する or 動的生成したコードをgadgetとして利用し て、ROPなどの他の方法と合わせて攻撃 • 現実世界で使うのは大変 41

  42. Variant 3のしくみ • 脆弱性 • 攻撃方法 42

  43. linuxの仮想アドレス空間 • 各プロセスの仮想アドレス空間に全物理メモリをマップしている(*1) ◦ 目的は実装の単純化と高速化 ◦ マップした領域をカーネル領域と呼ぶ 43 仮想アドレス空間 物理アドレス空間

    物理メモリ プロセスAのメモリ カーネル領域 *1) 実際のメモリマップはもっと複雑です
  44. アクセス権限 • 権限チェック: プロセスからカーネル領域にアクセスすると例外発生 • カーネル動作中(システムコール実行中、割込み処理中)のみアクセス可能 44 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ

    OK NG
  45. プロセスとカーネルの仮想アドレス空間 45 OK NG OK OK syscall発行/割込・例外発生 復帰 プロセス カーネル

  46. 問題のあるコード 46 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE];

    Dont_use = probe[(*p) * PAGE_SIZE]; • 内部的には以下2つの流れを並列実行 a. pへのアクセス権限チェック -> 無ければ流 れbの実行結果を捨てる -> 例外発生 b. (*p)をロード -> probe[(*p) * PAGE_SIZE] をロード
  47. 問題のあるコードの実行過程 47 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE];

    Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE] をロード
  48. 問題のあるコードの実行過程 48 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE];

    Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE]を ロード 2. 流れaが後で終了 a. 権限がないことがわかる -> 流れbの実行 結果を捨てる -> 例外発生 b. 流れaの完了待ち
  49. 問題のあるコードの実行過程 49 Char *p = SOME_KERNEL_ADDRESS; Char *probe[256 * PAGE_SIZE];

    Dont_use = probe[(*p) * PAGE_SIZE]; 1. 流れbが先に完了 a. pへのアクセス権限チェック中 … b. (*p)をロード-> probe[(*p) * PAGE_SIZE]を ロード 2. 流れaが後で終了 a. 権限がないことがわかる -> 流れbの実行 結果を捨てる -> 例外発生 b. 流れaの完了待ち 論理的に読んでいないはずの (*p)をprobe[]から復元可能
  50. Variant 3を利用したFlush+Reload攻撃の例 • メインルーチン a. SIGSEGVのシグナルハンドラを登録 b. setjmp()。登録時は処理cへ。longjmp()による復帰時は処理 eへ c.

    初期化: probe[]をnot on cacheへ d. 抜き取り。SIGSEGV発生 e. 復元 • SIGSEGVハンドラ a. longjmp()でメインルーチンの処理 bへ 50
  51. 現実的な攻撃方法 • 攻撃対象システムから一般ユーザ権限でプロセスを実行できればOK • 任意の物理メモリを読み出せる ◦ ファイルの中身 ◦ 他のユーザのメモリ •

    激ヤバ • ユーザ貸し、コンテナ貸しのようなマルチテナントシステムでとくにヤバい 51
  52. Variant 3への対策 • Kernel Page Table Isolation(KPTI) • KPTIの有無によるカーネル処理の違い •

    性能劣化要因 • PCID 52
  53. Kernel Page Table Isolation (KPTI) • プロセスのアドレス空間にカーネルのメモリをマップしない • Linux v4.15に取り込まれた

    & 各種distroのカーネルにもバックポートされた 53 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ OK NG(マップされてないので触りようがない )
  54. プロセスとカーネルの仮想アドレス空間(KPTIあり) 54 カーネルの仮想アドレス空間 プロセスの仮想アドレス空間 カーネル領域 syscall発行/割込発生 復帰

  55. KPTIが無い場合 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替えない ◦ TLBはフラッシュしない •

    コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない ◦ コンテキストスイッチ (これ以降CSと記載)したときはユーザ空間の TLBをフラッシュ 55
  56. KPTIがある場合 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替えない => 切り替える ◦

    TLBはフラッシュしない => する • コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない => する • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない => 切り替える ◦ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ 56
  57. 性能劣化要因 • ページテーブル切り替え増加 => sysが増加 • TLBフラッシュ増加(とくにコンテキストスイッチせずカーネル空間からユーザ空間に 復帰した場合) => TLBミス増加

    =>ユーザ空間で起きればuser増加、カーネル空 間で起きればsys増加) • その他諸々の追加処理の実行によるコスト => sys増加 • 上記一連の処理によるキャッシュ使用量増加 = キャッシュミス増加 => ユーザ空間 で起きればuser増加、カーネル空間で起きればsys増加) 57
  58. Process Context IDentifier(PCID) • TLBエントリごとに存在するコンテキスト(通常はプロセス)識別用タグ • 特定プロセス用のTLBエントリのみフラッシュ可能 ◦ コンテキストスイッチごとの TLB全フラッシュが不要に

    • Sandy Bridge(2011年)から追加 ◦ Haswell(2013年)より前はINVPCID命令が無いため、あまり役に立たない 58
  59. PCIDを使ったKPTIによる性能劣化の緩和 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替ない => 切り替える ◦

    TLBはフラッシュしない => する => しない • コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない => する => しない • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない => 切り替える ◦ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ => CSしたときは 切り替え前のプロセス用 TLBエントリをフラッシュ 59
  60. みなさんのマシンでの具体的な性能インパクト • 個々のシステムにおける各処理の性能を要計測 • マイクロベンチを取って個々の要因によってどれだけ劣化しうるかの基礎データを 持っていると分析しやすい ◦ カーネルブートパラメタ ”pti”によって有効/無効を切り替えられる ◦

    マイクロベンチだけ採取して「最大ナントカ %性能劣化!以上!」と騒ぐのはナンセンス • Intel曰く、新しい石ほど影響は小さい 60
  61. おしまい 61

  62. 参考文献1: 概要を掴むために • Reading privileged memory with a side-channel ◦

    Google Project ZeroによるSpectreとMeltdownの概要。まずはこれを読むといい ◦ https://googleprojectzero.blogspot.jp/2018/01/reading-privileged-memory-with-side.html • Exploiting modern microarchitectures: Meltdown, Spectre, and other attacks ◦ SpectreとMeltdownの概要説明。ハードの実装について若干書いてくれている ◦ https://fosdem.org/2018/schedule/event/closing_keynote/attachments/slides/2597/export/event s/attachments/closing_keynote/slides/2597/FOSDEM_2018_Closing_Keynote.pdf • KPTI/KAISER Meltdown Initial Performance Regressions, Brendan Gregg ◦ Meltdownの性能影響についてのマイクロベンチ採取例 ◦ http://www.brendangregg.com/blog/2018-02-09/kpti-kaiser-meltdown-performance.html 62
  63. 参考文献: 深く知るために • Spectre Attacks: Exploiting Speculative Execution ◦ Spectreの原著論文。完全理解したければこれを読みましょう

    ◦ https://spectreattack.com/spectre.pdf • Meltdown ◦ Meltdownの原著論文。完全理解したければこれを ◦ https://meltdownattack.com/meltdown.pdf • linuxカーネルソースのDocumentation/x86/pti.txt ◦ KPTIの目的、実装、性能インパクトについて書いている 63