公開日:2007年9月26日
独立行政法人情報処理推進機構
セキュリティセンター
本ページの情報は2007年9月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。
Unix系、GNU/Linux系のプラットフォームでは、プログラムの実行可能ファイルは「プロセス」という単位で実行される。ひとつのプロセスで目的を達成する場合もあるが、プログラムによっては、複数のプロセスを組み合わせて動作させることがしばしば行われる。
プログラムから別のプログラムを新しいプロセスとして起動する場合、いくつか注意が必要である。それは、起動されたプログラムは起動する側のプログラムが持っていた権限、ファイル記述子、環境変数などを自動で引き継ぐからである。
もし攻撃者がシステムに干渉して、本来のプログラムの代わりに自分のプログラムが子プロセスとして起動されるよう仕向けることができるなら、そのプログラムを通じてセキュリティ侵害が起こり得る。
プログラムから別のプログラムを呼び出す場面で起こり得る侵害には次のものがある。
管理者権限で動作する親プロセスが呼び出すことになっている子プログラムを同名の別のプログラムにすり替え、起動された偽プログラムが管理者権限を不正に取得するというもの。
プログラムファイルを物理的にすり替えるのではなく、PATH環境変数の値を変えておくことによって偽プログラムが起動されるように仕組むというもの。
親プロセスがオープンし、クローズしないままにしているファイル記述子に悪意の子プロセスからアクセスして、情報の漏えい・改ざんを起こすというもの。
内部でシェルが使われるタイプのAPIを用いて別のプログラムを起動するロジックに対して、そのシェルに想定外のコマンドを与えてシステムを不正に操る「コマンド注入攻撃」をしかけるというもの。
プログラムから別のプログラムを起動する際に利用できるAPIには複数あるが、次の考慮が必要である。
可能なら、別プログラムを起動するためのAPIとしてsystem()関数やpopen()関数の使用を避ける。
これらの関数は実行するコマンド文字列を引数としてとるが、この文字列はシェルによって解釈実行される。このコマンド文字列の途中にユーザ入力値等、プログラム外部から与えられた値が埋め込まれるようになっていると、コマンド注入攻撃を受けるおそれがある。
多少プログラミングの手間はあるが、別のプログラムを呼び出す際には、fork()関数とexecve()関数を組み合わせて用いる。
子プロセスを生成する際、vfork()関数の使用を避け、fork()関数を使う。
子プロセスを生成するfork()関数の実装は当初、親プロセスのメモリ内容をそっくり複製して子プロセスを作るというものだった。そのオーバーヘッドを避けるため、かつて作られたのがvfork()関数である。vfork()関数を使うと、別のプログラムをロードするまでのあいだ、子プロセスは親プロセスのメモリを借りて動作する。そのため、vfork()関数呼び出し前後のロジックにミスがあると、親プロセスの動作に大きな支障をきたすおそれがある。
現在のプラットフォームでは、fork()の実装は改善されている。仮想メモリの機能を利用して効率よく子プロセスのメモリ空間を初期化するようになっている。
可能なら、別プログラムを起動するためのAPIとしてexeclp()関数とexecvp()関数の使用を避ける。
これらの関数は、起動するプログラムの名前が「/」で始まっていないと、PATH環境変数を参照して起動するプログラムを探索する。常にフルパス名でプログラムを指定すればよいのだが、ディレクトリ修飾を省略してもプログラムを起動できるため、誤りに気づかないおそれもある。
現在のプロセスに新しいプログラムをロードするには execve() 関数を始めとする、PATH環境変数を参照しない API を使用する。
管理者権限をもつ親プロセスが子プロセスを起動すると、子プロセスはその権限を継承し、同じく管理者権限をもつようになる。これを利用して攻撃者は自分のプログラムを管理者権限で動作させるおそれがある。
子プロセスの実行に管理者権限が不要である場合、親プロセスは一時的あるいは永久に管理者権限を放棄して子プロセスを起動するようにする。
1 uid = USERID; // 一般ユーザのユーザID
2 // setuidプログラムならgetuid()で得てもよい
3 error = seteuid (uid); // 管理者権限の一時的な放棄
4
5 /* 管理者権限を持たずに行う処理 */
6 /* 子プロセスの起動をここで行う */
7
8 uid = 0; // rootのユーザID
9 error = seteuid (uid); // 管理者権限の復活
1 uid = USERID; // 一般ユーザのユーザID
2 // setuidプログラムならgetuid()で得てもよい
3 error = setuid (uid); // 管理者権限の永久放棄
4
5 /* 管理者権限を持たずに行う処理 */
6 /* 子プロセスの起動をここで行う */
execve() 等のAPIによって起動されたプログラムは、起動する側のプログラムから次の属性を継承する。
属性 | 関連する関数 |
---|---|
close-on-execの印が付いていないファイル記述子 | fstat(), read(), write()等 |
プロセス ID | getpid() |
親プロセス ID | getppid() |
プロセスグループ ID | getpgrp() |
アクセスグループ | getgroups() |
作業ディレクトリ | chdir() |
ルートディレクトリ | chroot() |
制御端末 | termios() |
リソースの使用状況 | getrusage() |
インターバルタイマ | getitimer() |
リソースの使用制限 | getrlimit() |
ファイルモードマスク | umask() |
シグナルマスク | sigvec(), sigsetmask() |
起動されるプログラムが継承し得るリソースのうち、ファイル記述子と環境変数は次の方法を用いて継承を制限する。
子プロセスを起動する前に、できるかぎりファイルおよびソケットを明示的にクローズする。
Unix系、GNU/Linux系のプラットフォームでは、親プロセスがオープンしたファイルやソケットのファイル記述子は子プロセスに引き継がれる。子プロセスの中で悪意あるプログラムが実行された場合、オープンされているファイル記述子を通じて情報の読み出し、改ざん、予定外の通信等がおこなわれるおそれがある。
明示的にファイル記述子をクローズするのではなく、他のプログラムをロードする際に自動でファイル記述子を閉じるよう指定する close-on-exec オプションを、fcntl()関数を用いて指定することができる。
/* 特定のファイル記述子に close-on-exec オプションを与える */
fcntl (filedesc, F_SETFD, 1);
起動されるプログラムに引き継ぐ環境変数は必要最小限にとどめるのがよい。
次のAPIを使うと環境変数が全て引き継がれるので、可能なら使用を避ける。
次のAPIを使うと、環境変数プールの内容を、起動する側のプログラムが指定できる。
親プロセスが子プロセスを生成して別のプログラムを動作させるとき、子プロセス側のプログラムは親プロセスが使用しているリソースへ不正に干渉する機会を得る。プログラムから別のプログラムを起動する際には、プログラムのすり替えへの対策、ファイル記述子や環境変数を初期化してからの起動などの対策が必要となる。