Buffer Overflowとは何か?ret2winとは何か?最初に理解したい”制御奪取”の形(第9回/全10回)

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

ここまでは、Buffer Overflowの基本から始まり、
・バッファとは何か
・配列とメモリの関係
・境界チェック不足
・スタックとローカル変数
・saved RBP と return address
・Segmentation Fault
・オフセット
・64bit環境での見方
までを学んできました。
 
ここまで理解できると、いよいよ次に見えてくるのが、「では、実際にプログラムの流れを変えるとはどういうことか」というテーマです。
 
その入口としてとても大切なのが、今回の「ret2win」です。
 ret2winは、エクスプロイト開発の入門でよく出てくる考え方です。
名前だけ見ると難しそうですが、本質はとてもシンプルです。
 ひとことで言うと、ret2winとは、「本来の戻り先ではなく、用意された別の関数へ戻るように流れを変えること」です。
 
今回は、
・制御奪取とは何か。
・ret2win とは何か。
・なぜ “win()" 関数がよく用意されるのか。
・return address を変えると何が起きるのか。
を、やさしく整理していきます。
 ※イメージです。

制御奪取とは何か

 まずは、制御奪取という言葉から考えてみましょう。
制御奪取とは、簡単に言えば、プログラムが進む先を、本来の流れとは違う方向へ変えることです。
普段、プログラムは作られた通りに動きます。
 
たとえば、
1."main()" が始まる
2."vuln()" を呼ぶ
3."vuln()" が終わる
4."main()" の続きへ戻る
という流れなら、本来はその通りに進みます。
 
ですが、もし途中でreturn addressが壊れたらどうなるでしょうか。
関数が終わったあとに、元の場所ではなく、別の場所へ飛んでしまうかもしれません。
これが、制御奪取の最初のイメージです。 [fuki-l]簡単にたとえるなら、これは帰り道の案内板を書き換えて、別の教室へ向かわせることに近いです。本来はA教室へ戻るはずだったのに、案内板が書き換えられていて、B教室へ行ってしまう。プログラムでも、return address が変わると、処理の進み方が変わるのです。[/fuki-l]
  

ret2winとは何か

 では、「ret2win」とは何でしょうか。
ret2winは、言葉を分けて考えるとわかりやすいです。
 
・"ret"は「return」、つまり関数から戻ること。
・"win"は「学習用に用意された、到達したい特別な関数」。
つまり ret2win とは、関数が戻るときに、本来の戻り先ではなく"win()"関数へ行くようにすることです。
 
本来なら、"vuln()"が終わったあとは “main()"の続きへ戻ります。
ですが、もしreturn addressを"win()"のアドレスに変えられたら、"vuln()"のあとで"win()"が実行される可能性があります。
これが ret2win の基本イメージです。
 
ここで大事なのは、ret2winが最初から何でもできる強力な攻撃そのものというより、「戻り先を書き換えると流れが変わる」ことを学ぶための最初の教材だという点です。
つまり ret2win は、制御奪取の入口を理解するための、とてもやさしい形なのです。
 
 

なぜ"win()"関数がよく用意されるのか

 入門用の問題や学習用コードでは、よく"win()"という関数が用意されています。
なぜそんな関数があるのでしょうか。
理由はとてもシンプルで、「本来は行かないはずの場所へ飛べたかどうか」をわかりやすく確認するためです。
 
たとえば"win()"の中で、
・特別なメッセージを表示する。
・フラグの一部を出す。
・成功したことがわかる処理をする。
ようにしておけば、そこへ到達したかどうかがすぐわかります。
 
