メインコンテンツまでスキップ

Spectreから1年:V8チームの視点

· 約13分
Ben L. TitzerとJaroslav Sevcik

2018年1月3日、Google Project Zeroと他の機関が、投機的実行を行うCPUに影響を与える新しいクラスの脆弱性、SpectreおよびMeltdownと名付けられた脆弱性の最初の3つを公表しました。CPUの投機的実行メカニズムを利用し、攻撃者はコード内の暗黙的および明示的な安全性チェックを一時的に回避し、メモリ内の許可されていないデータの読み取りを防止することができます。プロセッサの投機は、アーキテクチャレベルでは見えないマイクロアーキテクチャの詳細として設計されていましたが、注意深く作成されたプログラムは、投機的に許可されていない情報を読み、プログラム断片の実行時間などのサイドチャネルを通じてそれを公開することができます。

JavaScriptを使用してSpectre攻撃を仕掛けることができると示されたとき、V8チームは問題解決に取り組み始めました。私たちは緊急対応チームを結成し、Google内の他のチーム、他のブラウザベンダーのパートナー、およびハードウェアパートナーと緊密に連携しました。それらの専門家と連携して、攻撃的研究(概念実証用ガジェットの構築)および防御的研究(潜在的な攻撃に対する緩和策)の両方に積極的に取り組みました。

Spectre攻撃は以下の2つの部分で構成されています:

  1. 通常アクセスできないデータを隠されたCPU状態に漏洩させる。 既知のSpectre攻撃はすべて、投機を使用してアクセス不能なデータのビットをCPUキャッシュに漏洩させます。
  2. 隠された状態を抽出してアクセス不能なデータを回収する。 これには、攻撃者が十分に精度の高いクロックを必要とします。(驚くべきことに、エッジしきい値法などの技術では非常に低解像度のクロックでも十分です。)

理論的には、攻撃の2つの要素のいずれかを無効にするだけでも十分です。ただし、そのいずれかの部分を完全に無効にする方法を知らないため、CPUキャッシュに漏洩する情報量を大幅に削減する緩和策と、隠された状態を回収するのを困難にする緩和策を設計および実装しました。

高精度タイマー

投機的実行から生じる非常に小さな状態変化は、それに対応して非常に小さな、ほとんど不可能なほど小さなタイミング差を生み出します — ナノ秒単位の差です。これらの差を直接検出するには、攻撃者プログラムに高精度タイマーが必要です。CPUはそのようなタイマーを提供しますが、Webプラットフォームはそれを公開していません。Webプラットフォームで最も精度の高いタイマーであるperformance.now()の解像度は1桁のマイクロ秒であり、当初はこの目的には使用できないと考えられていました。しかし、2年前、マイクロアーキテクチャ攻撃を専門とする学術研究チームが、Webプラットフォームにおけるタイマーの利用可能性を調査した論文を発表しました。彼らは、同時変更可能な共有メモリやさまざまな解像度回復技術により、ナノ秒解像度にまで高めることができるタイマーの構築が可能であると結論づけました。このようなタイマーは、個々のL1キャッシュヒットやミスを検出するのに十分な精度を持ち、通常はSpectreガジェットが情報を漏洩させる方法です。

タイマーの緩和策

小さなタイミング差を検出する能力を妨害するために、ブラウザベンダーは多方面からのアプローチを取りました。すべてのブラウザで、performance.now()の解像度が低下(Chromeでは5マイクロ秒から100マイクロ秒に)され、解像度回復を防ぐためにランダム一様ジッタが導入されました。すべてのベンダーと協議の上、Spectre攻撃に使用できるナノ秒タイマーの構築を防ぐために、全ブラウザでSharedArrayBuffer APIを即時かつ遡及的に無効化するという前例のない措置を取ることを決定しました。

増幅

