セキュアなプログラマー: 競合状態を防ぐ

リソース競合に要注意

競合状態とは何か、そして、なぜそれがセキュリティーの問題になるのかを学びましょう。この記事ではUNIXライクのシステムで起こりがちな競合状態への対処方法として、ロック・ファイルを正しく作る方法やロック・ファイルに代わる方法、ファイルシステムの扱い方、共有ディレクトリーの扱い方、そして何よりも、/tmpディレクトリーで一時ファイルを正しく作るにはどうすべきか、等について説明します。また、シグナル処理についても少し学ぶことにします。

David Wheeler (dwheelerNOSPAM@dwheeler.com), Research staff member, Institute for Defense Analyses

David WheelerDavid A. Wheelerはコンピューター・セキュリティの専門家で、大規模・高リスク・ソフトウェア・システムの開発技術の改善に取り組んできました。Secure Programming for Linux and Unix HOWTOの著者でありCommon Criteriaの検証者です。また記事「Why Open Source Software/Free Software? Look at the Numbers!」やSpringer-Verlag刊のAda95: The Lovelace Tutorialも書いており、IEEEの書籍Software Inspection: An Industry Best Practiceの共著者、主任編集者でもあります。この記事は著者の個人的な意見であり、Institute for Defense Analyses(防衛分析研究所)の意見を表すものではありません。著者の連絡先はdwheelerNOSPAM@dwheeler.comです。



2004年 10月 07日

「Mallory」は盗まれたパスワードを使って、Linuxを実行している重要なサーバーにログインします。そのアカウントに制限はかかっているのですが、Malloryはどうすれば問題を引き起こせるかを知っています。Malloryは、おかしな振る舞いをする、ちょっとしたプログラムをインストールして実行します。そのプログラムはあらゆるプロセスを使って、/tmpディレクトリーで様々なシンボリック・リンク・ファイルを生成・削除してしまいます。(シンボリック・リンク・ファイルはsymlinkとも呼ばれますが、アクセスされるとリクエスターを別ファイルにリダイレクトします。)Malloryのプログラムは数多くの様々なsymlinkを生成・削除し続けますが、そのどれもがすべて、特別なファイル、/etc/passwd、つまりパスワード・ファイルを指すのです。

このサーバーでは用心のために毎日、セキュリティー・プログラムであるTripwire(具体的には古いバージョンである2.3.0)を実行していました。Tripwireは多くのプログラムと同様、起動すると一時ファイルを作ろうとします。Tripwireは /tmp/twtempa19212という名前のファイルを探しても見つからないので、これが一時ファイルの名前として適当であると考えます。ところがTripwireがチェックした後で、Malloryのプログラムは正にその名前を持つsymlinkを作るのです。これは偶然ではありません。Malloryのプログラムは、正にTripwireが使う可能性の高いファイル名を生成するように作られているのです。そうするとTripwireはそのファイルを開き、一時情報を書き込み始めます。ところがTripwireは新しい空のファイルを作る代わりに、パスワード・ファイルを上書きするのです。パスワード・ファイルが破壊されてしまうので、この後は誰も、つまり管理者であっても、システムにログインできなくなります。場合によってはもっと悪いことも起こり得ます。Malloryの攻撃によって、サーバーに保存されている重要データを含めて、どんなファイルでも上書きされてしまう可能性があるのです。

競合状態への入門

上の例はあくまでも仮定の話であり、Malloryも攻撃者の名前として昔からあるものです。しかしその攻撃と、攻撃にさらされる脆弱性は全く一般的なものです。多くのプログラムは、競合状態と呼ばれるセキュリティー問題に対して脆弱なのです。

競合状態が起こるのは、予期せぬ順序でイベントが起きるために同じリソースへの競合が生じ、プログラムが本来の姿で動作しなくなるためです。注意して欲しいのですが、競合状態は同じプログラムの別々の部分が競合することで起こるとは限りません。セキュリティー問題の多くは、外部の攻撃者が予期せぬ方法でプログラムを妨害することによって起きます。例えばTripwire V2.3.0では、あるファイルが存在しないと判定するとそのファイルを作ってしまいます。その2つのステップの間に攻撃者がそのファイルを作れることは考慮に入れていません。何十年も前は、競合状態は大した問題ではありませんでした。当時コンピューター・システムは一度に一つの単純なプログラム・シーケンスを実行しているに過ぎず、それに割り込んだり、競合したりするものはありませんでした。ところが今日、普通のコンピューターは同時に実行する大量のプロセスやスレッドを抱え、複数のプロセッサーが同時に別々のプログラムを実行しています。そのため柔軟性は増していますが、危険性もあります。こうしたプロセスやスレッドが同じリソースを共有している場合には、お互いに競合する可能性が出てきます。実際、競合状態に対する脆弱性はソフトウェアの脆弱性として最も一般的なものです。そしてUNIXライクのシステムでは、/tmpや /var/tmpなどのディレクトリーは競合状態を起こしやすい、誤った使い方をされがちなのです。

