
非常に非常に非常に非常に非常に非常に非常に長い割り込み
非常に非常に非常に非常に非常に非常に長い割り込みでシステム管理モード(SMM)を悪用する。
実は、SMM(すべてのx86 CPUのバックグラウンドで不可視に動作する、安全で特権的な実行環境)を、途方もなく長く実行される機械語命令だけで破壊できることが判明した。
SMMでは、すべてのコアが同時にSMM内かSMM外のどちらかにいなければならない。この前提がなければセキュリティモデルは機能しない。あるスレッドがSMMに入ると、他のすべてのスレッドもSMMに入るように強制されるからだ。
これを破るには、SMMに参加すべきことに気づかないほど忙しい誰かが必要なだけだ。
仕組みは次のような感じだ:
core 0 - 長い命令を開始
|
|
|
core 1 - core 0 を SMM に招待
|
|
|
core 1 - SMM に入る
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - core 0 を待つ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
core 1 - 諦める
core 1 - 秘密の SMM 処理を行う
core 1 - SMM を終了
|
|
|
core 0 - SMM に参加
この時点で、core 1 は SMM の外にいて core 0 は SMM の中にいるため、core 1 は core 0 を攻撃できる。ここに落とし穴がある:これを機能させるには、非常に非常に非常に長い命令が必要だ——これまで想定されたどの命令よりも長い命令が。現代の CPU のほとんどの機械語命令は高速だ:add は 1 サイクルで完了する。core 1 に core 0 の待機を諦めさせるには、core 0 で約 4,000,000,000 サイクル——実時間で 1 秒以上——かかる命令が必要だ。
x86 ファームウェアは、CPU コアが SMM に入ると次のコードを実行する:
for (Timer = StartSyncTimer ();
!IsSyncTimerTimeout (Timer, mTimeoutTicker) && SyncNeeded;
)
{
mSmmMpSyncData->AllApArrivedWithException = AllCpusInSmmExceptBlockedDisabled ();
if (mSmmMpSyncData->AllApArrivedWithException) {
break;
}
CpuPause ();
}
このコードは、すべてのコアが SMM に入るか、最大 1 秒経過するか、どちらか早い方まで待機する。あるコアが SMM の外で実行を続けている間に別のコアに SMM コードを実行させるには、その外側のコアが丸々1秒間割り込み不可の状態を維持する必要がある——SMI は命令境界で取得されるため、2つの命令の間のわずかな隙間でも、保留中の SMI がコアを SMM に引き込んでしまう。したがって、遅延は単一の命令でなければならない:1秒間のランデブーを生き延びる、割り込み不可の単一の操作だ。
禁断の1秒命令に到達する方法は多数あり、正確なアプローチはプラットフォームごとに異なる。しかし、おおよそ次のようになる:高レイテンシの MMIO アドレスを見つけ、CPU にそのアドレスから可能な限りゆっくり読み取らせる——読み取りに非常に時間がかかる未文書化領域を悪用し、ISA が提供する最も広いロードを使用して単一の命令でできるだけ多くのバイトを移動させ、他のコアに同じバスを競合させてさらに遅くする。1回の読み取り、1つの命令で、CPU はほぼ1秒間その命令に拘束される。
提供されている概念実証は、Zen 3 Ryzen 7 5800H 向けに調整されており、0xfcc68860 の遅い MMIO からの広い xmm ロードが、全コアのランデブーを破るのに十分な時間ストールする:
mov $0xfcc68860, %rsi ; 対象の MMIO アドレス
vmovdqu (%rsi), %xmm0 ; 非常に非常に長いロード
PoC はこれを利用して2つのコアを互いに競わせる。一方のコアは長い命令によって SMM の外に保持される——非常に遅いロードのタイトループだ:
/* 犠牲コア:約1秒のロードでスピンし、SMI に応答する余裕がない */
for (;;)
asm volatile ("vmovdqu (%0), %%xmm0" :: "r"(mmio) : "xmm0");
一方、別のコアがコアごとの SMI カウンタを設定する:
#define MSR_PERF_CTL0 0xc0010200 /* AMD コア パフォーマンス イベント選択 MSR */
#define MSR_PERF_CTR0 0xc0010201 /* ペアとなる 48 ビット カウンタ */
for (int cpu = 0; cpu < ACTIVE_CPUS; cpu++) {
msr_write(cpu, MSR_PERF_CTL0, 0x43002b); /* EN | OS | USR | イベント 0x2b */
msr_write(cpu, MSR_PERF_CTR0, 0); /* カウントをゼロにする */
}
次に、SMI の嵐を発生させる:
asm volatile ("outb %%al, $0xb2" :: "a"(0)); /* ポート 0xb2 を叩く -> #SMI */
そして、各コアの集計を読み戻す:
/* ...嵐を発生させ、各コアの集計を読み戻す... */
uint64_t delta = smi_max - smi_min;
if (delta)
puts("!!! コアが SMM の外で実行された");
カウントが乖離した場合、それはあるコアが他のコアが SMM に引き込まれている間に SMM の外で実行を続けたことを意味する——他のコアが処理した SMI をそのコアは見逃したのだ。
これをより明確に示すために、不必要に派手でまったく無意味な GUI の背後で概念実証を実行し、各コアの SMI カウンタを追跡して、大幅に遅延したコアが必要な同期を破るまで、それらが完全に同期して実行される様子を観察できる:

