第5章 プログラム配置対策
子プロセスからの侵害対策

別のプロセスの起動

Unix系、GNU/Linux系のプラットフォームでは、プログラムの実行可能ファイルは「プロセス」という単位で実行される。ひとつのプロセスで目的を達成する場合もあるが、プログラムによっては、複数のプロセスを組み合わせて動作させることがしばしば行われる。

プログラムから別のプログラムを新しいプロセスとして起動する場合、いくつか注意が必要である。それは、起動されたプログラムは起動する側のプログラムが持っていた権限、ファイル記述子、環境変数などを自動で引き継ぐからである。

もし攻撃者がシステムに干渉して、本来のプログラムの代わりに自分のプログラムが子プロセスとして起動されるよう仕向けることができるなら、そのプログラムを通じてセキュリティ侵害が起こり得る。

起こり得る侵害

プログラムから別のプログラムを呼び出す場面で起こり得る侵害には次のものがある。

(1) 偽の子プログラムを用いた権限昇格

管理者権限で動作する親プロセスが呼び出すことになっている子プログラムを同名の別のプログラムにすり替え、起動された偽プログラムが管理者権限を不正に取得するというもの。

(2) PATH環境変数を利用した子プログラムのすり替え

プログラムファイルを物理的にすり替えるのではなく、PATH環境変数の値を変えておくことによって偽プログラムが起動されるように仕組むというもの。

(3) 親プロセスのファイルへの干渉

親プロセスがオープンし、クローズしないままにしているファイル記述子に悪意の子プロセスからアクセスして、情報の漏えい・改ざんを起こすというもの。

(4) コマンド注入攻撃

内部でシェルが使われるタイプのAPIを用いて別のプログラムを起動するロジックに対して、そのシェルに想定外のコマンドを与えてシステムを不正に操る「コマンド注入攻撃」をしかけるというもの。

図16: 子プロセスからの侵害

プログラム起動APIの選択

プログラムから別のプログラムを起動する際に利用できるAPIには複数あるが、次の考慮が必要である。

(1) コマンド注入攻撃のおそれのあるAPIの使用を避ける

可能なら、別プログラムを起動するためのAPIとしてsystem()関数やpopen()関数の使用を避ける。

これらの関数は実行するコマンド文字列を引数としてとるが、この文字列はシェルによって解釈実行される。このコマンド文字列の途中にユーザ入力値等、プログラム外部から与えられた値が埋め込まれるようになっていると、コマンド注入攻撃を受けるおそれがある。

多少プログラミングの手間はあるが、別のプログラムを呼び出す際には、fork()関数とexecve()関数を組み合わせて用いる。

(2) vforkの使用を避ける

子プロセスを生成する際、vfork()関数の使用を避け、fork()関数を使う。

子プロセスを生成するfork()関数の実装は当初、親プロセスのメモリ内容をそっくり複製して子プロセスを作るというものだった。そのオーバーヘッドを避けるため、かつて作られたのがvfork()関数である。vfork()関数を使うと、別のプログラムをロードするまでのあいだ、子プロセスは親プロセスのメモリを借りて動作する。そのため、vfork()関数呼び出し前後のロジックにミスがあると、親プロセスの動作に大きな支障をきたすおそれがある。

現在のプラットフォームでは、fork()の実装は改善されている。仮想メモリの機能を利用して効率よく子プロセスのメモリ空間を初期化するようになっている。

(3) 環境変数 PATH を利用する関数の使用を避ける

可能なら、別プログラムを起動するためのAPIとしてexeclp()関数とexecvp()関数の使用を避ける。

これらの関数は、起動するプログラムの名前が「/」で始まっていないと、PATH環境変数を参照して起動するプログラムを探索する。常にフルパス名でプログラムを指定すればよいのだが、ディレクトリ修飾を省略してもプログラムを起動できるため、誤りに気づかないおそれもある。

現在のプロセスに新しいプログラムをロードするには execve() 関数を始めとする、PATH環境変数を参照しない API を使用する。

高い権限を持たせない

