Buffer Overflowとは何か?配列・ポインタ・メモリの関係をやさしく理解する(第2回/全10回)

黒ネコと学ぶ、Buffer Overflow 超基礎シリーズ

前回は、Buffer Overfloとは何かを学びました。
小さなメモリの箱であるバッファに、入りきらないデータを書き込むことで、となりのメモリまで壊してしまう現象でしたね。
ですが、ここで次の疑問が出てきます。
[fuki-r]「なぜ、となりのメモリまで壊れるのか?」
「メモリの中では、いったい何が起きているのか?」[/fuki-r]
この疑問を理解するためには、次の3つの考え方がとても大切です。
・配列
・ポインタ
・メモリ
[fuki-l]Buffer Overflow は、ただ「長い文字列で壊れる」というだけではなく、配列がメモリ上でどう並び、そこへどう書き込まれるかを知ることで、ぐっと理解しやすくなります。[/fuki-l]
 今回の第2回では、
・配列とは何か
・メモリはどう並んでいるのか
・ポインタはなぜ関係するのか
・なぜ配列の外まで書いてしまうと危険なのか
をやさしく整理していきます。
 ※イメージです。

配列とは何か

 まずは、配列から見ていきます。
配列とは、同じ種類のデータを、連続して並べて保存する入れ物です。
たとえばC言語で、
char buf[8];
と書くと、1文字を入れる"char"型の場所が8個並んだ箱を作ることになります。
 
イメージとしては、ロッカーが8個並んでいる感じです。
buf[0]
buf[1]
buf[2]
buf[3]
buf[4]
buf[5]
buf[6]
buf[7]
 
それぞれの箱には1文字ずつ入れられます。
つまり、配列は「1個の大きな箱」ではなく、「小さな部屋が横一列に並んでいるもの」と考えるとわかりやすいです。
 
たとえば"HELLO"という文字列を入れると、だいたい次のようなイメージになります。
buf[0] = 'H’
buf[1] = 'E’
buf[2] = 'L’
buf[3] = 'L’
buf[4] = 'O’
buf[5] = '\0'
 
ここで最後の“\0″(ヌル文字)は、C言語の文字列の終わりを示す特別な印です。
この印があることで、コンピュータは「ここで文字列が終わります」と判断できます。
 
つまり、文字列を入れるときは、見えている文字だけでなく、最後の"\0″ぶんも必要になります。
[fuki-l]この時点で大切なのは、「配列には使える範囲が決まっている」ということです。[/fuki-l]
  

メモリは「小さなマス目」が並んでいるイメージです

 次に、メモリのイメージを持ちましょう。
メモリは、ざっくり言えば、「小さなマス目がずらっと並んでいる場所」です。
それぞれのマス目には、少しずつデータを入れることができます。
 
簡単にたとえるなら、メモリは「方眼ノート」のようなものです。
1マス1マスに文字や数字を書き込み、それが連続して並んでいます。
 
たとえば"char buf[8];"を作ると、メモリの中では8個のマスを連続して使うイメージです。
[buf0][buf1][buf2][buf3][buf4][buf5][buf6][buf7]
 
ここに"HELLO"を入れるなら、
[ H ][ E ][ L ][ L ][ O ][\0][ ? ][ ? ]
のようになります。
※"?"の部分はまだ何が入っているかわからない場所です。
 
ここで重要なのは、「配列のマスは連続して並んでいる」ということです。
だからこそ、書き込みすぎると、その続きのマスにもそのまま進んでしまいます。
[fuki-l]つまり、Buffer Overflow は、「壁にぶつかって止まる」のではなく、そのままとなりのマスへ進んでしまうイメージなのです。・・・この感覚がとても大事ですね。[/fuki-l]
 

配列の外に書くとなぜ危険なのか

 ここで、Buffer Overflow の核心に近づきます。
 配列には使ってよい範囲があります。
たとえば"char buf[8];"なら、使ってよいのは
buf[0]
buf[1]
buf[2]
buf[3]
buf[4]
buf[5]
buf[6]
buf[7]
までです。
 
ところが、もし9文字目、10文字目、その先まで書こうとするとどうなるでしょうか。
コンピュータは、人間のように「そこは buf の外だからやめよう」と毎回やさしく止めてくれるわけではありません。
そのため、プログラムの書き方によっては、「bufの外の領域にもそのまま書いてしまう」ことがあります。
 
ここで怖いのは、その外側が「ただの空き地」とは限らないことです。
メモリには、別の変数や、プログラムの動きに関わる情報が置かれていることがあります。
 