SMM の唯一の保証——実行中は他の何も実行されない——は、単一の途方もなく長い命令によって崩壊する。
SMM のセキュリティは単純な前提に依存している:実行中は他の何も実行されない。
ここ には 100以上 の SMM TOCTOU CVE が 存在する:SMM ハンドラが 共有 メモリ内の 値を チェックし、その後 それを使用する。悪用に必要なのは、チェックと使用の間に その値を 書き換えることだけで、SMM 内部に入れる。しかし、これらの問題は実際の環境では休眠状態にあり、ほとんどパッチが適用されていない。その理由は1つの前提にある:悪用には SMM 実行中に共有メモリを変更する何かが必要であり、SMM ランデブーのため SMM の外に CPU コアが存在せず攻撃を開始できないからだ。唯一の侵入経路は DMA 対応ペリフェラルが CPU の背後で書き込むことだった——物理アクセス、悪意のあるデバイス——そのため、このクラス全体がハードウェア問題として切り捨てられている。
SMI の非同期化は、プラットフォームを安全に保っていた前提条件を除去する:物理アクセスやハードウェアを必要としない外部コアが、SMM 実行中に実行できるようになり——休眠中の CVE がソフトウェアから悪用可能になる。
おそらく存在しない。これがこの問題を従来の SMM 問題よりもいくらか興味深いものにしている理由だ。タイムアウトを維持すれば、ランデブーは簡単に破られる。タイムアウトを削除すれば、正当にスタックしたコアが最初の SMI でプラットフォームをハングさせる。タイムアウトを増やせば、SMM エントリごとに全コアを静止させなければならないマルチコアプラットフォームのパフォーマンスが低下する。最善の前進策は何か、あるいは前進策がそもそも存在するのかどうかは不明だ。
それまでの間、推奨される回避策は、長い命令を実行しないことだ。
概念実証のデフォルトの 0xfcc68860 での vmovdqu は、このマシン——Zen 3 Ryzen 7 5800H——での遅い箇所であり、他の場所ではおそらく該当しない。あなたのマシンでランデブーを破るには、ストールがあなたの SMM タイムアウトを超えるように長い命令を再調整する必要がある。これを行うためのヒントをいくつか示す:
-r xmm → ymm → zmm とステップアップする。make # smiiiiiiiiiiiiiiii をビルド
デフォルトは1台のマシンに調整されている。 Zen 3 Ryzen 7 5800H 以外では、長い命令を再調整するまで乖離は期待しないでほしい——プラットフォームへの移植を参照。
各コアの SMI カウンタの乖離を監視しながら、非常に非常に長い命令を繰り返し発火するツールを実行する:
sudo ./smiiiiiiiiiiiiiiii # デフォルト: 0xfcc68860 で -r xmm
フラグ:
| フラグ | デフォルト | 説明 |
|---|---|---|
-r xmm|ymm|zmm | xmm | タイミング測定用 MMIO 読み取りのベクタレジスタ幅(16/32/64 バイト)。SMI カウントの差が観測されない場合、ツールは次のサイズへのステップアップを推奨する。 |
-a <phys-addr> | 0xfcc68860 | MMIO タイミングループの対象物理アドレス(16進 0x... または10進)。 |
-h, --help | — | 使用方法を表示して終了。 |
smiiiiiiiiiiiiiiii は Christopher Domas(@xoreaxeaxeax)による研究プロジェクトです。