【注意】このサイトに記載されていることを他人に試すことは「不正アクセス禁止法」に該当する場合があります。詳しくはこちらから

Buffer Overflowとは何か?スタック上で何が起きるのか?関数呼び出しとローカル変数の関係(第4回/全10回)

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

ここまでの第1回から第3回では、Buffer Overflowの基本を学んできました。
バッファとは何か、配列とメモリがどう関係するのか、そして危険の本当の原因が「境界チェック不足」にあることも見てきましたね。

くろちゃん

ここで次の疑問が出てきます。
「では、実際にどこで壊れることが多いのか?」
「なぜ関数の中のバッファがそんなに大事なのか?」

この答えに深く関わるのが、今回のテーマである「スタック」です。
 エクスプロイト開発を学び始めると、必ずと言ってよいほど「スタック」という言葉が出てきます。
最初は難しそうに見えるかもしれませんが、考え方の土台はそれほど複雑ではありません。
 
今回は、
・スタックとは何か。
・関数を呼ぶと何が起きるのか。
・ローカル変数はどこに置かれるのか。
・なぜ関数の中のバッファが重要なのか。
を、なるべくやさしく整理していきます。
 
この回を理解すると、次回以降に出てくる「saved RBP」や、「return address」も、ぐっと理解しやすくなりますよ。
※イメージです。
 

スタックとは何か

 まず、「スタック」という言葉を身近なたとえで考えてみます。
スタックは、簡単に言えば、上にどんどん積み重ねていく作業台のようなものです。
 
たとえば、お皿を1枚ずつ上に重ねていく場面を想像してください。
・1枚置く。
・その上にもう1枚置く。
・さらにその上に置く。
そして使うときは、一番上のお皿から取っていきます。
 
このように、あとから置いたものを、先に取り出すという特徴があります。
 
コンピュータのスタックも、これに少し似ています。
プログラムが関数を呼び出すたびに、その関数で必要な情報を一時的に積んでいき、関数が終わると取り除いていきます。
 
つまりスタックは、「関数の実行に必要な情報」「一時的な値」「ローカル変数」などを置くための場所として使われます。

白猫先生

スタックは、「関数の作業用メモを一時的に積んでおく場所」です。

関数を呼ぶと何が起きるのか

 では、関数を呼ぶと、スタックの上では何が起きるのでしょうか。
 
たとえば、"main"関数の中から別の関数"vuln()"を呼ぶとします。
このときコンピュータは、ただジャンプするだけではありません。
「あとで元の場所に戻れるようにするための情報」や、「その関数で使うための一時的な情報」を準備します。
 
つまり関数を呼ぶというのは、単に処理を移すだけではなく、その関数専用の作業スペースを一時的に作ることでもあります。
 
この作業スペースは、よく「スタックフレーム」と呼ばれます。
難しく考えなくて大丈夫です。
最初は、「スタックフレーム = その関数のために用意された一時的な作業机」と考えるとわかりやすいです。
 
この作業机の上には、たとえば次のようなものが置かれます。
・ローカル変数。
・関数が終わったら戻るための情報。
・前の状態を覚えておくための情報。
 つまり、関数を呼ぶたびに、その関数専用の小さな作業場がスタック上にできるのです。
 
 

ローカル変数はどこに置かれるのか

 ここで、ローカル変数について見ていきます。
ローカル変数とは、関数の中で作られる変数のことです。
たとえば次のようなコードを考えてみます。
-----
void vuln(char *input) {
    char buf[16];
}
-----
この"buf"は、"vuln()"関数の中だけで使える変数です。
こうした変数は、ローカル変数と呼ばれます。
ローカル変数は、多くの場合、スタック上に置かれます。
つまり、関数が呼ばれたときに作られる一時的な作業机の中に置かれるのです。
 
ここが今回の大きなポイントです。
もし、"buf"のようなバッファがスタック上にあり、そこに入りきらないデータを書き込んでしまうと、その近くに置かれている別の情報まで壊してしまう可能性があります。
つまり、関数の中のローカル変数として作られたバッファは、Buffer Overflowの学習でとても重要な存在なのです。