攻撃的研究を進める中で、タイマーの緩和策だけでは十分ではないことが早い段階で明らかになりました。その理由の1つは、攻撃者が単にガジェットを繰り返し実行することで、累積的な時間差が単一のキャッシュヒットやミスよりもはるかに大きくなる可能性があるからです。私たちは、キャッシュラインを同時に多く利用する信頼性の高いガジェットを設計することができました。その結果、キャッシュ容量に基づいた600マイクロ秒もの大きなタイミング差を生じさせることができました。その後、キャッシュ容量に制限されない任意の増幅技術を発見しました。このような増幅技術は、秘密データを何度も読み取る試行に依存しています。

JITの緩和策

Spectreを利用してアクセス不可能なデータを読み取るには、攻撃者がCPUをトリックして通常アクセス不可能なデータを読むコードを投機的に実行させ、キャッシュに符号化させます。この攻撃を防ぐ方法は2つあります:

  1. コードの投機的実行を防ぐ。
  2. 投機的実行がアクセス不可能なデータを読むのを防ぐ。

私たちは(1)の実験を行い、IntelのLFENCEなどの推奨される投機バリア命令をすべての重要な条件分岐に挿入したり、間接分岐にはretpolinesを使用しました。しかし、このような強力な緩和策はパフォーマンスを大幅に低下させます(Octaneベンチマークで2–3倍の遅延)。代わりに私たちはアプローチ(2)を選択し、ミス投機のためにシークレットデータを読むことを防ぐ緩和シーケンスを挿入しました。この技術を次のコードスニペットで説明します:

if (condition) {
return a[i];
}

簡単のため、conditionが0または1であると仮定しましょう。上記のコードは、iが範囲外の場合にCPUがa[i]から投機的に読むことで通常アクセス不可能なデータにアクセスするため脆弱です。この場合、投機がcondition0のときにa[i]を読もうとするという重要な観察があります。私たちの緩和策は、このプログラムを書き直して、元のプログラムとまったく同じように動作しつつ、投機的にロードされたデータを漏洩しないようにします。

1つのCPUレジスタを予約し、このレジスタをポイズンとして、コードが誤った分岐で実行されているかどうかを追跡します。このポイズンレジスタは生成されたコード内のすべての分岐と呼び出しを通じて維持され、誤った分岐が発生するとポイズンレジスタが0になります。その後、すべてのメモリアクセスにインストルメントを追加し、現在のポイズンレジスタの値を使用してすべてのロード結果を無条件にマスクします。これによりプロセッサが分岐を予測(または誤って予測)するのを防ぐことはできませんが、誤った分岐によって生じた(潜在的に範囲外の)ロード値の情報を破壊します。以下に示すのはインストルメントされたコードです(aが数値配列だと仮定)。

let poison = 1;
// …
if (condition) {
poison *= condition;
return a[i] * poison;
}

追加されたコードは、プログラムの通常(アーキテクチャで定義された)動作には影響を与えません。投機的なCPUで実行するときだけマイクロアーキテクチャの状態に影響を与えます。プログラムがソースレベルでインストルメントされた場合、現代のコンパイラによる高度な最適化によってこのようなインストルメントが削除される可能性があります。V8では、コンパイルの非常に後の段階でこれらを挿入することでコンパイラが緩和策を削除するのを防いでいます。

またインタプリタのバイトコードディスパッチループやJavaScript関数呼び出しシーケンスにおける誤投機による漏洩を防ぐため、ポイズニング技術も使用します。インタプリタでは、バイトコードハンドラ(つまりある単一のバイトコードを解釈する機械コードシーケンス)が現在のバイトコードと一致しない場合、ポイズンを0に設定します。JavaScript呼び出しでは、ターゲット関数をパラメータ(レジスタ内)として渡し、受け取るターゲット関数が現在の関数と一致しない場合に各関数の先頭でポイズンを0に設定します。ポイズニング緩和策を導入した結果、Octaneベンチマークで20%未満の遅延が見られます。