管理者権限をもつ親プロセスが子プロセスを起動すると、子プロセスはその権限を継承し、同じく管理者権限をもつようになる。これを利用して攻撃者は自分のプログラムを管理者権限で動作させるおそれがある。

子プロセスの実行に管理者権限が不要である場合、親プロセスは一時的あるいは永久に管理者権限を放棄して子プロセスを起動するようにする。

(1) 管理者権限をもつプログラムが一時的に権限を手放すコード
1 uid = USERID;                // 一般ユーザのユーザID
2                              // setuidプログラムならgetuid()で得てもよい
3 error = seteuid (uid);       // 管理者権限の一時的な放棄
4 
5 /* 管理者権限を持たずに行う処理 */
6 /* 子プロセスの起動をここで行う */
7 
8 uid = 0;                     // rootのユーザID
9 error = seteuid (uid);       // 管理者権限の復活
(2) 管理者権限をもつプログラムが永久に権限を放棄するコード
1 uid = USERID;                // 一般ユーザのユーザID
2                              // setuidプログラムならgetuid()で得てもよい
3 error = setuid (uid);        // 管理者権限の永久放棄
4 
5 /* 管理者権限を持たずに行う処理 */
6 /* 子プロセスの起動をここで行う */

起動されたプログラムが継承するもの

execve() 等のAPIによって起動されたプログラムは、起動する側のプログラムから次の属性を継承する。

属性関連する関数
close-on-execの印が付いていないファイル記述子fstat(), read(), write()等
プロセス IDgetpid()
親プロセス IDgetppid()
プロセスグループ IDgetpgrp()
アクセスグループgetgroups()
作業ディレクトリchdir()
ルートディレクトリchroot()
制御端末termios()
リソースの使用状況getrusage()
インターバルタイマgetitimer()
リソースの使用制限getrlimit()
ファイルモードマスクumask()
シグナルマスクsigvec(), sigsetmask()

継承されるものへの対策

起動されるプログラムが継承し得るリソースのうち、ファイル記述子と環境変数は次の方法を用いて継承を制限する。

(1) ファイルとソケットを明示的にクローズする

子プロセスを起動する前に、できるかぎりファイルおよびソケットを明示的にクローズする。

Unix系、GNU/Linux系のプラットフォームでは、親プロセスがオープンしたファイルやソケットのファイル記述子は子プロセスに引き継がれる。子プロセスの中で悪意あるプログラムが実行された場合、オープンされているファイル記述子を通じて情報の読み出し、改ざん、予定外の通信等がおこなわれるおそれがある。

(2) close-on-execフラグの利用

明示的にファイル記述子をクローズするのではなく、他のプログラムをロードする際に自動でファイル記述子を閉じるよう指定する close-on-exec オプションを、fcntl()関数を用いて指定することができる。

/* 特定のファイル記述子に close-on-exec オプションを与える */
fcntl (filedesc, F_SETFD, 1);

(3) 環境変数は必要最低限のもののみ引き継ぐ

起動されるプログラムに引き継ぐ環境変数は必要最小限にとどめるのがよい。
次のAPIを使うと環境変数が全て引き継がれるので、可能なら使用を避ける。

・int execl(const char *path, const char *arg, ...);
・int execlp(const char *file, const char *arg, ...);
・int execv(const char *path, char *const argv[]);
・int execvp(const char *file, char *const argv[]);
・FILE *popen(const char *command, const char *type); 

次のAPIを使うと、環境変数プールの内容を、起動する側のプログラムが指定できる。

・int execve(const char *filename, char *const argv[],
             char *const envp[]); 
・int execle(const char *path, const char *arg, ...,
             char * const envp[]);

まとめ

親プロセスが子プロセスを生成して別のプログラムを動作させるとき、子プロセス側のプログラムは親プロセスが使用しているリソースへ不正に干渉する機会を得る。プログラムから別のプログラムを起動する際には、プログラムのすり替えへの対策、ファイル記述子や環境変数を初期化してからの起動などの対策が必要となる。