ARR30-C. 境界外を指すポインタや配列添字を生成したり使用したりしない
悪用されうる未定義動作は、次に挙げる処理が原因で生まれることがある。
- 演算結果が同じオブジェクト内または同じオブジェクトの終端をちょうど超えた位置を指さないようなポインタ算術演算を行う
- 演算式でそのようなポインタを使う
- メモリ内の有効なオブジェクトを指さないポインタを参照する
- 配列内の要素を参照しないような添字を使う
C 言語規格は、無効なポインタ操作が原因で未定義の動作(UB)が生じうる状況を次の通り挙げている。
UB |
説明 |
サンプルコード |
---|---|---|
配列オブジェクトおよび整数型内またはこれらをちょうど超えた位置を指すポインタの加算または減算が、同じ配列オブジェクト内またはそれをちょうど超えた位置を指さないという結果を生み出す。 |
境界外ポインタの生成 |
|
配列オブジェクトおよび整数型内またはこれらをちょうど超えた位置を指すポインタの加算または減算の結果が、その配列オブジェクトをちょうど超えた位置を指し、評価対象の単項 |
終端外ポインタの参照、終端外インデックスの使用 |
|
与えられた配列添字でオブジェクトに明らかにアクセスできるのに、配列添字が範囲を外れている(たとえば、 |
明らかにアクセスされる境界外インデックス |
|
参照対象のオブジェクトが該当配列の要素を提供しない場合に、構造体のフレキシブル配列メンバへのアクセスまたはそのメンバをちょうど超えた位置をさすポインタの生成が試みられる。 |
フレキシブル配列メンバの境界外ポインタ |
|
ライブラリ関数配列パラメータに渡されるポインタが、すべてのアドレス計算およびオブジェクトアクセスを有効にするような値をもたない。 |
ライブラリ関数による無効なアクセス |
違反コード (境界外ポインタの生成)
以下の違反コード例では、関数 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 |
メモリバッファ/配列の終端を超えるメモリアクセスを検出できる ループの境界が負の値になったときに検出できる 静的または動的に割り当てられた配列への境界外の読書きを検出できる |
V. 9.1 |
ABV.ITERATOR SV.TAINTED.LOOP_BOUND |
||
LDRA tool suite | V. 8.5.4 |
47 S |
部分的に実装済み |
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)