黒ネコと学ぶ、Buffer Overflow 超基礎シリーズ
前回は、Segmentation Faultがただの失敗ではなく、「どこかが壊れた」という大事なヒントになることを学びました。
ここで次に出てくる大切な考え方が、「オフセット」です。
Buffer Overflow の学習では、ただ「長い文字列を入れたら落ちた」で終わらせてはいけません。
本当に大切なのは、
・どこまで入力が届いたのか。
・何文字で、どの場所まで壊れたのか。
を論理的に考えることです。
このとき役立つのがオフセットという考え方です。
オフセットは、最初は少し難しそうに聞こえるかもしれません。
ですが、意味そのものはとてもシンプルです。
簡単に言えば、「ある場所から、目的の場所までの距離」です。
今回は、
・オフセットとは何か
・なぜ距離を考えることが大切なのか
・Buffer Overflow では何の距離を考えるのか
・感覚ではなく論理で考えるとはどういうことか
をやさしく整理していきます。
※イメージです。
オフセットとは何か
まず、オフセットという言葉の意味から見ていきましょう。
オフセットとは、簡単に言えば、「基準となる場所から、ある場所までどれくらい離れているか」を表す考え方です。
白猫先生
簡単に言うと、スタート地点から目的地までの距離のようなものです。たとえば、学校の正門から図書室まで20メートル離れているなら、「正門を基準にした図書室の位置」は20メートル先です。これと似た考え方が、オフセットです。
コンピュータのメモリでも、
ある場所を基準にして、別の場所まで何バイト離れているか
を考えます。
Buffer Overflow の学習でよく考えるのは、
・"buf"の先頭から。
・saved RBPまで。
・return addressまで。
どれくらい離れているか、という距離です。
白猫先生
つまりオフセットは、「バッファの最初から、壊したい場所まで何文字ぶん進めば届くのか」を考えるための大切な考え方なのです。
なぜ距離を考えることが大切なのか
では、なぜオフセット、つまり距離を考える必要があるのでしょうか。
理由はとてもシンプルです。
Buffer Overflow は、ただ長い入力を入れればよいわけではないからです。
たとえば、スタック上に次のような並びがあるとします。
・"buf[16]"
・saved RBP
・return address
このとき、もし return address に届かせたいなら、まずは"buf"を埋めて、その先のsaved RBPをこえて、さらに return address まで届かなければなりません。
つまり、何文字入れれば、どこまで届くのかを考える必要があります。
もし短すぎれば、return addressまで届きません。逆に、長すぎれば必要以上に壊してしまい、何が起きたのか分かりにくくなることがあります。
白猫先生
簡単に言うと、オフセットを考えるのは、「廊下を何歩進めば目的の教室に着くかを数える」ようなものです。適当に歩くだけでは、狙った教室にぴったり着けるとは限りません。
Buffer Overflow でも同じで、「何文字くらいかな」ではなく、「何バイトで届くか」を考えることが大切なのです。
Buffer Overflowでは何の距離を考えるのか
ここで、実際に何の距離を考えるのかを整理しましょう。
Buffer Overflow の学習でよく意識するのは、バッファの先頭からreturn addressまでの距離です。
なぜなら、return addressは関数が終わったあとに戻る場所を表す大切な情報だからです。
ここに届くと、プログラムの進み方そのものに影響する可能性があります。
たとえば、学習用のイメージとして、次のように並んでいるとします。
・0~15バイト目 : "buf"
・16~23バイト目: saved RBP
・24~31バイト目: return address
この場合、"buf"の先頭から return addressの位置までは24バイト離れている、と考えられます。
つまり、そこに届かせたいなら、まず24バイトぶん進める必要があります。
ここで大事なのは、「どこからどこまでを基準にしているか」を意識することです。
オフセットは、いつも「何かを基準にした距離」です。
Buffer Overflowでは、多くの場合「バッファの先頭を基準にして、どこまで届いたか」を考えます。
つまりオフセットとは、「メモリの中の地図を、数字で考える方法」とも言えます。
サンプルコードでやさしく理解する
ここで、学習用のシンプルなコードを見てみます。
-----
#include <stdio.h>
#include <string.h>
void vuln(char *input) {
char buf[16];
strcpy(buf, input);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vuln(argv[1]);
}
return 0;
}
-----
このコードでは、"vuln()"の中に"char buf[16];" があります。
つまり、16文字ぶんのローカルバッファです。
そして、
strcpy(buf, input);
によって、外から与えた文字列がそのままコピーされます。
長さの確認をしていないので、入力が長すぎると Buffer Overflow が起きる可能性があります。
ここで考えたいのは、入力した文字がどこまで届くのかです。
たとえば、
・8文字なら "buf"の途中まで。
・16文字なら "buf"をちょうど埋める。
・20文字なら "buf" の外へ少しはみ出す。
・さらに長ければ、その先へ進む。
というイメージが持てます。
もし、"buf"の先にsaved RBPや、return addressがあるなら、どの長さでどこまで届くかを考えることがとても大切です。
このコードを使って学ぶべきことは、「長い文字列で落ちるかもしれない」ということだけではありません。
本当に大事なのは、「16文字のバッファに対して、何文字入れると、その先のどこまで届くのか」を考えることです。
これが、オフセットの入口です。
感覚ではなく論理で考えるとはどういうことか
Buffer Overflowの学習では、ここからとても大切な姿勢が出てきます。
それが、「感覚ではなく、論理で考える」ということです。
たとえば、「とにかく長い文字列を入れてみよう」というやり方だけでは、運よく何か起きることはあっても、理解は深まりません。
それよりも、
・バッファは何バイトか。
・その先に何があるか。
・どこまで届かせたいか。
・そこまで何バイト必要か。
を順番に考えるほうが、ずっと大切です。
白猫先生
簡単に言うと、これは「暗算で適当に答えを出すのではなく、式を立てて考える」のに似ています。たまたま当たることよりも、なぜその長さになるのかを説明できることが大切なのです。
エクスプロイト開発では、この「説明できる考え方」がとても重要です。
つまりオフセットを学ぶことは、単に距離を覚えることではなく、メモリ破壊を論理的に言語化する練習でもあります。
これができるようになると、次のステップである
・どこが壊れたのか。
・何文字で届いたのか。
・どうやって正確に狙うのか。
も考えやすくなります。
Buffer Overflowとは何か?オフセットとは何か?どこまで壊れたかを論理的に考えるのまとめ
今回学んだ大事なことは、オフセットが「ある基準の場所から目的の場所までの距離」を表す考え方であり、Buffer Overflow では特にバッファの先頭から saved RBP や return address までの距離を考えるために重要だという点です。
Buffer Overflow は、ただ長い入力を入れるだけの話ではなく、何文字でどこまで届くのかを論理的に考えることが本質です。短すぎれば目的の場所に届かず、長すぎれば必要以上に壊してしまうかもしれません。そのため、バッファの大きさ、その先にある情報、そして狙いたい場所までの距離を順番に考える必要があります。
つまりオフセットとは、メモリの中の見えない地図を数字で理解するための大切な道具です。これを理解すると、次回学ぶ 64bit環境での見方や、より正確な制御奪取の考え方にもつながっていきます。