EXP08-C. ポインタ演算は正しく使用する
ポインタ演算を行う場合、ポインタに加算する値は、ポインタが参照する値の型のサイズに自動的にスケールされる。たとえば、4バイト整数のバイトアドレスに値を加算する場合、値は4をファクターとしてスケールされたのちポインタに加算される。ポインタ演算の動作原理を理解していないと、計算間違いをしてしまい、その結果バッファオーバーフローのような致命的エラーにつながるおそれがある。
違反コード
以下のコード例では、parseint(getdata()) が返す値を、int 型の要素を INTBUFSIZE個持つ配列 buf に格納している。[Dowd 2006] bufに挿入するデータが存在し(havedata()が示す)、buf_ptr が buf + sizeof(buf) の位置を超えてインクリメントされていなければ、buf_ptr が参照するアドレスに整数値が保存される。しかし、sizeof 演算子は buf の総バイト数を返し、これは多くの場合、buf の要素数の倍数になっている。buf の要素数が整数のサイズでスケールされ、buf に加算される。その結果、buf の終端を超えて整数が書き込まれないように行っているチェックは間違っており、バッファオーバーフローが起きる可能性がある。
int buf[INTBUFSIZE];
int *buf_ptr = buf;
while (havedata() && buf_ptr < (buf + sizeof(buf))) {
*buf_ptr++ = parseint(getdata());
}
適合コード
以下の適合コードでは、buf のサイズ INTBUFSIZE を直接 buf に加算して、これを上限として使用している。整数リテラル INTBUFSIZE は整数のサイズでスケールされ、buf の上限は適切にチェックされている。
int buf[INTBUFSIZE];
int *buf_ptr = buf;
while (havedata() && buf_ptr < (buf + INTBUFSIZE)) {
*buf_ptr++ = parseint(getdata());
}
(議論の余地はあるかもしれないが)より良い適合コードは、配列の終端の次の、存在しない要素のアドレスを使用する方法である。
int buf[INTBUFSIZE];
int *buf_ptr = buf;
while (havedata() && buf_ptr < &buf[INTBUFSIZE] {
*buf_ptr++ = parseint(getdata());
}
C 標準では、buf[INTBUFSIZE] には要素が存在しなくてもこのアドレスが使えることは保証されているため、この方法が使えるのだ。
違反コード
以下のコード例は OpenBSD オペレーティングシステムに見つかった欠陥に基づいている。整数 skip は struct big 型のポインタにオフセット値として加算されている。調整されたポインタを目的のアドレスとして memset() で使用している。しかし、skip を struct big ポインタに加算する際、skip の値は struct big のサイズで自動的にスケールされ、32バイトになる。(整数は4バイト、long long 整数は8バイト、構造体のパディングはないと仮定) このため、memset() 呼び出しは意図せぬメモリ領域に書き込みをしてしまう。
struct big {
unsigned long long ull_1; /* typically 8 bytes */
unsigned long long ull_2; /* typically 8 bytes */
unsigned long long ull_3; /* typically 8 bytes */
int si_4; /* typically 4 bytes */
int si_5; /* typically 4 bytes */
};
/* ... */
size_t skip = offsetof(struct big, ull_2);
struct big *s = (struct big *)malloc(sizeof(struct big));
if (!s) {
/* malloc() エラーの処理 */
}
memset(s + skip, 0, sizeof(struct big) - skip);
/* ... */
free(s);
s = NULL;
同様の状況は OpenBSD の make コマンドにも発生した。[Murenin 2007]
適合コード
上記のコード例を修正するには、struct big ポインタを char * にキャストすることである。これにより、skip は1でスケールされる。
struct big {
unsigned long long ull_1; /* typically 8 bytes */
unsigned long long ull_2; /* typically 8 bytes */
unsigned long long ull_3; /* typically 8 bytes */
int si_4; /* typically 4 bytes */
int si_5; /* typically 4 bytes */
};
/* ... */
size_t skip = offsetof(struct big, ull_2);
struct big *s = (struct big *)malloc(sizeof(struct big));
if (!s) {
/* malloc() エラーの処理 */
}
memset((char *)s + skip, 0, sizeof(struct big) - skip);
/* ... */
free(s);
s = NULL;
リスク評価
ポインタ演算をきちんと理解して使用しないと、攻撃者が任意のコードを実行することになる恐れがある。
|
レコメンデーション |
深刻度 |
可能性 |
修正コスト |
優先度 |
レベル |
|---|---|---|---|---|---|
|
EXP08-C |
高 |
中 |
高 |
P6 |
L2 |
自動検出(最新の情報はこちら)
|
ツール |
バージョン |
チェッカー |
説明 |
|---|---|---|---|
|
LDRA tool suite |
V. 8.5.4 |
45 D |
部分的に実装済み |
| PRQA QA-C | 8.1 |
2930 |
部分的に実装済み
|
4ヤード足す3フィートの長さは?初歩的な計算からも明らかだが、'7'という答えは間違いだ。単位を考慮に入れずに計算した生徒が出すような答えだな。正しい計算方法は、2つの数字を共通の単位に変換することだ。
このレコメンデーションにあるコード例は、異なるもの(シングルバイトデータもしくはマルチバイトの構造体)を表す数字の比較を扱う正しい方法と間違った方法について表している。違反したコード例では、単位を考慮しないで数値を加算しているが、適合したコード例では型をキャストして、ある数字を適切な単位の別の数字に変換している。
ROSEは、異なる単位を含むポインタ演算式を探すことで違反を検出できる。「異なる単位」はやっかいな部分だが、以下のようなヒューリスティックな(発見的)手法を使えば式中の単位を特定しようとすることはできる。
fooオブジェクトへのポインタの単位はfooである。char *へのポインタの単位は 'byte' である。- すべての
sizeofもしくはoffsetof式の単位は 'byte' である。 fooオブジェクト(例foo[variable]) の配列インデックスにつかう変数の単位はfooである。
ポインタ演算式に加えて、配列のインデックス式も検査の対象となりうる。array[index] は単に "array + index" をつづめただけだからだ。
関連するガイドライン
| CERT C++ Secure Coding Standard | EXP08-CPP. Ensure pointer arithmetic is used correctly |
| ISO/IEC TR 24772:2013 | Pointer Casting and Pointer Type Changes [HFC] Pointer Arithmetic [RVG] |
| ISO/IEC TS 17961 (ドラフト) | Adding or subtracting a byte count to an element pointer [cntradd] |
| MISRA-C | Rule 17.1 Rule 17.2 Rule 17.3 Rule 17.4 |
| MITRE CWE | CWE-468, Incorrect pointer scaling |
参考資料
| [Dowd 2006] | Chapter 6, "C Language Issues" |
| [Murenin 2007] |
翻訳元
これは以下のページを翻訳したものです。
EXP08-C. Ensure pointer arithmetic is used correctly (revision 121)
