char buf[BUFFER_SIZE]; sprintf(buf, "%*s", sizeof(buf)-1, "long-string"); /* WRONG */ sprintf(buf, "%.*s", sizeof(buf)-1, "long-string"); /* RIGHT */ |
静的に確保したバッファを使う時には、引数となっている元になる領域と渡す先の 領域の長さをよく考えなければいけません。 そして、入力や処理中に出る中間の結果も注意深くチェックしてください。
もう一つの選択肢は、固定長のバッファを使わずに、文字列すべてを動的に確保する 方法です。 この方法は GNU のプログラミング・ガイドラインで推奨していて、プログラムで どんな大きさの入力も扱えるようになります(メモリを越えない限り)。 動的に文字列を確保した際に起こる問題は、確保したメモリを越えて動作させて しまう点にあるのは、言うまでもありません。メモリは、バッファオーバーフロー を起こすのではないか、と気にしている部分ではなく、プログラムのどこか他の所で 使い切ってしまうかもしれません。これではメモリをどこにも確保できません。 また、動的な確保はメモリを効率良く利用できない恐れがありますので、理論的に そのプログラムが動き続けるのに十分な仮想メモリがあったとしても、メモリを オーバーして動作してしまう可能性は十分にあります。 さらに、プログラムはメモリをオーバーしてしまう前に、おそらく多量の仮想メモリを 使います。こうなるとたいてい「スラッシング」に陥ります。スラッシングが起こると、 コンピュータはディスクとメモリ間での情報の受け渡しにすべての時間を費やして しまいます(生産的な処理をするかわりに)。 これはサービス拒否攻撃と同じ影響を与えます。 入力の大きさに対して理にかなった制限を設けると効果があります。 普通プログラムで動的に文字列を確保するなら、メモリを使い果たしても フェイル・セーフになるように設計しなければなりません。
もう一つの方法は、OpenBSD で採用している strlcpy(3) と strlcat(3)です。これは Miller 氏と de Raadt氏[Miller 1999]が作成しました。 これは機能を最小限に抑え、静的な大きさを持つバッファを採用しています。C の 文字列をコピーし、連結するのに異なるインタフェースを採用しています(エラーを 起こしにくい)。 これらの関数のソースとドキュメントは、 ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 で利用でき、新しい BSD スタイルのオープンソースライセンスを採用しています。
まずプロトタイプを挙げます。
size_t strlcpy (char *dst, const char *src, size_t size); size_t strlcat (char *dst, const char *src, size_t size); |
strlcpy は、NUL で終端した元の文字列から、その size - 1 の文字をコピー し、NIL で終端します。 strlcat は、NIL で終端してある文字列を末尾に追加します。 多く見積もっても size - strlen(dst) - 1 バイトを追加し、NIL で終端します。
strlcpy(3) と strlcat(3)は、たいていの Unix ライクなシステム にデフォルトではインストールされません。これが欠点と言えば欠点です。 OpenBSD では、<string.h> の一部になっています。 これはたいした問題ではありません。これらは小さな関数で、自作プログラムのソース の中に入れたり(少なくともオプションとして)、独立したパッケージとして読み込め ます。 こういったケースでは autoconf を使って自動化するのも可能です。 さらに多くのプログラムでこれらの関数を使えば、Linux ディストリビューション や他の Unix ライクなシステムの標準構成の一部となるのもそう遠くはない でしょう。 また最近になって、これらの関数は「glib」ライブラリに取り込まれました(私が パッチを提供してこのようになりました)。したがって、最近のバージョンの glib を 使えば利用できます。 glib ではこれらの関数は g_strlcpy と g_strlcat となっていて(strlcpy や strlcat ではありません)、glib ライブラリの命名規則に沿った形になっています。
また strlcat(3) は、長さが 0 もしくは NIL 文字が処理先の文字列(指定した 文字数の中で)にない場合に若干文法が変わっています。 OpenBSD では、長さが 0 ならば処理先の文字列の長さは 0 とみなします。 また長さが 0 以外で NIL 文字が処理先の文字列(文字数分)に無い場合は、 処理先の長さは指定したものと等しいとみなします。 これら規則によって、文字列への NIL の組み込みを徹底しています。 あいにく、少なくとも Solaris は(現時点では)この規則にしたがっていません。 理由は、オリジナルのドキュメントにそういう記述がないからです。 私は Todd Miller 氏と話し、OpenBSD の文法が正しいと合意しました (Solaris が正しくないことにも)。 理由は単純です。どんな条件下であっても、strlcat や strlcpy は処理先が指定 した文字列の大きさを越えているかどうかを調べた方が良いのにもかかわらず、 それをしていないからです。 そのような方法をとると、core ダンプしてしまうか(メモリ範囲外にアクセスした ため)、ハードウェアに悪影響を与えるかもしれません(メモリマップド I/O を通じて)。 つまりこうです。
a = strlcat ("Y", "123", 0); |
C 用のツールセットには、自動的に文字列を動的確保してくれるものがあります。 それは Forrest J. Cavalier III 氏の「libmib allocated string functions」 で、http://www.mibsoftware.com/libmib/astring から入手できます。libmib は 2 種類あり、「libmib-open」は X11 と似た独自の ライセンスにしたがっていますので、明らかにオープンソースです。 このライセンスは、修正や再配布を認めていますが、再配布には別の名前を選択 しなければいけませんし、「完全にテストされていない」と開発者は記載しています。 libmib-mature を引き続き手に入れるなら、申し込みに費用がかかります。 ドキュメントはオープンソースではありませんが、自由に利用できます。
(Lucent Technologies の)Arash Baratloo 氏や Timothy Tsai氏、Navjot Singh 氏 が、Libsafe を開発しました。このライブラリは、スタック破壊攻撃に弱いことで 知られるているライブラリ関数のいくつかにラッパを被せます。 このラッパ(開発者は、「ミドルウェア」の一種と呼んでいます)は、動的にロード される単なるライブラリで、strcpy(3)のような C のライブラリ関数を修正した バージョンが入っています。 この修正済みのバージョンは、オリジナルの機能を実装してありますが、ある意味で どんなバッファオーバーフローも現在のスタック・フレームの中に封じ込めます。 当初の性能分析では、ライブラリのオーバヘッドはとても小さいとしています。 Libsafe のドキュメントとソースコードは http://www.bell-labs.com/org/11356/libsafe.html から取得できます。 Libsafe のソースコードはオープンソースの LGPL ライセンスに完全に準拠して いて、Linux ディストリビュータは利用に関心を持ちつつあります。
Libsafe の解決手段は多少は役に立つように思えます。 確かに Linux ディストリビュータは Libsafe の採用を検討した方が良く、その解決 方法はその他の人たちも同様に検討するに価します。 たとえば、Linux ディストリビューショの Mandrake(バージョン 7.1) は採用して います。 ソフトウェア開発者にとっては Libsafe は手の込んだ防御をするのに便利な仕組み ですが、本当にバッファオーバーフローを防げるわけではありません。 コードを開発している時に、Libsafe だけに頼るべきではない理由がいくつかあります。
Libsafe は、明らかにバッファオーバーフローの問題を持っている、既知のわずかな 関数だけを防御します。 これを書いている時点では、防御できる関数のリストは、このドキュメントで 問題を抱えているとした関数のリストよりかなり短くなっています。 また、あなた自身が書いた(たとえば while ループ中)バッファオーバーフローを 起こすコードは防御してくれません。
libsafe がディストリビューションに入っていたとしても、インストールした方法 によって利用に差が出ます。 ドキュメントでは LD_PRELOAD を設定して libsafe の防御を有効にするように 推奨していますが、問題はユーザがその環境変数の設定をはずせるところにあり ます。これでユーザが実行するプログラムに対する防御は無効になってしまい ます。
Libsafe は、リターンアドレスがスタック上にあるバッファオーバーフローに対して だけ効果があります。 ヒープやプロシジャー・フレームにあるその他の変数では、あいかわらずオーバー してしまいます。 【訳註:プロシジャー・フレームとは、登録済みレジスタとローカル変数が入って いるスタック・セグメントです。activation record ともいいます】
あちこちにあるコンピュータ・システムすべてで libsafe(もしくは似たもの)が 利用できると断言できない限り、自分のプログラムは libsafe が無いつもりで 防御しなければいけません。
LibSafe は登録済みのフレーム・ポインタがスタック・フレームそれぞれの先頭 にあることを仮定しているように見えます。これは常に真とは言えません。 コンパイラ(gcc のような)は最適化をかけてしまいます。特に「-fomit-frame-pointer」 というオプションは libsafe に必要と思われる情報を削除してしまいます。 つまり、libsafe がうまく動かないプログラムがあるかもしれないのです。
libsafe の開発者たち自身も、ソフトウェア開発者たちが libsafe だけに頼って いてはいけないことを知っています。 彼らによれば、