Buffer Overflowとは何か?なぜ入力が多すぎると壊れるのか?境界チェック不足の正体(第3回/全10回)
前回は、配列・ポインタ・メモリの関係を見ながら、Buffer Overflowがなぜ起きるのかをイメージしました。
配列はメモリの中で連続して並んでおり、その範囲をこえて書き込むと、となりの領域まで壊してしまう可能性がある、という話でした。
ここで次に出てくる大事な疑問があります。 [fuki-r]「では、なぜそんな危ない書き込みが起きるのか?」
「入力が長いだけで、どうして壊れてしまうのか?」[/fuki-r]この答えに深く関わるのが、今回のテーマである「境界チェック不足」です。
「入力が長いだけで、どうして壊れてしまうのか?」[/fuki-r]この答えに深く関わるのが、今回のテーマである「境界チェック不足」です。
Buffer Overflow は、単に「長い文字列が悪い」わけではありません。
本当に大事なのは、「その入力が、用意した箱にちゃんと入る大きさかどうかを確認していないこと」です。
今回は、
・境界チェックとは何か
・なぜ確認不足が危険なのか
・C言語ではなぜこの問題が起きやすいのか
・危険な書き方と、安全を意識した書き方はどう違うのか
をやさしく説明いたします。
※イメージです。
境界チェックとは何か
まず、「境界」という言葉から考えてみましょう。
境界とは、簡単に言えば「ここまでが使ってよい範囲です、という線」のことです。
たとえば、ノートに四角い記入欄があったら、その中が自分の書いてよい範囲です。
その外まで書いてしまうと、となりの欄にはみ出してしまいます。
配列でも同じです。
たとえば、
char buf[8];
と書いたなら、"buf" に使ってよい範囲は8文字ぶんです。
この8文字ぶんの範囲が、"buf" の境界です。
では、境界チェックとは何でしょうか。
それは、
「これから入れようとしているデータは、この箱にちゃんと収まりますか?」と確認することです。
たとえば、8文字しか入らない箱に対して、5文字を入れるなら問題ありません。
ですが、11文字を入れるなら危険です。
その違いを、書き込む前に確かめる必要があります。 [fuki-l]つまり境界チェックとは、書き込む前にサイズを確認して、はみ出さないようにすることです。
[/fuki-l]この確認がないと、Buffer Overflowが起きやすくなります。
[/fuki-l]この確認がないと、Buffer Overflowが起きやすくなります。
なぜ「入力が多すぎる」と壊れるのか
では、なぜ入力が多すぎると壊れるのでしょうか。
答えはとてもシンプルです。
「箱の大きさよりも大きなデータを、そのまま入れようとするから」です。
現実の世界で考えるとわかりやすいです。
・8人乗りの車に12人乗ろうとする。
・小さい引き出しに長すぎる棒を入れようとする。
・100mlのコップに300mlの水を入れようとする。
どれも無理がありますね。
コンピュータのメモリでも同じです。
“buf[8]" なら、そこに入れられる量には限界があります。
それをこえるデータを書き込むと、本来の箱の外へ進んでしまいます。
ここで大事なのは、「入力が長いこと自体が絶対に悪いわけではない」という点です。
たとえば、すごく長い名前や文章を扱うこと自体は悪くありません。
問題なのは、その長い入力を、小さい箱へ無理に入れてしまうことです。 [fuki-l]つまり、危険なのは「長い入力」ではなく、「長い入力を、サイズ確認なしで小さい領域にコピーすること」なのです。これが Buffer Overflowの本質です。[/fuki-l]
C言語でこの問題が起きやすい理由
では、なぜ C言語ではこの問題がよく話題になるのでしょうか。
それは、C言語が「メモリをかなり直接的に扱える言語」だからです。
この特徴は、速くて自由度の高いプログラムが書けるという大きなメリットでもあります。
その一方で、プログラムを書く人がきちんと注意しないと、安全確認が足りなくなることがあります。
たとえば、文字列をコピーする関数の中には、「コピー先の箱の大きさを自動で守ってくれないもの」があります。
すると、プログラマが、
・この箱は何文字入るのか。
・これから入れるデータは何文字か。
・最後の “\0" ぶんも必要か。
を自分で考えなければなりません。
もしそこを考えないと、「とりあえずコピーできてしまう」ため、Buffer Overflow が起きます。
ここでC言語を悪者のように見る必要はありません。
大切なのは、「C言語では、便利さと引き換えに、サイズ管理の責任も自分で負う場面がある」という理解です。 [fuki-l]分かりやすく説明すると、C言語は「自由に使える広い作業場」のようなものです。好きなように動けますが、そのぶん安全確認も自分でしなければなりません。[/fuki-l]
危険な書き方と安全を意識した書き方
ここで、危険な書き方と、安全を意識した書き方を比べてみます。
まずは危険な例です。
—–
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8];
char input[] = “ABCDEFGHIJK";
strcpy(buf, input);
printf(“%s\n", buf);
return 0;
}
—–
このコードでは、"buf" は8文字ぶんの箱です。
一方で “input" は、それより長い文字列です。
それなのに、
strcpy(buf, input);
で、そのままコピーしています。
“strcpy"は便利ですが、ここでは、buf に入りきるかどうかを確認していません。
つまりこのコードは、小さい箱かどうかを見ずに、荷物を全部押し込んでいる状態です。
次に、安全を意識した例を見てみます。
—–
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8];
char input[] = “ABCDEFGHIJK";
strncpy(buf, input, sizeof(buf) – 1);
buf[sizeof(buf) – 1] = '\0’;
printf(“%s\n", buf);
return 0;
}
—–
こちらでは、
strncpy(buf, input, sizeof(buf) – 1);
によって、"buf"の大きさを考えながらコピーしています。
“sizeof(buf)"は、"buf"全体の大きさを表します。
ここでは8です。
そこから、"-1″しているのは、文字列の最後に必要な"\0″を入れるぶんを残すためです。
そして次の行、
buf[sizeof(buf) – 1] = '\0’;
で、最後を明示的に “\0" にしています。
これはとても大切です。
なぜなら、"strncpy" は場合によっては自動で文字列終端を入れないことがあるからです。
そのため、自分で「ここで文字列は終わりです」と印をつけているのです。
つまり安全を意識したコードでは、
・箱の大きさを確認する。
・入れすぎないようにする。
・文字列の終わりもきちんと処理する。
という考え方が入っています。
サンプルコードをやさしく読み解く
ここでは、先ほどの2つのコードの違いを、もう少しゆっくり整理します。
まず危険なコードです。
1.nanoエディターを立ち上げて、ソースを作成
nano bbo03-001.c
—–
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8];
char input[] = “ABCDEFGHIJK";
strcpy(buf, input);
printf(“%s\n", buf);
return 0;
}
—–
※プログラム入力完了後
「Ctrl」+ o で、プログラムの書き込み
「Ctrl」+ x で、nanoエディター終了です。
2.ソースをコンパイル
gcc bbo03-01.c -o bbo03-01
3.実行
./bbo03-01
“char buf[8];"
これは、8文字ぶんの入れ物を作っています。
つまり、小さい箱です。
“char input[] = “ABCDEFGHIJK";"
これは、コピーしたい文字列を用意しています。
この文字列は “buf" より長いです。
“strcpy(buf, input);"
ここが危険な場所です。
“strcpy"は「そのまま全部コピーする」という動きをします。
そのため、"buf"に入りきるかどうかを確認しないと、入りきらないぶんが外にはみ出す可能性があります。
“printf(“%s\n", buf);"
もしメモリが壊れていれば、ここで変な表示になったり、場合によってはクラッシュすることもあります。
次に、安全を意識したコードを見ます。
1.nanoエディターを立ち上げて、ソースを作成
nano bbo03-002.c
—–
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[8];
char input[] = “ABCDEFGHIJK";
strncpy(buf, input, sizeof(buf) – 1);
buf[sizeof(buf) – 1] = '\0’;
printf(“%s\n", buf);
return 0;
}
—–
2.ソースをコンパイル
gcc bbo03-02.c -o bbo03-02
3.実行
./bbo03-02
“strncpy(buf, input, sizeof(buf) – 1);"
ここでは、「buf の大きさをこえない範囲でコピーする」という考え方が入っています。
全部を入れようとするのではなく、入るぶんだけに制限しています。
“buf[sizeof(buf) – 1] = '\0’;"
文字列の最後の印を明示的に入れています。
これによって、"printf"などが安全に文字列の終わりを見つけやすくなります。[fuki-l]簡単に一言で言うと、危険なコードは、箱の大きさを見ずに荷物を全部入れる書き方で、安全を意識したコードは、箱の大きさを先に見てから、入るぶんだけ入れる書き方です。[/fuki-l]この違いが、境界チェックのあるコードとないコードの違いです。
Buffer Overflowとは何か?なぜ入力が多すぎると壊れるのか?境界チェック不足の正体のまとめ
今回学んだ大事なことは、Buffer Overflow の原因は「入力が長いこと」だけではなく、入力が入れ物の大きさに収まるかどうかを確認しない「境界チェック不足」にあるという点です。
配列には最初から使える範囲が決まっており、その範囲をこえて書き込むと、隣のメモリまで壊してしまう可能性があります。C言語ではメモリを自由に扱えるぶん、サイズ確認を自分で意識しなければならない場面があります。そのため、"strcpy" のように大きさを考えずにコピーすると危険です。一方で、箱の大きさを確認し、入るぶんだけコピーする書き方を意識すれば、Buffer Overflow の危険を減らすことができます。
つまり大切なのは、「長い入力は危険」と覚えることではなく、「書き込む前に、その箱に本当に入るかを確認する」という考え方を身につけることです。