見落としがちな整数関連の脆弱性(前編)もいちど知りたい、セキュアコーディングの基本(4)(1/2 ページ)

前回、前々回と、バッファオーバーフローの脆弱性についてみてきました。今回は、整数の取り扱いに関する脆弱性を取り上げたいと思います。

» 2013年05月16日 18時00分 公開

整数の脆弱性 2013

 いまをさかのぼること6年前の2007年。情報セキュリティ関連のリサーチを行う米国の非営利団体、MITREが、ソフトウェアの脆弱性のトレンドに関する調査レポートを公表しました。調査は2006年の1年間に発見された脆弱性を対象に行われ、レポートには、その年の脆弱性の傾向とその分析結果がまとめられています。

 さて、このレポートの概要には、整数オーバーフローについて書かれた次のような一節が見つかります。

整数オーバーフローは、過去数年の間、かろうじてトップ10入りする程度の脆弱性にすぎなかったが、(今回の調査では)OSベンダのセキュリティアドバイザリにおいて、バッファオーバーフローに次いで2番目に多い脆弱性であることが分かった。これは(OSのような)注目を集めるソフトウェアに研究者の興味が向かっていることを示唆しているのではないか


 この状況は、レポートの公表から6年が経過したいま、どのように変化しているのでしょうか(あるいは変化していないのでしょうか)。整数関連の脆弱性の「いま」をかいまみるべく、脆弱性データベースの最新のデータを基に、脆弱性の傾向を調査してみました。

Linuxカーネルと整数の脆弱性

 MITREのレポートには、「OSベンダのセキュリティアドバイザリにおいて……」と書かれていましたが、今回はオープンソースOSであるLinuxのカーネルにおける脆弱性を対象に調べてみました("linux kernel"を検索キーワードとして調べました)。

 Linux以外にもさまざまな種類のOSが存在しますが、デスクトップからサーバ、組み込み製品など、幅広く利用されているLinuxカーネルの脆弱性には、OSの脆弱性の縮図を見て取ることができるのではないかという狙いもあります。Windowsなどのクローズドソースな製品は、ソースコードを見て脆弱性の詳細を確認できないため、対象外としました。

 また、「いま」の傾向を知りたいので、2010年1月から2012年12月までの直近3年間にNVDに登録された脆弱性を調査対象としています。この期間に登録されたLinuxカーネルの脆弱性は331件ありますが、この数字をさらに「脅威」の高い脆弱性(CVSSのSeverity[Base Score Range]が7.0以上の[High]に分類される脆弱性)に限定すると、74件に絞り込まれます。

 さて、この74件の脆弱性のアドバイザリを調べ、脆弱性が作り込まれた原因を1件1件確認してまとめたものが図1です。

図1 2010年から2012年にかけて発見されたLinuxカーネルの脆弱性の原因

 図1を見ると、整数の処理に関する脆弱性の多さが目に留まります。約半数は整数関連の脆弱性です。中でも「整数オーバーフロー」の脆弱性は、全体の約15%を占めています。「整数値の範囲チェック不備」は、ネットワークパケットを通じて取得したデータのサイズや長さ、アドレス値などの整数データが、プログラムが想定する範囲の値であるかをそもそもチェックしていなかったり、チェックが不十分であるケースです。

 整数の脆弱性が他の脆弱性と異なる特徴として、脆弱性が「間接的」に悪用されるという点があります。

 典型的な例としては、整数の脆弱性が原因で変数の値が想定外になり、その値を動的メモリ割当てのバイト数として使用した結果、不十分なメモリ領域が確保され、データをメモリへコピーする際にバッファオーバーフローが発生し、コード実行につながるケースがあります。図の分類には記載しませんでしたが、整数オーバーフローや整数値の範囲チェック不備などが原因で発生する「バッファオーバーフロー」は、74件中15件と全体の2割を占めており、こちらも脅威が高く、また作り込まれやすい脆弱性であることは、前回前々回の連載でも見た通りです。

 脆弱性の原因は単純なバグであることが少なくありませんが、整数の取り扱いに関するバグもまたしかりです。そして、Linuxカーネルにおける整数の脆弱性を調査したデータから言えることは、2007年にMITREがレポートを公開した当時といまを比較しても、状況はあまり変わっていないのではないか、ということです。

 CやC++言語を使ってコーディングする以上、整数の問題を避けて通ることはできません。それは2013年のいまもあまり変わっておらず、プログラマには、整数に関する脆弱性が作り込まれるメカニズムと、その対策方法についての理解が求められているのです(それでも間違いは避けがたいのですが……)。

 では、整数関連の脆弱性について具体的に見ていきましょう。