しかし何よりもまず、少し用語を知っておく必要があります。UNIXライクのシステムでは、ユーザー・プロセスをサポートしています。そしてそれぞれのプロセスは通常、他のプロセスが触れることのできない、別々のメモリー領域を持っています。下にあるカーネルは、こうしたプロセスが一見同時に実行しているように見せます。マルチプロセッサーのシステムでは、プロセスは実際に同時に実行します。一つのプロセスは概念として一つ以上のスレッドを持ち、それらがメモリーを共有します。スレッドもまた、同時に実行します。スレッドはメモリーを共有するので、普通はプロセス間の場合よりもスレッド間の場合の方が、競合状態が起きる可能性が高くなります。マルチスレッドのプログラムをデバッグするのが難しいのは、正にこの理由によるものです。Linuxカーネルは基本設計がすっきりしています。つまりスレッドしかなく、一部のスレッドは他のスレッドとメモリーを共有し(これで伝統的なスレッドを実装することになります)ますが、あるスレッドは共有しません(これで別々のプロセスを実装することになります)。

競合状態を理解するためにまず、簡単なCステートメントを見てみましょう。

リスト1. 簡単なCステートメント
 b = b + 1;

非常に簡単に見えますよね。ところが、このコード行を実行しているスレッドが2つあり、bが2つのスレッドが共有する変数で、しかも値が5で始まる、と考えてみてください。可能性のある実行順序は次のようになります。

リスト2. 共有される「b」に関して考えられる実行順序
 (thread1) load b into some register in thread 1.
       (thread2) load b into some register in thread 2.
 (thread1) add 1 to thread 1's register, computing 6.
       (thread2) add 1 to thread 2's register, computing 6.
 (thread1) store the register value (6) to b.
       (thread2) store the register value (6) to b.

5から始め、2つのスレッドがそれぞれ1を加えたのですが、最後の結果は期待される7ではなく、6です。2つのスレッドが妨害し合うため、最終結果が誤ってしまうのです。

一般的に言って、スレッドは、全て同時に単一操作を行うようなアトミックな実行はしません。ですから任意の2つの命令間に別のスレッドが割り込み、何か共有のリソースを操作するかも知れません。こうした割り込みに対してセキュアなプログラムのスレッドが対応していなければ、そこに別のスレッドが割り込めてしまうかも知れません。セキュアなプログラムでは、別スレッドによる任意数のコードが途中で実行されたとしても、対になった操作はどれも影響を受けずに正しく動作する必要があります。ですから他のスレッドによる妨害の可能性がある場合には、プログラムがいつどんなリソースをアクセスしているのかを判断することが鍵になります。


競合状態を解決する

競合状態の解決方法として典型的なものは、プログラムがファイルやデバイス、オブジェクト、変数などを操作している時には、それに対して独占権をもつように保証することです。何かに対して独占権を持つプロセスはロッキングと呼ばれます。ロックを正しく扱うのは簡単ではありません。よくある問題はデッドロック(「deadly embrace」死の抱擁)で、ロックされたリソースが解放されるのをお互いが待ってしまい、プログラムが膠着状態に陥ることを言います。デッドロックの大部分は、(例えばアルファベット順にとか、「最も粗いもの」から「最も細かいもの」へ、など)同じ順序でロックを取得するように、全スレッドに対して要求することで防止できます。もう一つよくある問題はライブロック(livelock)で、プログラムがロックの取得・解放だけはできるものの、進行はできない、という状態です。そして一旦ロックが膠着状態になると、きれいに解放するのは非常に困難です。求められる通りいかなる場合においても適切にロックし、またロックを解放するようなプログラムを書くのは、はっきり言って困難な場合が多いのです。

よくある間違いは、必ずしもロックしないようにロック・ファイルを作ってしまうことです。皆さんは正しいロック・ファイルの作り方を学ぶべきですし、あるいは別のロック機構に切り換える必要があります。また常に危険な /tmpや/var/tmpなどの共有ディレクトリーや、シグナルの安全な使い方を含めて、ファイルシステムにおける競合も正しく処理すべきです。以下のセクションでは、これらに対するセキュアな対処方法を説明します。


ロック・ファイル

UNIXライクのシステムでは伝統的に、ロックを示すファイルを作ることによって、別々のプロセス間で共有されるロックを実装してきました。ある別のファイルを使ってロックを示すのは、強制的ロック(mandatory lock)ではなく通知ロック(advisory lock)の例です。言い換えると、OS(オペレーティング・システム)はロックによるリソース共有は強制しないので、そのリソースを必要とするプロセスは全て協力してロックを使う必要があります。これは原始的に思えるかも知れませんが、単純な考え方が全て悪いとは限りません。別ファイルを作ることによって、何がロックされているのかを含めて、システムの状態が見やすくなります。この手法を使う場合には、クリーン・アップを簡単にするために標準的に使われる細工、特に膠着状態を除去するための細工があります。例えば親のプロセスがロックをセットし、作業をさせるために子を呼び(親のみが、正しく動作するような方法で子を呼ぶようにします)、その子が戻る時に親がロックを解放します。あるいはcronジョブがプロセスIDを含むロックを見て、もしプロセスが生きていない場合にはロックを消去してプロセスを再起動するようにします。そして最後に、システム起動の一部としてロック・ファイルを消去し、システムが突然クラッシュしても、膠着状態のロックが後に残らないようにします。

