SPECTRE.VARIANT1
予測実行の悪用の可能性
スペクター (Spectre) は 2018 年初めに Google が発見し、公表したハードウェア脆弱性です (https://googleprojectzero.blogspot.ca/2018/01/reading-privileged-memory-with-side.htm)。この脆弱性は、さまざまなメモリーレベルで作動中に CPU 性能を向上させることを目的としたハードウェア最適化に起因します。スペクターはハードウェア脆弱性ですが、それが悪用される場合、そのハードウェアが実行するソフトウェアに特定のコードパターンが存在しているはずです。元の Google の公表文書は https://spectreattack.com/spectre.pdf から入手可能であり、脆弱性をさらす数々のパターン、そしてそれを経由して漏洩する機密データへの不正アクセスを可能にするプログラム例が示されています。この脆弱性にはさまざまなバリアントがあります。バリアント 1 は元の文書で過剰なまでに論じられていますが、静的検出における主要な関心事です。それは、他のバリアントとは異なり、一般的なアプローチでは簡単に緩和できないようであり、ソースで特定のコードパターン検出が必要になるからです。バリアント 1 の脆弱性により、境界を越えたアレイの読み取りによって被害者のマシンの RAM から機密データを漏洩させる可能性があります。以下に示すのは、この脆弱性の高レベルな概要を明らかにする、簡略化されたコードスニペットです。
struct array {
unsigned long length;
unsigned char data[];
};
struct array *createArrayOfSize(unsigned int size);
void vulnerable_pattern(unsigned long offset)
{
struct array *arr1 = createArrayOfSize(5); /* small array */
struct array *arr2 = createArrayOfSize(0x500); /* a large array */
unsigned char secret_value;
if (offset < arr1->length) { /* untrusted data controls the branch */
secret_value = arr1->data[offset]; /* out of boundary read in a mispredicted speculative execution */
unsigned char value2 = arr2->data[secret_value * 256]; /* arr2->data[secret_value * 256] will be loaded to the CPU cache */
/* resulting in a measurable side effect */
/* some more code */
} else {
/* code for the else branch */
}
}
- 機密データは、配列 arr1->data の境界外メモリ領域に置かれます。
- この機密データ (攻撃者は合法的にアクセスできる) は、CPU により最近アクセスされて、実際にレジスタ一の 1 つにキャッシュされたと考えられます。
- CPU キャッシュには「オフセット」変数もあると考えられます。
- 逆に、arr1->length および arr2->data はキャッシュされていないと考えられます。
- 14 行目で、コントロールは 'offset' を 'arr1->length' と比較する必要があります。
- 'offset' はキャッシュにヒットし、一方 'arr1->length' はキャッシュから外れます。CPU は、それを次レベルのキャッシュ (または DRAM) からロードするように要求します。
- 'arr1->length' の値が CPU レジスタの 1 つに表示されるには、通常数百回もの CPU サイクルを要します。待機してこれらのサイクルを無駄にするのではなく、CPU は予測による分岐の実行を開始します。実行する特定の分岐は、このコードの過去の実行に基づいた CPU 分岐予測機能により選択されます。この機能の最も一般的な合法的呼び出しは 'offset' の有効なインバウンド値を含むので、最も可能性が高い分岐は「真」の分岐となります。また、分岐予測機能は攻撃者に望ましい分岐を選択するように攻撃者によって「訓練」される可能性があります。予測実行の場合、 'arr1->length' が CPU レジスタにロードされるまでに分岐予測機能が分岐を誤予測する結果になると、CPU は分岐のそれ以降の命令実行を中止して他の分岐を実行するだけです。
- 予測実行時に、値 'arr1->length' が CPU L1 キャッシュにロードされる間に、コントロールは 15 行目に達します。
- 悪意を持って選択された値 'offset' は機密データを指します。そのデータ読み取りはキャッシュにヒットし、すぐに 'secret_value' に保存できるようになります。
- 17 行目の 'arr2->data[secret_value * 256]' 読み取りはキャッシュにありません。次レベルのキャッシュ (または DRAM) からロードされるように要求されます。
- 値 'arr1->length' は最終的に L1 キャッシュに達します。CPU は分岐が誤予測されたことを認識し、その実行は中止され、コントロールは代わりに 'else' 分岐の実行を開始します。
- 'arr2->data[secret_value * 256]' の値が L1 キャッシュに達します。
攻撃者にはまだ直接アクセス可能なデータはありませんが、この実行の結果として、攻撃者は CPU に副作用を残します。すなわち、'arr2->data[secret_value * 256]' の値は CPU の L1 キャッシュにコピーされており、一方で 'arr2->data' アレイの他の要素は次レベルのキャッシュまたは DRAM のみでまだ使用可能です (ハードウェアメモリー操作のこれ以上の詳細を無視した場合)。この副作用を測定し、データ漏洩を完了させるために攻撃者に必要なことは、'arr2->data' の要素を 1 つずつ読み取り (256 の手順によります。攻撃者にはそれを実行する何らかの便利な方法があるものと推測されます)、それぞれの読み取りにかかる時間を測定することだけです (これは簡単に実行できるものと主張されています。Timing Attack を参照してください)。'arr2->data[secret_value * 256]' を除くすべての読み取りは遅く、一方で 'arr2->data[secret_value * 256]' の読み取りは速く、それにより 'secret_data' の値が明らかになります。
この脆弱性を緩和するには、次レベルのキャッシュから要求されたすべての値が実際にそのレジスターに達するまで CPU に待機するよう強制する 'arr2->data[secret_value * 256]' にアクセスする前に、特別な「固有」機能を分岐のどこかから呼び出します。Intel のコンパイラーは、Intel のアーキテクチャーでこれを実行する関数 '_mm_lfence()' を提供します。
コード例
code.c:17:9: [SPECTRE.VARIANT1] 潜在的に信頼できないデータ 'offset' は、予測分岐実行の 15 行で、配列 'arr1->data' の境界を超えた任意のデータの読み取りに使用できます。
struct array {
unsigned long length;
unsigned char data[];
};
struct array *createArrayOfSize(unsigned int size);
void vulnerable_pattern(unsigned long offset)
{
struct array *arr1 = createArrayOfSize(5); /* small array */
struct array *arr2 = createArrayOfSize(0x500); /* a large array */
unsigned char secret_value;
if (offset < arr1->length) { /* untrusted data controls the branch */
_mm_lfence(); /* serialization intrinsic that stops speculative execution */
secret_value = arr1->data[offset]; /* no vulnerability exists so no defect is reported */
unsigned char value2 = arr2->data[secret_value * 256];
/* some more code */
} else {
/* code for the else branch */
}
}
- _mm_lfence
function_name - FENCE
チェッカーは上記のレコードを解釈し、関数 'function_name' がキャッシュが RAM の状態と同期するまで CPU を待機させるか、予測実行でのデータ漏洩が何らかの別の方法で取るに足らない問題にするものととります。そのパスに沿った解析は停止されます。
外部参考資料
セキュリティトレーニング
Secure Code Warrior が提供しているアプリケーションセキュリティトレーニング教材。