5.2. C と C++ でのライブラリによる解決策

C/C++ における解決策として、バッファオーバーフローの問題を抱えていない関数 ライブラリの利用があります。 はじめのサブセクションでは、「標準 C ライブラリ」を使った解決方法を説明し ます。効果はありますが、欠点もあります。 次のサブセクションでは、バッファオーバーフローに対して、固定長と動的に再確保 する両方法でセキュリティ上よく発生する問題を説明します。 次のサブセクションでは、strlcpy や libmib といった、その他さまざまなライブラリ について説明します。

5.2.1. 標準 C ライブラリによる解決策

C でバッファオーバーフローを防ぐ「常套」手段(C++ でも使われています)は、 バッファオーバーフローを防いでいる標準 C ライブラリを呼び出すことです。 この解決方法は、標準関数である strncpy(3)と strncat(3)次第でどうにでも なります。 この解決策をとるには注意が必要です。使い方が意外と面倒で、正しく扱うのが 難しいからです。 strncpy(3)はコピー元の文字列がコピー先以上の長さなら、コピー先の文字列の終端 に NIL をセットしません。したがって、strncpy(3)を呼出した後に、コピー先の終端 に NIL を必ずセットするようにしてください。 同じバッファを何回も使い回したいなら、strncpy()を使う時に、バッファには実際 必要なものより 1 文字小さくして渡し、使う前に最後の文字にいったん NIL を セットしてください。これは効果があります。 strncpy(3)、strncat(3)とも、書き込みできる領域の残りの大きさを引数で渡す 必要がありますが、この残量の計算をよく間違います(ここで間違ってしまうと、 バッファオーバーフロー攻撃を許してしまいます)。 どちらの関数も仕組み上、バッファオーバーフローが発生したかどうかを簡単に 確認できません。 結果として、代替え関数である strncpy(3)は strcpy(3)に比べて、パフォーマンスが 劣ります。これは strncpy(3)がコピー先の残り領域を NIL で埋めるためです。 私はこの最後の点について驚いた様子の電子メールをいくつか受け取りました。 しかしこの点は Kernighan 氏 と Ritchie 氏共著の第二版 [Kernighan 1988、 249 頁]に確かに載っており、この動作は Linux や FreeBSD、Solaris の man にも載っています。 この strcpy から strncpy への変更は性能の著しい低下を意味し、たいていの 場合これはよろしくない結果になります。

注意!。 strncpy(s1, s2, n)は、s2 のある部分だけをコピーする場合にも使えます。ここ では n が strlen(s2) より小さい値です。 このように使われた場合、strncpy()自身は基本的にバッファオーバーフローに対して 防御する仕組みを持っていません。つまり、n が s1 のバッファより必ず小さくなる ように、独立に処理する必要があります。 また、このように使う場合、普通 strncpy()は n 文字をコピーした後に NIL を付け 加えません。 このことが、strncpy()を使ったプログラムが安全であるかどうかを判断しがたい ものにしています。

sprintf()を使ってもバッファオーバーフローは防げます。しかし、そうするには 注意が必要です。 お薦めしがたい間違いを簡単に犯してしまいます。 sprinf の制御文字にはいろいろと便利な指定方法(たとえば「%s」)があります。 そして制御を指定する部分には、オプションフィールド長(たとえば、「%10s」)を 指定したり、精度(たとえば、「%.10s」)を指定できたりします。 これらは似たように見えますが(違いはピリオドだけ)、まったく異なります。 フィールド長で指定する場合、最小値を指定するだけでは、 バッファオーバーフローを防ぐのにはまったく役立ちません。 これとは対照的に、精度で指定する方法では最大値を指定し、指定した文字列は 文字列変換指定に基づいて出力されます。つまり、これがバッファオーバーフロー を防ぐのに役立ちます。 文字列を扱う場合、精度で指定する方法だけが全体の最大長を指定できる ことを忘れないでください。他の変換指定ではまた違う意味になります。 長さに「*」を指定すると、最大長をパラメタで渡すことができます(たとえば sizeof()の結果を)。 例で簡単に示せますので、ここでは sprintf()を使ったバッファオーバーフローを防ぐ 悪い例と良い例を挙げておきます。
 char buf[BUFFER_SIZE];
 sprintf(buf, "%*s",  sizeof(buf)-1, "long-string");  /* WRONG */
 sprintf(buf, "%.*s", sizeof(buf)-1, "long-string");  /* RIGHT */