ロックを示す別ファイルを作る場合には、よくおかしがちな間違があります。creat()またはそのopen()に等価なもの(モードO_WRONLY | O_CREAT | O_TRUNC)を呼んでしまうことです。つまり、たとえロック・ファイルが既に存在する場合であってもrootは常にこの方法でファイルを作れるので、ロックはrootに対しては正しく動作しないということです。単純な解決方法は、open()をフラグO_WRONLY | O_CREAT | O_EXCLで使うことです(そして同じ所有者による他のプロセスがロックを取得しないように、パーミッションを0にセットします)。O_EXCLの使い方に注意してください。これが「独占」ファイル(exclusive file)を作る正式な方法なのです。これはローカル・ファイルシステムのrootに対しても動作します。この単純な手法はNFS V1やV2では動作しません。こうした古いバージョンのNFSを使ったリモート・システムでロック・ファイルが動作すべき場合の対処方法として、Linuxドキュメンテーションには次のように書かれています。「同じファイルシステム(ホスト名やpidを含めた)で固有ファイルを作り、link(2)を使ってロック・ファイルへのリンクを作り、固有ファイルに対してstat(2)を使ってリンク・カウントが2にまで増加したかどうかをチェックする。link(2)コールの戻り値を使ってはならない。」

ロックを表すためにファイルを使っている場合には、こうしたロック・ファイルは必ず、攻撃者が操作できないような場所に置きます(例えばこうしたファイルを削除したり、妨害ファイルを追加したりできないようにします)。典型的な手法としてはパーミッションを設定し、特権を持たないプログラムでは全くファイルを追加削除できないようなディレクトリーを使います。こうしたロック・ファイルを追加削除できるのは、絶対に信頼できるプログラムのみ、というようにしておくのです。

LinuxシステムではFilesystem Hierarchy Standard (FHS) が広く使われており、FHSにはこうしたロック・ファイルに対する標準的な扱い方も含まれています。あるマシン上でサーバーが一度以上実行しないようにしたいだけであれば、/var/run/NAME.pidという名前を持つプロセス識別子を作り、そのファイル内容をプロセスIDとします。同様に、デバイスのロック・ファイルなどに関するロック・ファイルは /var/lockに置きます。


ロック・ファイルに代わる手段

ロックを示すために別ファイルを使うのは古いやり方です。別の方法としてPOSIXレコード・ロックを使うものがあり、fcntl(2)で任意ロック(discretionary lock)として実装します。POSIXレコード・ロックはほとんど全てのUNIXライクのプラットフォームでサポートされており(POSIX.1で必須となっています)、ファイル全体だけではなくファイルの一部をロックすることもでき、読み取りロックと書き込みロックとの違いを処理することができます。またプロセスが死んだ場合には、そのPOSIXレコード・ロックは自動的に削除されます。

別ファイルやfcntl(2)任意ロックを使う方法は、全てのプログラムが協力した場合にのみ、うまく動作します。こうした方法が気に入らなければ、代わりにSystem V流の強制ロックを使うこともできます。強制ロックを使うと、ファイル(またはその一部)をロックでき、ロックに対する全てのread(2)write(2)がチェックされます。しかもロックを保持しないプロセスはどれでも、そのロックが解放されるまで待たされるのです。この方が便利に思えるかも知れませんが、欠点もあります。root権限を持つプロセスまでも強制ロックによって待たされてしまい、これがサービス不能攻撃を容易にしてしまう場合が多いのです。実際、サービス不能攻撃が起きると非常に深刻な問題になるので、(そうならないように)強制ロックを使わないようにする場合が多いと言えます。強制ロックは広く使われていますが、どこでも使えるわけではありません。LinuxやSystem Vベースのシステムではサポートしていますが、一部のUNIXライクのシステムではサポートしていません。Linuxで強制ファイル・ロックを許すためには、明示的にそのファイルシステムをマウントする必要があるため、デフォルトでは強制ロックをサポートしない構成が多くなっています。

プロセス内部では、スレッドもロックする必要があります。この詳細を解説した数多くの本が出ていますが、重要な点は、全ての場合を網羅するように十分注意する必要がある、ということです。つまり特別な場合を忘れてしまったり、ある場合を正しく処理しなかったりすることが、ありがちなのです。基本的にロックを正しく行うのは難しく、ロック処理の間違いは悪用されやすいと言えます。プロセス内部のスレッドに対して多くのロックが必要な場合には、ロック管理の多くを自動的に行うような言語や言語構造体を使うことを考慮すべきです。JavaやAda95など、多くの言語にはこうした言語構造体が組み込まれているため、正しい結果が得られる可能性が高くなります。

もし可能であれば、全くロックを必要としないようなプログラムを開発することが一番です。つまり一度に一つのクライアント・リクエストのみを受け付け、そのリクエストが完了するまで処理してから次のリクエストを受け付けるような、ある意味でプロセス内の全オブジェクトを自動的にロックするような単一のサーバー・プロセスにするのです。そうした単純な設計であれば、やっかいなロック問題とは無縁になります。もしロックが必要であるなら、ほとんど何にでも単一ロックを使うなど、とにかく単純にしておくことが得策です。ただしこうした設計ではパフォーマンスに問題が起きる場合もあるので、常に現実的ではないかもしれません。特に単一サーバーのシステムでは、一つの処理が長時間とりすぎないようにすべきですが、こうした設計も一考の余地はあります。ロックを多用するシステムでは欠陥が生じやすく、ロック管理のためにパフォーマンスも低下しがちなのです。


