久保 正樹(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回のサンプルコードは、TIFFファイルの読み書きを行うためのライブラリ「libTIFF v3.6.1」です。
はじめに
この連載では、最初に問題のあるコードを示します。前回に引き続き今回も、まずはコードだけを見て、どこに問題があるのか考えてみてください。
コードの後には、コードに含まれる脆弱性を見つけるためのヒントや、コードが行おうとしていることを理解するために役立つ背景知識などを説明します。コードを見ただけではどこに問題があるのか分からない、といった場合は、これらの説明を手がかりに考えてみてください。
どこに問題があるのか分かったら、次にどのように修正すべきかを考えましょう。修正方法は一通りとは限りません。むしろ、複数の修正方法が考えられることが多いと思います。
最後に、実際にどのような修正が行われたか説明します。自分が考えた修正案と比較してみてください。
サンプルコード
さて、皆さんにコードレビューしていただくのは、TIFFファイルの読み書きを行うためのライブラリであるlibTIFF v3.6.1です。libTIFFはUNIXやLinux系OS、Windows、OpenVMSなどさまざまなプラットフォームで利用できる、可搬性の高いオープンソースのライブラリです。さまざまなアプリケーションに利用されています。
以下がその脆弱なコードです。どのような問題がこのコードにあるのか考えてみましょう。
■tiff-v3.6.1 libtiff/tif_dirread.c より抜粋/* Fetch a tag that is not handled by special case code. */ static int TIFFFetchNormalTag(TIFF* tif, TIFFDirEntry* dp) { ... if (dp->tdir_count > 1) { /* array of values */ char *p = NULL; switch (dp->tdir_type) { ... case TIFF_ASCII: case TIFF_UNDEFINED: /* bit of a cheat ... */ /* * Some vendors write strings w/o the trailing * NULL byte, so always append one just in case. */ cp = CheckMalloc(tif, dp->tdir_count+1, 1, mesg); if ( (ok = (cp && TIFFFetchString(tif, dp, cp))) != 0 ) cp[dp->tdir_count] = '\0'; /* XXX */ break; } ... static char * CheckMalloc(TIFF* tif, tsize_t n, const char* what) { char *cp = (char*)_TIFFmalloc(n); if (cp == NULL) TIFFError(tif->tif_name, "No space %s", what); return (cp); }
TIFFとlibTIFF
脆弱性は、libtiff version 3.6.1のIFDのIFD Entryを処理する関数TIFFFetchNormalTagに存在しました。問題の解説を始める前にまず、TIFFのファイルフォーマットについて簡単に説明します。
TIFF(Tagged Image File Format)は1986年にMicrosoftとAldus(後にAdobeが買収)が策定した画像ファイルフォーマットの1つです。TIFFには、解像度や符号化方式が異なる複数の画像データを1つのファイルにまとめて格納できるという特徴があり、特定のアプリケーションに依存しないため、さまざまなプログラムや組込み機器がTIFFをサポートしています。
TIFFファイルの先頭8バイトはイメージファイルヘッダ(Image File Header)と呼ばれ、これはIFD(Image File Directory)を参照します。TIFFファイルには複数の画像を格納できるので、その場合は画像ごとにIFDエントリが存在することになります。各IFDエントリには、画像に関するデータと実画像データへのポインタが含まれます(図1)。
■図1 TIFFのヘッダ脆弱性の解説
IFDエントリのカウント値を示すtdir_countが1より大きいとき、タイプが'TIFF_ASCII'もしくは'TIFF_UNDEFINED'であれば、動的メモリ割り当てを行う_TIFFmalloc()が呼び出されます。_TIFFmalloc()は内部でmalloc()を呼び出すだけの関数です。このとき、割り当てるメモリのサイズは、式dp->tdir_count + 1の結果の値になります。
攻撃者がtdir_countの値として不正な0xFFFFFFFFを持つファイルをプログラムに読み込ませることができると、いったい何が起こるでしょうか。加算0xFFFFFFFF + 1の結果は符号無し整数のラップアラウンドの結果ゼロになり、malloc()はこのゼロを引数にとって呼び出されます。サイズ0のメモリ割り当ては言語仕様上「処理系定義」の動作となります。WindowsやLinuxなどの処理系では、たまたま呼び出しが成功し、プログラマが意図したよりも小さなメモリ領域が確保されることがあります。
この想定外のメモリ割り当てが行われた後でmemcpy()などメモリに書き込みを行う関数が実行されると、ヒープバッファオーバーフローが発生してしまいます。攻撃者が何らかの方法で、細工したTIFFファイルをユーザーに開かせることができれば、上述の処理が行われ、任意のコードが実行される恐れがあるわけです。
ユーザーからの入力、つまりTIFFファイルのヘッダ情報が適切かどうかの入力値検証(Input Validation)が不十分であったため、今回のような脆弱性が作り込まれてしまいました。
脆弱性の修正方法
この脆弱性は、CheckMalloc()を修正することで対応されました。以下がその修正内容です。皆さんが想定した通りの修正でしょうか。
--- tiff-v3.6.1/libtiff/tif_dirread.c 2003-12-22 17:22:15.000000000 +0900 +++ tiff-3.7.1/libtiff/tif_dirread.c 2004-12-21 04:29:27.000000000 +0900 @@ -62,11 +63,20 @@ static char * -CheckMalloc(TIFF* tif, tsize_t n, const char* what) +CheckMalloc(TIFF* tif, size_t nmemb, size_t elem_size, const char* what) { - char *cp = (char*)_TIFFmalloc(n); + char *cp = NULL; + tsize_t bytes = nmemb * elem_size; + + /* + * XXX: Check for integer overflow. + */ + if (nmemb && elem_size && bytes / elem_size == nmemb) + cp = (char*)_TIFFmalloc(bytes); + if (cp == NULL) TIFFError(tif->tif_name, "No space %s", what); + return (cp); }
修正されたコードでは、nmembの値とelem_sizeの値がゼロでないことをチェックし(size_t型変数が負の値をとらない性質を利用)、その上でbytesの計算結果が正しいことをチェックしています。bytes / elem_size == nmembをチェックすることで、元の式bytes = nmemb * elem_sizeで何らかの乗算エラー(この場合、符号無し整数のラップアラウンド)が発生していることを検出できます。malloc()は、この入力値検査をパスした場合にのみ、呼び出されるよう修正されていることが分かります。また、CheckMalloc()の引数の数が増えているので、これに応じて呼び出し側も修正されています。
CERT C セキュアコーディングスタンダード
前回に引き続き、今回の脆弱性も整数エラーがバッファオーバーフローを引き起こすというパターンでしたが、このパターンで作り込まれる脆弱性の数は少なくありません。この脆弱性を作り込まないためのガイドラインとして、CERT Cセキュアコーディングスタンダードから以下の2つを紹介します。
これらの情報を参考に、セキュアコーディングを実践していきましょう!
参考情報
- 『C/C++ セキュアコーディング』 Robert C. Seacord 著、JPCERTコーディネーションセンター 訳、アスキー、2006年11月
- 『CERT C セキュアコーディングスタンダード』 Robert C. Seacord 著、久保正樹/戸田洋三 訳、アスキー、2009年9月
- TIFF 6.0 Specification(PDF)
- 槻ノ木隆の「BBっとWORDS」その96「TIFFの特徴」
- INT30-C. 符号無し整数の演算結果がラップアラウンドしないようにする
- MEM04-C. サイズ0のメモリ割り当てを行わない
- CVE-2004-1308