「バッファオーバーフロー」は、セキュリティの欠陥として頻繁に発生します。 技術的にはプログラムの実装上の問題ですが、あまりに頻繁に発生し、かつ 重大な問題を抱えているので、あえて独立して項目を立てました。 この問題がいかに重要かは、CERT の勧告の内 1998 年の 13 の内の 9、1999 年の少なくとも半分以上がバッファオーバーフロー関連であることで明らか です。 Bugtraq による非公式な調査でも、おおよそ 2/3 の回答がバッファオーバー フローがセキュリティの脆弱さの原因としています(残りの回答は「設定ミス」 が原因としています) [Cowan 1999]
訳註:Bugtraq は、セキュリティ関連の情報をやり取りする ML です。 ML のアーカイブ が公開されています。
バッファオーバーフローが発生するのは、固定長のバッファ領域にある値(文字列 など)を、その領域を越えて書き続けてしまう場合です。 これらの現象は、ユーザからの入力をバッファに読み込む時にも起こりますし、 プログラムのまったく違った処理の最中でも起こる可能性があります。
安全性が求められるプログラムでバッファオーバーフローを許してしまうと、 往々にして攻撃者に悪用される恐れがあります。 バッファが C のローカル変数で実装されていた場合、攻撃者はその関数の中で 望みのコードを強制的に実行させる手段としてオーバフローを利用できてしまいます。 バッファがヒープ領域にあっても、状況が改善するわけではありません。攻撃者 は、この状態でもプログラム中の変数をいじることができます。 さらに詳しいことは、Aleph1 [1996]、Mudge [1995]を参考にするか、 http://destroy.net/machines/security/ にある 「Stack Smashing Security Vulnerabilities」を見てください。
訳註:ヒープ領域は、プログラムで利用するデータを格納する領域で、 利用時に動的に割り当てられ、利用が済むと解放された後、再利用に回されます。 C では、auto 変数や malloc(3) で確保された領域がこれに当たります。
プログラム言語の中には、そもそもこういった問題に影響されないものもあります。 つまり自動的に配列の大きさを調整したり(たとえば Perl)、バッファオーバー フローを見つけだし、防ぐしくみを標準で備えているもの(たとえば Ada95)があり ます。 残念なことに、 C はバッファオーバーフローを防ぐ手段をまったく持っておらず、 C++ でもたわいなくこの問題を発生させることができます。
C ユーザは、確保されている領域を越えることはありえないと確証できなければ、 境界をチェックしない危険な関数を使うべきではありません。 通常使用を避けるべき関数には、strcpy(3)、strcat(3)、sprintf(3)や gets(3) があります。 その代わりに strncpy(3)や、strncat(3)、snprintf(3)や fgets(3)を使用することを 勧めます。詳しくは下記で論じます。 strlen(3)は NIL キャラクタが終端にあることを仮定しているので、NIL が必ず存在 すると確信できなければ、使用は避けるべきです。 その他にもバッファを越えてしまう恐れのある関数(その使い方によりますが) には、fscanf(3)、scanf(3)、vsprintf(3)、realpath(3)、getopt(3)、getpass(3)、 streadd(3)や strecpy(3)、strtrns(3)等があります。
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 [Miller 1999] 両氏によって開発された strlcpy(3)と strlcat(3)があります。 従来のコピーと連結とは異った(かつ間違いにくい)インタフェースを持ち、最小限 の努力で問題の解決を試みています。 ソースと関数のドキュメントは BSD スタイルのライセンスで、 ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 から利用できます。
他の取り組みとしては、固定長のバッファを使うかわりに、文字列すべての領域を 動的に再確保する方法もあります。 この手法は一般的で、GNU プログラミング ガイドラインで推奨しています。 その方法の 1 つとして自動的に文字列領域の再確保を行う C のツールである Forrest J. Cavalier III 氏が開発した「libmib allocated string functions」 があります。 http://www.mibsoftware.com/libmib/astringから利用 できます。 ソースはオープン・ソースの形式をとっていますが、ドキュメントはオープン・ ソースではありません。しかし自由に入手できます。
その他にも役に立つと思われるライブラリがあります。 たとえば、glib ライブラリは広くオープン・ソースのプラットフォーム上で利用され ています(GTK+ ツールキットは glib ライブラリを使用していますが、glib は GTK+ を使うことなしに単独で利用できます。 今の時点で、glib ライブラリの関数がバッファオーバーフローを防ぐために有効か どうかを分析して、問題ないことを示すことはできませんでしたが、期待でき そうな感じがします。 願わくば、このドキュメントの次以降の版では glib の関数がバッファオーバーフロー の問題を解決できることを確認したいと思っています。
まったく違った観点から解決をはかろうとするものに、領域の境界チェックを コンパイル時に行うものがあります([Sitaker 1999] のリストを参照して ください)。 私見ですが、防御にいろいろな手段を打った中の 1 つとして、そのようなツール は非常に有効ですが、この手法だけで防御するのは賢い手段とはいえません。 理由として 2 つは上げられます。 まず、そのようなツールは必要な防御の一部しか行うことができません(そして 「完璧な」防御を行おうとすると、通常の 12 から 30 倍遅くなります)。C と C++ はそもそもバッファオーバーフローを防ぐ手段を持ち合わせていません。 次に、オープン・ソースであるプログラムですと、何のツールを使ってコンパイル するかを決められているわけではありません。システムについてくるデフォルトの 「普通の」コンパイラを使うとセキュリティの弱点をさらすことになってしまい ます。
さらに有効なツールとして「StackGuard」があります。これは「ガード」する ための値(「カナリア(canary)」と呼びます)をリターンアドレスが書かれている 前に挿入して動作します。バッファオーバーフローが発生してリターンアドレスを 書き換えると、カナリアの値が(おそらく)変更され、実際に使用される前にシステム が検出します。 これは非常に有効なのですが、リターンアドレス以外の値(これを使用してもシス テムを攻撃できます)を書き換えるバッファオーバーフローには対処できません。 StackGuard を強化して、カナリアを他のデータに対しても使えるようにしたものが、 「PointGuard」です。 PointGuard は自動的にある値(たとえば関数のポインタやロングジャンプ・バッファ) を保護します。 しかし他の変数を PointGuard を使って保護する場合、プログラマの介在が必要と なります(プログラマはどのデータをカナリアで保護しなければいけないのかを 認識しなければいけない)。 これは有効な半面、本来保護すべきなのに必要がない、とうっかり判断してしまい、 いとも簡単に保護を省略してしまう場合が考えられます。 StackGuard や PointGuard、またそれと同様なものについての詳細は Cowan [1999] を参照してください。
訳註:鳥類のカナリアは、炭鉱で一酸化炭素の増加や酸欠状態を「検知」 するために飼われていました。
これと関連して、Linux のカーネルを修正して、スタック・セグメント上でのプロ グラムの実行を禁止してしまう方法もあります。それを行うにはパッチが必要です (Solar Designer の パッチに含まれています。 http://www.openwall.com/linux/) このドキュメントを書いている時点では、まだカーネルに取り込まれていません。 技術的な理由の 1 つに、思ったほどその効果がでない点があげられます。 攻撃者は、対象にしているプログラムにすでに存在している他の「面白そうな」場所 (ライブラリやヒープ領域、スタティックなデータ・セグメント領域など)を呼び出せ てしまうからです。 また Linux はスタック領域でプログラムを実行する場合があります。例として、 シグナルや GCC の「トランポリン」の実装をする場合です。 Solar Designer の パッチでこのようなケースにも対応できますが、これがパッチを 複雑なものしている原因です。 個人的には Linux 本流に組み込まれてもいいかと思います。というのもこれによって いくぶんかでも攻撃が難しくなりますし、既存の攻撃のある部分は防御できるから です。 しかし Linus Torvalds 氏たちが考えているように、このパッチが見た目ほどさま ざまな防御ができない、比較的簡単にこの防御の裏をかくことができる、という点 については私も同意見です。 Linus Torvalds 氏がこのパッチを採用しない理由については、 http://lwn.net/980806/a/linus-noexec.htmlを参照してください。
訳註:トランポリン(trampoline)とは、プログラムが実行している最中に プログラム自身によって生成される、互いに独立した小さなオブジェクト・ コードを指します。
要するに、まずプログラムそのものでバッファオーバーフローを防ぐように開発 するのが大切です。 そのように開発した後に、StackGuard のようなツールやテクニックを使って、 さらに安全策を講じておくべきです。 ソースコードからバッファオーバーフローを追い出せるだけ追い出したら、 StackGuard はさらに効果を発揮します。というのも StackGuard が防御のために 呼ばれるような「致命的な弱点」を減らすことができるからです。
バッファオーバーフローは、他のプログラミング言語でも問題になっています。 Perl や Python、Ada95 のようなバッファオーバーフローを防ぐ言語であっても です。 C や C++ 以外の言語を使ったとしても、もちろんすべての問題を解決できるわけ ではありません。 詳しくは、後程論じる「正しい値でだけ呼び出すこと」にある NIL キャラクタの 扱いを参照してください。 また言語が提供している基本的な機能(たとえばランタイム・ライブラリ)が利用で きる環境でかつその機能が安全であることを保証するという問題も残っています。 そのような問題はあるにせよ、バッファオーバーフローを防ぐよう、安全なプロ グラム開発を行う場合は、他の言語の使用を真剣に考えるべきだと思います。