ファイルシステムを処理する

プログラムがセキュアであるためには、トラブルを引き起こすような方法で共有リソースを操作できないようにしておく必要がありますが、これは思ったほど容易でありません。共有リソースとして最も一般的なものはファイルシステムです。どのプログラムでもファイルシステムは共有するので、攻撃者がファイルシステムをいじって問題を引き起こせないようにするには、特別な工夫が要る場合があります。

セキュアであるべく作られたプログラムの多くでは、TOCTOU競合条件(TOCTOU:time of check - time of use、チェック時から使用時まで)と呼ばれる脆弱性を持っています。つまりプログラムが状況に問題がないかをチェックし、その後でその情報を使おうとするのですが、この2つのステップの間に攻撃者が状況を変更できてしまう、という意味です。これはファイルシステムで特に問題になります。多くの場合、このステップの間に攻撃者が通常のファイルやシンボリック・リンクを作ることができてしまいます。例えば特権プログラムが、ある名前のファイルが無いことをチェックした後そのファイルに書き込むために開こうとすると、攻撃者はこの2つのステップ間にそのファイル名を持つシンボリック・リンク・ファイル(例えば/etc/passwdなどのような致命的重要性を持つファイルなど)を作ることができてしまいます。

こうした問題は、次のようにいくつか簡単なルールを守ることで避けることができます。

  • 何かができるかどうか判定する際に、access(2)は使わないようにします。多くの場合、攻撃者はaccess(2)コールの後で状況を変更できてしまいます。ですからaccess(2)をコールして得られるデータは、もはや正しくないかも知れません。access(2)を使う代わりに、プログラムの特権を自分が意図したものに設定してから(例えば実効IDやファイルシステムID、実効gidを設定し、不必要なグループはsetgroupsを使ってクリアーするようにします。)、直接access(2)を呼んで、必要なファイルを開いたり作ったりするようにします。UNIXライクのシステムでは(バージョン1と2の、古いNFSシステムを除いて)、open(2)コールはアトミックです。
  • 新しいファイルを作るときには、モードO_CREAT | O_EXCLを使ってそのファイルを開くようにします(O_EXCLは、新しいファイルが作られた場合のみ、そのコールが成功するようにします)。最初は非常に限定的なパーミッションのみを与えるようにし、少なくとも任意ユーザーがそのファイルを変更するのは禁止するようにします。これはつまりumaskを使ったりopenのパラメーターを使ったりすることによって、初期アクセスをそのユーザーあるいはそのユーザー・グループのみに制限することを意味します。関連した競合条件があるからといって、ファイルを作った後でパーミッションを緩めないようにします。大部分のUNIXライクのシステムでは、パーミッションはファイルを開く際にチェックされるだけです。ですから攻撃者はパーミッション・ビットが「OKであった」と言っている間にファイルを開き、そのパーミッションを永遠に維持することによって、ファイルを開いたままにできてしまいます。権限を緩やかなものに変更する必要があるならば、後ですればよいのです。また、ファイルのオープンが失敗した場合にも備える必要があります。新しいファイルを必ず開けられる必要がある場合にはループを作り、(1)「ランダムな」ファイル名を作り、(2) O_CREAT | O_EXCLでファイルを開き、(3) 無事開けた場合にはループを停止する、ようにしておく必要があります。
  • ファイルのメタ情報に対して操作をする(つまり例えばそのファイルの所有者を変更したり、そのファイルをstatしたり、パーミッション・ビットを変更したりする)場合には、まずファイルを開き、次に開いたファイルに対して操作するようにします。ファイル名を使うような操作はできる限り避け、代わりにファイルのディスクリプターを使って操作を行うようにします。つまりchown()chgrp()chmod()など、ファイル名を使うようなファンクションの代わりに、fchown( )fstat( )fchmod( )などのシステム・コールを使うようにする、ということです。そうすることによって、プログラム実行中にファイルが入れ替えられること(つまり潜在的な競合状態)を防ぐことができます。例えば、あるファイルを閉じ、その後でchmod()を使ってそのファイルのパーミッションを変更する、という場合であれば、この2つのステップの間に攻撃者がファイルを移動したり削除したりすることができ、(/etc/passwdのような)別のファイルへのシンボリック・リンクを作ることができてしまいます。
  • プログラムがファイルシステムをウォークし、サブディレクトリーに対して再帰的に繰り返す場合には、ウォークしているそのディレクトリー構造を攻撃者が操作できてしまわないか十分注意する必要があります。よくある例は、プログラムを実行している管理者やシステム・プログラム、あるいは特権サーバーが、普通のユーザーが制御できるファイルシステムの部分をウォークしてしまうというものです。GNUファイル・ユーティリティー(fileutils)は再帰的にディレクトリーを削除したり移動したりできますが、V4.1以前では、ディレクトリー構造をウォークする場合には単純に「..」という特別なエントリーを追うだけでした。攻撃者はファイルが削除されている間に、下位レベルのディレクトリーを高位のレベルに移動することができたのです。そうするとfileutilsは「..」ディレクトリーずっと上までたどり、ファイルシステムのrootにまで達することもできます。あるタイミングで攻撃者がディレクトリーを移動すると、コンピューター上の全ファイルを削除できてしまいます。攻撃者が制御している可能性を考えれば、「..」や「.」を信用することはできません。

