JPCERT コーディネーションセンター

EXP33-C. 初期化されていないメモリからの読み込みを行わない

ローカル自動変数は、初期化前に使用されると予期せぬ値をとる可能性がある。C 標準には「自動記憶域期間を持つオブジェクトが明示的に初期化されていない場合、その値は不定となる」と記載されている [ISO/IEC 9899:2011]。(附属書 J 「未定義の動作」の 11 も参照。)

一般に、プログラムスタックを利用する処理系では、この値はその時点でスタックメモリに格納されている値が初期値となる。未初期化のメモリには 0 が格納されていることが多いが、必ずしもそうとは限らない。未初期化のメモリには不定の値が含まれており、オブジェクトの型によってはトラップ表現になる可能性がある。unsigned char 以外の型の左辺値によって未初期化のメモリを読み取ると未定義の動作となり (C 標準の附属書 J 「未定義の動作」の 10 と「未定義の動作」の 12」を参照)、プログラムで予期しない動作が発生し、攻撃を招く可能性がある。

さらに、malloc() などの関数で割り当てられるメモリの内容は不定であるため、初期化する前に使用すべきではない。

多くの場合、コンパイラは初期化されていない変数の使用に対して警告を出す。このような警告は「MSC00-C. 高い警告レベルで警告を出さずにコンパイルする」で推奨されている方法で解消する必要がある。それ以外の場合、コンパイラにより未初期化の変数が完全に最適化される。

違反コード

以下のコード例では、set_flag() 関数を使い、number が正の場合は変数 sign1 を設定し、number が負の場合は -1 を設定しようとしている。しかし、プログラマは number0 の場合の処理を記述していない。number0 であれば、sign は初期化されないままとなる。sign が初期化されないため、プログラムスタックを利用するようなアーキテクチャではプログラムスタックのその位置にある任意の値が使用される。これは、予期せぬまたは誤ったプログラム動作を引き起こす可能性がある。

void set_flag(int number, int *sign_flag) {
  if (sign_flag == NULL)
    return;

  if (number > 0)
    *sign_flag = 1;
  else if (number < 0)
    *sign_flag = -1;
}

int is_negative(int number) {
  int sign;

  set_flag(number, &sign);

  return sign < 0;
}

コンパイラは、初期化されていない変数のアドレスが関数に渡されると、その変数は関数内で初期化されるとみなす。このようなコードでは、変数が初期化されていないことを警告できるコンパイラは多くないため、プログラマはプログラムを精査してコードの信頼性を確保しなければならない。

適合コード

この欠陥は、取り得るすべてのデータの状態を考慮できなかったために生じる。(「MSC01-C. 論理的な完全性を追求する」を参照。)ひとたび問題を特定できれば、number が 0 になる場合を考慮することによって、簡単に修正できる。

また、パフォーマンスの理由により禁じられる場合を除き、ローカル変数を宣言直後に初期化することも、追加の多層防御策として検討してみる価値はある。コンパイラや静的解析ツールは、多くの場合未初期化の変数の使用を検出するが、ソースコードにアクセスできないオブジェクトコード内で初期化や使用が行われていると、問題を診断するのは困難または不可能である。

void set_flag(int number, int *sign_flag) {
  if (sign_flag == NULL)
    return;

  if (number >= 0) { /* number が 0 の場合の処理 */
    *sign_flag = 1;
  }
  else {
    assert(number < 0);
    *sign_flag = -1;
  }
}

int is_negative(int number) {
  int sign = 0;   /* 多層防御策としての初期化 */

  set_flag(number, &sign);

  return sign < 0;
}
違反コード

以下の違反コードでは、report_error() 関数のローカル変数 error_logmsg 実引数に初期化し忘れている [Mercy 2006]。プログラムスタックを利用するアーキテクチャでは、error_log はスタック上に割り当てられるが、その値は初期化されていないため、すでに書き込まれていた値が使われることになる。error_log が割り当てられる位置は do_auth() 関数の password 変数が割り当てられていた位置にあるため、sprintf() 呼び出しは、null バイトに達するまで password 内のデータをコピーする。配列 password 内の文字列長が配列 buffer のサイズよりも大きければ、バッファオーバーフローが発生する。

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

