こんにちは!セキュリティの世界へようこそ。
初めてプログラミングやインフラに関わるようになると、「バッファオーバーフロー」や「EIP/RIPの制御」といった、なんだか映画のハッカーが使いそうな難しそうな言葉を耳にしてドキッとしてしまいますよね。
でも、大丈夫です!一歩ずつ、身近な例えから紐解いていけば、誰でもそのメカニズムと対策をしっかり理解できるようになります。今回は、サイバーセキュリティの基礎中の基礎である「スタックバッファオーバーフロー」について、一緒に優しく学んでいきましょう!
—
1. 「スタックバッファオーバーフロー」ってなに?(おうちの鍵の例え)
まずは、プログラムがメモリ(記憶領域)をどのように使っているのか、イメージしてみましょう。
プログラムの中には、データを一時的に保管しておく「箱」のような場所がたくさん用意されています。これを「バッファ(あるいはスタック)」と呼びます。例えば、ユーザーが入力した名前やパスワードを一時的に入れておく箱だと想像してください。
ここで、こんな状況を考えてみてください。
- 箱の大きさ(許容量): 5文字分しか入らない小さな引き出し
- 実際に起きたこと: 10文字の長い文字を無理やりその引き出しに押し込もうとした!
当然、引き出しからは文字があふれてしまいますよね。これが「バッファオーバーフロー(あふれ出し)」です。
泥棒がやってくる!
現実の世界なら、引き出しが閉まらなくなって「あーあ」で済むかもしれませんが、コンピュータの世界では大問題になります。
この「一時的にデータをしまう箱」のすぐ隣には、プログラムにとって非常に重要な「次に進むべき道順(おうちの鍵や、次に開けるべき部屋の番号)」が書かれたメモ用紙が置いてあります。
もし、あふれ出たデータがこのメモ用紙まで侵食してしまったらどうなるでしょうか?
悪意のある攻撃者は、このメモ用紙の数字を自分の都合のいいように書き換えてしまいます。「次に進むべき場所」を、本来の安全なルートではなく、「攻撃者が用意した怪しいプログラムの場所」に書き換えてしまうのです。これが、プログラムの制御を乗っ取る「EIP(またはRIP)の制御」の正体です。
—
2. メモリの中の仕組みを覗いてみよう
もう少しだけ技術的なお話をしますね。「EIP」や「RIP」というのは、CPUが持っている「今、何番目の命令を実行しているか」を指し示すポインタ(目印)の名前です。
- 32bit環境の場合: この目印を
EIPと呼びます。 - 64bit環境の場合: この目印を
RIPと呼びます。
プログラムが関数(処理のまとまり)を呼び出すとき、スタックと呼ばれるメモリ領域には、「処理が終わったあと、どこに戻るべきか(リターンアドレス)」というアドレスが必ず記録されます。
下のC言語のサンプルコードを見てみてください。これは、あえて脆弱性を持たせた危険なプログラムの例です。
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *user_input) {
// 64バイト分の大きさの箱(バッファ)を用意しました
char buffer[64];
// 【危険!】入力されたデータの長さを確認せずに、そのまま箱にコピーしています
// もしuser_inputが64バイトより大きければ、バッファオーバーフローが発生します!
strcpy(buffer, user_input);
}
int main(int argc, char *argv[]) {
if (argc > 1) {
vulnerable_function(argv[1]);
}
printf("プログラムが正常に終了しました。\n");
return 0;
}
このコードの strcpy 関数は、コピー元のデータがいくら長くても、お構いなしに buffer へ詰め込もうとします。もしここで64バイトを超える入力を渡すと、隣接しているメモリ領域が破壊され、最終的に関数が終了して「元の場所に戻ろう」とした瞬間に、CPUの EIP(または RIP)が書き換わった不正なアドレスを読み込んでしまうのです。
—
3. どうやって防ぐの?現代のセキュリティ対策
「こんな怖い仕組みがあるなんて、プログラムを作るのは怖くなっちゃった……」と思いましたか?安心してください。現代のOSや開発ツールには、こうした攻撃をガッチリとガードしてくれる強力な仕組み(防御ヘッダーやコンパイラオプション)が標準で備わっています。
身近な防犯で言えば、二重ロックや防犯カメラ、頑丈なドアのようなものです。代表的な3つの対策を知っておきましょう。
① スタックカナリア(Stack Canary)
炭鉱のカナリアという話を聞いたことはありませんか?昔の炭鉱夫は、ガス漏れをいち早く察知するために小さな鳥を連れて洞窟に入りました。
これと同じで、プログラムの「大事なメモ用紙」の直前に、秘密の目印(カナリア値)を挟んでおきます。もし攻撃者がデータをあふれさせようとすると、この目印が真っ先に壊れます。プログラムは「あれ、目印が壊れている!誰かが不正に侵入しようとしたぞ!」と気づいて、即座にプログラムを強制終了させるのです。
② NX/DEP(No-Execute / Data Execution Prevention)
「データが置いてある場所では、プログラムを実行できないようにする」という強力なルールです。
たとえ攻撃者がデータを無理やりメモリに送り込んでも、「ここはデータ置き場だから、プログラムとして動かすのは禁止!」とCPUが拒否してくれます。これにより、送り込んだ悪意あるコードを実行させなくします。
③ ASLR(Address Space Layout Randomization)
プログラムが起動するたびに、メモリの配置(アドレス)をランダムに変えてしまう仕組みです。
攻撃者は「この場所に行けば悪意あるコードがある」と狙いを定めて攻撃を仕掛けますが、起動するたびにその場所がガラリと変わってしまうため、どこを指せばいいか分からなくなってしまいます。まるで、泥棒が入るたびに家中の部屋の配置がランダムに入れ替わるようなものです。
—
4. 開発現場で私たちにできること
私たちエンジニアが実務で安全なシステムを作るためには、こうしたOSの機能に頼るだけでなく、コードレベルでの安全性を意識することが何よりも大切です。
例えば、先ほどの危ない strcpy 関数の代わりに、サイズを指定して安全にコピーを行える関数(C言語なら snprintf など)を使用することが基本中の基本になります。
#include <stdio.h>
#include <string.h>
void safe_function(char *user_input) {
// 64バイト分の箱を用意
char buffer[64];
// 【安全】コピーする最大サイズを明確に指定し、あふれを防ぎます
// sizeof(buffer) - 1 とすることで、文字の終端(\0)のスペースも確実に確保します
snprintf(buffer, sizeof(buffer), "%s", user_input);
printf("安全にデータを処理しました: %s\n", buffer);
}
このように、小さな意識の積み重ねが、大きなインシデントを防ぐ防壁となります。
—
まとめ
- バッファオーバーフローとは、箱(バッファ)の許容量を超えるデータを無理やり詰め込んで、隣のメモリ領域を破壊してしまう現象です。
- EIP/RIPの制御は、その破壊によって「次に進むべき道順」を攻撃者に書き換えられ、プログラムの制御を乗っ取られてしまう仕組みのことです。
- 対策として、スタックカナリアやNX/DEP、ASLRといった防御機能がOSに備わっており、さらに開発者側も安全な関数を選ぶことで、強固なセキュリティを築くことができます。
難しそうに見えたセキュリティの仕組みも、基本の原理原則を知れば怖くありません。ぜひ日々の開発やインフラ構築の中で、「ここにデータを入れすぎたらどうなるかな?」という視点を大切にしてみてくださいね。一歩ずつ、安全で頼られるエンジニアを目指して一緒に頑張っていきましょう!
コメント