図1 下方成長スタックでのバッファ・オーバーフローの様子  関数内変数のバッファ・オーバーフローにより,関数の戻りアドレスが書き換えられるのが分かる。
図1 下方成長スタックでのバッファ・オーバーフローの様子  関数内変数のバッファ・オーバーフローにより,関数の戻りアドレスが書き換えられるのが分かる。
[画像のクリックで拡大表示]
図2 上方成長スタックでのバッファ・オーバーフローの様子  関数内変数のバッファ・オーバーフローでは,関数の戻りアドレスが書き換えられないように見える。
図2 上方成長スタックでのバッファ・オーバーフローの様子  関数内変数のバッファ・オーバーフローでは,関数の戻りアドレスが書き換えられないように見える。
[画像のクリックで拡大表示]
図3 例示したコードを悪用して戻りアドレスを書き換える様子  ポインタ渡しされているため,gets関数が直接buf配列に書き込める。標準のgets関数はバッファ・オーバーフロー問題を抱えるため,gets関数の戻りアドレスを書き換えられてしまう。
図3 例示したコードを悪用して戻りアドレスを書き換える様子  ポインタ渡しされているため,gets関数が直接buf配列に書き込める。標準のgets関数はバッファ・オーバーフロー問題を抱えるため,gets関数の戻りアドレスを書き換えられてしまう。
[画像のクリックで拡大表示]

 IT Pro読者の方なら,「バッファ・オーバーフロー」という言葉を何度も聞いたことがあるだろう。そう,代表的なセキュリティ・ホールの一つである。10月12日に公表されたWindows製品のセキュリティ・ホールの中にも,バッファ・オーバーフローに関するものが複数含まれる(関連記事)。

 これらのセキュリティ情報を読んで,以前,バッファ・オーバーフローを悪用する攻撃(詳細については後述)を解説する記事を書いたときのことを思い出した。書き上げた記事を査読したデスクは,筆者に次のように問いかけた。「バッファ・オーバーフローを悪用して戻りアドレスが書き換え可能なのは,スタックが下方成長するからじゃないか?」。考えもしなかったことである。しかし,言われてみると全くその通りのように思えた。

 「上方成長するスタックを使えば,バッファ・オーバーフローの脅威は無くなるのではないか?」。筆者としてはすぐにでも掘り下げて調べたかったが,忙しさにかまけて確認しないままでいた。そこで今回,「上方成長スタック」がバッファ・オーバーフロー攻撃に対する“万能薬”になりうるのかを改めて調べてみた。

上方成長スタックが良いと思うわけ

 バッファ・オーバーフローを悪用する手口の代表が,スタック領域に確保された固定長のバッファに多量のコードを送り込み,関数の「戻りアドレス」を書き換える手法である。戻りアドレスとは,関数が終了した際に,次にどこからプログラムを実行するかを示す位置情報である。つまり,この戻りアドレスを書き変えることができれば,任意のプログラムを実行できることになる。

 このような攻撃が可能なのは,C/C++言語の特性に関係がある。C/C++では一般に,関数内で使用する変数(バッファ)と戻りアドレスを,一緒にスタックに格納する。さらに文字列などの可変長データの管理がプログラマに任せられているため,プログラム・ミスによるバッファあふれを起こしやすい。x86互換プロセッサで稼働する一般的なOSにおいて,関数内で定義された自動変数でバッファ・オーバーフローが生じた場合のデータの増加の様子を図1に挙げた。

 図1に示した通り,関数の戻りアドレスは,引数データと共に最初期にスタックに積まれる。x86互換プロセッサのように,メモリーの上位アドレスから下位アドレスに向かって成長するスタック(downward growing stack)の場合,関数の戻りアドレスは(関数が使うスタック領域で)最上位部のアドレスに格納される。一方,バッファ内のデータは,通常のデータと同様に下位アドレスから上位アドレスに向かって増えていく。

 ここで,前述のデスクの問いが意味を持ってくる。メモリーの下位アドレスから上位アドレスに向かって成長するスタック(upward growing stack)を採用した場合,図1のシチュエーションにおける,バッファ・オーバーフローによるデータ増加は図2のようになる。つまり,どれだけデータが増えても,関数の戻りアドレスは書き変えられないことになる。

