Upgrade to Pro — share decks privately, control downloads, hide ads and more …

図解でわかるSpectreとMeltdown

 図解でわかるSpectreとMeltdown

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

Avatar for Satoru Takeuchi

Satoru Takeuchi PRO

February 17, 2018
Tweet

More Decks by Satoru Takeuchi

Other Decks in Technology

Transcript

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

    • Variant 1-3によって読み出すのは暗号化されたデータではなく、コード上は論理的 にアクセスできないデータ(例: 攻撃対象のメモリ、カーネルのメモリ) 5
  2. 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. 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. 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. 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. 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. 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. probe[]のサイズが256ではない理由 • あるデータのキャッシュへのロードが隣のデータも影響を与える • CPUはキャッシュライン(サイズは64バイト, 128バイトなど)単位でメモリを読み出す ◦ Probe[0]をキャッシュに入れると probe[1]もキャッシュに乗る •

    キャッシュラインにシーケンシャルアクセスすると次のラインをプリフェッチすること がある ◦ プリフェッチはページをまたがない • probe[256 * PAGE_SIZE]のほうがデータを抜ける可能性が高い 14
  9. OoO実行の流れ: 初期状態 16 Mov RAX, ($100) Mov RBX, ($200) コード

    値は2 Not on cache 値は3 On cache CPU RAX: 0 作業領域 レジスタ *) 実装詳細が気になる人のための検索ワード : 「リオーダバッファ」「レジスタリネーミング」 命令実行 ユニット RBX: 1
  10. 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. 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. 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. 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. 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. OoO実行の流れ: 前から順番に反映(リタイア) 22 Mov RAX, ($100) Mov RBX, ($200) コード

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

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

    値は2 on cache 値は3 On cache CPU RAX: 2 作業領域 レジスタ 命令実行 ユニット RBX: 3
  18. 分岐予測の流れ: 初期状態 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. • 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. • 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. • 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 For (i = 0; i < 1000; i++)

    Sum += i; • 1000回の評価中999回はtrue • i < 1000 == trueという予測はほぼ成功
  23. • 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. • 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. • 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 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. • 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. • 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. • 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. • 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. Variant 1を利用したFlush+Reload攻撃 • できること a. Fooの引数変更によって、攻撃対象プロセスがアクセス可能な任意のメモリを読み出せる b. カーネル内で実行すればカーネル内のメモリも読み出せる • 処理の流れ

    a. 初期化: Probe[]をキャッシュフラッシュ b. 教育: foo()を何度も呼び出して if文におけるCPUの分岐予測先をtrueにする c. 抜き取り: foo()を実行してif文の分岐予測を失敗させる -> 取りたいデータをprobe[]に抜く d. 復元 40
  32. 問題のあるコード 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 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 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 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. 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. Kernel Page Table Isolation (KPTI) • プロセスのアドレス空間にカーネルのメモリをマップしない • Linux v4.15に取り込まれた

    & 各種distroのカーネルにもバックポートされた 53 仮想アドレス空間 物理アドレス空間 プロセスAのメモリ OK NG(マップされてないので触りようがない )
  38. KPTIが無い場合 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替えない ◦ TLBはフラッシュしない •

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

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

    =>ユーザ空間で起きればuser増加、カーネル空 間で起きればsys増加) • その他諸々の追加処理の実行によるコスト => sys増加 • 上記一連の処理によるキャッシュ使用量増加 = キャッシュミス増加 => ユーザ空間 で起きればuser増加、カーネル空間で起きればsys増加) 57
  41. PCIDを使ったKPTIによる性能劣化の緩和 • ユーザ→カーネルへの遷移: syscall発行時, 割込/例外発生時 ◦ ページテーブルを切り替ない => 切り替える ◦

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