Buffer Overflowとは何か?クラッシュから考える:Segmentation Faultは何を教えてくれるのか(第6回/全10回)
前回は、関数の中のバッファの近くにある、saved RBPや、return addressのような大切な情報があることを学びました。
そのため、Buffer Overflowはただ文字があふれるだけではなく、プログラムの流れそのものに影響を与える可能性があることも見えてきました。
[fuki-r]ここで次に出てくるのが、よく知られたエラーのひとつである「Segmentation Fault」です。Buffer Overflowを学んでいると、プログラムを実行したときに突然止まってしまい、「Segmentation fault」や「core dumped」のような表示を見ることがあります。[/fuki-r]初めて見ると、とても怖く感じるかもしれません。
ですが、エクスプロイト開発の学習では、このクラッシュは単なる失敗ではありません。
むしろ、
「どこかが壊れた」
「想定外の場所に触ってしまった」
という大切なヒントになります。
今回は、
・Segmentation Faultとは何か。
・なぜクラッシュが起きるのか。
・クラッシュはなぜ学習に役立つのか。
・GDBでは何を見ればよいのか。
を、やさしく整理していきます。
※イメージです。
Segmentation Faultとは何か
Segmentation Faultは、簡単に言うと、
「プログラムが、触ってはいけないメモリに触ろうとして止められた状態」です。
コンピュータのメモリは、どこでも自由に読み書きしてよいわけではありません。
プログラムごとに、使ってよい範囲や、読んでよい場所、書いてよい場所がある程度決まっています。
もしプログラムが、
・存在しない場所を読もうとした。
・書いてはいけない場所に書こうとした。
・壊れたアドレスへ飛ぼうとした。
といったことをすると、OSが「それは危険です」と判断して、プログラムを止めます。
それが Segmentation Fault です。 [fuki-l]簡単に言うと、Segmentation Faultは、「立ち入り禁止エリアに入ろうとして、警備員にそこで止められること」に近いですね。[/fuki-l]プログラムが勝手に危険な場所へ進もうとしたので、強制的にストップされた、と考えるとわかりやすいです。
なぜBuffer Overflowでクラッシュが起きるのか
なぜ Buffer Overflowで「Segmentation Fault」が起きるのでしょうか。
理由は、バッファに入りきらないデータを書き込むことで、本来壊してはいけない情報まで壊してしまうからです。
たとえば、関数の中のバッファ “buf" があふれると、その近くにある
・saved RBP
・return address
・他の変数
・スタック上の大事な情報
に影響することがあります。
特に return addressが壊れると、関数が終わったあとに「本来戻るはずの場所ではない、変なアドレスへ飛ぼうとする」ことがあります。
するとCPUは、その飛び先が正しい場所でなければ、そこで処理を続けられません。その結果、Segmentation Faultが起こります。
[fuki-l]つまりクラッシュは、「何かがあふれて、プログラムの中の大事なものが壊れた」というサインなのです。[/fuki-l]ここで大切なのは、クラッシュそのものを怖がることではありません。
大切なのは、
・どの入力でクラッシュしたのか。
・何が壊れたのか。
を観察することです。
クラッシュは失敗ではなくヒントである
初心者のうちは、プログラムがクラッシュすると「全部失敗した」と感じてしまいがちです。
ですが、Buffer Overflowの学習では、クラッシュはとても大事な情報です。
なぜなら、クラッシュが起きるということは、
・入力がプログラムに影響を与えた。
・何か大事な領域まで届いた。
・単なる無視ではなく、実際に壊れた。
ことを意味するからです。
もちろん、クラッシュしただけでは攻撃成功ではありません。
ですが、少なくとも「何も起きなかった」よりは、ずっと多くの情報がある状態です。 [fuki-l]簡単に言うと、金庫を開けようとして失敗したけれど、「どのダイヤルを回したときに反応があったか」がわかったようなものです。[/fuki-l]完全に開いてはいなくても、反応があること自体が次のヒントになります。
エクスプロイト開発では、このように
・どんな入力で
・どんな壊れ方をして
・どこで止まったのか
を観察しながら、少しずつ理解を深めていきます。
だから Segmentation Faultは、ただのエラー表示ではなく「観察の出発点」としてとても重要です。
サンプルコードでやさしく理解します
ここで、学習用のシンプルなコードを見てみます。
1.ソースを作成します。
nano bbo06-001.c
—–
#include <stdio.h>
#include <string.h>
void vuln(char *input) {
char buf[8];
strcpy(buf, input);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vuln(argv[1]);
}
return 0;
}
—–
※プログラム入力完了後
「Ctrl」+ o で、プログラムの書き込み
「Ctrl」+ x で、nanoエディター終了です。
2.ソースをコンパイル
gcc bbo06-001.c -o bbo06-001
このコードでは、"main()"がコマンドライン引数を受け取り、それを “vuln()"に渡しています。
まず、
char buf[8];
で、8文字ぶんの小さなバッファを作っています。
次に、
strcpy(buf, input);
で、外から渡された文字列をそのままコピーしています。
ここでは長さの確認をしていないため、もし"input"が長すぎると、Buffer Overflowが起きる可能性があります。
たとえば、短い入力なら比較的安全に見えるかもしれません。
3.実行
./bbo06-001 AAAA
しかし、ずっと長い入力を与えると、"buf" の範囲をこえて書き込んでしまい、スタック上の別の情報にまで影響が及ぶことがあります。
4.実行
./bbo06-001 AAAAAAAAAAAAAAAAAAAA
このような入力で、環境によってはクラッシュが起きることがあります。
このコードの大事なポイントは、
・外から入力を渡せる。
・"buf"が小さい。
・長さ確認なしでコピーしている。
という3つです。
つまり、「長い入力を与えると壊れやすい実験用の入口」になっています。
GDBで何を見ればよいのか
クラッシュしたときに大切なのは、ただ「落ちた」で終わらせないことです。
ここで役立つのがGDBです。
GDBを使うと、プログラムがどこで止まったのか、何をしようとしていたのかを観察できます。
初心者の段階で、まず意識したいのは次の3つです。
1.どこで止まったのか
関数のどこでクラッシュしたのかを見ると、入力がどのタイミングで問題を起こしたのかがわかりやすくなります。
2.RIP がどこを指しているか
64bit環境では、RIPが今実行しようとしている場所を表します。
もしここが不自然な値になっていれば、return addressなどが壊れた可能性を考えられます。
3.スタックの中身がどうなっているか
スタックを確認すると、"A"などの入力がどこまで届いているかが見えることがあります。
これによって、「どこまで壊れたか」を考える手がかりが得られます。[fuki-l]簡単ににたとえるなら、GDBは「事故のあとに現場を調べるルーペ」のようなものです。[/fuki-l]ただ「事故が起きた」と言うだけではなく、
・どこで起きたのか。
・何がぶつかったのか。
・何が壊れたのか。
を見られるようになります。
Segmentation Faultそのものよりも、クラッシュしたあとの観察が学習ではとても大切です。
Buffer Overflowとは何か?クラッシュから考える:Segmentation Faultは何を教えてくれるのかのまとめ
今回学んだ大事なことは、Segmentation Faultが「ただの怖いエラー」ではなく、プログラムが触ってはいけないメモリに触ろうとして止められた結果であるという点です。
Buffer Overflow が起きると、ローカルバッファの外にまで書き込みが進み、saved RBPや return addressなどの大切な情報が壊れることがあります。その結果、関数が終わったあとに変な場所へ飛ぼうとして、Segmentation Fault が起きることがあります。ここで重要なのは、クラッシュを失敗として終わらせるのではなく、「何が壊れたのか」を知るヒントとして見ることです。どんな入力で止まったのか、どこで止まったのか、RIP やスタックがどうなっているのかを観察することで、Buffer Overflow の理解は大きく進みます。つまりクラッシュは、壊れた証拠であると同時に、次へ進むための大事な手がかりでもあるのです。