脆弱性の解説: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に登録されています。