久保 正樹(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回取り上げるコードは、C言語で書かれたオープンソースの画像処理ソフトウェア「ImageMagick」です。
はじめに
この連載では、最初に問題のあるコードを示します。前回に引き続き今回も、まずはコードだけを見て、どこに問題があるのか考えてみてください。
コードの後には、コードに含まれる脆弱性を見つけるためのヒントや、コードが行おうとしていることを理解するために役立つ背景知識などを説明します。コードを見ただけではどこに問題があるのか分からない、といった場合は、これらの説明を手がかりに考えてみてください。
どこに問題があるのか分かったら、次にどのように修正すべきかを考えましょう。修正方法は一通りとは限りません。むしろ、複数の修正方法が考えられることが多いと思います。
最後に、実際にどのような修正が行われたか説明します。自分が考えた修正案と比較してみてください。
オープンソースの画像処理ソフトウェア「ImageMagick」
今回はオープンソースの画像処理ソフトウェアであるImageMagickを取り上げます。ImageMagickは、GIF、JPEG、PNG、PDF、TIFFなど100種類以上の画像ファイルフォーマットに対応するCで書かれたプログラムです。ユーザーは、コマンドラインから呼び出すことでImageMagickが提供するさまざまな機能を利用できます。APIが公開されており、C++、Java、Lisp、.NET、Perl、PHP、Python、Rubyなどさまざまな言語で書かれたプログラムからその機能を呼び出すことも可能です。画像フォーマットの変換やサイズの変更など単純な作業から、画像ファイルの合成、動画の作成、独自の数学的処理を画像ファイルに適用するなどより高度な機能も提供しているのが特徴です。
画像処理プログラムには過去、さまざまな脆弱性が発見されてきました。ImageMagickもご多分に漏れず、CVEを検索するだけでも過去に37件の脆弱性が発見されていることが分かります。ちなみに、脆弱性の種類を分類すると表1のようになります。
■表1 ImageMagickに見つかった脆弱性の分類脆弱性の分類 | 件数 |
---|---|
バッファオーバーフロー | 13件(35%) |
整数オーバーフロー | 7件(19%) |
サービス運用妨害 | 6件(16%) |
コマンドインジェクション | 4件(10%) |
書式指定出力 | 3件(8%) |
ファイル入出力関連 | 1件(2%) |
符号エラー | 1件(2%) |
権限昇格 | 1件(2%) |
合計 | 37件 |
上記の表に挙げられた脆弱性の一つ一つを詳細に解析したわけではありませんが「バッファオーバーフロー」の原因を詳細に探れば、過去の連載でも紹介した整数オーバーフローが問題の根本的原因であるケースも見られるのではないかと推測されます。
さて、さっそく脆弱性を含むコードを見てみましょう。脆弱なコードは ImageMagick-6.3.4 の magick/blob.c で定義されている ReadBlobString() 関数にあります。
ImageMagickの脆弱性:その1
/* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % % % % + R e a d B l o b S t r i n g % % % % % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % ReadBlobString() reads characters from a blob or file until a newline % character is read or an end-of-file condition is encountered. % % The format of the ReadBlobString method is: % % char *ReadBlobString(Image *image,char *string) % % A description of each parameter follows: % % o image: The image. % % o string: The address of a character buffer. % */ MagickExport char *ReadBlobString(Image *image,char *string) { register const unsigned char *p; register long i; ssize_t count; unsigned char buffer[1]; assert(image != (Image *) NULL); assert(image->signature == MagickSignature); for (i=0; i < (long) MaxTextExtent; i++) { p=ReadBlobStream(image,1,buffer,&count); if (count != 1) { if (i == 0) return((char *) NULL); break; } string[i]=(char) (*p); if ((string[i] == '\n') || (string[i] == '\r')) break; } string[i]='\0'; return(string); }
MaxTextExtent は magick/MagickCore.h で下記のように定義されています。
# define MaxTextExtent 4096
また、ReadBlobString()の第2引数の string には MaxTextExtent バイト割り当てられていると仮定します。
char string[MaxTextExtent];
ReadBlobString()関数は、BLOBファイルから文字列を読み取る関数です。改行文字もしくはEOFに達すると読取りを終了します。第1引数には画像ファイルへのポインタを、第2引数にはこれから読み取る文字(バッファ)が置かれたアドレスを指定します。このコードにはどのような問題があるでしょうか。
脆弱性の解説
文字型配列 string の長さはMaxTextExtentです。forループの終了時、i には MaxTextExtent が入っています。そのため、string[i] = '\0' は用意されたバッファを1バイトはみ出してNULL文字を書き込んでしまいます。このように1バイトはみ出してバッファに書き込む脆弱性は、off-by-one errorと呼ばれることもあります。
この脆弱性が攻撃されると、プログラムを実行しているユーザーの権限で任意のコードを実行されてしまう危険があります。
ImageMagick バージョン6.3.5ではこの問題を次のように修正しました。
- for (i=0; i < (long) MaxTextExtent; i++) { + for (i=0; i < (MaxTextExtent-1L); i++) {
1バイトはみ出した書き込みを行わないために、ループ変数の範囲を-1Lしています。これにより string[i]='\0' は配列の最後の要素にナル文字を書込むことになります。
ImageMagickの脆弱性:その2
同じくImageMagicの別の脆弱性について考えてみましょう。ImageMagick-6.3.4 の coders/dib.c から解説の便宜上余分なコードを省略して掲載しています。
typedef struct _DIBInfo { unsigned long size; long width, height; ... } DIBInfo; /* %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % % % % % R e a d D I B I m a g e % % % % % % % %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % ReadDIBImage() reads a Microsoft Windows bitmap image file and % returns it. It allocates the memory necessary for the new Image structure % and returns a pointer to the new image. % % The format of the ReadDIBImage method is: % % image=ReadDIBImage(image_info) % % A description of each parameter follows: % % o image_info: The image info. % % o exception: return any errors or warnings in this structure. % */ static inline long MagickAbsoluteValue(const long x) { if (x < 0) return(-x); return(x); } static inline size_t MagickMax(const size_t x,const size_t y) { if (x > y) return(x); return(y); } static Image *ReadDIBImage(const ImageInfo *image_info,ExceptionInfo *exception) { DIBInfo dib_info; Image *image; ... size_t length; ssize_t count; unsigned char *pixels; unsigned long bytes_per_line; ... /* Microsoft Windows 3.X DIB image file. */ dib_info.width=(short) ReadBlobLSBLong(image); /* 式1 */ ... image->columns=(unsigned long) dib_info.width; /* 式2 */ ... /* Read image data. */ ... bytes_per_line = 4 * ((image->columns*dib_info.bits_per_pixel+31) / 32); length = bytes_per_line * image->rows; pixels = (unsigned char *) AcquireMagickMemory((size_t) MagickMax( bytes_per_line, image->columns+256) * image->rows*sizeof(*pixels)); /* 式3 */ if (pixels == (unsigned char *) NULL) ThrowReaderException(ResourceLimitError,"MemoryAllocationFailed"); if ((dib_info.compression == BI_RGB) || (dib_info.compression == BI_BITFIELDS)) { count=ReadBlob(image,length,pixels); ...
ReadBlobLSBLong() 関数のプロトタイプ宣言は下記の通りです。
unsigned long ReadBlobLSBLong(Image *image)
ReadDIBImage() はWindows標準画像ファイル形式であるBMP形式の画像を読み込み、メモリを割り当てて新しい画像へのポインタを返します。また、ReadBlobLSBLong()はunsigned long型の値を返します。どのような問題があるか分かりましたか?
脆弱性の解説:符号拡張の問題 (CVE-2007-4988)
LP64データモデル(longとpointerが64ビット、intは32ビット)を前提に解説します。このコードの問題は次の代入式にあります。
式1: dib_info.width=(short) ReadBlobLSBLong(image);
ReadBlobSBLong() 関数の返り値の型は unsigned long です。この値は short 型に明示的にキャストされたのち、long 型変数である dib_info.width に代入されます。この代入式を変数の型名だけで示すと次のようになります。
式1の型: long = (short) unsigned long
つまり、この代入式では次の2つの型変換が発生しています。
- 変換1. unsigned long → short
- 変換2. short → long
変換1は、より大きな型から小さな型への変換なので、元の値が保持されない可能性があります。具体的には、unsigned long 型変数 x の値が、0 <= x <= SHRT_MAX の範囲を超える場合、つまり元の値が変換後の型で表現できる値の範囲を超えている場合、変換後の値は意図せぬ値になります。
変換2では、変換元の型が short と符号付きであるため、「符号拡張」が発生します。
これらの変換で問題が発生するケースとして、ReadBlobLSBLong() の返り値が 0x8000 である場合について考えてみます。
変換1において、64ビットの符号無し整数0x0000...8000は上位48ビットが切り捨てられ、16ビット符号付き整数0x8000に変換されます。符号付き整数における最上位ビットは符号ビットの意味を持つので、0x8000は最上位ビットが1で下位ビットは全てゼロ、つまり16ビット符号付き整数の負の最大値になります。この値は変換2で64ビットの符号付き整数に変換されますが、その際、変換元の型が符号付きであるため、「符号拡張」が発生し、上位ビットには符号ビットが伝播することになります。符号ビットは1ですので、上位48ビットは全て1で埋められることになります。図にすると下記のようになります。

続く式2において、dib_info.width の値は、unsigned long にキャストされた後、image->columns に代入されています。
式2: image->columns=(unsigned long) dib_info.width;
構造体 Image の定義を見ると、メンバー変数 columns は unsignd long型であることが分かります。(magick/magick-type.h より抜粋)
typedef struct _Image { ... unsigned long columns, rows, depth, colors; ... } Image;
つまり、式1で得られた符号付き整数 0xFFFF...8000 が、式2において符号無し整数として解釈されることになります。結果として、image->columns には 18446744073709518848 という非常に大きな値が代入されることになります。
この非常に大きな値は、式3においてメモリ割り当て時のバイト数の計算に用いられています。
式3: AcquireMagickMemory( (size_t) MagickMax(bytes_per_line, image->columns+256) * image->rows * sizeof(*pixels));
AcquireMagickMemory()のシグネチャは次の通りです。引数の型は size_t です。
void *AcquireMagickMemory(const size_t size)
image->columns に非常に大きな値が渡されると、式3の引数の式の乗算演算において演算結果がsize_tを超え、ラップアラウンドが発生する可能性があります。その結果、ヒープバッファオーバーフローの脆弱性につながるというのが今回の脆弱性でした。このように、符号付きの型と符号無しの型との間の型変換は符号エラーにつながることが多く、注意が必要です。
ImageMagic6.3.5を見ると、この脆弱性は次のように修正されていることが分かります。
- image->columns=(unsigned long) dib_info.width; + image->columns=(unsigned long) MagickAbsoluteValue(dib_info.width);
MagickAbsoluteValue(x)は、xの値が0より小さい場合に‐xを返し、それ以外の場合は、xを返す関数です。この関数を途中に挟むことで、符号拡張の問題の発生を防ぐように修正されています。ただ、この修正には言語仕様上の未定義の動作が含まれています。それを説明するために、MagickAbsoluteValue() の定義を再掲します。
static inline long MagickAbsoluteValue(const long x) { if (x < 0) return(-x); return(x); }
引数が負の値であれば、単項マイナス演算の結果を返し、正の値であればそのまま返していますが、負の最大値に対して2の補数の単項マイナス演算(unary negation)を行うと、結果の値は long で表現できる値を超えるため、言語仕様上は未定義の動作となります。

今回の修正箇所についていえば、MagickAbsoluteValue()の結果の値は unsigned long 型にキャストされているため、「たまたま」多くの処理系では問題は発生しないと考えられますが、セキュアコーディングの観点からは好ましい修正とは言えません。また、絶対値を返す関数の返り値の型は、符号付き型である必要はないでしょう。
式1に残る疑問
今回紹介したコードの式1は、WindowsのBMPイメージファイル(DIB)のヘッダから各種の値を取り出すコードの一部で、Image Width (ビットマップの横幅、単位はピクセル) を取り出している箇所であると思われます。BMPのヘッダの構造は Wikipedia の [Windows bitmap](https://ja.wikipedia.org/wiki/Windows_bitmap)に解説されています。ヘッダの定義によると、Image Width は4バイトであり、それに対応するように ImageMagick の DIBInfo 構造値の width フィールドも long 型として定義されており、long 型の返り値を返す ReadBlobLSBLong() を使ってヘッダの値を読み取っています。しかし、式1 では、返り値をあえて short にキャストしています。今回の解説では、この点について明らかにできませんでしたが、なぜこのような実装になっているのかは疑問として残ります。
CERT C セキュアコーディングスタンダード
今回紹介した脆弱性を作り込まないコーディングについては、以下のルールに詳しく解説しています。こちらも併せて参照ください。