見落としがちな整数関連の脆弱性(前編):もいちど知りたい、セキュアコーディングの基本(4)(1/2 ページ)
前回、前々回と、バッファオーバーフローの脆弱性についてみてきました。今回は、整数の取り扱いに関する脆弱性を取り上げたいと思います。
整数の脆弱性 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を見ると、整数の処理に関する脆弱性の多さが目に留まります。約半数は整数関連の脆弱性です。中でも「整数オーバーフロー」の脆弱性は、全体の約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 標準データ型が取り得る値の範囲 |
今回は整数の脆弱性を、
- 整数オーバーフロー(ラップアラウンド)
- 符号エラー
- 切り捨て
- その他
の4つのカテゴリに分類し、詳しく見ていきましょう。
整数オーバーフロー(ラップアラウンド)
整数オーバーフロー(あるいはラップアラウンド)は、演算結果の値が、演算式の型で表現できる範囲を超える場合に発生します。オーバーフローが発生すると、値はプログラマが想定していなかった値になります(たとえば、小さな正の値や、負の値)。
プログラムがこの動作を想定して書かれているのであればよいのですが(符号なし整数のラップアラウンドは、必ず決まった動作をすることが言語仕様として規定されています)、想定外である場合や、未定義の動作となる符号付き整数のオーバーフローは、セキュリティ上の問題につながることがあります。
特にCやC++では、ループ処理の制御、メモリ割り当てやデータのコピー、文字列の結合といったローレベルの処理を行う際、オフセット値やサイズの計算に整数式が用いられることがあります。バイト数を求める式の値が整数オーバーフローの結果小さな値になり、必要なメモリ領域が確保されず、その後の処理でバッファオーバーフローが発生する、というのは典型的なパターンです。
LinuxカーネルのBluetooth関連のモジュールには、まさに先に述べたような脆弱性が存在しました。CVE-2011-2497の識別子を付されたLinuxカーネルの脆弱性は、Bluetoothのプロトコルスタックの中でもデータリンク層における処理を担当するL2CAP(Logical Link Control and Adaptation Protocol)プロトコルの実装に存在しました。脆弱性の見つかったl2cap_config_req()関数は、Bluetoothデバイスとホスト間の接続が完了し、データ転送が行われる前に呼び出され、データ転送の準備を行います。
l2cap_config_req()関数は、デバイスから受け取ったL2CAPパケットのヘッダーからコマンドサイズ(u16 cmd_len)を取得し、この値から設定要求ヘッダのサイズを引いた値を、符号付きローカル変数のlenに格納しています(2336行目)。整数オーバーフローが発生するのはこの減算演算においてです。
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;
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;
負の値を格納した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
Copyright © ITmedia, Inc. All Rights Reserved.