int do_auth(void) {
  char *username;
  char *password;

  /* ユーザから username と password を得る。無効ならば -1 を返す */
}

void report_error(const char *msg) {
  const char *error_log;
  char buffer[24];

  sprintf(buffer, "Error: %s", error_log);
  printf("%s\n", buffer);
}

int main(void) {
  if (do_auth() == -1) {
    report_error("Unable to login");
  }
  return 0;
}
違反コード

以下の違反コードでは、配列要素 a[n..2n] は for ループ内でアクセスされるときに初期化される。

void g(double *a, size_t n) {
  a = (double *)realloc(a, (n * 2 + 1) * sizeof(double));
  if (a != NULL) {
    for (size_t i = 0; i != n * 2 + 1; ++i) {
      if (a[i] < 0) {
        a[i] = -a[i];  /* 違反 */
      }
    }
 
    /* ... */
    free(a);
  }
}
適合コード

以下のコード例では、配列要素 a[n..2n] は for ループ内でアクセスされるときに 0 に初期化される。

void g(double *a, size_t n) {
  a = (double *)calloc(a, (n * 2 + 1) * sizeof(double));
  if (a != NULL) {
    for (size_t i = 0; i != n * 2 + 1; ++i) {
      if (a[i] < 0) {
        a[i] = -a[i]; 
      }
    }
 
    /* ... */
    free(a);
  }
}
違反コード

以下のコード例では、error_log が適切に初期化されるように report_error() 関数を書き換えている。

void report_error(const char *msg) {
  const char *error_log = msg;
  char buffer[24];

  sprintf(buffer, "Error: %s", error_log);

  printf("%s\n", buffer);
}

このコードにはまだ問題がある。msg が参照する null 終端バイト文字列が null 終端を含んで17バイトより大きいとバッファオーバーフローが発生する。またこのコードでは「マジックナンバー」を利用しているが、これは避けるべきである(「DCL06-C. プログラムロジックでリテラル値を表現するには意味のあるシンボル定数を使う」を参照)。

適合コード

以下の適合コードでは、マジックナンバーは抽象化され、オーバーフローは解消されている。

enum {max_buffer = 24};

void report_error(const char *msg) {
  const char *error_log = msg;
  char buffer[max_buffer];

  snprintf(buffer, sizeof(buffer), "Error: %s", error_log);
  printf("%s\n", buffer);
}
適合コード

より簡単で、間違いにくく、パフォーマンスのよい適合コードは以下のとおり。

void report_error(const char *msg) {
  printf("Error: %s\n", msg);
}
違反コード (mbstate_t)

以下のコード例では、関数 mbrlen() には、適切に初期化されていない自動の mbstate_t オブジェクトのアドレスが渡されるため、未定義の動作が引き起こされる。C 標準の附属書 J 「未定義の動作」の 200 を参照すること。

void f(const char *mbs) {
  size_t len;
  mbstate_t state;

  len = mbrlen(mbs, strlen(mbs), &state);

  /* ... */
}
適合コード (mbstate_t)

mbstate_t オブジェクトは、マルチバイト変換関数に渡される前に初期化されているか、以前のマルチバイト変換関数の呼び出しによって最新のシフト状態に対応する値に設定されていなければならない。以下の適合コードでは、mbstate_t オブジェクトをすべて 0 に設定することで初期変換状態に設定している。

void f(const char *mbs) {
  size_t len;
  mbstate_t state;

  memset(&state, 0, sizeof state);
  len = mbrlen(mbs, strlen(mbs), &state);

  /* ... */
}
違反コード (Entropy)

以下のコード例では、プロセス ID、時刻、未初期化のメモリ junk を使用して乱数生成器にシードを与える。junk に格納された値は不定であるため、この動作は未初期化のメモリをエントロピー源として使用する Debian のディストリビューションの特性である。 ただし、不定の値にアクセスすると未定義の動作が発生するため、コンパイラにより未初期化の変数へのアクセスが完全に最適化され、時刻とプロセス ID のみが残され、望ましいエントロピーが失われる可能性がある。

struct timeval tv;
unsigned long junk;