白猫先生

簡単にたとえるなら、関数は「その時間だけ使う机」、ローカル変数は「その机の上に置いた道具」です。もし机の上にインクをこぼしたら、自分のノートだけでなく、近くの大事なメモまで汚してしまうかもしれません。それと同じように、バッファがあふれると、その机の上にある他の情報まで壊す可能性があります。

 

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

 ここで、小さなサンプルコードを見てみます。
1.nanoエディターを立ち上げて、ソースを作成
nano bbo04-001.c
-----"
#include <stdio.h>
#include <string.h>
 
void vuln(char *input) {
    char buf[16];
    strcpy(buf, input);
    printf("buf: %s\n", buf);
}
 
int main(void) {
    vuln("HELLO");
    return 0;
}
-----
※プログラム入力完了後
「Ctrl」+ o で、プログラムの書き込み
「Ctrl」+ x で、nanoエディター終了です。
 
2.ソースをコンパイル
gcc bbo04-001.c -o bbo04-001
 
3.実行
./bbo03-001
 
このコードでは、"main()"から"vuln()"を呼び出しています。
まず "main()" が動きます。
そしてその中で、
vuln("HELLO");
が実行されると、コンピュータは"vuln()"のための作業スペースをスタック上に用意します。
その中で、
char buf[16];
によって、16文字ぶんのバッファが作られます。
この"buf"はローカル変数なので、"vuln()"の作業机の中に置かれているイメージです。
 
次に、
strcpy(buf, input);
で、受け取った文字列を"buf"にコピーします。
今回の"HELLO"は短いので、16文字ぶんの箱に十分収まります。
 
そして、
printf("buf: %s\n", buf);
で、その内容を表示しています。
 
このコード自体は、短い入力なら大きな問題は起きにくいです。
ですが、ここで大切なのは、bufが関数の中のスタック上にあるという点です。
つまり、もし"input"がとても長ければ、"buf"に収まりきらず、"vuln()"の作業スペースの他の部分にまで影響する可能性があるのです。
 
 

なぜ関数の中のバッファが重要なのか

 ここが今回の一番大切な部分です。
なぜ関数の中のバッファが重要なのでしょうか。
 
答えは、「スタック上には、ローカル変数だけでなく、関数の動きに関わる大事な情報も一緒に置かれているから」です。
つまり、関数の中のバッファがあふれると、単に文字列が壊れるだけではなく、その関数がどう戻るか、どう動くかに関わる情報まで影響を受ける可能性があるのです。
 
今の段階では、まだ細かい名前を全部覚えなくて大丈夫です。
大切なのは、
・関数が呼ばれると専用の作業スペースができる。
・その中にローカル変数のバッファが置かれる。
・その近くには、もっと大事な情報もある。
・だからバッファの「あふれ」は危険。
という流れを理解することです。

白猫先生

簡単にたとえるなら、関数の中のバッファは、ただのメモ欄ではありません。大事な指示書が置かれた机の上にあるメモ欄のようなものです。そのメモ欄からインクがあふれたら、周りの重要な紙まで汚してしまいます。

 

Buffer Overflowとは何か?スタック上で何が起きるのか?関数呼び出しとローカル変数の関係のまとめ

 
今回学んだ大事なことは、スタックが関数の実行に必要な情報を一時的に積んでおく場所であり、関数が呼ばれるたびに、その関数専用の作業スペースが作られるという点です。
この作業スペースはスタックフレームと考えることができ、その中にはローカル変数や、関数の動きに関わる情報が置かれます。"char buf[16]"のような関数の中のバッファは、多くの場合スタック上に置かれるため、もしそこに入りきらないデータを書き込むと、バッファの外だけでなく、その近くにある別の大事な情報まで壊してしまう可能性があります。
つまり、関数の中のバッファが危険なのは、単に文字があふれるからではなく、関数そのものの動きに影響する場所にあるからです。