gbase64.cの問題点
g_base64_encode()では、整数オーバーフローが発生する可能性があります。base64変換した結果を収めるメモリの大きさを計算する部分を見てみましょう。
/* len は base64 エンコードする元データの長さ */ out = g_malloc(len * 4 / 3 + 4);
base64エンコードするとその大きさはおよそ4/3倍になります。素直に変換後の長さを計算しており、分かりやすいコードなのですが、入力データの長さlenはとても大きな値になっているかもしれません。
(len * 4 / 3 + 4)を計算する過程は以下のようになるでしょう。
ステップ1: a = (len * 4) ステップ2: b = a / 3 ステップ3: c = b + 4
lenがとても大きな値の場合、ステップ1の計算結果が整数型で表現できる大きさを超えてしまう可能性があります。その場合、あふれた桁の部分は無視され、計算結果は思っていたよりもごく小さな値となってしまいます(C言語仕様ではmodwrapセマンティクスと呼ばれています)。
実際に試してみよう
計算結果がラップアラウンドしてしまう様子を確認してみましょう。
[int-a.c] #include <stdio.h> #include <errno.h> int main(int argc, char *argv[]){ if (2 <= argc){ errno = 0; long i = strtol(argv[1], (char **)NULL, 10); if (errno == 0){ printf("%ld * 4 == %ld\n", i, i * 4); printf("%ld * 4 /3 == %ld\n", i, (i * 4)/3 ); } } }
long型が32ビットで表現される環境で動かしてみるとこうなりました。
[実行結果] $ ./int-a 1073741825 1073741825 * 4 == 4 1073741825 * 4 / 3 == 1 $
1073741825のビット表現は以下のようになります。
MSB LSB 0100 0000 0000 0000 0000 0000 0000 0001
31ビット位置と1ビット位置の2か所に1、それ以外は0です。4倍するということは結局左向きに2ビット分シフトするのだと考えれば、結果が4になることが理解しやすいでしょう。例え数学的な計算結果が整数型に収まる値であっても、途中で結果があふれてしまっていては、正しい値を求めることはできないのです。
どのような影響があるか
この計算結果はg_malloc()の引数に使われています。つまり、確保するメモリ領域が意図していたよりもずっと小さくなる、という結果を招きます。そこにデータを書き込んでいくことで、ヒープオーバフローが発生します。この問題にはCVE-2008-4316という識別番号がつけられていますが、その説明にはこうあります。
(CVE-2008-4316 の Description より)
Multiple integer overflows in glib/gbase64.c in GLib before 2.20
allow context-dependent attackers to execute arbitrary code via a
long string that is converted either (1) from or (2) to a base64
representation.
(日本語訳)
GLib の 2.20 より前のバージョンの glib/gbase64.c には
複数の整数オーバフローがある。実行時の状況にもよるが、
長い文字列に関する
(1) base64 からの変換、あるいは
(2) base64 への変換
によって、任意のコードを実行させることが可能になる。
base64変換を使うアプリケーションといえば、メールアプリやwebブラウザなどが思い浮かびます。攻撃コードを仕込んだスパムメールの閲覧、あるいは細工したwebページのアクセスによって、被害者のPC上で(ユーザの権限で)攻撃コードが実行される可能性があるということです。
glibではどのように修正したか
glibでは以下のように、除算を先に行うように修正することで、整数オーバーフローの可能性を排除しました。
/* g_malloc() の引数式が G_MAXSIZE 以上にならないことを確認している */ if (len >= ((G_MAXSIZE - 1) / 4 - 1) * 3) g_error(......); // len が大き過ぎる場合はエラーにする out = g_malloc((len / 3 + 1) * 4 + 1);
また、g_malloc()の直前のif文により、最終的な計算結果が所定の範囲に収まっているかどうかのチェックも加えています。
len >= ((G_MAXSIZE - 1) / 4 - 1) * 3
この条件式を変形してみると
((len * 4/3) + 4) + 1 >= G_MAXSIZE
となります。つまりg_malloc()の引数に与えた式がG_MAXSIZEより小さい値でなければ、エラー処理を行うようにしたのです。ある式の計算結果が想定した範囲に収まっていることを確認する時、計算過程でラップアラウンドが発生することを避けるためには、このように条件式を変形して扱うことが必要になります。
計算式の中でオーバーフローを起こさないように注意しよう
「数学的な」計算式では、そのなかの部分式の値の大きさについて考慮することはあまりないかもしれません。しかし、プログラムに実装する計算過程では、各部分式についてその値が適切な範囲に収まることまで確認すべきです。
また、メモリを動的に確保するmalloc()では、確保するメモリサイズを指定する引数の値を正しく計算するために細心の注意を払うことが必要です。malloc()の引数に複雑な計算式を使っていたり、引数の値の範囲チェックを行なっていない場合には何らかの問題があると思ってまず間違いありません。
皆さんが今までに書いたコードでmalloc()を使っているものがあれば、もう一度見直してみてください。そして、攻撃される可能性がないかどうか、きっちり確認することをおすすめします。
参考資料
- CVE-2008-4316:Multiple integer overflows in glib/gbase64.c in Glib before 2.20
- [glib]Diff of /trunk/glib/gbase64.c
- oCERT#2008-015 glib and glib-predecessor heap overflows
- RFC 4648:The Base16, Base32, and Base64 Data Encodings
- INT08-C:全ての整数値が範囲内にあることを確認する
- INT30-C:符号無し整数の演算結果がラップアラウンドしないようにする
- INT32-C:符号付き整数演算がオーバーフローを引き起こさないことを保証する
- MEM35-C:オブジェクトに対して十分なメモリを割り当てる