黒ネコと学ぶ、Buffer Overflow 超基礎シリーズ
前回は、スタックとは何か、そして関数が呼ばれるとスタック上にその関数専用の作業スペースが作られることを学びました。
その作業スペースの中には、ローカル変数だけでなく、関数の動きに関わる情報も置かれている、というところまで見ました。
ここで次の疑問が出てきます。
くろちゃん
その「大事な情報」とは、具体的に何なのか? なぜ関数の中のバッファがあふれると危険なのか?
この答えに関係するのが、今回のテーマである「saved RBP」と「return address」です。
名前だけ見ると少し難しそうですが、考え方はシンプルです。
関数は、処理が終わったら「元の場所」に戻らなければなりません。
また、スタックを正しくたどるための目印も必要です。
そのために使われる大切な情報が、saved RBPとreturn addressです。
今回は、
・関数はどうやって元の場所へ戻るのか。
・return address とは何か。
・saved RBP とは何か。
・それらが壊れると何が起きるのか。
を、できるだけやさしく整理していきます。
※イメージです。
関数はどうやって元の場所に戻るのか
まずは、関数が終わったあとにどうやって元の場所へ戻るのかを考えてみましょう。
たとえば "main()"の中から"vuln()"を呼び出したとします。
このときコンピュータは、ただ"vuln()"に移動するだけでは困ります。
なぜなら、"vuln()"の処理が終わったあとに、また"main()"の続きへ戻らなければならないからです。
つまり関数を呼ぶときには、「終わったら、どこへ戻るか」という情報を覚えておく必要があります。
白猫先生
学校で簡単に説明すると、学校で別の教室へ用事に行くときに、「終わったら元の教室へ戻る」というメモを持っていくようなものです。もし戻る先を忘れてしまったら、用事が終わってもどこへ行けばよいかわかりません。
コンピュータでも同じで、関数を呼ぶときには、「戻る先の情報」をきちんと保存しておきます。
この「戻る先の情報」が、あとで出てくる「return address」です。
return address とは何か
では、「return address」とは何でしょうか。
return addressとは、簡単に言えば、「関数の処理が終わったあとに戻る場所を表す情報」です。
たとえば、"main()" の中にこんな流れがあるとします。
1."vuln()"を呼ぶ。
2."vuln()"が終わる。
3.その続きの処理をする。
このときコンピュータは、"vuln()" に入る前に「戻る先はここです」という位置を覚えておきます。それが return address です。
白猫先生
学校で簡単に説明すると「return address」は「作業が終わったら戻る教室番号」のようなものです。
この情報が正しければ、関数は終わったあとに元の流れへ自然に戻れます。
ですが、もしこの情報が壊れてしまったらどうなるでしょうか。
本来戻るはずの場所ではなく、別の場所へ飛んでしまうかもしれません。
つまり、プログラムの流れそのものが変わってしまう可能性があります。
これが、Buffer Overflowで「return address」が特に重要になる理由です。
saved RBP とは何か
次に「saved RBP」を見てみましょう。
RBPは、ざっくり言えば「今のスタックフレームの基準になる目印」のようなものです。
前回、関数が呼ばれると、その関数専用の作業スペースがスタック上に作られると学びました。
その作業スペースの中で、「ここを基準に見ればわかりやすい」という位置を示す目印がRBPです。
そして新しい関数に入るときには、前の関数で使っていたRBPをあとで元に戻せるように保存しておきます。この保存されたRBPが「saved RBP」です。
学校で簡単に説明すると、「saved RBP」は「前の作業机の位置を覚えておくためのメモ」のようなものです。なぜこれが必要かというと、関数の中で新しい作業机を使ったあと、終わったら元の机の状態に戻る必要があるからです。
初心者のうちは、saved RBPの細かい役割を完璧に覚えなくても大丈夫です。
今の段階では、
・RBP はスタックフレームの目印。
・saved RBP は前の目印を保存したもの。
と理解できれば十分です。
そして、Buffer Overflow の学習では、「saved RBP」のさらに近くに「return address」があることが多いというイメージを持つことが大切です。
スタック上ではどう並んでいるのか
ここで、スタック上の並びをイメージしてみましょう。
関数の中に "char buf[16];" のようなローカル変数があるとします。
このとき、学習用の簡単なイメージでは、スタック上に次のように並んでいると考えると理解しやすいです。
"buf"、saved RBP、return address
つまり、ローカル変数のバッファの近くに、関数が戻るための大事な情報が置かれているのです。
この並びは環境や最適化などで細かく変わることもありますが、学習の入口としてはこのイメージで大丈夫です。
ここで重要なのは、bufに入りきらないデータを書き込むと、そのすぐ近くにある「saved RBP」や「return address」にまで届く可能性がある。ということです。
白猫先生
学校で簡単に説明すると、机の上に、「自分のメモ帳」「前の作業を戻すためのメモ」「次に戻る場所のメモ」が並んでいるようなものです。もし自分のメモ帳にインクをあふれさせたら、となりにある「戻る場所のメモ」まで汚してしまうかもしれません。そうなると、作業が終わったあとに正しい場所へ戻れなくなります。
この感覚がとても大切です。
サンプルコードでやさしく理解する
ここで、小さなサンプルコードを見てみます。
1.ソースを作成します。
nano bbo05-001.c
-----
#include <stdio.h>
#include <string.h>
void win(void) {
printf("You reached win function!\n");
}
void vuln(char *input) {
char buf[16];
strcpy(buf, input);
}
int main(void) {
vuln("AAAA");
printf("Back to main.\n");
return 0;
}
-----
このコードには、学習のための大事な要素が入っています。
2.コンパイルします。
gcc bbo05-001.c -o bbo05-001
3.実行します。
./bbo05-001
このプログラムより、まず
void win(void) {
printf("You reached win function!\n");
}
という "win()" 関数があります。
これは今すぐ何か攻撃するためというより、「本来とは別の場所へ飛ぶとこういう関数へ行けるかもしれない」という考え方を学ぶための準備です。
次に、
void vuln(char *input) {
char buf[16];
strcpy(buf, input);
}
の"vuln()"関数です。
ここにはローカル変数"buf[16]"があります。
この"buf"はスタック上に置かれるローカルバッファです。
そして"strcpy(buf, input);"は、入力をそのままコピーします。
もし"input"の入力が短ければ大きな問題は起きにくいです。ですが、もし非常に長い入力なら、"buf"に収まりきらず、その近くにある「saved RBP」や「return address」にまで影響する可能性があります。
最後に "main()" では、
vuln("AAAA");
printf("Back to main.\n");
としています。
本来なら"vuln()"が終わったあと、"main()"の続きである"printf("Back to main.\n");"に戻ります。この「続きに戻る」という動きを支えているのが return address です。
つまりこのコードは、
"vuln()" に入る。
"buf"を使う。
終わったら"main()"の続きへ戻る
という普通の流れを持っています。
そして、その普通の流れが成り立つために、「saved RBP」と「return address」が大切だとわかります。
Buffer Overflowとは何か?saved RBP と return address はなぜ大事なのか?のまとめ
今回学んだ大事なことは、関数が終わったあとに元の場所へ戻るためには「return address」が必要であり、スタックフレームの基準を元に戻すためには「saved RBP」が使われるという点です。
return address は「処理が終わったらどこへ戻るか」を示す大切な情報で、saved RBP は前のスタックフレームの目印を保存したものです。これらはローカル変数のバッファの近くに置かれることがあるため、関数の中の "buf" に入りきらないデータを書き込むと、そのあふれたデータが saved RBP や return address にまで影響する可能性があります。
特に、return addressが壊れると、本来戻るべき場所ではない別の場所へ流れが変わるかもしれません。つまり Buffer Overflowが危険なのは、単に文字列を壊すだけではなく、プログラムの進み方そのものを変える可能性があるからなのです。