次のページ 前のページ 目次へ

5. バッファオーバーフローの回避

非常に一般的なセキュリティ的欠陥に「バッファオーバーフロー」がある. 技術的には,バッファオーバーフローはプログラムの内部的実装の問題だが, バッファオーバーフローは一般的かつ重大な問題なので,この問題の説明に 章をひとつ割くことにする. この問題がどれだけ重要かというと,CERT による勧告の 1998 年 9, 13 号, そして 1999 年の勧告の少なくとも半分がバッファオーバーフロー に関係している. Bugtraq における非公式の調査によると, セキュリティの弱点の一番多い原因はバッファオーバーフローであると回答者 の約 2/3 が感じている(残りの回答者は「設定ミス」であるとしている) [Cowan 1999].

バッファオーバフローが起こるのは,値の集合(普通は文字列)を固定長の バッファに書き込む際にバッファの終端を超えて書き込み続けたときである. バッファオーバーフローはユーザからの入力をバッファに読み込む時に起こる 可能性があるが,他の種類の処理をプログラム中で行っている最中にも起こる 可能性がある.

安全であるべきプログラムでバッファオーバーフローを許すと,普通は 攻撃者が悪用できてしまう. バッファが C 言語のローカル変数ならば,オーバーフローを利用して 攻撃者の好きなコードを実行する関数を呼ばせることができる (詳しくは [Aleph1 1996] を参照すること). バッファがヒープ中にあればまだましであるが,攻撃者はこれを利用して プログラム中の変数を制御することができる.

プログラミング言語によっては,本質的にこの問題の影響を受けない. その理由としては,言語が自動的に配列のサイズを変える場合(例: Perl)や, 普通はバッファオーバーフローを検出して防ぐ場合(例: Ada95)がある. しかし,C はバッファオーバーフローを防ぐ手段を全く持たないし,C++ も この問題を起こすような使い方が簡単にできてしまう.

5.1 C/C++ における危険

C を使う場合は,境界を絶対に超えないことが確実な場合を除き, 境界をチェックしない危険な関数の使用を避けるべきである. 多くの場合に避けるべき関数としては, strcpy(3), strcat(3), sprintf(3), gets(3) 等がある. これらの関数はそれぞれ strncpy(3), strncat(3), snprintf(3), fgets(3) で置き換えるべきであるが,詳しくは後で説明する. 関数 strlen(3) は,文字列の終端の NIL 文字が見つかることが確かで ない限りは利用を避けるべきである. この他に(使い方によっては)バッファオーバーフローを許すかもしれない関数 としては,fscanf(3), scanf(3), vsprintf(3), realpath(3), getopt(3), getpass(3), streadd(3), strecpy(3), strtrns(3) がある.

5.2 ライブラリを用いた解決方法(C/C++)

C/C++ における解決方法の一つは,バッファオーバーフローの問題を持たない ライブラリ関数を使うことである.

C でバッファオーバーフローを起こさないための「標準的」な解決方法は, このような問題を受けない C ライブラリの標準関数を使うことである. このアプローチでは,標準ライブラリ関数の strncpy(3) と strncat(3) に 深く頼ることになる.このアプローチを使う場合は注意すること: これらの 関数のセマンティクスは少し変わっていて,正しく使うことが難しい. 関数 strncpy(3) は,コピー元の文字列の長さがコピー先の長さ以上の場合, コピー先の文字列を NIL で終端しない.したがって,strncpy(3) を呼び出し た後は,コピー先の文字列の最後の文字に必ず NIL を設定すること. strncpy(3) と strncat(3) のいずれでも,利用可能な容量の残りを渡してや らなければならないが,この計算は間違いやすい(そして,この計算を間違う とバッファオーバーフロー攻撃を許すことになる). どちらの関数も,バッファオーバーフローが起きたかどうかを調べる簡単な 仕組みを持っていない. 最後に,strncpy(3) は置き換えの対象である strcpy(3) よりも性能的には不 利である.その理由は,strncpy(3) はコピー先の残りの領域を 0 で埋めるか らである.

別の方法として(これは OpenBSD よりもらってきた方法である), Miller と de Raadt が開発した strlcpy(3) と strlcat(3) がある[Miller 1999]. これは最小化アプローチであり,,文字列のコピーと連結を標準の関数群と異 なる(そして間違いが起こりにくい)インタフェースで C 言語に提供する. これらの関数のソースコードと文書は, ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 からBSD 形式のライセンスに基づいて入手できる.

さらに別の方法としては,固定長のバッファを用いず,全ての文字列を動的に 割り当て直す方法がある. この汎用的な方法は,GNU プログラミングガイドラインが推奨している. C 言語で文字列の動的な再割り当てを自動的に行うためのツールセットの一つ として,Forrest J. Cavalier III による「libmib allocated string functions」 がある.これは http://www.mibsoftware.com/libmib/astring から入手できる. このソースコードはオープンソースである.文書はオープンソースではないが, 自由に入手できる.

