前ページで紹介した「切り捨て」の反対は、「符号拡張/ゼロ拡張」です。切捨ての発生が「大きい型から小さい型への変換」時であるのに対し、拡張はその逆、「小さい型から大きい型への変換」時に発生します。その際、存在しなかった上位ビットは、元の型の符号の有無によって下記の2通りの方式で埋められます。
この規則を意識せずにコーディングすると、予期せぬ値を生み出してしまうことがあります。例えば、下記の単純なコードを考えてみましょう。
short len; ... len = get_len(); memmove(dst, src, len);
このコードはsrcの内容をlenバイトdstへコピーしようとしていますが、get_len()の返り値が0より小さい値である場合、memmove()関数には負の値のlenが渡されてしまいます。
そこで下記のように修正してはどうでしょうか?
short len; ... len = get_len(); memmove(dst, src, (unsigned int)len);
符号なし整数であれば負の値にはならないので、問題ないと言えるでしょうか? 残念ながら、shortからunsigned intへの変換時に符号拡張が発生するため、ゼロより小さいlenの値は、キャストの結果、非常に大きい正の値として解釈されてしまいます(memmove関数の第3引数の型はsize_tなので、明示的にキャストしなくても問題は発生します)。
このように、バッファにデータをコピーする関数のサイズ引数(符号なしの型として宣言されている)に、小さい符号付き整数を渡した結果、符号拡張が発生し、バッファオーバーフローにつながるというのはよくある脆弱性です。
それでは、符号拡張が原因で作り込まれた脆弱性事例として、sendmail 8.12.8およびそれ以前のバージョンに作り込まれた脆弱性(CVE-2003-0161)について見てみましょう。
Michal Zalewski氏が発見し、「CVE-2003-0161」として公開された脆弱性の概要には次のように書かれています。
The prescan() function in the address parser (parseaddr.c) in Sendmail before 8.12.9 does not properly handle certain conversions from char and int types, which can cause a length check to be disabled when Sendmail misinterprets an input value as a special "NOCHAR" control value, allowing attackers to cause a denial of service and possibly execute arbitrary code via a buffer overflow attack using messages, a different vulnerability than CVE-2002-1337.
Sendmail 8.12.9 より前のバージョンにおいて、アドレスパーサー(parseaddr.c)のprescan()関数は、一部のchar型からint型への変換を適切に行っていなかった。そのため、Sendmailは入力データを制御用の値である"NOCHAR"であると誤解釈し、入力データの長さチェックを無効にされる。攻撃者は、メールのメッセージを利用して、サービス運用妨害を引き起こしたり、バッファオーバーフローにより任意のコードを実行したりする可能性がある。
sendmailのparseaddr()関数はメールアドレスをパースしてユーザー名とドメインに分解する関数ですが、この中で呼び出されるprescan()関数は、メールアドレスの余計な空白文字やコメント(丸カッコで囲まれる文字列)を削除したり、マッチングのとれない二重引用符や丸カッコの正規化を行う関数です。例えば、
taro(" yamada ") @ jpcert.or.jp
のようにコメントやスペースを含む一見不正に見えるメールアドレスも、RFC 2822のaddr-specの仕様に沿った正しいアドレスです。prescan()関数はRFC 2822の仕様に沿ったパーサーとして、このようなアドレスを正規化する役割を果たします。今回の脆弱性は、このprescan()関数に見つかりました。コードの関連部分のみを抜粋し、若干のコメントを付け加えたコードを下記に示します。
char ** prescan(char *addr, int delim, char pvpbuf[], int pvpbsize, char **delimptr, unsigned char *toktab) { register char *p; register char *q; register int c; ... p = addr; do { /* read a token */ for (;;) { /* store away any old lookahead character */ if (c != NOCHAR && !bslashmode) { /* see if there is room */ if (q >= &pvpbuf[pvpbsize - 5]) { usrerr("553 5.1.1 Address too long"); if (strlen(addr) > MAXNAME) addr[MAXNAME] = '\0'; returnnull: if (delimptr != NULL) *delimptr = p; CurEnv->e_to = saveto; return NULL; } /* squirrel it away */ *q++ = c; } /* read a new input character */ c = *p++; if (c == '\0') { // メールアドレスの終端に到達したにも関わらず、ダブルクオートのマッチングがおかしかったり、 // コメントが')'で終端されてなかったり、'<'のマッチングがおかしい場合のシンタックスエラー処理 p--; } ... /* chew up special characters */ *q = '\0'; if (bslashmode) { bslashmode = false; /* kludge \! for naive users */ if (cmntcnt > 0) { c = NOCHAR; continue; } else if (c != '!' || state == QST) { *q++ = '\\'; continue; } } if (c == '\\') { bslashmode = true; } ... /* see if this is end of input */ if (c == delim && anglecnt <= 0 && state != QST) break; ... } } while (c != '\0' && (c != delim || anglecnt > 0)); ... }
定数NOCHARの宣言は下記の通りです。
#define NOCHAR -1 /* signal nothing in lookahead token */
NOCHARは-1として定義されており、文字列の処理時に何らかのエラーが発生していることを示します。変数pはユーザーが入力したメールアドレスを参照するポインタです。do-whileのループはすべての文字列を読み込んだ際に終了します。
さて、このループ処理のなかで、バッファのサイズチェックを行っているのは下記のif文です。
/* see if there is room */ if (q >= &pvpbuf[pvpbsize - 5]) { ...
そして、このif文が評価されるのは、
if (c != NOCHAR && !bslashmode)
が真に評価され、if文内のブロックの処理が行われる場合のみです。つまり、c != -1であり、かつbslashmodeが偽である場合にのみ、if文のブロック内に置かれたバッファのサイズチェックが行われる仕様になっています。
問題は入力文字の読み取りを行う下記の1行です。
c = *p++;
pはchar型、cはint型であるため、この代入式では符号拡張が発生します。メールアドレスの中に0xFFが入っていると、符号拡張の結果cには0xFFFFFFFF(== NOCHAR)が代入されることになります。メールアドレスとして0x5C(バックスラッシュ文字)と0xFF(NOCHAR)が繰り返す0x5C0xFF0x5C0xFF...というパターンの文字列をprescan()に処理させることで、配列のサイズチェックを回避し、入力文字列(バックスラッシュ)のコピーを行わせることが可能です。pvpbuf[]のサイズは1256バイトです。これより長い細工されたメールアドレスを処理させられると、prescan()の呼出し元であるparseaddr()のローカル配列pvpbufにおいてバッファオーバーフローが発生します。
// # define PSBUFSIZE (MAXNAME + MAXATOM) // # define MAXNAME 256 /* max length of a name */ // # define MAXATOM 1000 /* max atoms per address */ char pvpbuf[PSBUFSIZE];
修正版のsendmail-8.12.9では、入力文字の読み取りを行うコードが下記のように修正されています。
c = (*p++) & 0x00ff;
ビット単位の論理AND演算子を使うことで、int型に符号拡張された*pの値の下位8ビットだけを取り出し、cに代入しています。結果として、ユーザーが入力した文字の文字コードが保持されたままcに格納されることになります。
C言語で文字列操作を行う場合、今回のコードの変数pのように、「単なるchar型」を使って文字データを取り出し、(より大きい型の)変数に代入するコードを書くことが少なくありません。そんな時は、char型が(ほとんどの場合)符号付き型であり、変数への代入時に符号拡張が発生することを思い出し、結果として得られる値が想定とは異なる値にならないか、チェックしましょう。
今回は整数の切り捨てと符号拡張の問題を取り上げました。整数の取り扱いに関しては、他にもいくつか注意すべきポイントがあります。CERT C セキュアコーディングスタンダードの整数のセクションも併せて一読をお勧めします。
LP64 への変換のためのガイドライン
http://docs.oracle.com/cd/E19683-01/816-3971/6ma7e09nn/index.html
CWE-197: Numeric Truncation Error
http://cwe.mitre.org/data/definitions/197.html
CWE-194: Unexpected Sign Extension
http://cwe.mitre.org/data/definitions/194.html
Copyright © ITmedia, Inc. All Rights Reserved.