gettimeofday(&tv, NULL);
srandom((getpid() << 16) ^ tv.tv_sec ^ tv.tv_usec ^ junk);

RSA 暗号化など、予測の不可能性に基づくセキュリティプロトコルでは、エントロピーの喪失によりシステムの安全性が低下することがある [Wang 2012]。

処理系固有の詳細

この違反コードでは、OS X 10.6 は junk の値を保持するが、OS X 10.7 と OS X 10.8 は保持しない。

適合コード (Entropy)

上記の違反コードを解決するには、乱数生成器用の信頼性の高いソースを使用する。このコード例では、リアルタイムクロックのほかに CPU クロックを使用して、乱数生成器にシードを与える。

#include <time.h>
     
double cpu_time;
struct timeval tv;
unsigned long junk;

cpu_time = ((double) clock()) / CLOCKS_PER_SEC;
gettimeofday(&tv, NULL);
srandom((getpid() << 16) ^ tv.tv_sec ^ tv.tv_usec ^ junk);
リスク評価

未初期化の変数にアクセスすると未定義の動作が発生し、プログラムが予期せぬ動作を引き起こすことがある。場合によっては、このようなセキュリティの欠陥により任意のコードが実行される可能性がある。

エントロピーの作成のために未初期化の変数を使用すると、コンパイラの最適化によってこのメモリアクセスが削除される可能性があるため、問題がある。VU#925211 には、このコーディングエラーによって引き起こされる脆弱性の例が示されている。

ルール

深刻度

可能性

修正コスト

優先度

レベル

EXP33-C

P12

L1

自動検出
ツール バージョン チェッカー 説明
Compass/ROSE    

このルールへの単純な違反を自動的に検出できるが、誤検知となる場合もある。未初期化変数を引数にとる関数内における初期化など、より複雑な場合は検出できない可能性がある。しかし Compass/ROSE は上記 2 つ目の違反コード例は検出でき、1 つ目の違反コード例も検出できるように拡張することは可能である。

Coverity 6.5 UNINIT 実装済み

Coverity Prevent

5.0

NO_EFFECT

未初期化変数の使用を検出できるが、struct の未初期化メンバは検出できない。Coverity Prevent では、このルールへの違反をすべて検出できるわけではないので、さらなる検証が必要である。
Fortify SCA    

このルールへの違反を検出できるが、初期化が他の関数で行われている場合は誤検知となる。

GCC 4.3.5  

-Wuninitialized フラグを使用することで、このルールへの違反を検出できる場合もある。

Klocwork

9.1

UNINIT.HEAP.MIGHT
UNINIT.HEAP.MUST
UNINIT.STACK.ARRAY.MIGHT
UNINIT.STACK.ARRAY.MUST UNINIT.STACK.ARRAY.PARTIAL.MUST
UNINIT.STACK.MUST

 

LDRA tool suite

V. 8.5.4

57 D
69 D

実装済み

PRQA QA-C 8.1 2961 (D)
2962 (A)
2963 (S)
2971 (D)
2972 (A)
実装済み
Splint 3.1.1    
関連する脆弱性

CVE-2009-1888 はこのレコメンデーションへの違反の結果である。SAMBA (3.3.5 まで) の一部のバージョンでは、初期化されていない可能性のある 2 つの変数 (アクセス権など) をとる関数を呼び出す。攻撃者はアクセス制御リストを迂回し、保護されたファイルにアクセスするためにこれを悪用する [xorl 2009]。

関連するガイドライン
CERT C++ Secure Coding Standard EXP33-CPP. Do not reference uninitialized memory
ISO/IEC TR 24772:2013 Initialization of Variables [LAV]
ISO/IEC TS 17961 (ドラフト) Referencing uninitialized memory [uninitref]
参考資料
[Flake 2006]  
[ISO/IEC 9899:2011] Section 6.7.9, "Initialization"
[Mercy 2006]  
[Wang 2012] "More Randomness or Less"
[xorl 2009] "CVE-2009-1888: SAMBA ACLs Uninitialized Memory Read"
翻訳元

これは以下のページを翻訳したものです。

EXP33-C. Do not reference uninitialized memory (revision 152)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter