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

ARR30-C. 境界外を指すポインタや配列添字を生成したり使用したりしない

ARR30-C. 境界外を指すポインタや配列添字を生成したり使用したりしない

悪用されうる未定義動作は、次に挙げる処理が原因で生まれることがある。

C 言語規格は、無効なポインタ操作が原因で未定義の動作(UB)が生じうる状況を次の通り挙げている。

UB

説明

サンプルコード

46

配列オブジェクトおよび整数型内またはこれらをちょうど超えた位置を指すポインタの加算または減算が、同じ配列オブジェクト内またはそれをちょうど超えた位置を指さないという結果を生み出す。

境界外ポインタの生成

47

配列オブジェクトおよび整数型内またはこれらをちょうど超えた位置を指すポインタの加算または減算の結果が、その配列オブジェクトをちょうど超えた位置を指し、評価対象の単項 * 演算子のオペランドとして使用される。

終端外ポインタの参照、終端外インデックスの使用

49

与えられた配列添字でオブジェクトに明らかにアクセスできるのに、配列添字が範囲を外れている(たとえば、int a[4][5] と宣言された場合の左辺値式 a[1][7])。

明らかにアクセスされる境界外インデックス

62

参照対象のオブジェクトが該当配列の要素を提供しない場合に、構造体のフレキシブル配列メンバへのアクセスまたはそのメンバをちょうど超えた位置をさすポインタの生成が試みられる。

フレキシブル配列メンバの境界外ポインタ

109

ライブラリ関数配列パラメータに渡されるポインタが、すべてのアドレス計算およびオブジェクトアクセスを有効にするような値をもたない。

ライブラリ関数による無効なアクセス

違反コード (境界外ポインタの生成)

以下の違反コード例では、関数 f()indexの値を検証してから整数配列 table のオフセットとして使用しようとしている。しかし、関数は負の index 値を受け取ってしまう。index がゼロより小さいと、関数の return 文の中の加算式の動作は、未定義動作 46 になる。一部の処理系では、加算単独でハードウェアトラップが発生することがある。また、別の処理系では、加算が、参照されるとハードウェアトラップを発生させるような値を生成する。さらに、table とは別のオブジェクトを指す参照可能ポインタを生成する処理系もある。このようなポインタを使ってオブジェクトにアクセスすると、情報漏洩が起きたり、間違ったオブジェクトを変更してしまったりする可能性がある。

enum { TABLESIZE = 100 };

static int table[TABLESIZE];

int* f(int index) {
  if (index < TABLESIZE)
    return table + index;

  return NULL;
}
適合コード

以下の修正方法では、ポインタ演算に用いると無効なポインタを生み出すような不正な index の値を検出し拒否する。

enum { TABLESIZE = 100 };

static int table[TABLESIZE];

int* f(int index) {
  if (0 <= index && index < TABLESIZE)
    return table + index;

  return NULL;
}
適合コード

より単純で効率的な別の修正方法としては、符号無し型を使い負の値のチェックを不要にしつつ、indexの境界外の正の値を受け付けないようにする。

enum { TABLESIZE = 100 };

static int table[TABLESIZE];

int* f(size_t index) {
  if (index < TABLESIZE)
    return table + index;

  return NULL;
}
違反コード (終端外ポインタ参照)

以下の違反コード例は、Windows 分散コンポーネントオブジェクトモデル(DCOM)リモートプロシージャコール(RPC)インタフェースの欠陥を抱えたロジックであり、W32.Blaster.Worm が悪用したものである。エラーは、GetMachineName() 関数(長い文字列からホスト名を抽出する)の while ループが正しく制限されていないことにある。pwszTemp が指す文字配列の最初の MAX_COMPUTERNAME_LENGTH_FQDN + 1 個の要素にバックスラッシュ文字が存在しない場合、最後の繰り返し処理でオブジェクトの終端外を指すポインタを参照し、結果として攻撃可能な未定義動作 47 が発生する。実際の攻撃は、実行中のプログラムに実行コードを注入することで行われた。Blaster ワームによる経済的損失は、5 億 2,500 万ドル以上と推定されている [Pethia 2003]。

Common Weakness Enumeration データベースでは、このプログラミングエラーについて CWE-119「Failure to constrain operations within the bounds of a memory buffer」と CWE-121「Stack-based buffer overflow」で解説している。