PA-RISCは上方成長スタックを採用

 さて,スタックはなぜ下方成長するのだろうか。それはメモリーを効率的に利用するためだ。

 プログラム実行時に割り当てられたメモリー空間には,データを格納する領域として,スタック領域のほかにヒープ領域やプログラム自身(これもデータである)を格納する領域も確保される。これらの領域とスタック領域は,それぞれどれだけのサイズを必要とするのか,プログラムを実行してみないと分からない。そのため,この両者の成長方向を逆にして,自由に利用できる領域の両端に配置することで,メモリーを最大限有効利用できるようにするのである。

 このような理由があるため,マイクロコンピュータ向けのプロセッサは,ほとんどが下方成長スタックを採用している。インテル製のプロセッサでも,8080以降ずっとこの下方成長スタックを使っている。なお,4040など8080以前のプロセッサでは,プロセッサ内部にスタック用のメモリーを別途用意する方式を採用していた。この辺りについては,米Intel社の技術者であったStanley Mazor氏の文書が詳しい。

 しかし,メモリー空間が拡大している現在,メモリーの効率利用というだけでは下方成長スタックを採用する理由として小さい。ソフトウエアの互換性を無視して言えば,セキュリティなど何か別のメリットがあれば,上方成長スタックを採用しても良いはずである。実際,すべてのプロセッサが下方成長スタックを採用しているわけではない。例えば,米Hewlett Packard社のUNIXワークステーションで使われていたPA-RISCシリーズのプロセッサが上方成長スタックを採用している。

 そこでPA-RISCに焦点を当てて調べると,米SteelEye社のプロダクト・アーキテクトであるJames Bottomley氏が「The State of Play for Linux 2.6 on PA-RISC」というプレゼンテーション資料で,「上方成長スタックがセキュリティを提供する」との意見を述べているのを発見した。まさにデスクと同じことを言っているではないか。さらに検索エンジンを駆使すると,デスクと同じ考えの人をほかにも多数発見した。

短いコードであっさり否定

 ところが,である。さらに調べを進めると,セキュリティ問題についての報告や議論をするメーリング・リスト「BugTraq」のアーカイブに,1997年にHewlett Packard社のBill Sommerfeld氏が投稿した記事を発見した。そこには,上方成長スタックが安全でない場合の例として,以下のようなコードが挙げてあった。


foo()
{
char buf[10];
gets(buf);
}

 短いながら,的確に問題点を指摘するコードである。この例では,gets関数を呼ぶ前にスタック上にbuf配列が確保される。そしてgets関数によって,buf配列にデータが書き込まれる。つまり上方成長スタックでは,buf配列があふれた場合に,gets関数の戻りアドレスが書き変えられてしまうのである(図3)。

 世の中に,うまい話がそう転がっているわけがない。上方成長スタックで問題が解決するのなら,もっとそうした声が専門家から上がるはずである。どうやら,スタックの成長方向は,安全性とは無関係のようだ。上方成長スタックはバッファ・オーバーフローの万能薬にはならない。

 専門家にも話を聞いた。「一般論としては,スタックの方向はバッファあふれ攻撃を防ぐ上ではあまり関係ない。確かに一部のケースでは順方向にスタックを伸ばすことで攻撃が困難になるケースは存在するが,既に記事中でも指摘の通り,画期的な安全性を得られるような変更ではない」(独立行政法人産業技術総合研究所 情報セキュリティ研究センター ソフトウェアセキュリティ研究チームの大岩 寛氏)。

 バッファ・オーバーフロー攻撃を検知して防御する方法は,過去の記者の眼でも触れたように,さまざまなものが考案され,実用に供されている。ただしこれらは「基本的には根本から完璧にあらゆるタイプのメモリ破壊攻撃を防げるものではなく,攻撃者にとって厄介な状況を作ることで攻撃を面倒にする性質のもの」(大岩氏)に過ぎない。

 上方成長スタックに限らず,バッファ・オーバーフロー攻撃の万能薬になる対策は存在しない。効果を過信せず,注意してプログラミングすることが必要である。また,長期的にはC/C++以外の言語を利用するなどの対策も考慮すべきである。

 なお同氏は,ANSI-Cのすべての範囲で,バッファあふれを含むあらゆるメモリー破壊を防止するC言語処理系「FailSafe-C」の実装に取り組んでいる。「3年程度で完成を目指したい」(同氏)というから大いに期待したい。