信頼できないユーザーと共有する可能性のあるディレクトリーには、可能な限りファイルを置かないようにします。それができない場合にも、ユーザー間で共有されるようなディレクトリーは使わないようにします。信頼できる、特別なプロセスのみがアクセスできるディレクトリーを、ためらわず作るべきです。

伝統的な共有ディレクトリーである、/tmpや /var/tmpを避けることができないか検討してみてください。ある場所から別の場所にデータを送る場合に単純にパイプを使えば、プログラムを単純化することができ、潜在的なセキュリティー問題を解消することができます。一時ファイルを作る必要がある場合には、どこか別の場所に一時ファイルを保存できないかを検討してみてください。特権プログラムを書いているのでなければ、これは特に考慮すべきことです。つまり特権プログラムでなければ、ホーム・ディレクトリーとして「/」を持つrootユーザーの扱いに注意しながら、そのユーザーのホーム・ディレクトリーに一時ファイルを置いた方が安全です。そうしておけば、たとえ一時ファイルを「正しく」作らなかったとしても、攻撃者がユーザーのホーム・ディレクトリーの内容を操作することはできないので、普通は攻撃者が問題を引き起こすことはできません。

しかし、必ずしも共有ディレクトリーを常に避けることができるとは限りません。ですから /tmpのような共有ディレクトリーの扱い方を理解する必要があります。これはかなり複雑な話題なので、次のセクションで単独で扱うことにしましょう。


共有ディレクトリー(/tmpなど)

共有ディレクトリーの基礎

信頼すべきプログラムが、信頼できない可能性のあるユーザーとディレクトリーを共有する場合には、十分に注意する必要があります。UNIXライクのシステムで最も一般的な共有ディレクトリーは/tmpや/var/tmpですが、セキュリティー脆弱性の多くは、これらディレクトリーの使い方を誤っていることから生じています。/tmpディレクトリーは元々、一時ファイルを作るのに便利な場所として作られたものです。一時ファイルは普通、他の誰かと共有すべきものではありません。ところが /tmpディレクトリーは2番目の使い方、つまりユーザー間での共有オブジェクトを作るための標準的な場所としての使い方をされるようになってしまいました。こうした共有ディレクトリーには複数の使い方があるため、攻撃から守るためのアクセス制御をOS(オペレーティング・システム)が強制しにくいのです。ですから攻撃を避けるためには、正しく使うことが非常に重要になります。

共有ディレクトリーを使う場合には、そのディレクトリーとファイルのパーミッションが適切かどうかを確認する必要があります。当然のことですが、共有ディレクトリーで作るファイルに読み書きできるのは誰なのかを制限する必要があります。ところがUNIXライクのシステムで複数ユーザーがディレクトリーにファイルを追加できる場合で、特権プログラムからそのディレクトリーにファイルを追加しようとする場合には、そのディレクトリーのstickyビットを必ずセットするようにします。(stickyビットの無い)普通のディレクトリーでは、書き込み特権を持っていれば誰でも(つまり攻撃者も)、ファイルを削除したりリネームしたりといった、あらゆる問題を引き起こすことができてしまいます。例えば信頼できるプログラムがそうしたディレクトリーでファイルを作り、信頼できないユーザーが削除やリネームをする、ということができるのです。UNIXライクのシステムでは、共有ディレクトリーのstickyビットはセットしておく必要があります。stickyディレクトリーでは、ファイルのリンクを外したりリネームしたりできるのはrootまたはファイル所有者のみです。/tmpや/var/tmpなどのディレクトリーは、問題を避けるために通常はstickyとして実装します。

プログラムは時々、ゴミのような一時ファイルを残す場合があります。そのため大部分のUNIXライクのシステムでは、/tmpや /var/tmpなど特別なディレクトリーにある古い一時ファイルを自動的に削除します(「tmpwatch」というプログラムはこれを行います)。またあるプログラムでは、特別な一時ファイル・ディレクトリーから自動的にファイルを削除します。これは一見便利なように思えますが、攻撃者がシステムをビジー状態に陥れ、アクティブなファイルまで古いファイルにしてしまうことができてしまうのです。その結果、実際に使われているファイルまで、システムが自動的に削除してしまうかもしれません。そうすると次には何が起きるのでしょうか? 攻撃者は同じ名前を持つ別のファイルを作ろうとするかも知れませんし、あるいはシステムに別プロセスを作らせ、同じファイル名を再度使用させるかも知れません。その結果は、正に大混乱です。これはtmpwatch問題と呼ばれます。これを防ぐためには、一時ファイルをアトミックに作った場合には必ず、ファイルを開いたときに取得するファイル・ディスクリプターまたはファイル・ストリームを使う必要があります。ファイルを再度開いたり、ファイル名をパラメーターとして使う操作を行ったりすべきではありません。必ずファイル・ディスクリプター、またはそのファイルに関連のストリームを使うようにします。そうしないとtmpwatch競合の問題が起きてきます。ファイルを開けられるのは誰かをパーミッションで制限している場合であっても、ファイルを作ってから閉じ、それを再度開いたりしてはいけません。