WebAssemblyの緩和策はより簡単です。主な安全性チェックはメモリアクセスが範囲内であることを確認することです。32ビットプラットフォームの場合、通常の範囲チェックに加えて、すべてのメモリを次の2の累乗まで埋め、ユーザー提供のメモリインデックスの上位ビットを無条件にマスクします。64ビットプラットフォームにはそのような緩和策は必要ありません。実装では範囲チェックに仮想メモリ保護を使用しているためです。また、場合によって脆弱になる可能性がある間接分岐を使用する代わりにswitch/case文をバイナリ検索コードにコンパイルすることを試みましたが、いくつかのワークロードではこれはコストが高すぎます。間接呼び出しはretpolinesで保護されています。

ソフトウェア緩和策は持続不可能な道

幸運なのか不運なのか、我々の攻撃的な研究は防御的な研究よりも遥かに迅速に進み、Spectreによる可能性のあるすべての漏洩をソフトウェアで緩和することが不可能であることをすぐに発見しました。これにはさまざまな理由があります。まず、Spectreの対策に費やされたエンジニアリングの努力は、脅威レベルに対して不釣り合いでした。V8では、通常のバグによる直接的な範囲外の読み取り(Spectreよりも速く、直接的)や範囲外の書き込み(Spectreでは不可能で、より悪い)、潜在的なリモートコード実行(Spectreでは不可能で、はるかに悪い)など、より深刻な多数のセキュリティ脅威に直面しています。次に、設計して実装した複雑な緩和策は大きな複雑性を伴い、技術的な負債となり攻撃面を実際に広げてしまう可能性がありますし、性能上のオーバーヘッドもありました。さらに、マイクロアーキテクチャの漏洩に対する緩和策のテストと維持は、その緩和策が計画どおりに機能し続けることを確認するのが難しいため、ガジェット自体を設計するよりもさらに難しいです。少なくとも一度は重要な緩和策が後のコンパイラの最適化によって効果的に無効化されました。そして、Spectreのいくつかのバリアント、特にバリアント4に対する効果的な緩和策は、パートナーであるAppleがそのJITコンパイラで問題に取り組むために英雄的な努力をしたにもかかわらず、ソフトウェアでは単純に不可能であると分かりました。

サイト分離

我々の研究は、原理的には未信頼のコードがSpectreとサイドチャネルを使用してプロセスの全アドレス空間を読み取ることができるという結論に達しました。ソフトウェア緩和策は多くの潜在的なガジェットの効果を減少させますが、効率的でも包括的でもありません。唯一効果的な緩和策は、機密データをプロセスのアドレス空間から移動することです。幸いなことに、Chromeは従来の脆弱性による攻撃面を減少させるため、サイトを異なるプロセスに分離する努力を何年にもわたり続けていました。この投資は成果を挙げ、サイト分離を可能な限り多くのプラットフォームに向けて2018年5月までに本番化し展開しました。このようにして、Chromeのセキュリティモデルはレンダラープロセス内で言語による機密性を仮定しなくなりました。

Spectreは長い旅であり、業界のベンダーや学術界との協力の素晴らしさを浮き彫りにしました。これまでのところ、ホワイトハットはブラックハットに先行しているようです。好奇心旺盛な実験者やプロの研究者による概念証明用ガジェット以外では、実際の攻撃は未だ知られていません。これらの脆弱性の新しいバリアントは今後も少しずつ出現し続ける可能性があり、それがある程度の間続く可能性があります。我々はこれらの脅威を追跡し、真剣に受け止めています。

プログラミング言語とその実装に関するバックグラウンドを持つ多くの人々と同様に、安全な言語が適切な抽象境界を強制すること、つまり型付けされたプログラムが任意のメモリを読むことを許さないというアイデアは、我々の精神的モデルの基盤となってきた保証でした。我々のモデルが間違っていたという結論は憂鬱なものです——今日のハードウェアではこの保証は真実ではありません。もちろん、安全な言語が優れたエンジニアリングの利点を持っており、未来の基盤となり続けると信じていますが…今日のハードウェアでは少し漏れてしまいます。

より詳しく知りたい読者は我々のホワイトペーパーに詳細を掘り下げることができます。