error_status_t _RemoteActivation(
      /* ... */, WCHAR *pwszObjectName, ... ) {
   *phr = GetServerPath(
              pwszObjectName, &pwszObjectName);
    /* ... */
}

HRESULT GetServerPath(
  WCHAR *pwszPath, WCHAR **pwszServerPath ){
  WCHAR *pwszFinalPath = pwszPath;
  WCHAR wszMachineName[MAX_COMPUTERNAME_LENGTH_FQDN+1];
  hr = GetMachineName(pwszPath, wszMachineName);
  *pwszServerPath = pwszFinalPath;
}

HRESULT GetMachineName(
  WCHAR *pwszPath,
  WCHAR wszMachineName[MAX_COMPUTERNAME_LENGTH_FQDN+1])
{
  pwszServerName = wszMachineName;
  LPWSTR pwszTemp = pwszPath + 2;
  while ( *pwszTemp != L'\\' )
    *pwszServerName++ = *pwszTemp++;
  /* ... */
}
適合コード

以下に示す適合コードでは、GetMachineName() 関数の while ループが制限されており、バックスラッシュ文字を検出するか、NULL 終端文字(L'\0')を検出するか、バッファの終端に到達するとループが終了する。また、wszMachineName の中にバックスラッシュ文字が見つからない場合であってもバッファオーバーフローは発生しない。

HRESULT GetMachineName(
  wchar_t *pwszPath,
  wchar_t wszMachineName[MAX_COMPUTERNAME_LENGTH_FQDN+1])
{
  wchar_t *pwszServerName = wszMachineName;
  wchar_t *pwszTemp = pwszPath + 2;
  wchar_t *end_addr = pwszServerName + MAX_COMPUTERNAME_LENGTH_FQDN;
  while ( (*pwszTemp != L'\\')
     &&  ((*pwszTemp != L'\0'))
     && (pwszServerName < end_addr) )
  {
    *pwszServerName++ = *pwszTemp++;
  }

  /* ... */
}

この修正方法は説明のために示したものであり、必ずしも Microsoft が採用した修正であるとは限らない。特に、バックスラッシュが見つかる保証はないため、修正は正しくない可能性がある。

違反コード (終端外インデックスの使用)

「終端外ポインタ参照」エラーと同様に、以下に示す違反コード例の insert_in_table() 関数は、配列の終端をちょうど超えた要素へ値を格納してしまう、という点以外は問題のないインデックスを使用する。

まず、インデックス pos をバッファのサイズと比較する検証が間違っている。インデックスが size と同じである場合、バッファの終端を1つ超えたメモリ位置に value を格納してしまう。

また、インデックスが size より大きい場合、バッファのサイズを拡張する前に size の値を変更する。realloc() の呼出しがバッファサイズの拡張に失敗すると、関数の次の呼び出しで size の元の値と同じかまたはその値より大きい pos 値を使用して、バッファの終端を1つ超えたメモリ位置へ value を再び格納してしまう。

Common Weakness Enumeration データベースでは、このプログラミングエラーについてCWE-122「Heap-based buffer overflow」と CWE-129「Improper validation of array index」で解説している。

static int *table = NULL;
static size_t size = 0;

int insert_in_table(size_t pos, int value) {
  if (size < pos) {
    int *tmp;
    size = pos + 1;
    tmp = (int*)realloc(table, sizeof *table * size);
    if (tmp == NULL) {
      return -1;  /* 割当て失敗を示す */
    }
    table = tmp;
  }

  table[pos] = value;
  return 0;
}
適合コード

以下の適合コードは、<= 演算子を使用してインデックス pos を正しく検証しており、realloc() の呼出し成功が確認できない限り size を変更しない。

static int *table = NULL;
static size_t size = 0;

int insert_in_table(size_t pos, int value) {
  if (size <= pos) {
    int *tmp = (int*)realloc(table, sizeof *table * (pos + 1));
    if (tmp == NULL) {
      return -1;   /* 割当て失敗を示す */
    }

    /* realloc が成功した場合のみサイズを変更 */
    size  = pos + 1;
    table = tmp;
  }

  table[pos] = value;
  return 0;
}
違反コード (明らかにアクセスされる境界外インデックス)

以下の違反コード例は、7 行 5 列で構成される matrix を行優先で宣言している。init_matrix 関数は、35 個の要素すべてに対して反復操作を行い、関数の引数 x の値で初期化する。しかし、多次元配列は行優先で宣言されている一方で、関数は列優先で要素に対する反復操作を行うため、外側のループの最初の繰り返しのとき j の値が値 COLS に到達すると、関数は要素 matrix[0][5] へのアクセスを試みる。matrix の型は int[7][5] であるため、添字 j は境界外であり、このアクセスは未定義動作 49に該当する。

static const size_t COLS = 5;
static const size_t ROWS = 7;

static int matrix[ROWS][COLS];

void init_matrix(int x) {
  for (size_t i = 0; i != COLS; ++i)
    for (size_t j = 0; j != ROWS; ++j)
      matrix[i][j] = x;
}
適合コード

以下の適合コードでは、多次元オブジェクトが宣言されたときと同様、行優先で matrix の要素を初期化することで、境界外インデックスを使用しないようにしている。

static const size_t COLS = 5;
static const size_t ROWS = 7;

static int matrix[ROWS][COLS];

void init_matrix(int x) {
  for (size_t i = 0; i != ROWS; ++i)
    for (size_t j = 0; j != COLS; ++j)
      matrix[i][j] = x;
}
違反コード (フレキシブル配列メンバ外のポインタ)

以下の違反コード例において、関数 find() は、フレキシブル配列メンバ buf の要素に対する反復処理を、2 番目の要素から始めている。しかし、関数 g() は配列メンバ bufに記憶域を割り当てていないため、find() の中の式 first++ は、配列 buf の要素が存在しない場合に、終端を1つ超えた要素を指すポインタを生成しようとする。この動作は未定義の動作 62 となる。

struct S {
  size_t len;
  char   buf[];   /* フレキシブル配列メンバ */
};

char *find(const struct S *s, int c) {
  char *first = s->buf;
  char *last  = s->buf + s->len;

  while (first++ != last)   /* 未定義動作 */
    if (*first == (unsigned char)c)
      return first;

  return NULL;
}

void g() {
  struct S *s = (struct S*)malloc(sizeof (struct S));

  if (s == NULL) {
    return -1;  /* 割当て失敗を示す */
  }
  s->len = 0;
  /* ... */
  char *where = find(s, '.');
  /* ... */

  return 0;
}
適合コード

以下の適合コードでは、現在ポインタが指している値の次の値の存在が確認されない限り、ポインタをインクリメントしない。

struct S {
  size_t len;
  char   buf[];   /* フレキシブル配列メンバ */
};

char *find(const struct S *s, int c) {
  char *first = s->buf;
  char *last  = s->buf + s->len;

  while (first != last)   /* ここでインクリメントしない */
    if (*++first == (unsigned char)c)
      return first;

  return NULL;
}

void g() {
  struct S *s = (struct S*)malloc(sizeof (struct S));

  if (s == NULL) {
    return -1;  /* 割当て失敗を示す */
  }

  s->len = 0;
  /* ... */
  char *where = find(s, '.');
  /* ... */

  return 0;
}
違反コード (ライブラリ関数による無効なアクセス)

以下の違反コード例では、関数 f()fread() を呼び出し、size バイトのwchar_t型データをntimes個、BUFSIZEの要素から成る配列wbufに読み込む。しかし、nitems の値の計算に使用される式は、wchar_t のサイズが char のサイズとは異なり 1 を超える可能性があるという事実を考慮していない。このため、fread()wbuf の終端外を指すポインタを生成し、このポインタを使って、配列の存在しない要素に値を割り当てる可能性がある。この動作は 未定義動作 109 となる。この未定義動作は、コード注入攻撃でよく利用される典型的なバッファオーバーフローとして現れることが多い。

Common Weakness Enumeration データベースでは、このプログラミングエラーについて CWE-121「Access of memory location after end of buffer」と CWE-805「Buffer access with incorrect length value」で解説している。

void f(FILE *file) {
  wchar_t wbuf[BUFSIZ];

  const size_t size = sizeof *wbuf;
  const size_t nitems = sizeof wbuf;

  size_t nread;

  nread = fread(wbuf, size, nitems, file);
  /* ... */
}
適合コード

以下の適合コードは、fread() がファイルからの読み出しに使う個数の上限を正しく計算している。

void f(FILE *file) {
  wchar_t wbuf[BUFSIZ];

  const size_t size = sizeof *wbuf;
  const size_t nitems = sizeof wbuf / size;

  size_t nread;

  nread = fread(wbuf, size, nitems, file);
  /* ... */
}
違反コード

以下のコード例では、整数 skip が、ポインタ s に加算される際にスケールされ、s が参照するオブジェクトの境界外を指すかもしれない。

struct big {
  unsigned long long ull_1;
  unsigned long long ull_2;
  unsigned long long ull_3;
  int si_4;
  int si_5;
};
 
int g(void) {
  size_t skip = offsetof(struct big, ull_2);
  struct big *s = (struct big *)malloc(4 * sizeof(struct big));
  if (s == NULL) {
    return -1;  /* 割当て失敗を示す */
  }
 
  memset(s + skip, 0, sizeof(struct big) - skip);  /* 違反 */
 
  /* ... */

  return 0;
}
適合コード

以下の適合コードは、skip をスケールしていない。

struct big {
  unsigned long long ull_1;
  unsigned long long ull_2;
  unsigned long long ull_3;
  int si_4;
  int si_5;
};
 
int g(void) {
  size_t skip = offsetof(struct big, ull_2);
  struct big *s = (struct big *)malloc(4 * sizeof(struct big));
  if (s == NULL) {
    return -1;  /* 割当て失敗を示す */
  }
 
  memset(skip, 0, sizeof(struct big) - skip);  
 
  /* ... */

  return 0;
}
リスク評価

境界外を指すポインタや配列添字にアクセスして書込みを行うと、バッファオーバーフローが発生したり、脆弱なプロセスの権限で任意のコードが実行されたり、意図せぬ情報漏えいが発生する可能性がある。

ルール

深刻度

可能性

修正コスト

優先度

レベル

ARR30-C

P9

L2

自動検出(最新の情報はこちら

ツール

バージョン

チェッカー

説明

Compass/ROSE

   

このルールの違反を検知するように設定することは可能。まず次のパターンのコードを探す。

   for (LPWSTR pwszTemp = pwszPath + 2; *pwszTemp != L'\\'; *pwszTemp++;)

反復に使う変数がポインタであり、ポインタはインクリメントされ、ループの条件にポインタの上限が設定されていない。このようなケースを検知できれば、意味が同じで構文が異なる、実際の違反コード例を検知することができる

Coverity

6.5

ARRAY_VS_SINGLETON

NEGATIVE_RETURNS

OVERRUN_STATIC OVERRUN_DYNAMIC

メモリバッファ/配列の終端を超えるメモリアクセスを検出できる

ループの境界が負の値になったときに検出できる

静的または動的に割り当てられた配列への境界外の読書きを検出できる

Klocwork

V. 9.1

ABV.ITERATOR SV.TAINTED.LOOP_BOUND

 
LDRA tool suite V. 8.5.4

47 S
476 S
64 X
68 X
69 X

部分的に実装済み
PRQA QA-C 8.1 3680
3681
3682
3683
3685 (U)
3686
3688
3689 (U)
3690
3692
部分的に実装済み
関連する脆弱性

CVE-2008-1517 はこのルールへの違反が原因で生じた。バージョン 10.5.7 より前の Mac OSX では、xnu カーネルが未検証のユーザー入力インデックスを使って配列にアクセスしたため、攻撃者は、配列の長さを超えるインデックスを渡してメモリの外部にアクセスすることで、任意のコードを実行することができた [xorl 2009]。

関連するガイドライン
ISO/IEC TR 24772:2013 Arithmetic Wrap-around Error [FIF]
Unchecked Array Indexing [XYZ]
ISO/IEC TS 17961 (Draft) Forming or using out-of-bounds pointers or array subscripts [invptr]
MITRE CWE CWE-119, Failure to constrain operations within the bounds of a memory buffer
CWE-121, Stack-based buffer overflow
CWE-122, Heap-based buffer overflow
CWE-129, Unchecked array indexing
CWE-788, Access of memory location after end of buffer
CWE-805, Buffer access with incorrect length value
参考資料
[Finlay 2003]  
[Microsoft 2003]  
[Pethia 2003]  
[Seacord 2013] Chapter 1, "Running with Scissors"
[Viega 2005] Section 5.2.13, "Unchecked Array Indexing"
[xorl 2009 ] "CVE-2008-1517: Apple Mac OS X (XNU) Missing Array Index Validation"
翻訳元

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

ARR30-C. Do not form or use out of bounds pointers or array subscripts (revision 45)

Top へ

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