理屈上 sprintf()はとても便利です。それは複雑なフォーマットを指定できるから です。 ただ、sprintf()で間違いを犯すのも簡単です。 フォーマットが複雑なら、必ず変換先の大きさをすべてにおいて 最大の大きさとするフォーマットにしてください。しかし、精度を指定する部分は 1 つのパラメタの大きさしか指定できません。 生成する出力が複雑な場合は、「考えうる最大の」値を決めるのは困難です。 考えうる最大長の組み合わせに対して、プログラムが十分な空きを確保していなければ、 バッファオーバーフローの脆弱さが残されたままになっているでしょう。 また、sprintf()は処理が完全に終わった後に、NUL を変換先に加えます。後で加えた この文字は忘れやすく、これが「1 バイト間違った(off-by-one)」エラーを起こす 原因になります。 このように動作すると、状況によっては痛い目にあうことになるかもしれません。

また、上記のコードで気にかけておいて欲しいのは、sizeof()という処理が 配列の大きさになる点です。 「buf」が確保したメモリのポインタになるようにコードを変更すると、 「sizeof()」の処理すべてを修正しなければなりません(さもないと、sizeof は ポインタの大きさを計ってしまい、これはたいていの値にとって十分な領域とは いえません)。

5.2.2. 静的もしくは動的に確保したバッファ

strncpy のような関数は、静的に確保したバッファを扱うのに便利です。 バッファは「使い物になる十分な大きさ」で確保してあり、確保した時からずっと 同じ大きさのまま、という方針でプログラムを作っています。 もう 1 つの方法は、必要な大きさのバッファを動的に確保する方法です。 どちらの方法もセキュリティに密接な関連があります。

固定長のバッファを使う場合に、共通したセキュリティ上の問題があります。それは 固定長のバッファはやられやすい、という事実です。 これは strncpy(3)や strncat(3)、snprintf(3)、strlcpy(3)、strlcat(3)他が 抱えている問題です。 攻撃者はいかにも長い文字列を設定する、というのが基本的な考えです。その結果 その文字列が切り捨てられると、最終的には攻撃者が望んだ状態になります (開発者が意図した結果ではなく)。 ひょっとすると、文字列がいくつかの小さな部分から構成されている場合もあります。 攻撃者は、最初の部分にバッファを埋めつくすだけの長い文字列を入れて、後の文字列 をまとめる作業を無効にするかもしれません。 ここで、具体例をいくつか挙げてみます。

静的に確保したバッファを使う時には、引数となっている元になる領域と渡す先の 領域の長さをよく考えなければいけません。 そして、入力や処理中に出る中間の結果も注意深くチェックしてください。

もう一つの選択肢は、固定長のバッファを使わずに、文字列すべてを動的に確保する 方法です。 この方法は GNU のプログラミング・ガイドラインで推奨していて、プログラムで どんな大きさの入力も扱えるようになります(メモリを越えない限り)。 動的に文字列を確保した際に起こる問題は、確保したメモリを越えて動作させて しまう点にあるのは、言うまでもありません。メモリは、バッファオーバーフロー を起こすのではないか、と気にしている部分ではなく、プログラムのどこか他の所で 使い切ってしまうかもしれません。これではメモリをどこにも確保できません。 また、動的な確保はメモリを効率良く利用できない恐れがありますので、理論的に そのプログラムが動き続けるのに十分な仮想メモリがあったとしても、メモリを オーバーして動作してしまう可能性は十分にあります。 さらに、プログラムはメモリをオーバーしてしまう前に、おそらく多量の仮想メモリを 使います。こうなるとたいてい「スラッシング」に陥ります。スラッシングが起こると、 コンピュータはディスクとメモリ間での情報の受け渡しにすべての時間を費やして しまいます(生産的な処理をするかわりに)。 これはサービス拒否攻撃と同じ影響を与えます。 入力の大きさに対して理にかなった制限を設けると効果があります。 普通プログラムで動的に文字列を確保するなら、メモリを使い果たしても フェイル・セーフになるように設計しなければなりません。

5.2.3. strlcpy と strlcat

