バッファオーバーフロー体験
あふれるとは何が起きることか
前回書いた my_strcpy は、写す先の広さを見ていませんでした。標準の strcpy も同じです。写す元の '\0' が出るまで書き続けます。
char name[9];
strcpy(name, "Yamamotokurosawa"); /* 17バイト必要なのに9バイトしかない */name は9バイトぶんしか用意されていないのに、17バイト書き込まれます。はみ出した8バイトは、name の隣に置かれていた何かの上に乗ります。第2章で見たとおり、ローカル変数はスタックの上に順番に並んでいます。隣にあるのは別の変数かもしれないし、関数から戻る先を覚えている場所かもしれません。
これが バッファオーバーフロー です。厄介なのは次の点です。
- その場では落ちないことがある。壊した場所が使われるまで症状が出ない
- 症状が出たときには、原因の行はとっくに通り過ぎている
- 入力の長さ次第で、攻撃者が「戻る先」を狙って書き換えられる
Cで報告される深刻な脆弱性の多くは、元をたどるとこの形です。落ちるかどうかは環境まかせで、確実に壊れるとすら言えません。だから今回の課題では、わざとあふれさせるプログラムは書きません。壊れ方は上の説明で押さえて、あふれない書き方を手に入れるほうに時間を使います。
器の広さを関数に教える
安全にするための考え方は1つだけです。写す関数に、写す先が何バイトあるかを一緒に渡します。
int safe_copy(char *dest, int size, const char *src) {
int i = 0;
while (i + 1 < size && src[i] != '\0') {
dest[i] = src[i];
i++;
}
dest[i] = '\0';
if (src[i] != '\0') {
return 1;
}
return 0;
}条件が i < size ではなく i + 1 < size なのは、最後の1バイトを終端のために空けておくためです。ここを間違えると、器いっぱいに文字を詰めて終端が入らない、という一番たちの悪いバグになります。
ループを抜けたあと src[i] を見れば、写しきれたかどうかが分かります。まだ文字が残っていれば切り詰めたということなので、呼び出し側に知らせます。黙って切るだけだと、名前が途中で切れていることに誰も気付けません。
strncpy という標準関数もありますが、写す元が長いときに終端を置いてくれないという落とし穴があります。名前が似ているだけで安全版ではないので、strncpy を使うときも自分で終端を置く必要があります。
では、入力がどれだけ長くても壊れない受け取り方を書いてみましょう。
要件
- safe_copy(char *dest, int size, const char *src) を自分で書く
- 写す先の広さを超えて書き込まない。終端のヌル文字は必ず置く
- 切り詰めたら 1、全部入ったら 0 を返す
- 1行目に写した結果の名前を表示する
- 2行目に、切り詰めた場合は 切り詰めました、入りきった場合は そのまま入りました と表示する
入出力例
main("Tanaka") → "Tanaka
そのまま入りました"
main("Nakamura") → "Nakamura
そのまま入りました"
main("Yamamotokurosawa") → "Yamamoto
切り詰めました"