戸田 洋三(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回取り上げるコードは、GUIツールキット「gtk+」で使われているライブラリglibの中で定義されている関数g_base64_encode()です。
はじめに
2009年末から6回にわたりセキュアコーディングに関する連載をさせていただきましたが、今回からは少々構成を変え、実際に公開されているライブラリやアプリケーションなど、脆弱性が発見されたコードを題材にして新しい連載を進めていきたいと思います。
今回の連載では、最初に問題のあるコードを示します。まずはこのコードだけを見て、どこに問題があるのか、考えてみてください。
コードの後には、コードに含まれる脆弱性を見つけるためのヒントや、コードが行おうとしていることを理解するために役立つ背景知識などを説明します。コードを見ただけではどこに問題があるのか分からない、といった場合は、これらの説明を手がかりに考えてみてください。
どこに問題があるのか分かったら、次にどのように修正すべきかを考えましょう。修正方法は一通りとは限りません。むしろ、複数の修正方法が考えられることが多いと思います。
最後に、実際にどのような修正が行われたか説明します。自分が考えた修正案と比較してみてください。
サンプルコード
今回取り上げるコードを以下に示します。ここで取り上げているのはGUIツールキット「gtk+」で使われているライブラリglibの中で定義されている関数g_base64_encode()です。この短いコードのなかに脆弱性があります。まずは、このコードだけを見て、どんな問題があるのか考えてみてください。
■コード例(glib/gbase64.c revision 7897より)/** * g_base64_encode: * @data: the binary data to encode * @len: the length of @data * * Encode a sequence of binary data into its Base-64 stringified * representation. * * Return value: a newly allocated, zero-terminated Base-64 encoded * string representing @data. The returned string must * be freed with g_free(). * * Since: 2.12 */ gchar *g_base64_encode (const guchar *data, gsize len) { gchar *out; gint state = 0, outlen; gint save = 0; g_return_val_if_fail (data != NULL, NULL); g_return_val_if_fail (len > 0, NULL); /* We can use a smaller limit here, since we know the saved state is 0 */ out = g_malloc (len * 4 / 3 + 4); outlen = g_base64_encode_step (data, len, FALSE, out, &state, &save); outlen += g_base64_encode_close (FALSE, out + outlen, &state, &save); out[outlen] = '\0'; return (gchar *) out; }
この関数は、引数として受け取ったデータをbase64エンコーディングした結果を返します。base64エンコーディングは、SJISやUTF-8文字列、あるいはバイナリデータなどのように最上位ビットが1になっている文字(オクテット)を含むデータを、最上位ビットが必ず0となる文字だけで表現するものです。
昔のメールサーバの実装には、メールはUS-ASCII文字(各文字の最上位ビットは0)のみで構成されているものと想定し、メールデータ中のオクテットの最上位ビットを強制的に0にしてしまうものがありました。そのような状況でも正しくメールを送受信できるように考案された技術の1つがbase64エンコーディングです。詳細はRFC 4648を参照ください。
base64エンコーディングでは、8ビット3文字(合計24ビット)を6ビット単位に分割し、US-ASCII4文字に変換します。つまりbase64エンコードした結果は、単純計算で元データの4/3倍の長さになるわけです。
受け取ったデータを実際にbase64エンコーディングに変換する作業は、g_base64_encode()から呼び出しているg_base64_encode_step()およびg_base64_encode_close()で行われます。これはメモリ上に一度に置けないような大きなデータの変換を考慮しているためで、元データを分割し、g_base64_encode_step()で順番に変換していくことを想定した設計になっているのです。
例えば、元データを10個に分割して扱う場合は、g_base64_encode_step()を呼び出してはその出力を処理する、という手順を10回繰り返します。元データをすべて入力し終ったら、最後にg_base64_encode_close()を呼び出し、まだ出力せずに残っているデータがあればパディングビットを加えて最後の出力を得ます。
今回の題材としているg_base64_encode()では、変換結果は一度にすべてメモリ上に収まるという前提で、変換結果全体を書き出すメモリを先にg_malloc()で確保しています。そのため、g_base64_encode_step()の呼び出しは1回だけになっています。また、変換結果をC言語の文字列として扱うために、変換結果の末尾にnull終端文字である'\0'を追加しています。
さて、g_base64_encode()の問題がどこにあるか、あなたは見つけられましたか? また、修正コードはできましたか?
では、解説編にいきましょう。
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#include 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:オブジェクトに対して十分なメモリを割り当てる