Cプログラムにおける整数の脆弱性の種類

 Cの各整数型は、表現できる最小値と最大値が決まっており、その範囲は処理系(アーキテクチャ)によって異なります。この当たり前のことをついうっかり忘れてしまい、演算結果の整数値やプログラムが外部から受け取る整数値が、それを格納する「箱」に収まらない場合、整数の脆弱性が発生します。

 2の補数表現を使う典型的な32ビット環境で、各型が取り得る値の範囲を図2に示しておきます。

ビット幅 最小値 最大値
signed char 8 -128 127
unsigned char 8 0 255
short 16 -32768 32767
unsigned short 16 0 65535
int 32 -2147483648 2147483647
unsigned int 32 0 4294967295
long 32 -2147483648 2147483647
unsigned long 32 0 4294967295
long long 64 -9223372036854775808 9223372036854775807
unsigned long long 64 0 18446744073709551615
図2 標準データ型が取り得る値の範囲

 今回は整数の脆弱性を、

  1. 整数オーバーフロー(ラップアラウンド)
  2. 符号エラー
  3. 切り捨て
  4. その他

の4つのカテゴリに分類し、詳しく見ていきましょう。

整数オーバーフロー(ラップアラウンド)

 整数オーバーフロー(あるいはラップアラウンド)は、演算結果の値が、演算式の型で表現できる範囲を超える場合に発生します。オーバーフローが発生すると、値はプログラマが想定していなかった値になります(たとえば、小さな正の値や、負の値)。

 プログラムがこの動作を想定して書かれているのであればよいのですが(符号なし整数のラップアラウンドは、必ず決まった動作をすることが言語仕様として規定されています)、想定外である場合や、未定義の動作となる符号付き整数のオーバーフローは、セキュリティ上の問題につながることがあります。

 特にCやC++では、ループ処理の制御、メモリ割り当てやデータのコピー、文字列の結合といったローレベルの処理を行う際、オフセット値やサイズの計算に整数式が用いられることがあります。バイト数を求める式の値が整数オーバーフローの結果小さな値になり、必要なメモリ領域が確保されず、その後の処理でバッファオーバーフローが発生する、というのは典型的なパターンです。

 LinuxカーネルのBluetooth関連のモジュールには、まさに先に述べたような脆弱性が存在しました。CVE-2011-2497の識別子を付されたLinuxカーネルの脆弱性は、Bluetoothのプロトコルスタックの中でもデータリンク層における処理を担当するL2CAP(Logical Link Control and Adaptation Protocol)プロトコルの実装に存在しました。脆弱性の見つかったl2cap_config_req()関数は、Bluetoothデバイスとホスト間の接続が完了し、データ転送が行われる前に呼び出され、データ転送の準備を行います。

