ソフトウェアの脆弱性についてのニュースを見るとしばしば バッファ・オーバーフローが原因だったと書かれています。 このバッファ・オーバーフローはありがちなバグの一つですが コンピュータの動作を完全に乗っ取る可能性があるので 非常に問題視されています。 (Wikipedia - バッファオーバーラン)
そのバッファ・オーバーフローについて 実例を挙げて解説します。 ここでは C 言語で書いていますが 他の言語でも同様のことが起こる可能性があります。
#include <stdio.h>
#define OFFSET 0x60
void test(int i) {
printf("TEST %d in.\n", i);
if (i>0) {
test(i-1);
} else {
char cp[0];
int j;
cp[OFFSET] = 100;
for (j=0; j<0x100; j++) {
//printf("%p %x\n", cp+j, cp[j]);
}
}
printf("TEST %d out.\n", i);
}
int main() {
test(3);
return 0;
}
というソースコード (download) をコンパイルして実行すると
$ gcc test.c
$ ./a.out
TEST 3 in.
TEST 2 in.
TEST 1 in.
TEST 0 in.
TEST 0 out.
TEST 100 out.
TEST 2 out.
TEST 3 out.
のように出力されました。 TEST 100 out.という表示にあるように 呼び出し元の関数の自動変数を変更出来ました。 ちなみに OFFSET の値は私の環境でのもので、 コンパイル時の環境や最適化オプションによって変える必要があります。 実験する際にはその下のコメント化された printf を使って スタックをダンプしたものを読んで適当に調整してください。
ここでは自動変数を一つ変更しただけなので被害は小さいですが、 OFFSET 値によっては 関数のリターン・アドレスを変更することが出来ます。 最悪の場合は攻撃者が書いたコードの中にリターンして コンピュータを完全に乗っ取ってしまいます。 このようなバッファ・オーバーフローが起こる典型的な状況は
- 配列の変数を用意する
- ユーザーからの入力を受け付ける
- 入力を配列に書き込む
- 入力の大きさが配列の大きさを越えたときに そのまま書き込むとバッファ・オーバーフロー
というものです。
バッファ・オーバーフロー対策としては バッファの大きさの範囲内にしか書き込まないように プログラムを作るしかありません。 例えば、 C 言語の場合には標準関数の gets(), scanf() などが バッファの長さを確認せずに書き込むので危険だとされています。 プログラミング規約としてこれらの使用そのものを禁止して その代わりに同様の機能を持っていて バッファ長を指定することの出来る代替関数を使う というように決めている例も多いです。
プログラミング言語によっては 書き込むときに必ず配列の長さを確認して バッファ・オーバーフローを起こさない ようにしているものもあります。 この場合は配列の長さを確認する時間が余分にかかりますが その代わりに飛躍的に安全性が高まります。 CPU 資源が潤沢にある環境であれば こちらの選択の方が有効ではないかと思われます。 ただし、そのような言語であっても C 言語などのより高速な手段で作られた 関数を使っていることがあります。 大抵の実装ではそのような関数の中では保護が働かないので 実装の品質を調査する必要があるかもしれません。
ちなみにバッファ・アンダーフローというものもあります。 バッファ・オーバーフローの逆で バッファの下限よりも小さい場所を読み書きする ことに由来するバグです。 C 言語では配列の下限が常に 0 であり それより下にアクセスすることが稀なことや、 Windows や Linux では スタックがアドレスの小さい方向へ成長していくので リターン・アドレスの書き換えには繋がりにくいことなどから 大きな問題が発生することは少ないようです。 そうは言っても 潜在的な危険性はバッファ・オーバーフローと同等であり 注意が必要だという点は変わりません。 対策もバッファ・オーバーフローと同様に 配列の下限以下にアクセスしないことです。