富樫 一哉(JPCERTコーディネーションセンター) [著]
配列をコピーする際、記憶領域の扱いを適切に行わないとバッファオーバーフローにつながります。コピー元のデータ量またはコピー先の記憶領域のサイズをチェックするのが基本的な対策になりますが、誤って脆弱性を引き起こすことも多々あります。今回は、こういった配列コピー時に犯しやすい誤りについて解説します。
配列コピー時に犯しやすい誤り
データをコピーする先の記憶領域として十分なサイズが確保されていないと、バッファオーバーフローに繋がります。例えば、コピー先の領域として確保するサイズが不十分だったり、コピー先の領域は意図どおり確保していても、コピーするデータのサイズを誤って指定してしまう場合などです。
典型的な誤りとしては、文字列コピーにおけるNULL終端バイト分のサイズの配慮し忘れがあります(関連ルール「STR31-C. 文字データとNULL終端文字を格納するために十分なサイズの領域を確保する」を参照)。
また、コピーサイズを計算する際に整数オーバーフローが発生したり、正の値を処理することを想定しているロジックに負の値を持った符号付き整数を渡すことで、暗黙の変換により大きな正の値として扱われて問題を起こすケースもあります(関連ルール「STR31-C. 文字データとNULL終端文字を格納するために十分なサイズの領域を確保する」「INT32-C. 符号付き整数演算がオーバーフローを引き起こさないことを保証する」を参照)。
こういったコーディング上の誤りが脆弱性に繋がるケースも少なくありません。例えば、オープンソースのネットワーク侵入検知システムであるSourcefire Snortの旧バージョンには、改ざんされたパケットを送りつけることでサービス停止妨害や任意のコード実行が可能になる脆弱性(CVE-2006-5276)が存在しました。
コーディング時における基本的な対策として、(1)コピー先のサイズに応じてコピーするデータ量を制限する、あるいは(2)コピー先に十分なサイズの領域を確保する、ということを確実に行う必要があるのです。
今回はこういった配列コピー時に犯しやすい誤りについて解説します。
コピーサイズとして誤った値を指定
次に示すコード例は、memcpy()を用いて配列のコピーを行います。memcpy()のサイズ引数としてコピー元の記憶領域サイズを指定していますが、これはコピー先の記憶領域サイズよりも大きいため、バッファオーバーフローが発生します。
■コード例1(コピー先の領域に基づいた制限を行っていない)enum { WORKSPACE_BOUND = 256 }; void func(const int *src, size_t num_elem) { int dest[WORKSPACE_BOUND]; memcpy(dest, src, num_elem * sizeof(int)); //コピー先の領域に基づいた制限を行っていない /* ... */ }
文字列操作関数strncpy()の使用においても同じような誤りを犯しがちです。
また、配列コピーの際、配列要素型のサイズを考慮し忘れるとコピーデータの欠損に繋がります。
■コード例2(配列要素型のサイズを考慮していない)enum { WORKSPACE_BOUND = 256 }; void func(const int *src, size_t num_elem) { int dest[WORKSPACE_BOUND]; memcpy(dest, src, WORKSPACE_BOUND); //配列要素型のサイズを考慮していない /* ... */ }
これらのコーディングエラーは、単体テストレベルで明らかになる部類のものですが、コーディング段階で十分注意しておきたいものです。
コピーするデータ量をコピー先の記憶領域に基づき制限
コピーするデータ量をコピー先の記憶領域に基づいて制限する場合、コピー元のデータがコピー先の領域に収まることをあらかじめ確認する必要があります。
■コード例3(コピー元のデータがコピー先に収まることを確認)enum { WORKSPACE_BOUND = 256 }; void func(const int *src, size_t num_elem) { int dest[WORKSPACE_BOUND]; if (num_elem > WORKSPACE_BOUND) { /* コピー先の記憶領域が不足するためエラー処理などの対応 */ } memcpy(dest, src, sizeof(int) * num_elem); /* ... */ }
コピー元のデータ量に基づきコピー先の記憶領域を動的に確保
コピー元のデータ量に基づいてコピー先の記憶領域を動的に確保する場合には、以下のようなコードになるでしょう。動的に確保するコピー先領域のサイズを計算する際、演算結果が整数オーバーフローを引き起こさないことをチェックする必要があります。
■コード例4(動的にコピー先の領域を確保)void func(const int *src, size_t num_elem) { int *dest; if (num_elem > SIZE_MAX/sizeof(int)) { /* 整数オーバーフロー時の処理 */ } dest = (int *)malloc(sizeof(int) * num_elem); if (dest == NULL) { /* 動的領域確保に失敗した場合の処理 */ } memcpy(dest, src, sizeof(int) * num_elem); /* ... */ free(dest); }
サイズを示す変数は符号なし整数を用いて符号エラーを避ける
以下に、上記のコードを一部変更したコード例を示します。コピーする配列の要素数を示す値を符号付き整数(int num_elem)で受けており、渡された配列の要素数が上限値未満であることを確認しています。しかしこのコードでは、配列要素数として負の値が渡される可能性を考慮していません。もしも配列要素数として負の値が渡された場合、上限値検査をパスし、malloc()の引数として符号付き型から符号なし型への(暗黙の)変換が発生、その結果、巨大な正の値として取り扱われてしまいます。
■コード例5(符号付きの整数に対する範囲チェックが不十分)void func(const int *src, int num_elem) { int *dest; if (num_elem < SIZE_MAX/sizeof(int)) { //負の値に対する下限チェックがない dest = (int *)malloc(sizeof(int) * num_elem); ・・・ }else{ /* 整数オーバーフロー時の処理 */ } ・・・
この例では、負の値を取りうる配列要素数に対して下限検査が行われていないため、プログラマが意図していない非常に大きな記憶領域が確保されてしまう可能性があります。また、memcpy()などの引数として負の値が渡されるケースでは、コピー先領域を超えたサイズを指定することになり、バッファオーバーフローに繋がってしまうでしょう。
上記のようなコード例においては、下限チェックを加えることにより引数が正の値であることを確認する必要があります。そもそも、オブジェクトのサイズや配列のインデックスのような正の値を取るべき変数には、最初からsize_t型やunsigned int型などの符号なしの型を利用すべきでしょう。
文字列をはじめとする配列データの操作のために領域を確保したり、必要なサイズを計算することは多くあります。そのような場合に整数オーバーフローやバッファオーバーフローに繋がらないよう、コーディング時に注意を払いましょう。
今後の連載について
これまで6回にわたりセキュアなコーディングを行うために心がけるポイントや方法について解説してきたこの連載ですが、いったん、ここで終了させたいと思います。
しかし、まだまだカバーできていないトピックが「CERT C セキュアコーディングスタンダード」には残っています。これらについては、記事の形式をコードレビュー形式に変えた上で、近いうちに新しい連載として開始する予定です。
こうご期待。