もう一つの方法は、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 も strlcat もパラメタでコピー先のバッファを実際の大きさで指定でき ます(コピーする文字数の最大値ではありません)。また、結果が NIL で終端する ことを保証しています(大きさが 0 より大きい限りは)。 忘れてならないのは、大きさには NIL 用の 1 バイトを入れておく必要があること です。

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);
正しい返り値は 3(0+3=3)ですが、Solaris は 4 になります。これは処理先の 「大きさ」の長さを越えた文字を正しく見ていないためです。 差し当たり、大きさが 0 もしくは処理先に NIL 文字が無い場合を避けるように お薦めします。 glib の将来のバージョンでは、この相違点が隠されて、常に OpenBSD の文法を 用いる予定です。

5.2.4. libmib

C 用のツールセットには、自動的に文字列を動的確保してくれるものがあります。 それは Forrest J. Cavalier III 氏の「libmib allocated string functions」 で、http://www.mibsoftware.com/libmib/astring から入手できます。libmib は 2 種類あり、「libmib-open」は X11 と似た独自の ライセンスにしたがっていますので、明らかにオープンソースです。 このライセンスは、修正や再配布を認めていますが、再配布には別の名前を選択 しなければいけませんし、「完全にテストされていない」と開発者は記載しています。 libmib-mature を引き続き手に入れるなら、申し込みに費用がかかります。 ドキュメントはオープンソースではありませんが、自由に利用できます。

5.2.5. C++ std::string クラス

C++ で開発する人たちは、言語に組み込まれている std::string クラスを利用でき ます。 このクラスは動的な方法を採用していて、必要に応じて記憶領域を増やしていきます。 しかし注意しないといけないのは、クラスのデータが「char *」に置き換わると (たとえば data() もしくは c_str()を使って)、再びバッファオーバーフローの問題が 表面化する点です。したがって、メソッドを使用する場合には注意が必要になります。 c_str()は常に NIL で終端した文字列を返しますが、data()の場合はどうなるか わかりません(実装次第ですが、ほとんどは NIL で終端しません)。 data()の使用を避けるか、どうしても使わなければならないなら、そのフォーマット を当てにしないでください。

他の文字列ライブラリを使っている開発者も同様にたくさんいますが、そのような ライブラリは、他の多数のライブラリや自作の文字列ライブラリと組み合わせに なっています。 そのようなライブラリを使う場合には、とりわけ注意を払ってください。他の文字列 クラスの多くは、自動的にクラスを「char *」タイプに変換してしまうルーチンが 入っています。 その結果、知らないうちにバッファオーバーフローの脆弱さにはまっている可能性 があります。

5.2.6. Libsafe

(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 の開発者たち自身も、ソフトウェア開発者たちが libsafe だけに頼って いてはいけないことを知っています。 彼らによれば、

バッファオーバーフロー攻撃に対する最適策は、プログラムの欠陥の修正であること は周知の事実です。 しかし、欠陥を持ったプログラムを修正するには、プログラムに欠陥があること を知る必要があります。 libsafe やその他のセキュリティ対策を使用する本当のメリットは、まだ脆弱さ を知られていないプログラムが、今後の攻撃に備えるという点にあります。

5.2.7. その他のライブラリ

glib(glibc ではなく)ライブラリは広く利用できるオープンソースのライブラリで C プログラマに対してたくさんの便利な関数を提供しています。 たとえば、GTK+ や GNOME は両者とも glib を使っています。 以前にも指摘しましたが、glib バージョン 1.3.2 には私が提供したパッチが g_strlcpy() と g_strlcat()に適用してあります。今後のバージョンが広く利用 されれば、移植性の高いこれらの関数の利用がもっと簡単になるはずです。 現状では、glib ライブラリの関数がバッファオーバーフローを防ぐか否かの分析を 私は結論づけられません。 しかし、glib 関数の多くは自動的にメモリを確保し、失敗を横取り して、訳もわからずに動かなくなります(たとえば、かわりに別のことを しようとするために)。 結果的に、glib 関数の大部分は、安全が求められるプログラムでは利用できない 場合が多くあります。 GNOME のガイドラインでは g_strdup_printf()のような関数の使用を推奨して います。プログラムがメモリ例外を起こした場合、すぐにクラッシュしてもかまわない なら、使用してもかまいません。 しかしそれが受け入れ難いなら、そのようなルーチンを使用するのは、適切ではあり ません。