おい、最近のインシデントログを見たか? Webアプリケーションの脆弱性ばかりに気を取られているが、足元のエンドポイントやバイナリレベルの防御がガタガタだと、たった一本のメモリ破壊脆弱性から一網打尽にされる。今日はその象徴とも言える「SEH(Structured Exception Handling)オーバーフロー」と、それを叩き潰すための防壁――SafeSEHとSEHOPについて叩き込む。
暗号理論や公開鍵基盤でどれだけ堅牢な通信路を作ろうが、エンドポイントのメモリ管理がザルであれば、攻撃者は平文の秘密鍵をメモリ上から引っこ抜いていく。セキュリティを志す者なら、スタックの底で何が起きているのか、その泥臭い現実を知る必要がある。
ペンキを塗り替えるだけの気休めではなく、OSの根幹を守る仕組みの裏側を覗いてみよう。
—
1. なぜSEHが攻撃者の「お気に入りの標的」になるのか?
Windows環境におけるバッファオーバーフロー対策として、長年DEP(Data Execution Prevention / NXビット)やASLR(Address Space Layout Randomization)が語られてきた。これらがあるおかげで、スタックに直接シェルコードを置いて実行するような古き良き攻撃は難しくなった。
しかし、攻撃者は諦めない。彼らは「例外処理(Exception Handling)」というOSの正当な機能に目をつけた。
アプリケーションが不正なメモリアクセスなどでクラッシュした際、Windowsは例外をキャッチし、登録された例外ハンドラ(SEH)を呼び出して復旧を試みる。このSEHのアドレスポインタはスタック上に置かれることが多い。
もし、バッファオーバーフローによってローカル変数を溢れさせ、その先にあるSEHのレコード(_EXCEPTION_REGISTRATION_RECORD)を上書きできたらどうなるか?
クラッシュの瞬間にCPUは攻撃者が書き換えたアドレス(例えば、NOPスレッドやスタックピボット命令へのポインタ)へジャンプし、制御を奪う。これがSEHオーバーフロー攻撃(SEH Overwrite)の基本原理だ。
攻撃のメカニズム(概念的PoC)
C言語などでよくある、脆弱なバッファコピー実装をイメージしてほしい。
#include <windows.h>
#include <stdio.h>
void vulnerable_function(char *user_input) {
char buffer[512];
// 512バイトのバッファに対して、境界チェックなしで入力をコピー
// ここで溢れたデータが、スタック上のSEHチェインを書き換える
strcpy(buffer, user_input);
}
int main(int argc, char **argv) {
if (argc > 1) {
vulnerable_function(argv[1]);
}
return 0;
}
攻撃者は、bufferをあふれさせ、スタック上のSEHポインタを次のように書き換える。
1. 例外ハンドラのアドレス(Handler): 攻撃者が用意したROP(Return-Oriented Programming)ガジェットや、POP POP RET命令のアドレスに書き換える。
2. 次のレコードへのポインタ(Next): ショートジャンプ(短距離ジャンプ命令)を仕込み、ハンドラコードへ無理やり誘導する。
この古典的かつ強力な手法を防ぐために生まれたのが、SafeSEHとSEHOPだ。
—
2. SafeSEH:コンパイル時における「例外ハンドラのホワイトリスト検証」
最初に登場した防御機構が SafeSEH だ。これは実行時ではなく、バイナリのビルド時(コンパイル時)に組み込まれる。
SafeSEHの仕組み
SafeSEHが有効化されたバイナリには、コンパイラ(Visual Studio等)によって「正当な例外ハンドラのアドレス一覧(テーブル)」がPEヘッダ内に埋め込まれる。
アプリケーション実行中に例外が発生し、OSが例外ハンドラを呼び出そうとする直前に、以下の検証を行う。
1. 呼び出そうとしている例外ハンドラのアドレスが、PEヘッダのSafeSEHテーブルに存在するかを確認する。
2. テーブルに存在しない場合、OSは「不正なハンドラへのジャンプ」とみなし、即座にプロセスを強制終了(クラッシュ)させる。
これにより、スタック上で書き換えられた偽のハンドラアドレスを指す試みは、すべて検知・ブロックされる。
開発・ビルド時の設定(Visual Studio / C++)
現代の開発において、SafeSEHを有効にすることは基本中の基本だ。リンカーオプションで明示的に制御できる。
<!-- MSBuild / vcxproj における設定例 -->
<PropertyGroup>
<!-- SafeSEHを有効化する(32ビット環境用) -->
<ImageHasSafeExceptionHandlers>true</ImageHasSafeExceptionHandlers>
<!-- モダンなコンパイラではデフォルト有効だが、念のためリンカーフラグを確認 -->
<!-- 命令行引数としての指定: /SAFESEH -->
</PropertyGroup>
※注意点として、SafeSEHは32ビット(x86)バイナリ特有の防御機構である。64ビット(x64)環境では、例外ハンドラテーブルの構造自体が異なり、後述のSEHOPやOSレベルの構造によって保護されているため、SafeSEHの明示的な指定は不要(あるいは無視される)となる。
—
3. SEHOP:実行時における「例外ハンドラチェインの整合性検証」
SafeSEHは非常に有効だが、弱点もあった。それは「サードパーティ製の古いDLLや、SafeSEHに対応していない古いライブラリがプロセス内にロードされている場合、モジュール全体のSafeSEHが無効化されてしまう」という仕様上の抜け穴だ。
そこで登場したのが SEHOP(Structured Exception Handling Overwrite Protection) である。こちらはコンパイル時ではなく、Windows OS(Vista以降、デフォルトはWin8以降で常時有効)が提供する実行時防御機構だ。
SEHOPの仕組み
SEHのレコードは、スタック上で単なるポインタの集まりではなく、単方向連結リスト(チェイン)として構成されている。
正当なプログラムであれば、このチェインの終端(一番最初、あるいは一番古いレコード)には、OSが用意した特別な番兵(エンドマーカー)が存在しなければならない。
SEHOPは、例外が発生した際に以下のチェックを行う。
1. スタック上のSEHチェインを、先頭から終端まで順にたどる。
2. チェインの構造が途中で途切れていないか、あるいはポインタがおかしなメモリ領域を指していないかを検証する。
3. 終端に特定のファイナライズ用レコード(End of Chain / 0xFFFFFFFF や特別なポインタ)が存在するかを確認する。
バッファオーバーフローによってSEHチェインが上書きされると、この美しい連結構造や終端の番兵が破壊される。SEHOPはこの違和感を検知し、攻撃が実行される前にプロセスを強制終了させる。
OS全体またはアプリケーション単位でのSEHOP有効化(レジストリ設定)
サーバー環境や特殊なレガシー環境でSEHOPの挙動を確認・強制するには、レジストリの調整が必要になる場合がある。以下はインフラ担当者が知るべき実務的な設定だ。
# PowerShellを用いたSEHOPのグローバル有効化(要管理者権限・再起動が必要な場合あり)
# 既存のMitigationOptionsレジストリキーを安全に操作するスニペット
$Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\kernel"
$Name = "MitigationOptions"
# 既存の値を確認しつつ、SEHOPを強制有効化するフラグを付与するロジック
# 実務ではシステムの安定性に影響を与えるため、ステージング環境で十分に検証すること
Write-Host "Checking mitigation options..."
通常、モダンなWindows ServerやWindows 10/11では、DEPと同様にデフォルトで有効化されている。しかし、非対応の古いインハウス製アプリケーションを移行する際に、SEHOPが原因でクラッシュ(False Positive)を起こすケースがある。その場合は、アプリケーション単位で例外リストに登録するか、根本的にレガシーコードを改修する必要がある。
—
4. セキュリティチーフからの実務的アドバイス:モダンな開発者が取るべき対策
SafeSEHやSEHOPは、いわば「最後の安全ネット(セーフティネット)」だ。これらに頼りきりになるのは、鍵のかかっていない家で、防犯カメラの性能を誇っているようなものだ。
実務において、我々エンジニアが徹底すべき原則をまとめる。
1. メモリ安全性の高い言語・書き方の選択
C/C++での生ポインタ操作や、危険な関数(strcpy, sprintf, getsなど)の使用を社内規約で完全に禁止せよ。どうしてもC/C++を使うなら、Bounds-checkedな関数(strncpy_sなど)を使用するか、RustやGoといったメモリ安全な言語へのリプレイスを検討しろ。
2. コンパイルオプションの総点検(Hardening)
ビルドパイプライン(CI/CD)において、以下のフラグが確実に有効になっているかを自動テストの要件に組み込め。
- ASLR / DEP (NX) の強制
- SafeSEH (32bitビルド時)
- Control Flow Guard (CFG) / CET (Control-flow Enforcement Technology): 近年のハードウェアレベルの制御フロー保護。ここまでやれば完璧に近づく。
3. 静的解析ツール(SAST)とファジングの導入
コードレビューだけでバッファオーバーフローを完全に見抜くのは人間の目には限界がある。ビルドプロセスにSASTを組み込み、さらにネットワークやファイル入力を受け取るバイナリには、必ずFuzzing(ファジング)を実施して、予期せぬ例外が発生しないことを証明してからリリースしろ。
セキュリティは「点」ではなく「面」で守るものだ。暗号理論でデータ秘匿を担保しつつ、エンドポイントのバイナリ保護でOSの深部を固める。この両輪があって初めて、プロフェッショナルなシステムと言える。
手を動かすエンジニアよ、今日のビルド設定とコードを見直そう。脆弱性は、常に最も油断している隙間から忍び寄るものだ。
コメント