セキュリティセンターTOP > セキュアプログラミング講座 > C/C++言語編 > 著名な脆弱性対策 > バッファオーバーフロー: #2 ソースコード記述時の対策

第10章 著名な脆弱性対策
バッファオーバーフロー: #2 ソースコード記述時の対策

バッファオーバーフロー対策の最も基本的な項目のひとつとして、メモリあふれが起こらないようにソースコードを記述するということが挙げられる。

ロジックの記述においては、領域長を意識したプログラミングを行い、使用するライブラリ関数も領域長を引数として受け取り、それを超えてメモリへの書き出しを行わない関数を用いる。

バッファオーバーフロー: #2 ソースコード記述時の対策
図10-2: バッファオーバーフロー: #2 ソースコード記述時の対策

脆弱性を生むことで著名な関数 gets() は、C11仕様(2011年)から削除された。

領域長とデータ長を意識したプログラミング

文字列の転記の場面で領域長とデータ長を意識したプログラミングを行う。

(1) 領域に収まりきることを確認してから転記

データを転記する際に、長さ検査ロジックを省略しない。例えば、次のコード例では source が指すデータ長が想定以上に長いとあふれが起こり得る。

例 バッファオーバーフローが起こり得るケース
 1 #define MAXSIZE 256
 2
 3 int foo(char *source) {
 4    char dest[MAXSIZE];
 5
 6    strcpy(dest, source);
 7    ......
 8 }

このような場合、例えば、次のコード例の10行目のような検査ロジックを記述するとともに、13行目のような転送バイト数の上限を指定できる関数を使用するべきである。

例 長さ検査ロジックとあふれを未然に防ぐ関数呼び出し
 1 #define MAXSIZE 256
 2
 3 int foo(char *source) {
 4    char dest[MAXSIZE];
 5
 6    if (source == NULL)                   //追加: 不当な引数を考慮する
 7        return ERROR;
 8                                                                
 9    memset (dest, 0, MAXSIZE);            //追加: 領域のゼロクリア
10    if (MAXSIZE - 1 < strlen(source))     //追加: 領域長とデータ長の
                                                    比較(あふれの検査)
11        return ERROR;
12
13    strncpy (dest, MAXSIZE - 1, source);  //変更: 転送バイト数に上限の
                                                    ある関数の使用
14    ......
15 }

(2) 文字列終端のナル '\0' を意識する

C言語で文字列データを扱う際に常に意識する必要があるものとして、文字列の終端のナル文字 '\0' の存在である。

上記の例では、変数 dest は MAXSIZE バイトの領域のうち、最後の1バイトを除いた (MAXSIZE - 1) バイトしか内容を保持することができない。従って、上記の11行目は次のようであってはならない。

11    if (MAXSIZE < strlen(source))    //ダメな例

(3) ループカウンターの値が行き過ぎないように

上記の10行目を、memset() を使わずにループを用いてクリアする場合、次のようにすると dest の領域の外への書き込みが生じる。

for (k = 0; k <= MAXSIZE; k++)
    dest[k] = 0;

変数 k の値は、0 から (MAXSIZE - 1) までの値を取り、決して MAXSIZE に到達してはならない。本来は次のように書くべきであった。

for (k = 0; k < MAXSIZE; k++)
    dest[k] = 0;

(4) バッファを動的に確保

データのサイズをあらかじめ決められない場合、malloc() 等を使用して、メモリを確保することが考えられる。ただし、サイズが計算式による値の場合、計算式による整数オーバーフローを起こさないよう注意が必要である。

また、メモリの解放し忘れ(メモリリーク)にも注意する必要がある。

