戸田 洋三(JPCERTコーディネーションセンター) [著]
今回はsizeofオペレータをとりあげます。sizeofは、引数に与えたオブジェクトや型名から、その型のデータがメモリ上に占めるバイト数を求めるオペレータです。皆さんも、メモリ領域を動的に確保したいときや文字列操作などに関連して使ったことがあるでしょう。まずはsizeofオペレータの使用上の注意について説明し、その後でsizeofオペレータの誤用例を2つ紹介します。
はじめに
今回はsizeofオペレータをとりあげます。sizeofは、引数に与えたオブジェクトや型名から、その型のデータがメモリ上に占めるバイト数を求めるオペレータです。皆さんも、メモリ領域を動的に確保したいときや文字列操作などに関連して使ったことがあるでしょう。まずはsizeofオペレータの使用上の注意について説明し、その後でsizeofオペレータの誤用例を2つ紹介します。
sizeofオペレータ使用上の注意
「sizeofオペレータの使用上の注意 その1」は、プログラマが意図する正しい引数を渡すこと、です。メモリ上のオブジェクトのコピーや移動などの操作は、オブジェクトのメモリ上のサイズにもとづいて行います。オブジェクトのサイズを間違っていたら、アクセス違反やオーバフローといった脆弱性につながることは容易に想像できるでしょう。
「sizeofオペレータの使用上の注意 その2」は、引数に副作用を持つような式を渡さないこと、です。C言語仕様では、sizeofオペレータが引数を評価するのは引数の型が可変長配列型であるとき、それ以外の場合は引数を評価しない、と明記しています(C言語仕様セクション6.5.3.4)。
例えば、次のようなコードではaの値はインクリメントされません。
int a = 14; int b = sizeof(a++); printf("%d\n", a);
3行めのprintfが出力するaの値は14のままです。sizeofは「オペレータ」(演算子)なのでかっこの中の式は、関数のように評価されるわけではありません。かっこの中はa++という式として認識されますが、sizeofオペレータにとってその計算をすることが仕事ではなく、指定された引数の型(サイズ)が分かればよいのです。
sizeofオペレータは引数の型を知りたいだけなので、不要な計算をしないのだ、と理解するとよいでしょう。コード上には a++ と書いてあるのにそれが評価されないというのは、プログラマの誤解を招くので避けるべきです。
ちなみに引数に可変長配列型の式が渡される例として、C言語仕様には以下のコード例が掲載されています(C言語仕様セクション6.5.3.4 EXAMPLE 3)。
#includesize_t fsize3(int n) { char b[n+3]; // variable length array return sizeof b; // execution time sizeof }
可変長配列bのサイズは関数 fsize3の実引数nに依存しており、実行時にbを評価してはじめてそのサイズが分かるというわけです。
「sizeof オペレータの使用上の注意 その3」は、間違ってポインタ自身のサイズを求めないように注意、です。これは注意その1とかぶりますが、あえて書きました。例えば、以下のコード例はdouble型データの配列を動的に確保しようとしています。
double *allocate_array(size_t num_elems){ double *d_array; if (num_elems > SIZE_MAX/(sizeof d_array)){ /* エラー処理 */ } d_array = (double *)malloc((sizeof d_array) * num_elems); if (d_array == NULL){ /* エラー処理 */ } return d_array; }
2か所使われている sizeofオペレータの引数がどちらもポインタなので、返り値はポインタ自身のサイズになります。実際に求めたいのはポインタが指している先のメモリ領域のサイズなので、sizeof(d_array)ではなくsizeof(*d_array)と書くべきです。あるいは型名を使ってsizeof(double)としてもいいでしょう。
「その1」と「その3」で挙げた問題が実際のライブラリで見つかっています。標準的な画像データを扱うライブラリとIMAPサーバのデーモンで見つかった例を以下に紹介します。
libpngでの誤用例
libpngライブラリ は、png形式の画像データを扱う定番のライブラリです。さまざまなアプリケーションが libpngライブラリを組み込んで使っています。そのような重要な位置付けにあるライブラリに、「使用上の注意 その1」で説明したような簡単な間違いがひそんでいました。
png形式の画像データは、chunkと呼ぶ単位にまとめられたデータの集まりとして表現されます。chunkには必須の(必ず存在しなければならない)ものと、必須ではない(存在しなくてもよい)ものが定義されています。必須ではないchunkの一つとして「sPLT chunk」があります。画像データを減色して扱うときの補助的な情報を提供するものです。libpngライブラリの内部では、sPLT chunkのデータを格納するデータ構造としてpng_sPLT_tとpng_sPLT_entryという2つの構造体を使っています。
■libpng-1.2.13の「 png.h」から
/* * The following two structures are used for the in-core representation * of sPLT chunks. */ typedef struct png_sPLT_entry_struct { png_uint_16 red; png_uint_16 green; png_uint_16 blue; png_uint_16 alpha; png_uint_16 frequency; } png_sPLT_entry; typedef png_sPLT_entry FAR * png_sPLT_entryp; typedef struct png_sPLT_struct { png_charp name; /* palette name */ png_byte depth; /* depth of palette samples */ png_sPLT_entryp entries; /* palette entries */ png_int_32 nentries; /* number of palette entries */ } png_sPLT_t;
sPLT chunkに対応するデータ構造はpng_sPLT_tです。sPLT chunkには複数のパレット情報を含めることができるため、個々のパレット情報を独立した構造体 png_sPLT_entryに格納し、png_sPLT_t構造体には png_sPLT_entryの配列へのポインタを持たせています。
さて、libpngライブラリが提供する関数の中に、sPLT chunkを組み立てるライブラリ関数 png_set_sPLT()がありました。この関数の中で、sPLT chunkのために動的にメモリを確保しています。そのときにやってしまったのが2つの構造体の取り違いでした。個々のパレット情報を入れる構造体png_sPLT_entryのためのメモリ領域を確保しようとして、malloc()の引数にpng_sPLT_tのサイズを指定していたのです。
これを修正するパッチは単純で、sizeofの引数に指定する構造体の名前を直すだけです。ライブラリ自身のソースコードの修正は単純ですが、システム全体の対応は、それほど単純ではありません。libpngライブラリを使っているアプリケーションはどれか、それらは静的リンクされているのか動的リンクされているのかをすべて調べ、静的リンクされているアプリケーションについては、修正済みライブラリを使ってコンパイルし直すといった対応が必要になります。
この問題にはCVE-2006-5793という番号が付けられており、DoS攻撃に使われる危険があるとされています。MITREのページで関連情報を見てみると、Linux ベンダやAppleなどに加えて、Google Android SDKにも影響があったことが分かります。
Cyrus imapdでの誤用例
次にCyrus imapdにおけるsizeofオペレータの誤用例を見てみましょう。以下のコードはCyrus imapdのコードからの抜粋です。
■「cyrus-imapd-2.2.13/sieve/script.c」から
int do_action_list(..., char *actions_string, ...){ ...... switch(a->a){ case ACTION_REJECT: ...... snprintf(actions_string + strlen(actions_string), sizeof(actions_string) - strlen(actions_string), "Rejected with: %s\n", a->u.rej.msg); break; case ACTION_FILEINTO: ...... snprintf(actions_string + strlen(actions_string), sizeof(actions_string) - strlen(actions_string), "Filed into: %s\n", a->u.fil.mailbox); break; case ACTION_KEEP: ...... snprintf(actions_string + strlen(actions_string), sizeof(actions_string) - strlen(actions_string), "Kept\n"); break; case ACTION_REDIRECT: ...... snprintf(actions_string + strlen(actions_string), sizeof(actions_string) - strlen(actions_string), "Redirected to %s\n", a->u.red.addr); break; ...... } }
このswitch文は、a->aの内容に応じた処理を行い、actions_stringが指すメモリ領域にログ情報を書き足していく部分です。各case節の中では、actions_stringの指すメモリ領域の、既に書き込まれている文字列の直後に新たにログ情報を書き込んでいます。その際、確保されているメモリ領域をはみ出さないように、snprintf()を使って、sizeof(actions_string)-strlen(actions_string)という式によって、書き込む文字数を制限しています。.
しかし actions_stringはポインタでした。従ってsizeof(actions_string) は、actions_stringが指すメモリ領域のサイズではなく、ポインタのサイズを表しています。それは意図していたものよりずっと小さい数値であることは間違いありません。その結果、(ログが書き込まれていくにつれて)sizeof(actions_string)-strlen(actions_string)は負の値になってしまうと考えられます。しかし、snprintf()の引数としてはsize_t、すなわち符号無し整数型として扱われるため、大きな正の値として解釈され、実質的に書き込む文字数を制限できていないということになります。
さらに悪いことに、書き込むログ情報を、攻撃者がある程度操作できる状況でした。攻撃者は入力を工夫することにより、actions_stringの指すメモリ領域の後ろに任意の値を書き込ませることができたのです。
上書きされる部分に、プログラムの挙動を制御する変数やライブラリ関数のエントリテーブルなどが置かれていた場合、攻撃者はプログラムを乗っ取ることができる可能性が高くなります。実際、この問題は任意のコード実行に繋がる脆弱性としてCVEに登録されています(CVE-2009-2632、CERT/CC VU#336053)。
この問題を修正するために、開発者は、sizeofオペレータによるメッセージ領域のサイズの計算を止めるという方法を選択しました。
■確保するメモリ領域の大きさを表すシンボルを定義
#define ACTIONS_STRING_LEN 4096
■snprintf()はすべて次のように修正:
snprintf(actions_string + strlen(actions_string), ACTIONS_STRING_LEN - strlen(actions_string), "Redirected to %s\n", a->u.red.addr);
これによって、当初の意図どおりの動作を得ることができました。
■「cyrus-imapd-2.2.13p1/sieve/script.c」から
#define ACTIONS_STRING_LEN 4096 int do_action_list(..., char *actions_string, ...){ ...... switch(a->a){ case ACTION_REJECT: ...... snprintf(actions_string + strlen(actions_string), ACTIONS_STRING_LEN - strlen(actions_string), "Rejected with: %s\n", a->u.rej.msg); break; case ACTION_FILEINTO: ...... snprintf(actions_string + strlen(actions_string), ACTIONS_STRING_LEN - strlen(actions_string), "Filed into: %s\n", a->u.fil.mailbox); break; case ACTION_KEEP: ...... snprintf(actions_string + strlen(actions_string), ACTIONS_STRING_LEN - strlen(actions_string), "Kept\n"); break; case ACTION_REDIRECT: ...... snprintf(actions_string + strlen(actions_string), ACTIONS_STRING_LEN - strlen(actions_string), "Redirected to %s\n", a->u.red.addr); break; ...... } }
今回は、sizeofオペレータの使用上の注意点とそれに関連した誤用例を紹介しました。今回紹介した2つの誤用例はどちらも過去のバージョンも含めてソースコードが公開されています。具体的にどのような間違いだったか、どのように修正したのか、を確かめてみてください。
参考資料
- C言語仕様セクション6.5.3.4 sizeof
- EXP01-C: ポインタが参照する型のサイズを求めるのにポインタのサイズを使わない
- ARR01-C. Do not apply the sizeof operator to a pointer when taking the size
- EXP06-C: sizeof 演算子のオペランドは副作用を持たせない
- PNG (Portable Network Graphics) Specification, Version 1.2 4. Chunk Specifications
- CVE-2006-5793
- Cyrus IMAPd buffer overflow vulnerability
- CVE-2009-2632