つまり"win()"は、「ここへ行けたら制御奪取の基本が理解できている」と確認するためのゴール地点です。 [fuki-l]簡単にたとえるなら、"win()"は「隠し部屋のようなもの」です。普段の正しいルートでは入れないけれど、道案内を書き換えるとそこへ行けるかもしれない。その「隠し部屋に入れた」という事実が、流れを変えられた証拠になります。[/fuki-l]実際のプログラムでは、こんなに都合よく"win()"が用意されていないことも多いです。
ですが、学習ではまず「流れを変える」という感覚をつかむことが大切なので、ret2win は非常に良い練習になります。
 
 

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

 ここで、学習用のシンプルなコードを見てみます。
—–
#include <stdio.h>
#include <string.h>
 
void win(void) {
    printf(“Congratulations! You reached win().\n");
}
 
void vuln(char *input) {
    char buf[16];
    strcpy(buf, input);
}
 
int main(int argc, char *argv[]) {
    if (argc > 1) {
        vuln(argv[1]);
    }
    return 0;
}
—–
このコードには、ret2win を理解するための大切な要素が入っています。
 
まず、
void win(void) {
    printf(“Congratulations! You reached win().\n");
}
は、到達できたら成功がわかる特別な関数です。
普段の流れでは、"main()"から直接"win()"は呼ばれていません。
つまり通常実行では、ここには来ません。
 
次に、
void vuln(char *input) {
    char buf[16];
    strcpy(buf, input);
}
では、"buf[16]"というローカルバッファがあります。
そして、"strcpy(buf, input);"で長さ確認なしに入力をコピーしています。
 
このとき、もし"input"が長すぎれば、"buf"の外へ書き込みが進み、その先にあるsaved RBPや、return addressに影響する可能性があります。
 
最後に “main()" では、
if (argc > 1) {
    vuln(argv[1]);
}
としています。
本来なら “vuln()" が終わったあと、そのまま “main()" の続きへ戻って終了します。
ですが、もしreturn addressが"win()"のアドレスに変わっていたら、"vuln()"のあとに"win()"が実行される可能性があります。
 
このコードから学ぶべきことは、「バッファのあふれが、文字列の破壊だけでなく、戻り先の変更につながる可能性がある」ということです。
 
 

ret2winで本当に学ぶべきこと

 ret2winを学ぶとき、初心者が勘違いしやすい点があります。
それは、「win()に飛べれば終わり」と考えてしまうことです。
もちろん、学習問題としては"win()"に到達できれば成功です。
ですが、本当に大事なのはそこではありません。
 
ret2win で学ぶべき本質は、
・return addressは戻り先を決める大事な情報である。
・その情報が壊れるとプログラムの流れが変わる。
・Buffer Overflowは、その入口になりうる。
・制御奪取は「どこへ飛ばすか」を考える作業でもある。
ということです。
 
つまり ret2win は、「戻り先を変えると流れが変わる」という制御奪取の基本を、非常にわかりやすく教えてくれる教材なのです。 [fuki-l]簡単にたとえるなら、ret2winは、帰り道の行き先をこっそり書き換えて、別の部屋へ行かせる練習です。[/fuki-l]この感覚が身につくと、次の段階である"ROP"や"ret2libc"のような、もっと複雑な話にもつながっていきます。
 
 

Buffer Overflowとは何か?ret2winとは何か?最初に理解したい"制御奪取"の形のまとめ

 今回学んだ大事なことは、ret2winが「関数から戻るときに、本来の戻り先ではなく"win()"関数へ流れを変える」という、制御奪取のもっともわかりやすい形だという点です。
return addressは関数が終わったあとに戻る場所を示す大切な情報であり、もしBuffer Overflowによってその値が変わると、プログラムは元の流れとは違う場所へ進む可能性があります。
学習用の"win()"関数は、その"別の場所へ飛べたかどうか"を確認しやすくするためのゴール地点です。つまり、ret2winの本質は、単に特別な関数へ飛ぶことではなく、return addressを変えることでプログラムの流れそのものを変えられる、という考え方を理解することにあります。
これは制御奪取の最初の一歩であり、今後のより高度なエクスプロイト学習につながる大切な土台になります。