Stickyディレクトリーやファイルに対するパーミッション制限は第一歩に過ぎません。攻撃者はセキュアなプログラムにおけるアクションとアクションの間に、何らかのアクションを紛れ込ませるかも知れません。よくある攻撃としては、プログラム実行中に共有ディレクトリーの中に、他のファイルへのシンボリック・リンクを作ったり外したりするものです。/etc/passwdや /dev/zeroなどはその対象の好例です。攻撃者にとっては、「対象となるファイル名は存在しない」とセキュアなプログラムが判断する状況を作ることが目標なのです。そうした状況を作ってしまえば、攻撃者は別のファイルへのシンボリック・リンクを作ることができ、セキュアなプログラムが何か特別な操作を行うと、開くつもりのないファイルを開いてしまうことになります。多くの場合、こうして重要なファイルが削除されたり変更されたりするのです。あるいは別のパターンとして、書くことを許されているような普通のファイルを攻撃者が生成したり消去したりすることによって、攻撃者が制御する「内部」ファイルを特権プログラムに生成させてしまうものがあります。

こうした共有ディレクトリーでファイルを作る場合の一般的な注意として、あるファイルを使うつもりで生成する時には、そのファイル名が存在していないことを保証し、そのファイルをアトミックに生成するようにします。チェック後で、しかも生成前に、別プロセスが同じファイル名でそのファイルを生成することができるので、ファイル生成前のチェックでは効果がありません。予測できないファイル名や変わったファイル名を使うという手段は、それだけでは効果がありません。攻撃者は成功するまで何度も繰り返しを行うことができてしまうからです。ですから、新しいファイルを作るか失敗するか、そのいずれか以外はしない操作とする必要があります。UNIXライクのシステムではこれが可能ですが、実際にどうすべきかを知っておく必要があります。

共有ディレクトリーに対する対策

残念なことに、答えにならないような答えがたくさんあります。一部のプログラムでは、一時ファイル名を作るためにmktemp(3)tmpnam(3)を直接呼んだ後、大丈夫であろうという想定で単純にそのファイルを開くのです。これは悪い案の好例です。実際、tmpnam(3)はスレッドに対して信頼性が低く、信頼できる方法でループを扱えないので、使うべきではありません。1997年の「Single UNIX Specification」ではtmpfile(3)を使うように推奨していますが、残念ながらこれを一部の古いシステムで使うのは、安全なことではありません。

共有(sticky)ディレクトリーで安全に一時ファイルを生成するために、Cで一般的に使われる手法としては、umask()の値を制限的な値に設定してから、繰り返し(1)ランダムなファイル名を生成し、(2)それをO_CREAT | O_EXCL(アトミックにファイルを生成し、生成できていないと失敗します)を使って開き、(3)オープンが成功したら停止する、というものです。

実際にこれを直接行う必要はなく、単純に、これを行うライブラリー・ファンクションmkstemp(3)を呼べばよいのです。一部のmkstemp(3)実装ではumask(2)を制限値に設定しないので、最初にumask(2)を呼んでファイルを制限値に設定した方が賢明です。些細なことですが、mkstemp(3)は環境変数TMPやTMPDIRを直接はサポートしないので、それが重要なのであれば追加の作業が必要になります。

GNOMEのプログラミング指針では、一時ファイルをセキュアに開くため共有(一時)ディレクトリーでファイルシステム・オブジェクトを生成する場合には、次のCコードを使用するように推奨しています。

リスト3. 一時ファイル用として推奨されるCコード
char *;
 int fd;
 do {
   filename = tempnam (NULL, "foo");
   fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
   free (filename);
 } while (fd == -1);

セキュアではないファンクションtempnam(3)が使われていますが、そのセキュリティーの弱さを補うためにO_CREATO_EXCLを使ってループ内にラップされているので、この使い方で問題ないことに注意してください。うまいことにtempnam(3)は通常TMPDIRを使用するので、必要ならばこれを使って一時ファイルをリダイレクトすることができる、という副次的な効果もあります。ファイル名を解放する(free())必要があることに注意してください。終了後はファイルをclose()して、unlink()する必要があります。小さなことですが、この手法ではtempnamをセキュアでない方法で使うため、コンパイラーやセキュリティーのスキャンによって様々な疑似警告が出てしまうのが欠点です。mkstemp(3)ではこの問題はありません。

この開き方で分かるのは、Cの標準IOライブラリーが少しおかしいということです。O_EXCLfopen()を使うための標準的な方法がないので、通常のC流の方法でファイルを開き、安全に一時ファイルを作ることができません。Cの標準IOライブラリーを使いたい場合にはopen()を使い、次にモード「w+b」でfdopen()を使ってファイル・ディスクリプターをFILE *に変換します。

Perlプログラマーであれば、セキュアに一時ファイルを作るためのクロス・プラットフォームの手段を提供しているFile::Tempを使うべきです。ただし適切に使うためには、そのドキュメンテーションをよく注意しながら読んでください。File::Tempには、安全ではないファンクションへのインターフェースもあるのです。safe_levelをHIGHに設定しておけば追加のセキュリティー・チェックが呼び出されるので、そのように設定することを強くお勧めします。実はこれは、大部分のプログラミング・ライブラリーにも当てはまるのです。つまり大部分のライブラリーでは、セキュアな操作へのインターフェースとセキュアではない操作へのインターフェースの両方を含むので、ドキュメンテーションをよくチェックしてセキュアな方を選ぶようにします。