例 動的な領域確保
 1 #define MAXSIZE 256
 2
 3 int foo(char *source) {
 4    size_t size;
 5    char   *dest;
 6
 7    if (source == NULL)            // 不当な引数を考慮する
 8        return ERROR;
 9
10    size = strlen(source) + 1;     //追加: データ長を求める
11    dest = (char*)malloc(size);    //追加: ヒープに領域を確保
12    if (dest == NULL)              //追加: 確保失敗なら、エラーリターン
13        return ERROR;
14                                                                
15    memcpy (dest, size, source);   //追加: バイト数分の転記
16
17    ......
18
19    free(dest);                    //追加: ヒープの領域を解放
20    return OK;
21 }

対策されたランタイムライブラリ

文字列処理を行う際、従来の strcpy() 等の関数を使うのではなく、データの転記の際に領域あふれを未然に防いだりエラーとして検出してくれるランタイムライブラリを使う方法である。例えば、次のようなライブラリがある。

(1) Managed String ライブラリ [GNU/Linux]

Burch, Long, Seacord らによる、文字列操作に特化したライブラリである。文字列の型は string_m を使用し strcreate_m(), strcpy_m(), strcat_m() 等、約25の関数が用意されている。

参考URL https://www.securecoding.cert.org/confluence/display/c/Managed+String+Library

(2) ISO/IEC 9899:2011 Annex K [Windows]

Microsoft Visual Studio 2005 以降で利用できるランタイムライブラリであり、出力バッファのサイズを引数に渡す等のセキュリティが強化されている。これらは C11仕様にも規定された。

例えば、次のものをはじめとする 68 の関数が用意されている。

char*   gets_s    (char *buff, size_t buff_size);
errno_t memcpy_s  (void *dest, size_t dest_size, void *source,
                   size_t source_size);
errno_t fopen_s   (FILE** pFile, const char *filename, const char *mode);
int     fprintf_s (FILE *stream, const char *format [, argument ]...);
int     fscanf_s  (FILE *stream, const char *format [, argument ]...);
errno_t _itoa_s   (int value, char *buffer, size_t sizeInCharacters,
                   int radix);
errno_t strncat_s (char *strDest, size_t bufferSizeInBytes,
                   const char *strSource, size_t count);

参考URL https://msdn.microsoft.com/ja-jp/library/wd3wzwts(VS.80).aspx

(3) SafeStr ライブラリ [GNU/Linux, Windows]

MessierとViegaにより開発された「Safe C String Library (SafeStr)」は、ライブラリ内部でバッファオーバーフローを起こさないようにした文字列操作のライブラリである。

SafeStr は、文字列を安全に扱うために、文字列領域は動的に割り当てられ、文字列操作により、容量が不足すると領域の再割り当てが行われる。

SafeStr を使用するには、最初に safestr_create() を呼び出し SafeStr 文字列を作成する必要がある。

ライブラリの関数は、strcat () に相当する safestr_append() や、文字を置き換えるsafestr_replace() 等の関数が用意されている。

SafeStr の使用後は safestr_free () を呼び出しメモリを解放する必要がある。

GNU/Linux では、次の URLからダウンロードして READMEに従ってインストールすれば利用できる。

参考URL http://www.zork.org/safestr/

(注:2014年4月以降サーバ応答無の可能性大)
例 SafeStr の使用例 example1.c
 1 #include <safestr.h>
 2 #include <stdio.h>
 3 #include <stdlib.h>
 4
 5 int main () {
 8    safestr_t str;
 9
10    str = safestr_create("hello\n", 0);
11    printf ("str=[%s]\n", (char *)str);
12    safestr_free (str);
13
14    return 0;
15 }
例 上記プログラムのコンパイルと実行
$ gcc example1.c -l safestr -l pthread -o example1
$ LD_LIBRARY_PATH=/usr/local/lib
$ export LD_LIBRARY_PATH
$ ./example1
str=[hello]
$

まとめ

文字列データを転記する場面においては、領域長とデータ長を常に意識してプログラミングを行う。またその際、領域あふれを未然に防いだり、領域あふれを検出できる「対策されたランタイムライブラリ」の使用が効果的である。