久保 正樹(JPCERTコーディネーションセンター) [著]
戸田 洋三(JPCERTコーディネーションセンター) [著]
本連載では、脆弱性を含むサンプルコードを題材に、修正方法の例を解説していきます。今回はLinuxカーネルのソースコードに注目してセキュリティコードレビューを行いたいと思います。
今回はLinuxカーネルのソースコードに注目してセキュリティコードレビューを行いたいと思います。Linuxのカーネルはネタの宝庫!といってもよいほど数多くの脆弱性が発見・修正されており、NIST(米国国立標準技術研究所)のNational Vulnerability Databaseで「Linux kernel」をキーワードに単純に検索するだけでも過去に624件の脆弱性が見つかっていることが分かります(執筆時点)。また、2010年は既に42件の脆弱性が登録されています。
サンプルコード
以下のコードはLinuxカーネル2.6.30のdrivers/net/tun.cから抜粋したものです。それでは、このコードのどこに問題があるか考えてみましょう。
/* Poll */ static unsigned int tun_chr_poll(struct file *file, poll_table * wait) { struct tun_file *tfile = file->private_data; struct tun_struct *tun = __tun_get(tfile); struct sock *sk = tun->sk; unsigned int mask = 0; if (!tun) return POLLERR; DBG(KERN_INFO "%s: tun_chr_poll\n", tun->dev->name); poll_wait(file, &tun->socket.wait, wait); if (!skb_queue_empty(&tun->readq)) mask |= POLLIN | POLLRDNORM; if (sock_writeable(sk) || (!test_and_set_bit(SOCK_ASYNC_NOSPACE, &sk->sk_socket->flags) && sock_writeable(sk))) mask |= POLLOUT | POLLWRNORM; if (tun->dev->reg_state != NETREG_REGISTERED) mask = POLLERR; tun_put(tun); return mask; }
Linuxのtunデバイスはソフトウェア的にネットワークインターフェイスを実現するもので、VPNの実装などに使われています。上に挙げた関数tun_chr_poll()はtunデバイスに対してpoll()システムコールをかけたときに呼び出される関数です。tunデバイスが生きていることを確認した後(if(!tun)という行)、tunデバイスの状態に応じてPOLLINやPOLLOUTなどのビットマスクをローカル変数maskに設定していき、最後に関数自体の返り値としています。
脆弱性の解説:NULLポインタ参照の脆弱性とコンパイラによる最適化
この関数のなかではtunの値がNULLであるかどうかのチェックが行われていますが、その前にポインタskの初期値としてtun->skの値が使われています。この値の参照とNULLチェックの順序がマズイことに気づけるかどうかが今回のポイントです。
今回のコードでは、2つの問題があります。これを順番に説明していきましょう。
Nullポインタ参照の脆弱性
まず1つ目はNULLポインタ参照の脆弱性です。この関数のなかでは、引数の一つであるfileをたぐってtunデバイスの情報を持つ構造体tunを取り出しています。有効な値を取り出せているかどうかを確認するため、tunの値がNULLでないかどうかをチェックしています。しかし、NULLチェックの前に、ローカル変数skを初期化するためにtunの値を使ってしまっています。
struct sock *sk = tun->sk;
もし、tunの値がNULLポインタだった場合、このコードではNULLポインタを参照することになります。
NULLポインタを参照すると、多くの場合、アクセス違反が発生してプログラムは異常終了するでしょう。この関数はカーネルの一部として実行されるデバイスドライバのコードの一部です。ユーザー空間のプログラムと違って、デバイスドライバでアクセス違反が発生するとカーネルごとクラッシュしてしまうことになります。しかも、tunデバイスはユーザー空間のプログラムとイーサネットなどのネットワークインターフェイスの間にたってデータのやりとりを行うことを想定しています。そのデバイスドライバに問題があれば、ネットワーク越しに悪意のあるデータを送信して処理させることで、サービス運用妨害(DoS:denial-of-service)攻撃に悪用される危険が高くなります。
さらに、ARMやXScaleのようなアーキテクチャでは、NULLポインタ参照の脆弱性を悪用してコード実行が可能になるという研究があります。これらのアーキテクチャでは、0番地のアドレスがメモリ上にマップされているのみならず、Exception Vector Tableと呼ばれる特別な意味を持っています。Supervisor(SVC)モードで実行されるリアルタイムOSではメモリアクセスが制限されておらず、0番地へのアクセスが可能となり、これを悪用してコード実行が可能になるというものです。これは米ジュニパーネットワークスの研究者により発表され、話題になりました。興味のある方は、巻末の参考情報に挙げたBarnaby Jack氏の「Vector Rewrite Attack」(PDF)を読むと良いでしょう。
じつはARMやXScaleのようなアーキテクチャでなくても、Linuxで同様の攻撃手法を適用することが可能です。システム設定の内容にも依存しますが、多くのLinuxシステムで使われている設定において、NULLポインタ参照の脆弱性を悪用してコード実行につながる攻撃が可能であるということが発見されたのです。
コンパイラの最適化に関する問題
ここで2つ目の問題です。じつは今回のようなコードでは、if(!tun)のチェックがコンパイラの最適化によって削除されてしまうことがあります。
NULLポインタ参照はCの仕様上、未定義の動作(undefined behavior)に該当します。未定義の動作となるコードをどのように扱うかはコンパイラに任されており、例えば、未定義の動作となるような状況は発生しないという前提で、より高速なコードを生成するために最適化の対象とすることが許されています。コンパイラgccの最適化では、まさにこのような処理を行っています。
今回のコードでは、if(!tun)でtunがNULLポインタになっていないかどうかをチェックしています。しかし、その前に既にtun->skという形でtunの値を参照しています。if(!tun)まで実行が正常に行われているということはtunにはNULLでない有効な値がはいっており、if(!tun)によるチェックは必ず素通りするはずです。そこでgccの最適化では、if(!tun)の部分を不要なコードとして削除してしまうのです。
これはプログラマの意図に反した、望ましくない最適化です。コードレベルではきちんとエラーチェックを行っているつもりでも、コンパイラが出力したオブジェクトコードではその部分が抜け落ちてしまうのですから、通り一遍のコードレビューでは問題を発見できません。
コードが未定義の動作となるかどうかをコードレビューで発見するのは容易なことではありません。Cの言語仕様にしたがい、可搬性のある(portableな)コードを書くことは、このような観点からも重要だと言えます。
以上2つの問題により、NULLポインタ参照を起こすコードから、アクセス違反で停止せずに実行が継続し、攻撃者が用意したコードを実行してしまうといったことが可能になります。
実際の修正内容
さて、それでは次にこの脆弱性の修正がどのように行われたかを見てみましょう。NULLポインタ参照の脆弱性、そしてコンパイラの最適化にまつわる問題、この2つを理解していれば、コードの修正は自ずと明らかになります。NULLポインタチェックをまず最初に行うこと、値を参照するのはその後とするよう、順序を修正していることが分かります。
- struct sock *sk = tun->sk; + struct sock *sk; unsigned int mask = 0; if (!tun) return POLLERR; + sk = tun->sk; + DBG(KERN_INFO "%s: tun_chr_poll\n", tun->dev->name);
この脆弱性はCVE-2009-1897としてCVEに登録されています。
コンパイラの最適化と脆弱性にまつわる事例
コンパイラの最適化に関する問題は2007年にgccのメーリングリストでも議論を呼びました。以下に当時の投稿を引用します(訳文は著者による)。
最近興味深い問題に遭遇した。この問題、GNUARMのドキュメントでも触れておくべきではないだろうか。 問題は、gccの最適化レベル2以上でコンパイルすると、一度ポインタの値を使用あるいはテストしたコードの後ろにおかれたNULLポインタのチェックが削除されてしまうというもの。ARM7のようなハードではハードウエア的にメモリ管理を行う仕組みが存在しないため、プログラムはエラーを報告することなく黙って終了してしまう。 以下のコードをみてほしい。void bad_code(void *a) { int *b = a; int c = *b; static int d; if(b) { d = c; } }-O2もしくはそれ以上でコンパイルすると、bの条件文はおそらく実行されないだろう(関数に0が渡されようが渡されまいが、d = cが必ず実行される)。 オプティマイザがこのような動作をする理由は、bが最初に使用されるとき("int c = *b"の行)、もしbの値がゼロであるならばハードウェアフォルトが発行されるはずであり、これに続くテストである"if(b)"は不必要であると仮定して削除するのである。 以下のコンパイルフラグを使うとこの問題を防げる。-fno-delete-null-pointer-checksARM-7をターゲットに-O2以上でコンパイルする際は常にこのフラグを使う方が良いだろう。 出典: subject: Compiler silently removes null pointer checks - msg#00003 List: gcc.cross-compiling.arm URL: はhttp://osdir.com/ml/gcc.cross-compiling.arm/2007-10/msg00003.html
オーバーフロー対策での事例
未定義の動作とコンパイラの最適化の問題には、他にもセキュリティ上の問題につながった事例があります。例えば、
char *buf; size_t len;
のような型宣言を行い、下記のようにポインタ演算を使ってオーバーフローのチェックを行うコードを書くとどうなるでしょうか。
len = 1<<30; [...] if(buf+len < buf) {/* ラップアラウンドのチェック */ /* オーバーフロー時の処理を行うコード */ }
C/C++では、ポインタと整数との加減算を行った結果は、元のポインタが指している配列のなかの、整数の値だけずれた位置を指しているものと解釈されます。上記コードではbufに非負の値を加算しているわけですから、buf + lenはbufが指しているアドレスよりもずっと先にあるアドレスを意味し、buf + len >= bufが成り立ちます。もし、この計算結果が指しているアドレスが配列の外にはみ出したら未定義の動作となってしまいますから、そのような状況を無視すれば、buf + len >= bufは常に成立し、if文による余計なチェックを削除するという最適化が可能になります。実際、gcc 4.2およびそれ以降のバージョンで上記のコードをコンパイルすると、if文のチェックは最適化により削除されてしまいます。
このようなプログラマの意図に反する最適化を防ぐためには、
#include[...] if((uintptr_t)buf+len < (uintptr_t)buf) [...]
のようにオブジェクトの型をchar*というポインタ型からuintptr_tという符号無し整数型にキャストして、整数値の比較を行うコードにする手段があります。符号無し整数型の値に対しては、計算結果がその型で表現できないほど大きくなる場合はラップアラウンドした結果とする、ということが明確に規定されているからです(C99、セクション6.2.5)。なお、厳密に言うならば、uintptr_tはC99では省略可能とされており定義されていない処理系がありうること(C99、セクション7.18.1.4)、また、ポインタ型オブジェクトの表現を整数とみなしたものが単純にメモリアドレスの値になっているという前提はC99で保証されているわけではなく、あらゆるアーキテクチャで同じように動作するとは限らないことにご注意ください。
コンパイラによる最適化は効率的なコードを得るために重要な機能ではありますが、同時に脆弱なプログラムを生み出してしまう危険もあるので注意が必要です。未定義の動作や処理系定義の動作となる状況にどのようなものがあるかを把握すると共に、自分が使用する処理系の振る舞いについて正しく理解しておくことが重要です。
参考情報
- EXP34-C. NULLポインタを参照しない
- CVE-2009-1897
- Barnaby Jack. Vector Rewrite Attack. May 2007
- Dan Goodin. Clever attack exploits fully-patched Linux kernel. The Register. July 2009.
- Making NULL-pointer reference legal
- Ilja van Sprundel. Unusual bugs
- JVNVU#162289: ある種の範囲チェックを破棄するCコンパイラの最適化の問題
- 情報処理推進機構:情報セキュリティ技術動向調査(2008 年上期) 10 C コンパイラの最適化の問題