保護機構をやさしく理解するシリーズ。NXとは何か? なぜスタック上のコード実行が止められるのか(第2回/全10回)
前回は、現代のバイナリにはいくつもの保護機構があり、バグがあっても簡単には大きな問題へつながらないように工夫されていることを学びました。
その中でも今回取り上げる「NX(No-eXecute:実行しない)」は、保護機構の基本としてとても重要です。
Buffer Overflow を学び始めたばかりのころは、
・バッファにデータを書けた。
・その場所に何か入れられた。
・なら、そのまま実行できるのでは?
と考えたくなります。
ですが、現代の環境では、そう単純にはいきません。
その大きな理由の1つが「NX」です。 [fuki-r]NX をとてもやさしく言うと、「この場所はデータを置く場所であって、命令を実行する場所ではありません」と決める仕組みです。[/fuki-r]つまり、メモリの場所ごとに、「ここは実行してよい場所」なのか、「ここは実行してはいけない場所」なのかを分けているのです。
今回は、
・NXとは何か。
・なぜ必要なのか。
・スタック上のコード実行をどう止めるのか。
・Buffer Overflow とどう関係するのか。
を、やさしく順番に整理していきます。
※イメージです。
NXとは何か
NXは、ざっくり言えば「実行してはいけないメモリ領域を、実行不可にする仕組み」です。
NXはよく「No eXecute」の略として説明されます。
意味としては、そのまま「実行しない」です。
たとえばメモリのある場所に、文字列や数字などのデータが置かれているとします。
昔は、環境によっては、その場所に置いた内容をそのまま命令として扱えてしまう場合がありました。
しかし、それでは危険です。
本来は「ただのデータ置き場」だった場所で、命令まで動かせてしまうと、バグが起きたときの影響が大きくなりやすいからです。
そこで NX では、メモリの領域ごとに
・ここは読むだけ。
・ここは書ける。
・ここは実行できる。
といった性質を分けて考えます。
その結果、たとえばスタックのような「データを置くための場所」に対しては、「書けるけれど実行はできない」という設定にすることができます。 [fuki-l]簡単にたとえるなら、NXは「この部屋は荷物置き場であって、ステージではありません」というルールを作る仕組みです。荷物置き場に物は置けても、そこでショーは始められない。それが NX のイメージです。[/fuki-l]
なぜNXが必要なのか
では、なぜそんな仕組みが必要なのでしょうか。
理由は、「データを置く場所と、命令を実行する場所を分けないと危険だから」です。
コンピュータのプログラムは、本来
・命令を置く場所。
・データを置く場所。
をある程度分けて扱ったほうが安全です。
もしこの区別がゆるいと、バッファに入り込んだデータや、外から与えられた内容が、そのまま命令として動いてしまう可能性があります。
そうなると、本来はただの入力欄だった場所が、思わぬ実行場所になってしまいます。 [fuki-l]簡単にたとえるなら、学校で、教室は授業をする場所、倉庫は物を置く場所と分かれているほうが安全です。もし倉庫でも好きに授業やイベントができるようになっていたら、管理がとても大変になります。どこで何が始まるかわからなくなるからです。[/fuki-l]プログラムの世界でも同じで、「データ置き場はデータ置き場」「実行場所は実行場所」と分けることで、安全性が上がります。
NXは、その大事な境界線を作る仕組みなのです。
スタック上のコード実行がなぜ止められるのか
ここで、スタックとの関係を見ていきましょう。
これまでの「Buffer Overflowシリーズ」では、関数の中のローカル変数"buf"などがスタック上に置かれることを学びました。
つまりスタックは、関数の作業用メモや一時的なデータを置く場所です。
この性質を考えると、スタックは本来「データのための場所」です。
たとえば、
char buf[16];
のようなローカル変数は、スタック上に置かれることが多いです。
ここに文字列や入力データが入ります。
もし、NXがなければ、そのデータ置き場に置かれた内容を、そのまま命令として使えてしまう可能性があります。
それでは危険です。
そこで、NXは、スタックに対して「ここは書いてよいが、実行してはいけない場所」という扱いを与えます。
その結果、たとえBuffer Overflowでスタックにいろいろ書けたとしても、「その場で命令として動かすことは難しくなる」のです。
ここがとても重要です。
つまりNXは、Buffer Overflowそのものを消しているわけではありません。
バッファのあふれが起きる可能性は残るかもしれません。
ただし、その次の段階である「スタック上に置いたものをそのまま実行する」という流れを止めているのです。
サンプルコードでやさしく考える
ここで、学習用のシンプルなコードを見てみます。
—–
#include <stdio.h>
#include <string.h>
void vuln(char *input) {
char buf[16];
strcpy(buf, input);
printf(“buf: %s\n", buf);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vuln(argv[1]);
}
return 0;
}
—–
このコードでは、"vuln()"の中に"char buf[16];"があります。
これはスタック上に置かれるローカルバッファです。
そして、
strcpy(buf, input);
で、外から来た入力を長さ確認なしでコピーしています。
入力が長すぎれば、"buf" の外へ書き込みが進む可能性があります。
ここまでは、これまで学んだ Buffer Overflow の話です。
では、この後に NX がどう関係するのでしょうか。
ポイントは、「buf があるのはスタック上」だということです。
もし「スタックに何かを書けたなら、そのままそこで動かせる」と考えてしまうと、話は単純そうに見えます。ですが、NXが有効だと、スタックはデータを置く場所ではあっても、命令を実行する場所ではないので、その発想はそのまま通りません。
このコードから学ぶべきことは、「書けることと、実行できることは別」だという点です。
これは NX を理解するときのとても大切な感覚です。
NXがあると何が変わるのか
初心者のうちは、NXの効果を「スタックの上で動かせなくなる」とまず理解すれば十分です。
つまり、NX があると
・スタックにデータを書き込めるかもしれない。
・でも、そのデータをその場で命令として動かすのは難しい。
という状態になります。
これによって、守りが1つ増えるわけです。
簡単にたとえるなら、黒板にメモを書くことはできても、その黒板自体が急に舞台に変わって演劇が始まるわけではない、という感じです。つまり NX は、「ここは書く場所であって、動かす場所じゃない」というルールを守らせています。
このことからわかるのは、現代のバイナリでは、1つのバグが見つかっただけでは、そのまま思い通りには進まないということです。
NXがあることで、学習の次の段階では「では、実行不可ならどう考えるのか」という発想が必要になります。この先の、ret2libcや、ROPなども、こうした背景とつながっています。
ただし今は、そこまで進まなくて大丈夫です。
第2回としては、NXは「スタックのようなデータ置き場で命令を実行しにくくする守り」と押さえられれば十分です。
保護機構をやさしく理解するシリーズ。NXとは何か? なぜスタック上のコード実行が止められるのかのまとめ
今回学んだ大事なことは、NX が「実行してはいけないメモリ領域を実行不可にする」保護機構であり、特にスタックのようなデータ置き場で命令がそのまま動くのを防ぐ役割を持っているという点です。
スタックは本来、関数のローカル変数や一時的なデータを置く場所であり、命令を実行するための場所ではありません。NX は、その区別をはっきりさせることで、たとえ Buffer Overflow が起きてスタックに何かを書けたとしても、その場でその内容を実行しにくくします。つまり NX は、バグそのものを消すのではなく、「書けること」と「実行できること」を分けることで、問題がすぐに大きく広がらないようにする守りです。
大切なのは、スタックにデータがあることと、そのデータを命令として動かせることは別だ、という感覚を持つことです。