C言語入門|メモリ範囲外アクセスの恐怖

ここまでで、配列・ポインタ・添え字の正体が見えてきましたね。
「[] はただのメモリアクセス」「ポインタ演算で自由に番地を動かせる」。

……と聞くと、ちょっとワクワクしませんか?

でもその自由さには、とんでもなく重い代償がついてきます。
それが メモリ範囲外アクセス、いわゆる オーバーラン です。

C言語は、
危ないことができる → でも止めてはくれない
という、プロ向けの性格をしています。

添え字はただのアドレス計算だった

すでに学んだとおり、添え字は安全装置ではありません。

array[i]

は実際には、

*(array + i)

として評価されます。

つまり、

  • i が大きすぎても
  • i がマイナスでも

C言語は何も文句を言いません

オーバーランとは何か

オーバーランの定義

本来割り当てられていないメモリ領域に
読み書きしてしまうこと

これが起きると、

  • 他の変数を書き壊す。
  • 実行制御情報を書き壊す。
  • セキュリティ情報を漏らす。

といった、非常に危険な事態につながります。

メモリ配置を具体的に見てみよう

次のような変数が、たまたま連続して配置されていると仮定します。

int age;
int items[5];
char secret[16];

前提

  • 1セル=1バイト
  • int型=4バイト

メモリ配置イメージ(例)

危険なコード例(オーバーラン)

プロジェクト名:10-6-1 ソースファイル名: sample10-6-1.c

#include <stdio.h>

int main(void)
{
    int age = 25;
    int items[5] = {1, 2, 3, 4, 5};
    char secret[32] = "HELLO";

    items[-1] = 999;
    items[6] = 888;

    printf("age=%d\n", age);
    printf("secret=%s\n", secret);

    return 0;
}

次のコードを見てください。

実行結果(例)

age=999
secret=�ELLO

※ 環境によって結果は変わります
※ 正常に見えることもあります(それが一番怖い)

何が起きているのか

items[-1] の正体

実際の意味
items[-1]*(items - 1)

これは、

  • items の先頭アドレスから
  • int 1個分(4バイト)

つまり age の領域を書き換えています。

items[6] の正体

実際の意味
items[6]*(items + 6)

これは、

  • 配列の範囲を超えて
  • secret の一部を書き換える

という、非常に危険な操作です。

なぜエラーにならないのか

ここがC言語の最大の特徴です。

  • 添え字の範囲チェックをしない。
  • 実行時にも止めない。
  • コンパイル時にも警告しないことが多い。

つまり、

壊れているのに動いてしまう

という、最悪の状態が普通に起こります。

オーバーランが引き起こす本当の恐怖

オーバーランは、単なるバグでは終わりません。

  • プログラムの暴走
  • 強制終了
  • OSの重要情報の破壊
  • パスワードや秘密情報の漏えい

実際、過去の多くの脆弱性は
バッファオーバーラン が原因でした。

C言語プログラマが守るべき鉄則

オーバーランを常に警戒する

  • 配列サイズを必ず意識する。
  • 添え字の上限・下限を自分で管理する。
  • 「たまたま動いた」を信用しない。

C言語は、

自由を与える代わりに、責任をすべて押し付ける言語

です。

ハッカーという言葉について少しだけ

ちなみに、

  • ハッカー = 技術を極めた人
  • クラッカー = 悪意ある攻撃者

が本来の意味です。

C言語を深く理解している人は、
善にも悪にもなれる力を持ちます。

だからこそ、
正しく、慎重に、使いこなす必要があるのです。