Meltdown
概要
Meltdownは大きく分けると次の2要素から成る
対策としてはOS側でカーネルとアプリケーションのページテーブルを分割する
Toy Example を理解する
Meltdown.pdf にある Toy Example のコード
code:meltdown.asm
; rcx = kernel address, rbx = probe array
xor rax, rax ;1
retry: ;2
shl rax, 0xc ;4
jz retry ;5
probe array は攻撃者がデータ漏洩のために使う array
大まかな流れ:
投機的実行によってカーネルのメモリを読む
メモリの内容をページサイズ単位に増幅する
メモリの内容にアクセスしてキャッシュさせる
(Flush+Reloadでキャッシュされているアドレスを取得してデコードする)
コードの内容
1: xor rax, rax
rax どうしの XOR を取っている、つまりゼロにしているだけ
rax をこのあとのシフト演算で利用するので初期化している
2: retry:
そのまま。ただのラベル
なぜこれが必要かの詳細は後述
3: mov al, byte [rcx]
al: rax レジスタの下位バイト
al にカーネルメモリを1バイト読み出す
権限が無いので(architectualなレベルでは)例外を引き起こす
4: shl rax, 0xc
raxを12bit左シフトする
2^12 = 4096
4096バイト単位の値になるよう引き伸ばしている
これは近い値も同一のキャッシュラインに載せないようにするため
対象とするレジスタのキャッシュのサイズ以上であればいいので4096より小さくても動くかも(?)
5: jz retry
rax が0ならretryへジャンプ
なぜ必要なのかは後述
6: mov rbx, qword [rbx + rax]
probe array 上の rax にあたるアドレスのメモリを読み出している
これでraxが示すアドレスの内容がキャッシュラインに載る
キャッシュされているアドレスをFlush+Reloadで特定すれば、raxの内容がわかる
つまり、カーネルアドレスから読み出された1バイトのデータをarchitectualなレベルにて復元できる
つまり、アドレスが示す先のメモリに配置されているデータに意味はない
前提
これを動かしているプロセスはユーザ権限なので、kernel address にアクセスする権限は持たない
つまり architectualなレベルの動作では 3: mov al, byte [rcx]は例外を引き起こすのでその後のコードは実行されないことになる
ここで、権限チェックの概要とOut-of-Order Executionの関係を抑えておく
A. 権限チェックと例外の発生はどのように行われるか
厳密な動作はわからないが、一旦このサイトでの説明を参考にする
1. アクセス先のアドレスと現在のコンテキストをチェックし、現在のコンテキストが当該アドレスにアクセスしてよいかを調べる
アクセスの権限がある場合:
そのまま処理を実行
アクセスの権限がない場合:
1. ロード先のレジスタは0にクリア
2. 例外を起こして適切なハンドラに処理を投げる
関連
B. Out-of-Order Executionの意図
micro-architectualなレベルでは権限チェックをパスしてからデータを読みにいかなくてもよい
とりあえずアクセスの権限があるかどうかはともかく、とりあえずOut-of-Order Executionで計算しておく、権限がなければあとで必要なものをゼロ埋めして例外を起こせばいいという考え方
権限に基づいたアクセス制御はarchitectualなレベルで保証されていればよいため
攻撃にあたって攻撃者が知りたいことは2つ:
1. Out-of-Order Executionによる権限昇格が成功しているかどうか
2. ターゲットのメモリ内容
ここまでを踏まえてコードを再掲:
code:meltdown.asm
; rcx = kernel address, rbx = probe array
xor rax, rax ;1
retry: ;2
shl rax, 0xc ;4
jz retry ;5
1. 「Out-of-Order Executionによる権限昇格が成功しているかどうか」
これは言い換えると、「3~6 の命令の投機的な実行とmicro-architectual state の漏洩」と「architectualな権限チェックの一連の処理」のうち、どちらの処理が終わるのが先か になる
もしarchitectualな権限チェックの処理が先に終わるならraxの内容はゼロ埋めされて進むので、Flush+Reloadしたときにアドレス0がキャッシュされていることがわかる
投機的実行で読み出したカーネルのデータが0のとき、5: jz retry で投機的実行されているパスは無限ループに陥る
6: mov rbx, qward [rbx + rax]は実行されないのでキャッシュはされない
Flush+Reloadの段階では、キャッシュされている値がみつからないならカーネルのデータはゼロであったことがわかる
2. ターゲットのメモリ内容
メモリ内容は1バイトずつ読み出され、それが4096バイト単位のアドレスaに変換される
つまり1なら4096になる
6 が実行されるとprobe array上のaというアドレスがキャッシュされる
Flush+Reloadでキャッシュヒットしたアドレスがわかれば元の1バイトの値に復元できる
probe array のサイズは 4096 * 256 あればいい
1byteで表せる情報の種類は 1 byte = 8 bits で 2^8 = 256
これらを別のキャッシュに載せられればいいので4096を掛ける
伝送するビットのサイズの話
例では8bitずつ伝送している
256回のFlush+Reloadをする
mov で大きなデータを読み取れば、より多くのビット数を一度に伝送することが可能ではある
トランジェント命令シーケンスの中の命令数は攻撃のパフォーマンスには対して関係せず、ボトルネックになるのはFlush+Reloadに費やす時間
1ビットずつの伝送にすれば、キャッシュライン1のキャッシュヒットかキャッシュミスかのどちらかだけになる
伝送速度は速くなる
ただ単一ビット伝送の場合、Meltdownが失敗して0が帰ってきているときに気づきずらい
単一ビットで帰ってくるデータは50%であるのに対し、複数ビットならすべて0であることは稀だから
エラー削減と伝送速度のトレードオフ
でもやってみたらエラー率はそんなに変わらなかったから実験の例ではSingle bit transmitionでやってるよとのこと
Intel TSX を使うと
Intel TSX(Transactional Synchronization Extensions) 命令列を一つのトランザクションとしてグループ化できる
meltdown.asm に示したコードをTSXで囲むと途中で起こる例外が抑制される
これはカーネルで例外が処理されるよりも高速なので、結果的にCovert Channelの容量増につながる
攻撃者はカーネルのアドレスがどこか特定する必要がある
KASLRが適用されている場合、最大でもRandomizationのエントロピーは40bit
物理メモリが8GBなら128回のテストでKASLRを突破できる
40bit のアドレス空間: 2^40 byte = 1TB
8GB= 2^33 byte
2^40 / 2^33 で 2^7 = 128 回ってこと?
その他
実際の読み出し速度
3.2 KB/s~503 KB/s
再試行回数が攻撃の成功率に大きく影響する
単一ビット伝送では再試行回数を増やすことで、エラーレートが大幅に減少する
エラーレートの低減
Intel Core i5-6200U: 5.25% -> 0.67%(ループによる最適化) -> 0.008% (Intel TSX)
パフォーマンス
Intel TSXを使うと例外処理のオーバーヘッドがなくなってパフォーマンスが上がる
WIP
KAISER
AMD系プロセッサ