古いバージョンのNFS(バージョン1または2)ではO_EXCLを正しくサポートしていないので、これらのNFSのディレクトリーでO_EXCLを使っても動作しないことに注意してください。共有ディレクトリーが古いNFSを使って実装されている場合には、O_EXCLを使うと、プログラムがセキュアでなくなってしまいます。実は古いバージョンのNFSに対する複雑な対応措置として、link(2)stat(2)を使ったものがあるのです。そうした状況下でプログラムを動作させる必要がある場合には、何よりもLinuxopen(2)のマニュアル・ページを読んでください。ただしここでは、それに触れないことにします。たとえ皆さんのプログラムが古いバージョンのNFSで動作したとしても、他の大部分のプログラムにはこの対応措置が入っていないだろうと思われるからです。一時ディレクトリーでNFS V1またはV2を実行している場合には、他のプログラムは恐らく対応措置をとっていないので、結局セキュアなシステムにすることは困難です。ですからリモートにマウントされる一時ディレクトリーを使用する場合には、単純にNFS V3以上を要求した方がずっと賢明だと言えます。

mkdtemp(3)を使うこともできますが、普通はあまり良いことではありません。tempクリーナーが一時ディレクトリーを消去すべし、と判断してしまうかも知れないためです。

シェル・スクリプトを書く場合には、一時ファイル用にはパイプを使うか、ユーザーのホーム・ディレクトリーを使うようにします。/tmpや /var/tmpなどのディレクトリーは絶対に使うべきではありません。一般的に言って通常のシェルはファイル・ディスクリプターをサポートしないので、やがてtmpfileクリーナーによって失敗させられるようになります。tmpfileクリーナーが無く、とにかく単純に /tmpで一時ファイルを作る必要がある場合には、より明確な攻撃に対抗するために、少なくともmktemp(1)を使うようにします。これは(mktemp(3)ではなく)mktemp(1)は、O_EXCLモードを使って、競合状態をねらった典型的な攻撃に対抗できるからです。最も悪いのは、(実は残念なことに非常によく行われるのですが)攻撃者は「$$」が何なのかを推測できないだろう、と想定して、そうしたファイルに情報をリダイレクトしてしまう方法です。この方法では、生成に(使うべきはずの)O_EXCLモードを使いません。攻撃者は単純に、ありそうなファイルを事前生成したり、または、ありそうなファイルを繰り返し生成したり消去したりしながら、やがてプログラムの制御を奪うことができてしまいます。ですから次のようなシェル・スクリプトはまず確実に、深刻な脆弱性を持っています。

リスト4. 脆弱なシェル・スクリプト
echo "This is a test" > /tmp/test$$  # DON'T DO THIS.

一時ファイル名は、いかに「セキュアに」入手したかに関わらず、再使用(削除してから再生成)してはなりません。攻撃者は元々のファイル名を観察することによって、そのファイル名が2度目に作られる前にそのファイルを使ってしまうことができるのです。また当然のことですが、必ず適切なファイル・パーミッションを使うようにします。

そして後始末をするようにします。つまりexhitハンドラーを使うなりUNIXファイルシステムの意味体系を利用するなりして、ファイルを作った直後にファイルをunlink()するようにします。こうすることでディレクトリー・エントリーは無くなり、しかしファイル自体は、そのファイルを指す最後のファイル・ディスクリプターが閉じられるまでアクセスできます。そうすれば、ファイル・ディスクリプターを受け渡しながら、プログラム内部からそのファイルにアクセスし続けることができます。ファイルのアンリンクは、コード管理の面からも多くの利点があります。つまりプログラムがどのようにクラッシュしたとしても、そのファイルは自動的に消去されます。また維持管理者がファイル名を、セキュアでない使い方で使ってしまう可能性を低くできます(ファイル名ではなく、ファイル・ディスクリプターを使う必要があります)。即座にアンリンクすることによる欠点は、小さなことですが、管理者がディスク・スペースの利用状況を把握しにくくなる、ということです。

OSの中に対抗手段を組み込むことで多少の成功を見ている事実はありますが、現時点ではまだ広く使われてはいません。これに関しては参考文献にあるリストを参照してください、OpenWallプロジェクトによるSolar DesignerのLinuxカーネル・パッチやRaceGuard、またEugene TsyrklevichとBennet Yeeによるカーネル修正へのリンクを挙げてあります。


シグナル処理

競合状態はシグナル処理の場合にも発生します。プログラムには様々な種類のシグナルを処理するように登録できるのですが、シグナルというのは最も不運な時にも、つまり他のシグナルを既に処理している時にでも発生するものです。シグナルハンドラー内部で行っても良いのは、後で処理できるようにグローバル・フラグをセットすることくらいです。他にもまだ、シグナルハンドラー内でセキュアに行えることはいくつかありますが、多くはありません。そしてそれらを行う前には、シグナル処理に関して充分理解しておく必要があります。これは、シグナル内で安全に呼ぶことができるシステム・コールはごく僅かしかないためです。安全に呼ぶことができるのは、再入可能(re-entrant)な、あるいはシグナルによって割り込まれることのないコールのみです。ライブラリー・ファンクションを呼ぶことはできますが、安全に呼べるのはごく僅かです。free()syslog()など大部分のファンクションはシグナルハンドラー内部で呼ぶと大きなトラブルの元になります。詳しくはMichal Zalewskiが「Delivering Signals for Fun and Profit」という論文を書いているので、それを見てください。いずれにせよシグナルハンドラー内部では高度なハンドラーを作ろうなどとはせず、フラグをセットするだけにし、他には何もしない方が無難です。


