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)