黒ネコと学ぶ、保護機構をやさしく理解する
ここまでの保護機構シリーズでは、現代のバイナリを守る代表的な仕組みを1つずつ見てきました。
・NXは、データを置く場所でそのまま命令を動かしにくくする。
・Stack Canaryは、return addressの前に見張り役を置く。
・ASLRは、メモリ上の場所を毎回変えて予測を難しくする。
・PIEは、プログラム本体の位置も固定で考えにくくする。
・RELROは、大事な表を守る。
ここまで学ぶと、つい「NXはこういう仕組み」「Canaryはこういう仕組み」と、1つずつ個別に覚えたくなります。
もちろん、それ自体は大切です。
ですが、実際のバイナリを見るときに本当に重要なのは、「この守りがいくつ組み合わさっているか」です。
なぜなら、現代のプログラムは1つの強い壁で守られているというより、いくつもの壁を重ねて守られていることが多いからです。
今回は、
・なぜ1つずつではなく組み合わせで考えるのか。
・それぞれの保護機構がどんな役割を分担しているのか。
・組み合わさると何が難しくなるのか。
・初心者はどんな順番で見ればよいのか。
を、やさしく順番に整理していきます。
なぜ「組み合わせ」で考えるのか
まず大事なのは、保護機構は「1つだけで完璧に守るものではない」ということです。
たとえば、家を守ることを考えてみてください。
・玄関には鍵がある。
・窓には補助鍵がある。
・外には防犯カメラがある。
・夜はオートロックがある。
こうした守りは、それぞれ役割が少しずつ違います。
どれか1つだけで全部を守るわけではありません。
プログラムの保護機構も同じです。
たとえば NX があっても、「スタック上でそのまま動かしにくい」だけで、バグそのものが消えるわけではありません。
Canary があっても、「戻り情報の近くまで来たら見つけやすい」だけで、すべての異常を消すわけではありません。
ASLR や PIE も、「場所を予測しにくくする」のであって、バグそのものをなくしているわけではありません。
つまり、それぞれは違う弱点に対して別々の壁を作っているのです。
だからこそ、現代のバイナリを見るときは、「この守り単体で何をしているか」だけでなく、「いくつ重なって、全体として何を難しくしているか」で考える必要があります。
それぞれの保護機構は何を担当しているのか
ここで、これまで学んだ保護機構の役割を、組み合わせで見やすいように整理してみましょう。
1.NX
NXは、「その場で実行すること」を難しくする守りです。
スタックのようなデータ置き場で、書いた内容をそのまま命令として動かしにくくします。
2.Stack Canary
Canaryは、「大事な戻り情報の手前まで来たこと」を見つけやすくする守りです。
return address の前に見張り役を置くことで、異常に気づきやすくします。
3.ASLR
ASLRは、「場所を予測すること」を難しくする守りです。
スタックやライブラリなどの位置を毎回変えやすくします。
4.PIE
PIEは、「プログラム本体の位置を固定で考えること」を難しくする守りです。
ASLR と組み合わさることで、本体の関数位置も固定の目印になりにくくなります。
5.RELRO
RELROは、「大事な表を書き換えること」を難しくする守りです。
特に、外部関数の行き先情報のような表を守る方向で働きます。
こうして並べるとわかるように、どの保護機構も守っている場所や、難しくしていることが少しずつ違うのです。
組み合わさると何が起きるのか
ここが今回の一番大切な部分です。
保護機構は、1つずつでも意味があります。
ですが、本当に強さが出るのは、組み合わさったときです。
たとえば、次のような場面を考えてみましょう。
1.Buffer Overflowが起きそう
ローカルバッファ"buf"に長すぎる入力が入ったとします。
2.まず、Canaryが気づくかもしれない
大事な戻り情報の前に見張り役がいるので、そこまで届けば異常として止まりやすくなります。
3.もしそこを考慮しても、NX が待っている
スタックに何かを書けたとしても、その場で実行しにくくなっています。
4.さらに、ASLRとPIEがある
「では別の場所へ進むにはどこか」を考えようとしても、場所そのものが毎回変わりやすく、予測しにくくなっています。
5.さらに、RELROがある
大事な表の書き換えも、簡単にはできない方向になります。
つまり、1つの壁を考えればよいのではなく、いくつもの壁が順番に出てくるのです。
白猫先生
簡単にたとえるなら、これは学校の中に入るまでに、門、玄関、教室の鍵、先生の見回り、名簿管理まで全部あるようなものです。
1つだけなら突破しやすそうに見えても、何重にも重なると、急に難しくなります。
これが、保護機構を“組み合わせ”で考える理由です。
サンプルコードでやさしく考える
ここで、学習用のシンプルなコードを見てみます。
-----
#include <stdio.h>
#include <string.h>
void win(void) {
printf("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;
}
-----
このコードだけを見ると、初心者はこう考えたくなるかもしれません。
・"buf"がある。
・"strcpy"がある。
・長い入力ならあふれるかもしれない。
・なら"win()"に届くかもしれない。
Buffer Overflow の基礎を学んだ直後なら、自然な考え方です。
ですが、現代の実際の環境では、ここに保護機構が重なります。
たとえば、
・Canaryがあれば、return addressに近づく前に異常として止まりやすい。
・NXがあれば、スタックでそのまま動かしにくい。
・PIEがあれば、"win()"の位置も固定で考えにくい。
・ASLRがあれば、他の場所も毎回ずれやすい。
・RELROがあれば、大事な表の書き換えも難しい。
となります。
このコードから学ぶべきことは、「脆弱性がありそうに見えること」と「簡単にそこから先へ進めること」は別だという点です。
そして、その“別”を生んでいるのが、保護機構の組み合わせなのです。
保護機構をやさしく理解するシリーズ。保護機構は1つずつではなく“組み合わせ”で考えるのまとめ
今回学んだ大事なことは、保護機構は1つずつ独立して覚えるだけでは不十分で、実際には複数が重なって働く“組み合わせ”として考えることが重要だという点です。
NX は実行を、Canary は戻り情報への到達を、ASLR と PIE は場所の予測を、RELRO は大事な表の書き換えを、それぞれ難しくします。つまり、現代のバイナリは1枚の壁ではなく、何枚もの壁で守られている状態です。そのため、あるバグが見つかっても、それだけですぐに大きな影響へつながるとは限りません。
大切なのは、「この保護機構は何を難しくしているのか」を1つずつ理解し、そのうえで「これらが重なると、全体として何がしにくくなるのか」を考えることです。保護機構は、チームとして見ることで初めて本当の強さが見えてきます。