まとめ

この記事では、競合状態とは何か、なぜそれがセキュリティー問題の原因となるのかについて説明し、ロック・ファイルを正しく生成する方法と、ロック・ファイルを使わない方法についても説明しました。ファイルシステムの扱い方、特に /tmpディレクトリーで一時ファイルを作るなど、一般的な作業を行うために共有ディレクトリーをどう使うべきかについても解説しました。またシグナル処理についても簡単に触れ、少なくともどのように使えば安全かを説明しました。

当然のことですが、プログラムが競合状態に陥らないようにすることが重要です。しかし今日のプログラムは全てを自分で行うことはできず、コマンド・インタープリターやSQLサーバーなど、他のライブラリーやプログラムに対して要求を出す必要があります。そしてプログラムを攻撃するために最も一般的な手段は、他のプログラムに要求を出す部分を悪用することなのです。次回の記事では、脆弱性をさらすことなく他のプログラムを呼ぶ方法について解説する予定です。

参考文献

  • DavidがdeveloperWorksで書いている、セキュアなプログラマー・シリーズの他の記事も見てください。
  • David著のSecure Programming for Linux and Unix HOWTO(2003年3月、Wheeler刊)では、セキュアなプログラムを開発するための詳細を解説しています。
  • 2001年7月9日にJarno Huuskonenが書いているBugtraq e-mail "Tripwire temporary files"では、Tripwire V1.3.1と2.2.1、そして2.3.0 のsymlink競合条件に関する情報を説明しています。これはCVE-2001-0774です。
  • CVE-2002-0435はGNUファイル・ユーティリティーでの競合条件についての説明しています。
  • Perl 5.8 documentation of File::Tempがオンラインで入手できます。
  • Kris Kennaway's posting to Bugtraq about temporary files(2000年12月15日)でも一時ファイルに関して解説しています。
  • Michal ZalewskiによるDelivering Signals for Fun and Profit: Understanding, Exploiting and Preventing Signal-Handling Related Vulnerabilities(趣味と実益を兼ねてシグナルを提供する:シグナル処理に関連した脆弱性の悪用とその防止、2001年5月16-17日)では、競合状態を招くもう一つの要因である、シグナル処理を解説しています。
  • Michal ZalewskiによるProblems with mkstemp()では、一時ディレクトリーを自動クリーニングすることによって起こりうるセキュリティー問題を解説しています。
  • Security section of the GNOME Programming Guidelinesでは、一時ファイルの生成方法について適切な助言を提供しています。
  • 2004年版The Single UNIX Specification, Version 3(IEEE Std 1003.1の2004年版としても知られています)は、「UNIXライク」のシステムが何を行うべきかを記述した共通仕様です。
  • Solar DesignerによるLinux kernel patch from the OpenWall projectにはセキュリティー対抗策として興味深いものがいくつか含まれており、ファイルシステムにおける、ある範囲の競合状態に対する攻撃防止のためのアクセス制限などがあります。具体的には、あるディレクトリーで作られた信頼できないシンボリック・リンクをユーザーがたどらないように制限したり、ユーザーが読み書きアクセスできないファイルに対してハード・リンクを作れないように制限したりするようになっています。
  • USENIX Associationが2001年に開催したUsenix Security Symposiumで発表された、Crispin CowanとSteve Beattie、Chris Wright、そしてGreg Kroah-Hartmanの共著による論文「RaceGuard: Kernel Protection From Temporary File Race Vulnerabilities」では、RaceGuardを解説しています。RaceGuardは、ある種の攻撃を実行時に検出することで、一部の競合条件に対応できるようにしたLinuxカーネルの修正です。
  • 第12回USENIX Security Symposium(2003年8月)で発表されたEugene TsyrklevichとBennet YeeによるDynamic Detection and Prevention of Race Conditions in File Accessesでは、ファイルシステムでの競合条件に対応するための手法について解説しています。OpenBSDカーネルを修正することによって、あるファイルシステム操作が他の操作と妨害し合うことが分かるとファイルシステム操作は一時的に中断され、最初のプロセスがファイル・オブジェクトにアクセスして進めるようにするのです。
  • developerWorksのLinuxゾーンには、Linux開発者のための資料が他にも豊富に用意されています。
  • Linux上で実行する、IBMミドルウェア製品の無料試用版をダウンロードしてください。developerWorksのSpeed-start your Linux appセクションからWebSphere® Studio Application DeveloperやWebSphere Application Server、DB2® Universal Database、Tivoli® Access ManagerそれにTivoli Directory Serverが入手できます。またハウ・ツー記事や技術サポートも用意されています。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=231411
ArticleTitle=セキュアなプログラマー: 競合状態を防ぐ
publish-date=10072004