つまり、配列の外に書くというのは、「自分のメモ欄をはみ出して、となりの大事な欄まで上書きすること」なのです。
[fuki-l]コンピュータの世界では、この「はみ出し」が、ただのミスではなく脆弱性につながることがあるため、とても重要です。[/fuki-l]
 

ポインタは「場所」を表す考え方

 ここで、ポインタについてもやさしく触れておきます。
ポインタという言葉を聞くと、急に難しく感じるかもしれません。
ですが、最初はシンプルに考えて大丈夫です。
 
ポインタとは、「データそのものではなく、そのデータがある場所を表すもの」です。
もっとやさしく言うなら、「住所を持つもの」です。
 
たとえば、家そのものではなく「○○市△△町1-2-3」という住所を持っているのがポインタのイメージです。
 
C言語では、文字列のコピーや配列の操作で、この「場所」の情報をよく使います。
たとえば"strcpy(buf, “…")"のような関数は、
「buf がどこにあるか」
「コピー元の文字列がどこにあるか」
という場所の情報をもとに動いています。
 
つまり、Buffer Overflow は、「場所をたどって書いていく処理が、想定より先まで進んでしまう問題」とも言えます。
ここではまだ、ポインタを完全に理解しなくても大丈夫です。

[fuki-l]まずは、「ポインタ = データの住所に関係する考え方」と覚えておけば十分です。

そして、配列や文字列操作では、この「住所」がとても重要になる、と押さえておきましょう。[/fuki-l]
  

サンプルコードでやさしく理解する

 ここで、小さなサンプルコードを見てみます。
—–
#include <stdio.h>
 
int main(void) {
    char buf[8] = “HELLO";
    int x = 1234;
 
    printf(“buf = %s\n", buf);
    printf(“x = %d\n", x);
 
    return 0;
}
—–
このコードでは、2つの変数があります。
 
1つ目は、「char buf[8] = “HELLO";」です。
これは8文字ぶんの箱を作り、その中に"HELLO"を入れています。
“HELLO"は5文字ですが、C言語の文字列では最後に"\0″も必要なので、6個ぶんほど使うイメージです
それでも8個あるので、今のところは安全に入りそうです。
 
2つ目は、「int x = 1234;」です。
これは整数を入れるための変数です。
 
このコードだけを見ると、"buf"と"x"はまったく別のものに見えます。
ですが、メモリの中では、近い場所に置かれることがあります。
 
ここが大事です。 [fuki-l]もし"buf"に入りきらない長さのデータを書き込んだら、"buf"の外へはみ出し、その近くにある別のデータまで壊してしまう可能性があります。
たとえば"x"の値が変になったり、もっと重要な情報が壊れたりすることがあります。[/fuki-l]この感覚をさらに強めるために、次のようなコードも考えられます。
—–
#include <stdio.h>
#include <string.h>
 
int main(void) {
    char buf[8];
    int x = 1234;
 
    strcpy(buf, “ABCDEFGHIJK");
 
    printf(“buf = %s\n", buf);
    printf(“x = %d\n", x);
 
    return 0;
}
—–
このコードでは、"buf"は8文字ぶんしかありません。
それなのに、もっと長い文字列をコピーしています。
すると、"buf"の外にまで書き込む可能性があります。
 
このとき、"x"がたまたま近くに置かれていれば、"x"に影響が出るかもしれません。
もちろん、実際の並び方はコンパイラや環境によって変わることがありますが、学習の入口としては、「配列の外に書くと、近くにある別のデータにまで影響することがある」と理解するのが大切です。
[fuki-l]超簡単に説明すると、配列は横に並んだロッカー、ポインタはその場所の住所、メモリはそのロッカーがたくさん並ぶ場所ですね。[/fuki-l]

Buffer Overflowとは何か?配列・ポインタ・メモリの関係をやさしく理解するのまとめ

 今回学んだ大事なことは、配列は同じ種類のデータを連続して保存する箱であり、メモリは小さなマス目が並んだ場所のように考えられるという点です。
“char buf[8]"のような配列は、使える範囲が最初から決まっており、その範囲をこえて書き込むと、配列の外にある別のデータまで壊してしまう可能性があります。これが Buffer Overflow を理解するうえでとても重要です。
また、ポインタはデータそのものではなく、そのデータがある場所、つまり住所に関係する考え方です。配列や文字列操作では、この「どこに書くか」がとても大切になります。つまり Buffer Overflow は、ただ長い文字列を入れる問題ではなく、メモリ上のどの場所に、どこまで書いてしまうかという問題でもあります。