図3 LinuxカーネルにおけるL2CAPの位置付け(出典:http://www.6test.edu.cn/~lujx/linux_networking/0131777203_ch11.html

 l2cap_config_req()関数は、デバイスから受け取ったL2CAPパケットのヘッダーからコマンドサイズ(u16 cmd_len)を取得し、この値から設定要求ヘッダのサイズを引いた値を、符号付きローカル変数のlenに格納しています(2336行目)。整数オーバーフローが発生するのはこの減算演算においてです。

図4 Bluetoothデバイスとホスト間の接続

 L2CAPパケットのヘッダーが攻撃者によって改ざんされ、cmd_lenの値がゼロである場合、何が起きるでしょうか。 sizeof演算子の結果の型がunsigned long intであるような処理系の場合、cmd_len - sizeof(*req)という演算式の結果の型は、「通常の算術型変換」の結果、unsigned long int になります。したがって、計算結果の負の値はラップアラウンドの結果、最上位ビットの立った非常に大きな正の値として表現され、これが符号付き整数型であるlenに代入されると負の値として評価されることになります。

struct l2cap_chan {
...(中略)...
  __u8    conf_req[64];
  __u8    conf_len;
...(中略)...
};
struct l2cap_conf_req {
  __le16     dcid;
  __le16     flags;
  __u8       data[0];
} __packed;
[linux/kernel/git/torvalds/linux-2.6.git] / include / net / bluetooth / l2cap.h
2306 static inline int l2cap_config_req(struct l2cap_conn *conn, struct l2cap_cmd_hdr *cmd, u16 cmd_len, u8 *data)
2307 {
2308         struct l2cap_conf_req *req = (struct l2cap_conf_req *) data;
2309         u16 dcid, flags;
2310         u8 rsp[64];
2311         struct l2cap_chan *chan;
2312         struct sock *sk;
2313         int len;
2314 
2315         dcid  = __le16_to_cpu(req->dcid);
2316         flags = __le16_to_cpu(req->flags);
2317 
...(snip)...
2334 
2335         /* Reject if config buffer is too small. */
2336         len = cmd_len - sizeof(*req);
2337         if (chan->conf_len + len > sizeof(chan->conf_req)) {
2338                 l2cap_send_cmd(conn, cmd->ident, L2CAP_CONF_RSP,
2339                                 l2cap_build_conf_rsp(chan, rsp,
2340                                         L2CAP_CONF_REJECT, flags), rsp);
2341                 goto unlock;
2342         }
2343 
2344         /* Store config. */
2345         memcpy(chan->conf_req + chan->conf_len, req->data, len);
2346         chan->conf_len += len;
[linux/kernel/git/torvalds/linux-2.6.git] / net / bluetooth / l2cap_core.c

 負の値を格納したlenを使った処理が行われるのは2345行目です。lenはmemcpy()関数の第3引数で使用されています。memcpy()関数のシグネチャは次の通りです。

void * memcpy(void *restrict s1, const void *restrict s2, size_t n);

 第3引数はsize_t、つまり符号なし整数型です。ここで、負の値であるlenは非常に大きな正の値として評価され、結果としてバッファオーバーフローが発生してしまいます。整数オーバーフローがバッファオーバフローにつながる典型的な脆弱性であることが見て取れます。

 このコードに対する修正パッチを見てみましょう。

diff --git a/net/bluetooth/l2cap_core.c b/net/bluetooth/l2cap_core.c
index 56fdd91..7d8a66b 100644 (file)
--- a/net/bluetooth/l2cap_core.c
+++ b/net/bluetooth/l2cap_core.c
@@ -2334,7 +2334,7 @@ static inline int l2cap_config_req(struct l2cap_conn *conn, struct l2cap_cmd_hdr
 
        /* Reject if config buffer is too small. */
        len = cmd_len - sizeof(*req);
-       if (chan->conf_len + len > sizeof(chan->conf_req)) {
+       if (len < 0 || chan->conf_len + len > sizeof(chan->conf_req)) {
                l2cap_send_cmd(conn, cmd->ident, L2CAP_CONF_RSP,
                                l2cap_build_conf_rsp(chan, rsp,
                                        L2CAP_CONF_REJECT, flags), rsp);
修正パッチ

 これまで行われていたlenの値の上限チェックに加えて、整数オーバーフロー対策として、lenが負の値でないかの下限チェックを追加しているところがポイントです。

 整数オーバーフローは、符号付き整数と符号なし整数とでは、コンパイラの取り扱いが異なります。以下、簡単にまとめておきます。

符号付き整数:

  • 未定義の動作(Undefined Behavior)である
    • 未定義の動作とは?
      • 「可搬性がないもしくは正しくないプログラムの構成要素を使用したときの動作……この規格が何ら要求を課さないもの」(C言語仕様)
      • つまり、コンパイラはどんなコードを生成してもよい
      • 最適化により削除される(対応するオブジェクトコードが生成されない)可能性がある
    • 言語レベルでは検知できないが、ハードウェアレベルでは検知できる(発生時にflag registerのEFLAGSがセットされる)

符号なし整数:

  • 決してオーバーフローはしない("never overflow")
    • 上位ビットの切捨てが発生する
    • 「符号無しオペランドを含む計算は、決してオーバフローしない。すなわち、結果を符号無し整数型で表現できないときは、その型で表現し得る最大値より1だけ大きい数を法とする剰余を結果とする」(C言語仕様)

 CERT Cセキュアコーディングスタンダードでは、符号なし整数のラップアラウンドと符号付き整数のオーバーフローの2パタンーンに分けて、コーディングルールを紹介しています。

【参考リンク】

▼INT30-C. 符号無し整数の演算結果がラップアラウンドしないようにする

https://www.jpcert.or.jp/sc-rules/c-int30-c.html

▼INT32-C. 符号付き整数演算がオーバーフローを引き起こさないことを保証する

https://www.jpcert.or.jp/sc-rules/c-int32-c.html


       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。