
コンパイラの仕様とx86のアーキテクチャが原因
(1)サブルーチンに渡す引数をスタックに格納
もちろん,(4)の処理のときに,外部から送り込まれたデータの大きさを調べ,プログラムが確保したバッファの大きさよりも小さいことを確認するか,大きい場合には,はみ出した部分を切り捨てるなどの処理をしていれば,戻りアドレスが上書きされることはなく,送りつけられた不正なプログラムが動き出してしまうこともない。 だが,C言語やC++言語といった,現在主にシステム・プログラムの開発に使われているプログラミング言語には,バッファにデータを書き込む前にその大きさを確認することや,バッファの大きさを超えるデータの書き込みを禁止することは,文法上定められていない。つまり,確保したバッファに,その大きさを超えるデータを書き込んでも,文法上は間違いではないのである。そのため,うっかり危険なプログラムを書いてしまっても,コンパイル時にエラーにならないので,そのまま運用してしまうことがある。 最も危険なのは,そもそもプログラマが,C/C++言語には潜在的にプログラムが危険になってしまう恐れがあることや,上記のような攻撃方法があることなどを知らずにプログラムを書いてしまうことだ。 ちなみにBASIC,C#,Javaなど,文法上,確保したバッファ以外の領域にはデータを書き込めないよう定められているプログラミング言語もある。これらのプログラミング言語で開発すれば,原理上,バッファ・オーバーフロー攻撃は受けない(ただしこれら3つの言語ではOSのようなシステム・プログラムを開発できない)。 つまりバッファ・オーバーフローのぜい弱性は,(1)現在のC/C++コンパイラのほとんどが自動変数をスタック領域に取ること,(2)x86では関数からの戻りアドレスも同じスタック領域に格納すること,さらに(3)x86のスタックにはアドレスの上位から下位に向かってデータが積まれ,データ自体はアドレスの下位から上位に向かって書き込まれること——が重なったことに起因する。関数からの戻りアドレスを書き換え,バッファ上に送り込んだ不正プログラムにジャンプさせるのが,バッファ・オーバーフロー攻撃の典型例である。そのため,関数からの戻りアドレスが書き換えられていないことを調べるか,バッファ(データ領域)上でプログラムを実行させなければ,バッファ・オーバーフロー攻撃を受けてもセキュリティが脅かされることはない。
メモリーをデータ領域とプログラム領域に区別する 実は元々x86にも,ハードウエアによるデータ実行防止機能が実装されているのだが,Windowsはそれを使っていなかった(使えなかった)。 x86にはセグメントと呼ぶメモリー管理機構がある。セグメントとは,メモリー領域を分割して利用する仕組みで,プログラムを格納するコード・セグメント,データを格納するデータ・セグメント,スタック用のスタック・セグメントなどに分ける。このセグメント機構に,コードの実行を許可/禁止する機能があるのだ。コード・セグメントにのみプログラムの実行を許可しておけば,データ・セグメントやスタック・セグメントでプログラムを実行できない。バッファ・オーバーフロー攻撃によってスタック・セグメント上のプログラムにジャンプしてしまっても,実行されない。 だが,現在の32ビットWindowsはセグメント機構を使っていない。正確には,フラット・メモリー・モデルと呼ぶ,セグメント化しない(コード,データ,スタックが同じ1つのセグメントに置かれる)モードを使っている。16ビットWindowsでは,1つのセグメントの大きさが64Kバイトと小さかったため,セグメントを使わなければ64Kバイトを超えるメモリー領域を扱えなかった。その後x86は,32ビット化されたときに,フラット・メモリー・モデルを備え,セグメントを使う必要がなくなった。このモードでは,セグメントを使い分けなくても,OSとアプリケーションが連続した4Gバイトのアドレス空間にアクセスできるからだ。ところがこのモードでは,セグメントの切り替えという面倒な処理をする必要がなくなったものの,データ領域でのプログラム実行を禁止できないのである。
物理アドレス拡張モードのPTEに制御ビットを定義
従って,ハードウエアDEP機能を利用するためには,PAEモードでOSを起動する必要がある。実際,AMD64アーキテクチャのCPUを備えたPCでDEP機能を利用するときは,必ずPAEモードで起動する(図8)
動的に確保したメモリーでのプログラム実行に影響する そのため,プログラム内で確保したデータ領域にプログラムをロードしてそれを実行するようなプログラムを意図して作ったとしても,ハードウエアDEPはその実行を阻止してしまうことがある。例えば,圧縮した実行ファイルをメモリー上に展開し,それを実行するようなプログラムは注意が必要である。JIT(Just In-Time)コンパイルするようなプログラムも同様である。 これらのような処理をするには,メモリー領域を確保するのに,Cランタイム関数のmallocや,Win32 APIのHeapAllocを使ってはいけない。これらの関数はメモリーをデータ領域として確保するからだ。プログラムを格納するためのメモリー領域を確保するには,VirtualAlloc APIにPAGE_EXECUTEなどのフラグを指定して確保する必要がある。
例外ハンドラの安全性を確認するソフトウエアDEP 一般に,「0で除算した」などプログラムで例外が発生すると,例外ハンドラと呼ぶ,そのプログラムが用意した特別な関数にジャンプする(発生した例外に対応するハンドラがない場合は,Windowsがメッセージ・ボックスを表示して,そのプログラムが強制終了される)。 Windowsは「構造化例外ハンドラ」と呼ぶ仕組みを備えている(C++の例外ハンドラは,構造化例外ハンドラに似ているが,より機能を付加したものである)。それに対応したプログラムではスタックに例外ハンドラへのジャンプ・テーブル(複数のジャンプ先アドレスの一覧表)を用意する。構造化例外ハンドラを備えたプログラムで例外が発生したときの処理手順は次のようになる。
(1)例外が発生 ソフトウエアDEPは,上記処理の中の(3)と(4)の間に,(4)でジャンプしようとしている領域が実行可能領域としてマークされていることを確認する。もしもジャンプ先がデータ領域だった場合は,ジャンプしない。これによって,不正な例外ハンドラにジャンプし,実行されることを防ぐ。 コンピュータ・ウイルスの中には,自分の活動を検知されにくくするために,例外ハンドラを利用するものがある。例外ハンドラを自分が用意した不正プログラムに設定し,わざと例外を発生させるわけだ。こうすると,自身の中に用意した不正プログラムにジャンプするコードがプログラムの中で直接実行されるわけではないので,活動が分かりにくくなる。ソフトウエアDEPはこのような悪意のあるプログラムの実行を阻止できる。 (『日経Windowsプロ』2005年3月号掲載「今から備えるWindows Server 2003 SP1」より)
|