黒ネコと学ぶ、保護機構をやさしく理解する
前回は、NXによって「データを書く場所」と「命令を実行する場所」が分けられ、スタック上でそのまま命令を動かしにくくなることを学びました。
くろちゃん
すると次にこんな疑問が出てきます。
・スタック上の大事な戻り情報はどう守られているの?
・バッファがあふれて、return addressに届く前に、何か止める仕組みはあるの?
・プログラムが途中で急に止まるのは、何か見張りがいるから?
この疑問に深く関わるのが、今回の「Stack Canary」です。
Canary は名前だけ見ると少し不思議ですが、役割はとてもわかりやすいです。
白猫先生
簡単にたとえるなら、「return address の手前に置かれている見張り役」です。
もしバッファオーバーフローで大事な場所まで書き込みが進めば、その手前にいるこの見張り役も壊れる可能性があります。
すると、関数が終わるときに「見張りが壊れている、おかしい」と気づいて、プログラムが止まります。
今回は、
・Canary とは何か。
・なぜ必要なのか。
・スタックのどこに置かれるのか。
・どうやって異常を見つけるのか。
を、やさしく順番に整理していきます。
※イメージです。
Stack Canaryとは何か
Stack Canaryとは、簡単に言えば、スタック上の大事な戻り情報の前に置かれる、見張り用の値です。
Canary という名前は、昔、危険を早く察知するために使われたカナリアのたとえから来ています。
細かい歴史は今は覚えなくて大丈夫ですが、
イメージとしては、「何か異常が近づいたら、先にそれがやられることで危険を知らせる役」です。
スタックの中では、関数のローカルバッファの近くに
・saved RBP
・return address
のような大事な情報があります。
もしバッファがあふれてそこまで届いてしまうと、プログラムの流れに大きな影響が出る可能性があります。
そこで、その手前に「Canary」を置いておきます。
こうしておけば、バッファのあふれが進んできたとき、まずCanaryが壊れやすくなります。
そして関数が終わるときに、「最初に置いたCanaryの値と、今のCanaryの値が同じか?」を確認します。
もし同じなら、少なくともそこまでは無事だった可能性が高いです。
もし違っていれば、「誰かがここまで踏み込んできた」簡単にたとえるなら、と判断して止まります。
簡単にたとえるなら、Canaryは、「大事な金庫の前に置かれた見張り札」のようなものです。
なぜCanaryが必要なのか
では、なぜこんな見張りが必要なのでしょうか。
理由はとてもシンプルで、「バッファオーバーフローは、ローカル変数だけでなく、その先の大事な情報まで壊す可能性があるから」です。
たとえば関数の中に
char buf[16];
のようなローカルバッファがあるとします。
ここに長すぎる入力を書き込むと、"buf"の外までデータが進んでしまうことがあります。
その先には、スタックの流れを保つための情報や、戻り先に関わる情報があります。
特に、return addressが壊れると、関数が終わったあとに本来の場所へ戻れなくなるかもしれません。
ここで大切なのは、「return address が壊れてから気づくのでは遅い」ということです。
だからこそ、その前に見張りを置いておき、「そこまで来たら異常だ」と早めに気づく必要があります。
白猫先生
簡単にたとえるなら、Canaryは、教室の金庫の前に置かれた封印シールのようなものです。
もし誰かが金庫に触ろうとして前を通れば、先にそのシールが破れます。
先生はシールを見て、「おかしい、ここまで誰か来ている」と気づけます。
Canaryもそれと似ていて、大事な場所の手前で異常を知らせるための仕組みなのです。
Canaryはスタックのどこにいるのか
ここが今回のとても大事な部分です。
Canaryは、学習用の単純なイメージでは、スタック上でだいたい次のような位置関係で考えるとわかりやすいです。
・ローカルバッファ"buf"
・Canary
・saved RBP
・return address
つまり、ローカルバッファと大事な戻り情報の間にいるわけです。
これが重要です。
もし"buf"からあふれたデータが上へ進んでいけば、まずCanaryにぶつかります。
そのため、return addressに届くような大きなあふれがあれば、その前にCanaryが壊れている可能性が高いのです。
もちろん実際の並び方は環境や最適化、コンパイラの設定によって細かく変わることがあります。
ですが、初心者向けの理解としては、「Canary は return address の手前に置かれた見張り役」と考えるのがとても大切です。
白猫先生
簡単にたとえるなら、机の上に「自分のメモ帳」「見張り札」「戻る場所のメモ」の順で置かれているようなものです。
インクがメモ帳からあふれると、まず見張り札が汚れます。その時点で「この先の大事なメモも危ない」と気づけるのです。
サンプルコードでやさしく考える
ここで、学習用のシンプルなコードを見てみます。
-----
#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;
}
-----
このコードには"char buf[16];"というローカルバッファがあります。
そして"strcpy(buf, input);"によって、長さ確認なしで入力がコピーされます。
Buffer Overflowの視点では、長すぎる入力で"buf"の外へ書き込みが進む可能性があります。
では、Canaryがあると、何が違うのでしょうか。
ポイントは、"buf" のすぐ先に見張り役が置かれている、ということです。
そのため、長い入力で"buf"の外まで書き込めば、return addressに届く前に、Canaryが壊れているかもしれません。
すると関数の終わりで確認が行われたときに、「Canary の値が変わっている。異常だ」と判断され、プログラムが止められます。
このコードから学ぶべきことは、「バッファの外まで書ける可能性があること」と、「それでも守りがあると、その先へ簡単には進めないこと」の両方です。
つまり、CanaryはBuffer Overflowそのものをなくしているわけではありません。
あふれが起きる可能性はあっても、「大事な戻り情報が静かに書き換わる前に異常を見つけようとする」のです。
Canaryがあると何が変わるのか
初心者の段階では、Canaryの効果を「return address の前に置かれた見張りが、壊れたら止める」と理解すれば十分です。
これによって何が変わるのでしょうか。
Canary がない世界では、ローカルバッファのあふれが、そのままsaved RBPや、return addressに届いてしまうかもしれません。
しかし、Canaryがあると、その途中に見張りがいるため、
・大きなあふれが起きた。
・見張りが壊れた。
・関数の終わりで見つかった。
・その場で止められた。
という流れになりやすくなります。
つまり Canary は、「無事なふりをして関数が戻ってしまうこと」を防ぎやすくする仕組みです。
白猫先生
簡単にたとえるなら、これは「テスト用紙の前に貼られた封印シール」のようなものです。
もし誰かがその先の大事な部分に触ろうとすれば、まずシールが破れます。先生は提出前にそのシールを見て、「これはおかしい」と気づけます。
Canary も同じで、戻り先の情報が壊される前に、異常を見つけるための目印なのです。
保護機構をやさしく理解するシリーズ。Stack Canaryとは何か? return address の前にいる見張り役のまとめ
今回学んだ大事なことは、Stack Canaryがスタック上の大事な戻り情報の手前に置かれる見張り役であり、バッファオーバーフローがその近くまで届いたかどうかを見つけるための保護機構だという点です。
Canaryはローカルバッファとsaved RBP・return addressの間に置かれるイメージで理解するとわかりやすく、長すぎる入力でバッファの外まで書き込みが進めば、まずこの見張り役が壊れる可能性があります。そして関数の終わりでCanaryの値を確認し、変わっていれば異常としてプログラムを止めます。
つまり、Canaryは、バッファオーバーフローそのものを消す仕組みではなく、大事な戻り情報が静かに壊される前に「ここまで来ている」と気づくための防波堤です。大切なのは、Canaryを「return address の前にいる見張り役」としてイメージできるようになることです。