久保 正樹(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。
今回は、第2回でも取り上げたオープンソースの画像処理ライブラリ libtiff に最近見つかった脆弱性を、セキュリティコードレビューを行いながら見てみましょう。
サンプルコード
以下のコードは libtiff 3.9.2 の /libtiff/tif_getimage.c からの抜粋です。実際のコードではマクロを活用して関数定義を行っていますが、コードを読みやすくするために一部のマクロを展開下形で引用しています。それでは、このコードのどこに問題があるのかを考えてみましょう。
/* * YCbCr -> RGB conversion and packing routines. */ #define YCbCrtoRGB(dst, Y) { \ uint32 r, g, b; \ TIFFYCbCrtoRGB(img->ycbcr, (Y), Cb, Cr, &r, &g, &b); \ dst = PACK(r, g, b); } /* * 8-bit packed YCbCr samples w/ 1,2 subsampling => RGB */ static void putcontig8bitYCbCr22tile( TIFFRGBAImage* img, uint32* cp, uint32 x, uint32 y, uint32 w, uint32 h, int32 fromskew, int32 toskew, unsigned char* pp ) { uint32* cp2; (void) y; fromskew = (fromskew / 2) * 4; cp2 = cp+w+toskew; while (h>=2) { x = w; do { uint32 Cb = pp[2]; uint32 Cr = pp[3]; YCbCrtoRGB(cp[0], pp[0]); YCbCrtoRGB(cp2[0], pp[1]); cp ++; cp2 ++; pp += 4; } while (--x); cp += toskew*2+w; cp2 += toskew*2+w; pp += fromskew; h-=2; } if (h==1) { x = w; do { uint32 Cb = pp[2]; uint32 Cr = pp[3]; YCbCrtoRGB(cp[0], pp[0]); cp ++; pp += 4; } while (--x); } }
int32 および uint32 は /libtiff/tiff.h で次のように定義されています。
/* * Intrinsic data types required by the file format: * * 8-bit quantities int8/uint8 * 16-bit quantities int16/uint16 * 32-bit quantities int32/uint32 * strings unsigned char* */ (中略) #if SIZEOF_INT == 4 #ifndef HAVE_INT32 typedef int int32; #endif typedef unsigned int uint32; /* sizeof (uint32) must == 4 */ #elif SIZEOF_LONG == 4 #ifndef HAVE_INT32 typedef long int32; #endif typedef unsigned long uint32; /* sizeof (uint32) must == 4 */ #endif
tif_getimage.c には、画像データを読み込み、RGBA形式に変換するための、様々な関数が定義されています。今回問題となる putcontig8bitYCbCr12tile() は、画像データを YCbCr 色モデルデータから RGB 色モデルデータに変換する関数の1つです。変換時に用いられるダウンサンプリング(subsampling)率に応じて、同様の関数が複数個定義されています。
変数 fromskew と toskew は、画像の反転などの変形操作に利用されるパラメータです。while ループの中では、各ピクセルデータを指すポインタを動かしながら、各ピクセルの色情報を YCbCrtoRGB() を使って YCbCr 形式から RGB 形式に変換し、バッファに書き込むという処理を行っています。
脆弱性の解説:64ビット環境で問題になる整数変換 (CVE-2010-2233)
このコードで注目すべき箇所は、ポインタ操作を行う下記の式です。
式1: cp += toskew*2+w;
式1の変数の型に注目してみましょう。
ポインタ += int * 2(int型) + unsigned int
このコードが64ビット環境でコンパイル・実行されると、int 型の変数値が負の値である場合に問題が発生します。それでは順を追って見ていきましょう。
式1の右辺式は int * int(定数) + unsigned int となっており、部分式である乗算演算の結果の型は int なので、最終的に int + unsigned int の加算式として評価されます。加算における2つのオペランドは異なる型を持つため、いづれか一方の型に合わせるために型変換が行われます。さて、どちらの型に合わせるでしょうか? Cでは「通常の算術型変換」(usual arithmetic conversion)と呼ばれるルールがあり、このルールに基づいて型変換が行われます。
「通常の算術型変換」
足し算、引き算、かけ算、割り算といった「通常の算術」演算において、オペランドの型が異なる場合、正しく計算を行うためにオペランドを共通の型に揃える必要が生じます。C言語標準(C99)は「6.3.1.8 通常の算術型変換」にそのルールを定めています。プログラマが意識しなくても、コンパイラはこのルールに従い型のバランスを取る処理を行っているのです。整数に関する脆弱性を作り込まないためにしっかり覚えておきたいルールですので、これを機会に復習しておきましょう。
(出典:ISO/IEC 9899:1999、6.3.1.8 通常の算術型変換)
算術型のオペランドをもつ多くの演算子は、同じ方法でオペランドの型変換を行い、結果の型を決める。型変換は、オペランドと結果の共通の実数型(common real type)を決めるために行う。
(中略)
通常の算術型変換の規則は、次による。
(中略)
1. 両方のオペランドが同じ型をもつ場合、更なる型変換は行わない。
2. そうでない場合、両方のオペランドが符号付き整数型をもつ、又は両方のオペランドが符号無し整数型をもつならば、整数変換順位の低い方の型を、高い方の型に変換する。
3. そうでない場合、符号無し整数型をもつオペランドが、他方のオペランドの整数変換順位より高い又は等しい順位を持つならば、符号付き整数型をもつオペランドを、符号無し整数型をもつオペランドの型に変換する。
4. そうでない場合、符号付き整数型をもつオペランドの型が、符号無し整数型をもつオペランドの型のすべての値を表現できるならば、符号無し整数型をもつオペランドを、符号付き整数型をもつオペランドの型に変換する。
5. そうでない場合、両方のオペランドを、符号付き整数型をもつオペランドの型に対応する符号無し整数型に変換する。
「整数変換順位」という言葉が出てきましたが、これはビット幅の広い型ほど高い順位を持つことを定めたルールです。型が同じであれば、符号付き/符号無しにかかわらず順位は同じです。以下の表に各型とその順位の関係を示しておきます。
■図1.整数変換の順位と型今注目している算術演算
int + unsigned intでは、前述のルールの3つ目に従うことになります。つまり、両方のオペランドを unsigned int 型に統一した上で演算が行われ、結果の値の型は unsigned int になります。
次に unsigned int として得られた演算結果とポインタの加算演算が行われます。C99 におけるポインタと整数型との間の加減算の意味については、C/C++ セキュアコーディング入門の第2回の記事(ポインタ演算は正しく使用する)でとりあげたことがありますが、ここでは int が32ビット、ポインタが64ビットである LP64 データモデル環境でこの演算が実際にどう処理されるのかが問題となります。
ポインタも整数型も、どちらもCPUから見れば単なる数値データですから、それらの加減算は単純に加減算を行うアセンブラコードに翻訳されます。ただし、64ビット長データと32ビット長データの加減算なので、32ビット長データを64ビットに変換し、64ビット長データ同士の加減算が行われます。このとき、unsigned int は符号無し型であるため、ゼロ拡張が行われます。つまり、上位32ビットは単純に0で埋められます。
ここでゼロ拡張が行われることが問題です。演算結果の unsigned int 型データは、本来負の値も取り得る int 型の値として扱うべきデータでしたが、64ビットデータとしてゼロ拡張してしまうと、意図した値とは異なるビット表現になります。
式1: cp += toskew*2+w;
toskew の値は、画像を垂直方向に反転させる場合に負の値となります。負の値を取る場合、最終的なポインタの値は小さくなるべきですが、逆に大きくなる方向に計算されてしまいます。その結果、アクセスの許可されていないメモリ領域を参照し、プログラムは異常終了してしまいます。実際、この問題が発見されたのは libtiff ライブラリとして使用する画像処理プログラム ImageMagick において、特定の TIFF ファイルを処理する際にセグメンテーション違反が発生するという報告が発端でした。
問題の修正は、2 * toskew + w の結果を int 型変数に代入し、その値を使ってポインタの値を更新するように、コードを変更することで行われました。以下に修正パッチの内容を示します('+'記号から始まる行が修正で追加された行、'-'記号から始まる行が修正で削除された行)。
@@ -1939,6 +1940,7 @@ DECLAREContigPutFunc(putcontig8bitYCbCr12tile) { uint32* cp2; + int32 incr = 2*toskew+w; (void) y; fromskew = (fromskew / 2) * 4; cp2 = cp+w+toskew; @@ -1953,8 +1955,8 @@ cp2 ++; pp += 4; } while (--x); - cp += toskew*2+w; - cp2 += toskew*2+w; + cp += incr; + cp2 += incr; pp += fromskew; h-=2; }
パッチで修正された式の型情報を示すと以下のようになります。
int = 2(int型) * int + unsigned int ポインタ += int
ポインタ値 += int の式において、右辺は符号付き32ビット整数であり、コンパイラが出力するアセンブラコードでは、元の値の符号を保存する符号拡張を行って64ビット整数とするようになります。修正したコードを Intel の LP64 環境でコンパイルすると、cltq(Convert Long to Quad)命令が呼ばれていることが分かります。この命令は %eax を %rax に符号拡張する命令です。つまり64ビットへの変換時に、ゼロ拡張ではなく符号拡張が行われるよう修正されていることが分かります。
なお、今回の脆弱性はLP64データモデルでは問題になりますが、ILP64データモデルではおそらく問題にならないでしょう。ILP64データモデルでは、int型も64ビット長となります。この場合int32型やuint32型はint型よりも「小さい」型なので、算術演算の際にはまずそれぞれをint型に「変換」する「整数拡張」(integer promotion)と呼ばれる操作が行われます。整数拡張に関する説明は今回省略しましたが、どちらも元の値を表現する64ビット幅のint型に変換されます。つまり、(int32値 + uint32値)という式は (int値 + int値)という式に変換され、その結果は符号付き型であるint値となります。さらに(ポインタ値 + int値)という演算についても、ビット幅を拡張する必要なく行えるため、数学的な計算結果と同じ値を得ることができます。
ライブラリの脆弱性にはご注意を
この脆弱性は、ライブラリを使用して開発されたプログラムのクラッシュがきっかけで発見されました。(オープンソースの)ライブラリを使用してプログラムを開発する場合、プログラムはライブラリに存在する脆弱性の影響を受けることになります。脆弱性が発見されたり修正されたりした場合には、使用するライブラリを更新し、プログラムをアップデートする必要があるかもしれません。また、メンテナンスがされていないライブラリを使用する場合には、ライブラリの脆弱性を自分で修正しなければならないかもしれません。プログラムで使用するライブラリのアップデートや脆弱性に関する情報をしっかりウォッチすることが重要です。
今回ご紹介した脆弱性は、符号付きデータと符号無しデータの混在した整数演算が原因で作り込まれました。整数関連の脆弱性はCで書かれたプログラムでは比較的作り込んでしまいやすい問題です。整数演算を行うコードを書くときには、符号無し整数と符号付き整数が混在しないよう注意しましょう。また、混在する演算が必要な場合には、C言語仕様の型変換に関するルールを思い出し、プログラムが意図した通り動作すること(未定義の動作を含まないこと)を確認してください。