[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]
[画像のクリックで拡大表示]

 ようやくWindows XP Service Pack 2(SP2)が正式に公開された。このサービス・パックには「セキュリティ強化機能搭載」という名前が付けられているように,単なるバグ修正モジュールの集合ではない。コンピュータ・ウイルスなど,最近世間を騒がせているネットワーク(インターネット)からの脅威に対して,あらかじめ防御する仕組みが追加された。

 その1つが,「データ実行防止(Data Execution Prevention=DEP)」と呼ぶ機能だ。これは,簡単に言うと,データ領域でのプログラム実行を強制的に遮断する機能だが,大きく分けて2つの仕組みから成る。

Windows XP SP2のDEPには2種類ある

 1つはハードウエアによるもので,米Advanced Micro Devices(AMD)の64ビット・アーキテクチャAMD64が備えている「拡張ウイルス防止機能(Enhanced Virus Protection=EVP)」を利用している。

 NX(No Execute)とも呼ばれるこの機能は,仮想記憶の単位であるページごとにコード用/データ用という目印を付けておき,データ用としてマークしたページでプログラムが実行されようとすると,それを強制的に中断する(例外を発生する)。米Intelや米Transmetaなども対応を表明しているが,2004年8月現在,NXが実装されているのはAMD製の64ビットCPUだけである。そのほかのCPUを備えたPCでは,ハードウエアによるDEPは効かない。

 もう1つはソフトウエアによる機能で,実はRC2版(製品候補版2)から扱いが変わった。RC2では,ソフトウエアによるDEPはVisual C++の「/GS」オプションによるものだとされていた。それが製品版では,例外ハンドラのチェックによるものだとされている。製品版SP2でもVisual C++の「/GS」オプションがオンの状態でコンパイルされているが,それを(ソフトウエア)DEPと呼んでいない。

 ちなみにVisual C++に「/GS」オプションを付けてコンパイルすると,次のようなコードを生成する。

 まず,プロローグ・コード(スタックに自動変数領域を取るなどの処理をする関数突入時の初期化コード)で,スタック上の自動変数領域と関数からの戻りアドレス値との間に,カナリヤと呼ぶ目印(32ビットの値)を埋め込む。そして,エピローグ・コード(スタックに取った自動変数領域を解放するなどの処理をする関数脱出時の後始末コード)で,カナリヤの値を調べる。もしも,プロローグ・コードで格納した値と違う値であれば,プログラムの実行を強制終了する(例外を発生する)。「カナリヤ」は鳥のカナリヤを意味しており,その昔,炭鉱での毒ガス検知にカナリヤを使っていたことが由来だそうだ。

バッファ・オ-バーフロー攻撃を防ぐ

 ハードウエアによるDEP機能とVisual C++の「/GS」オプションは,どちらもバッファ・オーバーランまたはバッファ・オ-バーフローと呼ぶプログラムのバグを突いた攻撃を未然に防ぐためにある。BlasterやSasserなど,ネットワークに接続しただけで感染してしまうコンピュータ・ウイルス(ワーム)の多くが,バッファ・オ-バーフローを悪用したものだからだ。

 バッファ・オーバーフロー攻撃は,悪意のあるプログラムを含んだ大量のデータを外部から送信し,プログラムが内部に確保したデータ格納領域(バッファ)の境界を越えて書き込ませることで行う。バッファの境界の先には,その関数からの戻りアドレスが格納されており,境界を越えて書き込まれたデータは戻りアドレスの値を書き換えてしまう。このとき,戻りアドレスが格納されていた領域に,バッファ内に送り込んだ悪意のあるプログラムの先頭アドレスを上書きすれば,そのプログラムを実行させられるわけだ。

 この攻撃が可能な原因は,現在のCコンパイラのほとんどが自動変数をスタック領域に取ること,そしてWindowsが動作するx86アーキテクチャのCPUでは関数からの戻りアドレスも同じスタック領域に格納すること,さらにスタックにはアドレスの上位から下位に向かってデータが積まれ,データ自体はアドレスの下位から上位に向かって書き込まれることが重なったことである。

 そもそも,データを格納する領域であるスタックでプログラムを実行させなければ,バッファ・オーバーフロー攻撃を受けても,致命的なダメージを受けることはない。このような考えで実装されたのが,ハードウエアによるDEPだ。

 一方,バッファ・オーバーフロー攻撃を受けたときは,スタック上にある関数からの戻りアドレスが書き換えられる。そこで,バッファと戻りアドレスの領域の間に目印を置いておけば,バッファ・オーバーフロー攻撃を受けたときにその目印も書きえられているはずだ。このような考えによる実装が,Visual C++の「/GS」オプションによるコードである。

