【入門編】 整数オーバーフロー/アンダーフローに起因するメモリ破壊 – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

こんにちは!セキュリティの世界へようこそ。
日々の開発やインフラの管理、本当にお疲れ様です。新人のIT担当者さんや、「セキュリティってなんだか難しそうだな…」と感じている一般開発者の方に向けて、今日から使える実践的な知識を分かりやすくお届けしていきますね。

さて、私たちが普段使っているプログラムの中には、目に見えない「落とし穴」が隠されていることがあります。今回は、その中でも特に厄介で、攻撃者に狙われやすい「整数オーバーフロー/アンダーフローに起因するメモリ破壊」というテーマについて、身近な例えを交えながら一歩ずつ紐解いていきましょう!

—

1. 家の鍵で例える「整数オーバーフロー」の仕組み

まずは、この脆弱性がどんなものなのか、身近な「もの」に例えて考えてみましょう。

想像してみてください。あなたは、大切な荷物をしまうために、ものすごく大きな「箱」を作ろうとしています。この箱のサイズ(大きさ)を決めるために、プログラムは「アイテムの数」と「1つのアイテムの大きさ」を掛け算しますよね。

ここで、泥棒(攻撃者)がどうやってこの仕組みを破るかというと、「あまりにも巨大な数字を計算させて、計算機のメーターを一周させてしまう」んです。

車のオドメーター(走行距離計)をイメージしてください。99,999km走った車がさらに進むと、メーターは「00,000km」に戻ってしまいますよね。これと同じことが、プログラムの中の計算でも起きるんです。

1. 攻撃者は、プログラムが「メモリの領域を確保するサイズ」を計算するときに、わざと巨大な数値を入力します。
2. その結果、計算結果が型(入れ物)の限界を超えてあふれ出し(オーバーフロー)、「本当は10GB必要なのに、計算結果がなぜか『10バイト』になってしまった!」という現象が起きます。
3. プログラムは「なんだ、10バイト分の小さな箱でいいんだな」と勘違いして小さなメモリ領域を用意します。
4. しかし、実際にはその中に何GBもの膨大なデータが押し込まれようとするため、箱が盛大にパンクし、周囲のメモリを破壊してしまうのです。

これが、整数オーバーフローが引き起こすメモリ破壊の正体です。

—

2. 実際のコードで見る「計算ミスの瞬間」

では、もう少し具体的に、C言語のような低レイヤーのプログラミング言語を例に、なぜこれが起きるのかを見てみましょう。
(※もちろん、お仕事のコードではこんな危うい書き方は絶対に避けますよ!)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ユーザーから渡された個数とデータを使ってメモリを確保し、コピーする関数
void allocate_and_copy(int user_count, char *user_data) {
    // 【危険な計算】
    // 構造体のサイズと、ユーザーが指定した個数を掛け算しています。
    // もし user_count に極端に大きな値が入ると、計算結果がオーバーフローします。
    int total_size = user_count * 512; 

    printf("計算された確保サイズ: %d バイト\n", total_size);

    // オーバーフローによって total_size が「小さく」なってしまった場合、
    // 実際に確保されるメモリも小さくなります。
    char *buffer = (char *)malloc(total_size);

    if (buffer == NULL) {
        perror("メモリの確保に失敗しました");
        return;
    }

    // ここで本来の大きなデータをコピーしようとすると、
    // 小さな buffer に入りきらずに「ヒープオーバーフロー」が発生します!
    // 攻撃者は、このメモリ破壊を利用してプログラムの乗っ取りを狙います。
    memcpy(buffer, user_data, user_count * 512);

    // 処理が終わったらメモリを解放
    free(buffer);
}

int main() {
    // テスト用の呼び出し(実際にはここに不正な入力が入ります)
    printf("安全な開発への一歩を踏み出しましょう!\n");
    return 0;
}

このコードの恐ろしいところは、user_count * 512 という一見何気ない掛け算にあります。もし user_count に「通常の何万倍も大きな数値」が渡されると、C言語の整数型(32ビット整数など)の限界を超え、計算結果がプラスからマイナスに変わったり、意図しない小さな数字に化けてしまうのです。

—

3. どうやって防ぐの?現場で使える防御の知恵

「うわぁ、計算結果が勝手に変わっちゃうなんて怖すぎるよ…」と思いましたか?大丈夫です。しっかりとした対策を知っていれば、こうした脅威は確実にブロックできます。

ここからは、実務の現場で私たちが取り組むべき具体的な防衛策を見ていきましょう。

対策①:計算する前に「オーバーフローしないか」をチェックする

一番確実なのは、掛け算や足し算を行う前に、「この数字をかけたら、入れ物(型)の限界を超えちゃわないかな?」と事前にチェックすることです。

#include <limits.h> // 整数型の最大値などを取得するためのヘッダー

int safe_allocate_and_copy(int user_count, char *user_data) {
    int item_size = 512;

    // 【安全なチェック】
    // もし user_count が、(整数の最大値 / 1アイテムのサイズ) を超えていたら、
    // 次の掛け算で確実にオーバーフローすると事前に分かります。
    if (user_count > 0 && item_size > INT_MAX / user_count) {
        // エラー処理:計算が大きすぎるので処理を中断する
        return -1; 
    }

    int total_size = user_count * item_size;
    // ここで安全にメモリを確保する処理へ進む...
    return 0;
}

このように、「大きな値が来たらあらかじめ弾く」というガードを入れるだけで、泥棒の侵入経路を完全に断つことができます。

対策②:安全なコンパイラオプションやセーフライブラリを活用する

私たちが手動ですべての計算をチェックし続けるのは、どうしてもヒューマンエラーの元になってしまいます。そのため、開発インフラやビルドツール(コンパイラ)の機能にも頼りましょう!

  • セーフな算術ライブラリの利用:

計算時にオーバーフローを自動検知して安全に停止してくれるライブラリ(例: SafeInt など)を積極的に採用しましょう。

  • コンパイラの保護機能(スタックカナリアやASLR等):

万が一メモリ破壊の脆弱性が生まれてしまっても、プログラムが不正な書き換えを検知して即座にクラッシュ(強制終了)させる仕組みを、コンパイル時の設定で有効にしておきます。

—

4. まとめ:一歩ずつ、安全なコードを書く楽しさを知ろう

今回は、整数オーバーフローに起因するメモリ破壊という、一見すると難解なセキュリティのトピックについてお話ししました。

1. 攻撃のメカニズム: 巨大な数値を入力して計算結果を一周(オーバーフロー)させ、確保されるメモリサイズを誤認させる。
2. 影響: 確保された小さな箱に大きなデータを押し込むことでメモリが破壊され、システム乗っ取りなどの危険につながる。
3. 対策: 計算前に限界値を超えないかチェックする、安全なライブラリやコンパイラの設定を活用する。

セキュリティの対策は、一度にすべてを完璧にやろうとすると息切れしてしまいます。「あ、今回は掛け算をする前に大きな値が来ないかチェックしておこう」といった小さな意識の積み重ねが、あなたやあなたのチームが作るシステムを強固な要塞に変えてくれます。

日々の開発の中で、少しずつ「安全な書き方」を取り入れていきましょう。応援しています!

コメント

タイトルとURLをコピーしました