図解でわかる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. 5.

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

    • Variant 1-3によって読み出すのは暗号化されたデータではなく、コード上は論理的 にアクセスできないデータ(例: 攻撃対象のメモリ、カーネルのメモリ) 5
  2. 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命令などを使う
  3. 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]; }
  4. 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
  5. 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]; }
  6. 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
  7. 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命令などを使う
  8. 14.

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

    キャッシュラインにシーケンシャルアクセスすると次のラインをプリフェッチすること がある ◦ プリフェッチはページをまたがない • probe[256 * PAGE_SIZE]のほうがデータを抜ける可能性が高い 14
  9. 16.

    OoO実行の流れ: 初期状態 16 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ *) 実装詳細が気になる人のための検索ワード : 「リオーダバッファ」「レジスタリネーミング」 命令実行 ユニット RBX: 1
  10. 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
  11. 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
  12. 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)のデータ ほしい
  13. 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)のデータ 来た
  14. 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)のデータ 来た
  15. 22.

    OoO実行の流れ: 前から順番に反映(リタイア) 22 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU 2 RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 1 3
  16. 23.

    OoO実行の流れ: 前から順番に反映(リタイア) 23 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3 3
  17. 24.

    OoO実行の流れ: 完了 24 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3
  18. 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と予測
  19. 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)をロード
  20. 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の順にリタイア
  21. 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の順にリタイアしてから先に進む
  22. 30.

    分岐予測が効く典型的なコード 30 For (i = 0; i < 1000; i++)

    Sum += i; • 1000回の評価中999回はtrue • i < 1000 == trueという予測はほぼ成功
  23. 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]をロード
  24. 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]をストア中…
  25. 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の実行を捨てて先に進む
  26. 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と予想
  27. 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]をロード
  28. 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[...]をストア中…
  29. 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の実行結果は捨てる
  30. 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[]から復元可能
  31. 40.

    Variant 1を利用したFlush+Reload攻撃 • できること a. Fooの引数変更によって、攻撃対象プロセスがアクセス可能な任意のメモリを読み出せる b. カーネル内で実行すればカーネル内のメモリも読み出せる • 処理の流れ

    a. 初期化: Probe[]をキャッシュフラッシュ b. 教育: foo()を何度も呼び出して if文におけるCPUの分岐予測先をtrueにする c. 抜き取り: foo()を実行してif文の分岐予測を失敗させる -> 取りたいデータをprobe[]に抜く d. 復元 40
  32. 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] をロード
  33. 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] をロード
  34. 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の完了待ち
  35. 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[]から復元可能
  36. 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
  37. 53.

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

    & 各種distroのカーネルにもバックポートされた 53 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ OK NG(マップされてないので触りようがない )
  38. 55.

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

    コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない ◦ コンテキストスイッチ (これ以降CSと記載)したときはユーザ空間の TLBをフラッシュ 55
  39. 56.

    KPTIがある場合 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替えない => 切り替える ◦

    TLBはフラッシュしない => する • コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない => する • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない => 切り替える ◦ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ 56
  40. 57.

    性能劣化要因 • ページテーブル切り替え増加 => sysが増加 • TLBフラッシュ増加(とくにコンテキストスイッチせずカーネル空間からユーザ空間に 復帰した場合) => TLBミス増加

    =>ユーザ空間で起きればuser増加、カーネル空 間で起きればsys増加) • その他諸々の追加処理の実行によるコスト => sys増加 • 上記一連の処理によるキャッシュ使用量増加 = キャッシュミス増加 => ユーザ空間 で起きればuser増加、カーネル空間で起きればsys増加) 57
  41. 59.

    PCIDを使ったKPTIによる性能劣化の緩和 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替ない => 切り替える ◦

    TLBはフラッシュしない => する => しない • コンテキストスイッチ ◦ ページテーブルを切り替える ◦ TLBはフラッシュしない => する => しない • カーネル→ユーザへの遷移 ◦ ページテーブルを切り替えない => 切り替える ◦ CSしたときはユーザ空間の TLBをフラッシュ => CSしたときは全TLBをフラッシュ => CSしたときは 切り替え前のプロセス用 TLBエントリをフラッシュ 59
  42. 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
  43. 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