役に立つかもしれないライブラリは他にもある. 例えば,glib ライブラリはオープンソースのプラットフォームで広く利用可 能である(GTK+ ツールキットは glib を用いているが,glib は GTK+ とは別 に利用可能である). 今回は glib を解析して,glib のライブラリ関数がバッファオーバーフロー を起こさないことを明らかにすることはできなかったが,これは正しいようで ある. 本論文の後の版では,どの glib 関数を使えばバッファオーバーフローの問題 を避けられるかを確かめられると考えている.

5.3 コンパイルを行う解決方法(C/C++)

全く別のアプローチとして,境界チェックを行うコンパイル方法を用いること ができる(方法の一覧については [Sitaker 1999] を参照). 筆者の意見では,こういったツールは防御を何重にもする場合には非常に役立 つが,この技術を単独の防御手段として用いるのは賢明とは言えない. この理由は少なくとも 2 つある. まず最初の理由は,こういったツールのほとんどは一部の バッファオーバーフローしか防げない(「完全」に防ごうとすると,一般に 速度が 12-30 倍遅くなる). これは単に,C や C++ はバッファオーバーフローを防ぐようには設計されて いないからである. 次の理由としては,オープンソースのプログラムでは,プログラムのコンパイ ルする際に使われるツールがはっきりしない.したがって,あるシステムの デフォルトの「普通」のコンパイラを使ったとたんにセキュリティの欠陥が生 じることになる.

より便利なツールのひとつに「StackGuard」がある.このツールは「防御」の ための値(「canary(カナリア)」と呼ばれる)を復帰アドレスの前に挿入する. もしバッファオーバーフローによって復帰アドレスが書き換えられると, この canary 値が(多分)変化し,システムはそのアドレスを使う前に バッファオーバーフローを検出できる. このツールは非常に重要だが,他の値を書き換えるようなバッファオーバーフロー は防げない点に注意すること. StackGuard を拡張して他のデータ要素にも canary 値を追加できるツールが ある.これは「PointGuard」と呼ばれている. PointGuard は特定の値(例えば関数へのポインタや longjump バッファ等)を 自動的に保護する. しかし,PointGuard を使って他の型の変数を守るためには,プログラマが 特定の仲介作業を行う必要がある(プログラマは,canary 値を使って 守るべきデータを指定しなければならない). このツールは重要だが,保護はいらないと考えていたが実は保護が必要な データの保護を間違って省略してしまいやすい. StackGuard, PointGuard やこれらの代わりとなるツールに関する詳しい情報 ついては,Cowan [1999] を参照すること.

関連事項として,Linux カーネルを修正してスタックセグメントを実行可能で なくすることもできる.このようなパッチは実際に存在する( Solar Designer のパッチにこの機能が含まれている. パッチは http://www.openwall.com/linux/ から入手できる). しかし,この論文を書いている時点では,このパッチは Linux カーネルには 取り込まれていない.その根拠のひとつは,このパッチは見ためほど 防御効果がないことである.つまり,攻撃者は単に 既にプログラムに入っている他の「面白い」場所(例えば,ライブラリ内,ヒー プ内,静的データセグメント等)をシステムに呼ばせることができるのである. また,Linux はスタック内に実行可能なコードを必要とすることもある. 例えば,シグナルの実装や,GCC の「トランポリン」を実装する時である. [訳注: 「トランポリン」とは,入れ子になった関数に入るために実行時 に生成されるコードのことです] Solar Designer のパッチはこれらのケースにも対処できるが,そのために パッチが複雑になっている. 個人的にはこのパッチが本家の Linux 配布に組み込まれることを希望してい る.なぜなら,このパッチにより攻撃がいくらか困難になるし,ある範囲の 既存の攻撃を防ぐことができるからである. しかし,このパッチは見ためほど防御を強化しないことと,比較的容易に出し 抜ける点については筆者は Linus Torvalds たちと同意見である. この機能を含めないことに関する Linus Torvalds の説明は http://lwn.net/980806/a/linus-noexec.html で読むことができる.

要約すると,まずはバッファオーバーフローによる攻撃を受けない正しい プログラムを開発する方がよいということである. そして,それを行った上で,StackGuard 等の技術やツールを追加の安全ネット として使うとよい. コード自身からバッファオーバーフローを無くすように努力すれば, StackGuard もさらに効果的に使えるだろう.なぜなら, 防御のために StackGuard を呼び出すこととなる「鎧の隙間」が少なくなるか らである.

5.4 他の言語の使用

バッファオーバーフローの問題は,バッファオーバーフローに対して安全な Perl, Python, Ada95 といった他の言語でも問題となる. もちろん,こういった言語を使ったからといって全ての問題がなくなるわけで はない.特に「呼び出しの際に正しい値だけを使う」で述べられている NIL 文字に関する説明を参照すること. また,他の言語のインフラ(例えば実行時ライブラリ等)が利用可能かつ安全で あることを保証することの問題もある. ただしそれでも,バッファオーバーフローが起こらない安全なプログラムを 開発する際には,他の言語の使用を確かに検討すべきである.


次のページ 前のページ 目次へ