DEPは副作用を伴う

 このように,Windows XP SP2に実装されたDEPは,仕組みは異なるが,結果としてバッファ・オーバーフロー攻撃を未然に防ぐ。ただし,その仕組み上,副作用を伴うことも事実だ。

 ハードウエアによるDEPは,データ領域としてマークされたメモリー領域でのプログラム実行を遮断する機能である。システム・プログラム/ユーザー・プログラムを問わず,すべてのプログラムに対して効く。ただし,バッファ・オーバーフローとは関係なく,データ領域としてマークされたメモリー領域でプログラムが実行されようとすると,例外が発生する。

 そのため,確保したデータ領域にプログラムをロードしてそれを実行するようなプログラムを意図して作ったとしても,Windows XP SP2のDEPによって実行が阻止されてしまう(ただし,AMD64アーキテクチャのCPUを備えたPCで実行した場合である)。例えば,圧縮した実行ファイルをメモリー上に展開し,それを実行するようなプログラムは注意が必要である。JIT(Just In-Time)コンパイルするようなプログラムも同様だ。

 これらのプログラムは,メモリー領域を確保する際,Cランタイム関数のmallocや,Win32 APIのHeapAllocを使ってはいけない。これらの関数はデータ領域としてメモリーを確保するからだ。プログラムを格納するためのメモリー領域を確保するには,VirtualAlloc APIにPAGE_EXECUTEなどのフラグを指定して確保する必要がある。

図1●確保したメモリー領域でプログラムを実行させるサンプル・プログラム
Athlon 64搭載のPCで実行した。VirtualAlloc APIにPAGE_EXECUTE_READWRITEフラグを指定してメモリーを確保した場合は正しく動作するが,mallocなどで確保した場合は図のように例外が発生する。
(プログラムのソース・コードはこちら
 確保したメモリー領域でプログラムを実行する簡単なサンプル・プログラムを作って調べたところ,PAGE_EXECUTE_READWRITEフラグを指定してVirtualAlloc APIによって確保したときには正しく実行できたが,malloc関数で確保したり,VirtualAlloc APIを使ってもPAGE_READWRITEフラグを指定したときはDEPによってプログラムが強制終了させられた(リスト1図1[拡大表示])。独自開発したプログラムが,Windows XP SP2+AMD64の環境でDEPによって強制終了させられるようなら,メモリーの確保方法を疑ってみるとよいかもしれない。

Visual C++の「/GS」オプションによる影響は軽微

 一方,「/GS」オプションを付けてVisual C++でコンパイルしたときに生成される,カナリヤを使った処理による影響は軽微なものだ。このカナリヤを使った処理は,カナリヤ値が書き換えられたかどうかを調べるだけで,悪意のプログラムが実行されようとしているかどうかには無関係である。攻撃を受けたのではなく,単にプログラムのバグでバッファをオーバーフローしたときでも例外が発生する。だがこの場合は悪い副作用とは言えないだろう。

 またこの処理は,Windows XP SP2に含まれる各実行ファイルに埋め込まれている。そのため毎回プロローグ・コードでカナリヤ値を埋め込み,エピローグ・コードでカナリヤ値を調べる処理が実行される。つまり,余計な処理を毎回するので,それらの処理をしない場合に比べると,遅くなるのではないかという懸念がある。