こんにちは!日々の開発やインフラの保守、本当にお疲れ様です。新人のIT担当者や、これからセキュリティの勉強を始めようとしている開発者の方にとって、「メモリ管理」や「ヒープオーバーフロー」という言葉は、なんだか呪文のように難しく聞こえますよね。
でも、安心してください!一歩ずつ、身近な例えから紐解いていけば、決して越えられない壁ではありません。今回は、プログラムの足元をすくう「ヒープオーバーフロー」の仕組みと、それを防ぐモダンな仕組みについて、一緒に優しく学んでいきましょう!
—
1. 家の鍵に例える「メモリ管理」とヒープの仕組み
プログラムが動くとき、コンピュータのメモリ(作業机のようなもの)には、大きく分けていくつかの領域が用意されます。その中で、プログラムが動的に(必要になったその時に)使う領域を「ヒープ(Heap)」と呼びます。
例えば、あなたがダンボール箱(データ)を次から次へとリビング(ヒープ領域)に組み立てていく場面を想像してください。
- ダンボール箱を置くとき、C言語などの世界では
malloc()という関数を使って「これくらいの大きさの箱をください!」とOSにお願いします。 - このとき、OSやメモリ管理システム(Linuxの標準では
glibc mallocと呼ばれます)は、箱と箱の隙間に、「この箱のサイズはいくつで、次の箱はどこにあるか」というメモ書き(メタデータ)をこっそり貼り付けて管理しています。
この「管理用のメモ書き」が、今回の物語の主役です。
—
2. 泥棒のテクニック:ヒープオーバーフローとメタデータ改ざん
もし、プログラムを作る人がうっかりミスをして、「10個までしか入らないダンボール箱に、無理やり20個分の荷物を詰め込んでしまった(バッファオーバーフロー)」としたらどうなるでしょうか?
箱がパンクして、あふれ出た荷物は、隣にある「管理用のメモ書き(メタデータ)」をグシャッと踏みつぶしてしまいます。
これが、攻撃者が狙う「ヒープオーバーフロー」の瞬間です。
メタデータを書き換えて何をするの?
攻撃者は、このメモ書きを自分の都合の良いように書き換えます。例えば、「次の箱の住所は、あの重要な金庫(プログラムの重要な処理をする場所)の住所です」と嘘のメモを書くわけです。
そうすると、プログラムが次に箱を探そうとしたとき、攻撃者が指し示した「金庫の住所」を本当の住所だと勘違いしてアクセスしてしまいます。これが、「任意アドレス書き込み(任意の場所のデータを書き換える攻撃)」の正体です。代表的な手法として、古いLinux環境では unlink マクロの仕組みを悪用したり、fastbin dup と呼ばれるテクニックが使われたりします。
言葉だけだと難しいので、イメージしやすいように、脆弱性のあるC言語のコードを見てみましょう。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[]) {
// 64バイトのメモリスースを2つ、ヒープ上に確保します
char *buffer1 = (char *)malloc(64);
char *buffer2 = (char *)malloc(64);
if (argc > 1) {
// 【脆弱性】確保したサイズ(64バイト)を超えてコピーしてしまいます!
// これがヒープオーバーフローを引き起こし、buffer2の管理メタデータを破壊します
strcpy(buffer1, argv[1]);
}
// 後片付け
free(buffer1);
free(buffer2);
return 0;
}
このコードでは、buffer1 に対してコマンドライン引数から長すぎる文字列を流し込むと、隣にある buffer2 の管理領域(メタデータ)を盛大に破壊してしまいます。これが、恐ろしいメモリ破壊の第一歩です。
—
3. なぜ現代のシステムは安全なのか?(防御機構の進化)
「えっ、そんな簡単にプログラムって乗っ取られちゃうの!?」と不安になりましたか?
大丈夫です。現代のOSやコンパイラ(プログラムを機械語に翻訳するツール)は、この泥棒の手口を熟知しており、強力なガードマンを何重にも配置しています。
① 現代の glibc による安全チェック
今の Linux で使われている glibc malloc は、とても賢くなっています。
メモリを解放する free() 関数が呼ばれたとき、「あれ? このメタデータ、誰かに書き換えられてないか?」という整合性チェック(Sanity Check)を厳しく行うようになっています。
もし、メモ書きが不自然に書き換えられていると、プログラムは「危ない!」と気づき、その場で強制終了(アボート)して攻撃を防ぎます。
② その他の強力な守り(ASLRとNXビット)
さらに、システム全体でも以下のような防衛網が敷かれています。
- ASLR(Address Space Layout Randomization):
プログラムが起動するたびに、メモリの住所(アドレス)をランダムに変えてしまう仕組みです。これにより、攻撃者が「ここを書き換えればOK」と狙いを定めるのが極めて難しくなります。
- NXビット(No-Execute bit):
「データを入れる場所(ヒープなど)」では、プログラム(コード)を実行できないようにするハードウェアレベルの防護壁です。仮にメモリを書き換えられても、そこで悪意あるコードを動かすことはできません。
—
4. 現場の開発者が意識すべき対策とまとめ
私たちアプリケーション開発者やインフラエンジニアは、この仕組みを踏まえて日々の開発で何を心がければよいのでしょうか? 一歩ずつ、確実な対策を学んでいきましょう!
1. 安全な関数を使う
先ほど登場した危険な strcpy() や strcat() はなるべく使わず、サイズを指定して安全にコピーできる strncpy() や、安全な文字列操作ライブラリを積極的に選びましょう。
2. 入力を徹底的に検証する(バリデーション)
外部から受け取るデータが「想定内の長さ・形式か」を、処理の手前で必ずチェックする癖をつけましょう。
3. コンパイラの保護オプションを活用する
C/C++などでビルドする際は、スタック保護(-fstack-protector)や、モダンなコンパイラが提供するメモリ安全機能、警告オプションをしっかり有効にしましょう。
メモリの仕組みやヒープオーバーフローは、最初はとっつきにくく感じるかもしれませんが、「データの箱と、その隣にあるメモ書きの関係」という基本イメージさえ掴めば、怖くありません。
安全で堅牢なシステムを作る第一歩として、今日の知識をぜひ今後の開発やセキュリティ学習に活かしてみてくださいね。それでは、また次回の記事でお会いしましょう!
コメント