第8章 セキュアWindowsプログラミング
[8-2.]
プロセス間通信とバックドア
プロセス間通信機能を活用して,システムに常駐するサービスプロセスとユーザインタフェースをもつクライアントプロセスの連携で処理を進めるというソフトウェアの形態がある。サービスプロセスは通常高い権限で実行されるため,クライアントからの要求を無条件で受け入れて危険なシステム操作をしてしまわないよう,安全対策が必要である。



 PDF
常駐プログラム
アプリケーションソフトウェアの構築において,ユーザインタフェースと,データ処理エンジンのモジュールを分けて設けるといったことはよく行われる手法である。複雑なデータを取り扱ったり,何かのデバイスからの入力を監視したり,通信に対応する必要がある場合,データ処理エンジンの側のモジュールはシステムに常駐させる必要がある。
 
Windows 2000やWindows NTのプラットフォームには,システムに常駐させて稼動させることのできる「サービスプロセス」(あるいはたんに「サービス」)と呼ばれるプログラムの形態がある。
サービスプロセス
Windows のサービスプロセスは,Unix システムやLinux システムの「デーモン」プロセスに相当するが,その書き方に少々約束ごとがある。サービスプロセスをプログラミングする際に行う必要があるのは主に次のようなことだ。
  • サービス本体には,Windows のサービスコントロールマネージャからの指令(一時停止,再開,終了など)を受け入れる所定のインタフェースを実装する。
  • 実際にシステムでサービスを稼動させる際には,サービスコントロールマネージャのAPI を使ってサービスプロセスのインストール/アンインストールを行う。このため多くの場合,サービスのインストーラ/アンインストーラプログラムを用意することになる。
  • サービスからイベントログに記録を書き込む場合には,記録させるメッセージのテンプレートテキストデータをDLL で用意し,その情報をレジストリに書き込んでおく。
こうした「形」を整えた上で,サービスプロセスの中で本来の仕事をする主処理スレッドを稼動させる。すなわち,
  • 無限に動き続けるスレッドを設け,サービスプロセス本来の処理を行う。
そしてその上で,サービスコントロールマネージャから指令を受けた場合にこのスレッドの一時停止,再開,終了など所定の制御ができるようなロジックを周辺に配置しておく。
Win32 API のプロセス間通信機能
ユーザインタフェースを受け持つプログラムがバックグラウンドで稼動しているサービスプロセスと通信するには,システムに用意されているプロセス間通信機能を利用すればよい。Win32 APIのプロセス間通信機能には,クリティカルセクション,イベント,ミューテックス,セマフォによる同期機能と,メールスロットと名前つきパイプとによる通信機能がある(表1)。
表1 Win32 API のプロセス/スレッド間通信機能
同期機能
「クリティカルセクション」 同時には一つのスレッドだけがリソースにアクセスするような複数スレッド間同期に用いられる。クリティカルセクションは同一のプロセス内のスレッド間でのみ利用できる。
「ミューテックス」 共有リソースにアクセスできるスレッドを同時には一つに制限するための同期オブジェクト。複数プロセスにまたがって使用できる。
「イベント」 プロセス/スレッド間で「待ち」「通知」を行うための同期オブジェクト。
「セマフォ」 共有リソースに同時にアクセスできるスレッドの数を「一定数以下」という形で設定できる同期オブジェクト。
通信機能
「メールスロット」 メッセージをランダムにポストするプロセス/スレッドとそれを受け取って順次処理するプロセス/スレッドの組み合わせを構成する通信機能。
「名前付きパイプ」 複数のプロセス/スレッド間で双方向の通信チャネルを開設できる通信機能。
メールスロット
メールスロットはいわば簡易メッセージキューであり,メッセージの到着を待ち受けるプロセスと,メッセージをポスト(発信)するプロセスの間を取り持ってくれる仕組みである。送信側プログラムが送り出した「メッセージ」が次々とメールスロットにポストされ,読み出し側プログラムはそれを順に取り出して処理することができる(図1)。
図1 メールスロット
 図1 メールスロット
メールスロットはサービスプロセスに次々と情報を投入し収集させるといった場面で利用できる。ただし,接続先を強く意識した処理,とくにアクセスコントロールについてはメールスロットへの書き込みを特定のアカウントに制限できるといったことだけであり,きめ細かな処理を行う場合には名前付きパイプの方が向いている。
名前付きパイプ
「名前付きパイプ」はシステム上の複数プロセスが双方向にデータを交換できる通信チャネルである。生成時に設定する上限の個数の範囲内で同時に複数の名前付きパイプを設けることができ,1つのサーバプログラムが複数のクライアントプログラムと通信することが可能である(図2)。
図2 名前付きパイプ
 図2 名前付きパイプ
名前付きパイプを使うときには,メールスロットと同じように「メッセージ」の単位で通信をするモードと,バイトストリームとして読み書きするモードの2つのモードが選べる。
 
どちらのモードで通信する場合でも,名前付きパイプを生成し接続した後は,サーバ側・クライアント側とも通常のファイル入出力API を使用することで通信が行える。
インパーソネーション
名前付きパイプで特筆すべき機能に「インパーソネーション」がある。これは,名前付きパイプのサーバ側プログラムがクライアントのための処理を実行する際,自分自身のアカウント権限ではなくクライアント側のアカウント権限でスレッドの実行を行うというものである(図3)
図3 インパーソネーション
 図3 インパーソネーション
サーバプロセスは通常高い権限(多くの場合SYSTEM権限)で稼動しており,その権限のままクライアントの指示に従ってシステムのリソースにアクセスをしてしまうと,本来許されない参照や誤った更新をしてしまうおそれがある。
 
インパーソネーションはそういった問題を起こさないようにするための安全機構として利用できる。
サービスとの通信
システムに常駐するサービスプロセスとユーザインタフェースモジュールとの間で通信を行う場合には名前付きパイプを利用すると具合がよい。実際に名前付きパイプを利用して通信を行うサーバプログラムとクライアントプログラムの例を示そう。
 
リスト1は,サービスプロセスとして書かれたCommanderSvcというサーバプログラムの中の,クライアントからの通信を受けて動作する部分を抜粋したC++ソースコードである。40行目以降にあるMyServiceThread関数の中がサーバ処理の最も基本的なループを構成する部分である。
リスト1 名前付きパイプでリクエストを待ち受けるサービスCommanderSvc.cpp(主要部分)
  1 // 最大値
  2 #define MY_MAX_MESSAGE_SIZE 400
  3 #define MY_MAX_SESSIONS 10
  4
  5 // MySessionThread - クライアントからのメッセージを受け取り処理する
  6 void
  7 MySessionThread (HANDLE hPipe) {
  8     // 名前付きパイプからメッセージを読み出す
  9     char szMessage[MY_MAX_MESSAGE_SIZE + 1];
 10     memset (szMessage, '\0', sizeof (szMessage));
 11     DWORD bytesBeenRead;
 12     BOOL bOK = ReadFile (hPipe, szMessage,
 13         sizeof (szMessage) - 1, &bytesBeenRead, NULL);
 14     if (! bOK) {
 15         DisconnectNamedPipe (hPipe);
 16         CloseHandle (hPipe);
 17         return;
 18     }
 19
 20     // インパーソネーション(偽装)を行なう。
 21     // = 現在のスレッドの実行権限を一時的にクライアント側アカウントのものにする
 22     bOK = ImpersonateNamedPipeClient (hPipe);
 23     if (! bOK) {
 24         DisconnectNamedPipe (hPipe);
 25         CloseHandle (hPipe);
 26         return;
 27     }
 28
 29     // メッセージをコマンドとして実行する。
 30     system (szMessage);
 31
 32     // インパーソネーション(偽装)を解除する
 33     RevertToSelf ();
 34
 35     // 名前付きパイプの接続を解除し,クローズする
 36     DisconnectNamedPipe (hPipe);
 37     CloseHandle (hPipe);
 38 }
 39
 40 // MyServiceThread - サービスの主処理
 41 DWORD
 42 MyServiceThread (LPDWORD param) {
 43     // 無限ループ
 44     //(サービスコントロールマネージャから司令があれば中断される)
 45     for (;;) {
 46
 47         // 不特定多数のクライアントからの名前付きパイプへのアクセスを
 48         // 許可するためのセキュリティ属性の設定
 49         SECURITY_DESCRIPTOR secDesc;
 50         InitializeSecurityDescriptor (&secDesc, SECURITY_DESCRIPTOR_REVISION);
 51         SetSecurityDescriptorDacl (&secDesc, TRUE, NULL, FALSE);
 52         SECURITY_ATTRIBUTES secAttr;
 53         secAttr.nLength = sizeof (SECURITY_ATTRIBUTES);
 54         secAttr.bInheritHandle = FALSE;
 55         secAttr.lpSecurityDescriptor = &secDesc;
 56
 57         // 名前付きパイプの生成
 58         HANDLE hPipe = CreateNamedPipe ("\\\\.\\pipe\\commandersvc",
 59             PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE|PIPE_WAIT,
 60             MY_MAX_SESSIONS, 0, 0, 100, &secAttr);
 61         if (hPipe == INVALID_HANDLE_VALUE)
 62             return 1;
 63
 64         // 名前付きパイプにクライアントが接続しようとするのを待つ
 65         BOOL bOK = ConnectNamedPipe (hPipe, NULL);
 66         if (! bOK) {
 67             CloseHandle (hPipe);
 68             continue;
 69         }
 70
 71         // 一つの接続ごとに一つのスレッドを作って処理する
 72         DWORD dwThreadID;
 73         HANDLE hSessionThread = CreateThread (0, 0,
 74             (LPTHREAD_START_ROUTINE)MySessionThread, hPipe, 0, &dwThreadID);
 75         if (hSessionThread == NULL) {
 76             CloseHandle (hPipe);
 77             continue;
 78         }
 79     }
 80 }
ここでは,セキュリティ属性パラメタ(注1)を作成したあと,58 行目で名前付きパイプを生成している。この名前付きパイプは「\\.\pipe\commandersvc」という名前で生成しているが,最初の「\\.」は「現在のコンピュータ」を表す記法であり, 実際にはそのコンピュータ名が入った, たとえば「\\ foobar\pipe\commandersvc」のような名前でも参照されることになる。ここでは,名前付きパイプの生成に失敗したときには直ちに関数を終了するようにしているが,本来は上限の個数一杯までパイプが使われているときにはそのどれかが閉じられるのを待つなどの処理を行うところである。
 
ここで作った名前付きパイプに対して,65行目で接続を待ち受けている。クライアントからの接続があると,先へ進み,73行目で別のスレッドを起動してそのパイプの処理を委ねている。新しく作られたスレッドではMySessionThread 関数が実行される。
 
接続が起きるたびに作られるMySessionThread関数のスレッドでは,名前付きパイプからクライアントの指令を読み出して実行する。
 
バッファオーバーフローなどに注意しつつ,12行目で名前付きパイプからの読み出しを行っている。注目していただきたいのは,22行目のImpersonateNamedPipeClient関数の呼び出しだ。この呼び出しにより,このスレッドはこれ以降,名前付きパイプに接続しているクライアント側のアカウント権限で動作することになる。これは33 行目でRevertToSelf を呼び出すまで有効だ。このスレッドからのシステムリソースへのアクセスには,クライアント側の権限に基づいて制限が加えられることになる。
 
サーバの処理の中で別のプロセスを起動する場合を想定し,ここでは簡単のため,クライアントが送ってきた文字列をラインモードのコマンドとしてsystem 関数で実行している。30 行目がそうである。
(注1・ここで設定しているセキュリティ属性パラメタは,誰にでもこの名前付きパイプへのアクセスを許す「制限の緩い」アクセスコントロールリスト(DACL)を含むものである。実際の現場では特定のグループのアカウントだけにアクセスを許可するといったアクセスコントロールリストを設定していただきたい。)
クライアントプログラム
このサービスプロセスに接続してコマンドを投入するCommanderという簡単なクライアントプログラムのC++ソースコードをリスト2に示す。
リスト2 名前付きパイプを使うクライアントプログラムCommander.cpp
  1 #include <windows.h>
  2 #include <stdio.h>
  3
  4 main (int numArgs, char** ppArgs) {
  5     if (numArgs < 2) {
  6         printf (" 使い方: commander コマンド...\n");
  7         return 1;
  8     }
  9
 10     // 名前付きパイプへの接続
 11     HANDLE hPipe = CreateFile ("\\\\.\\pipe\\commandersvc",
 12         GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING,
 13         FILE_ATTRIBUTE_NORMAL, NULL);
 14
 15     if (hPipe == INVALID_HANDLE_VALUE) {
 16         printf (" サービスに接続できません.\n");
 17         return 2;
 18     }
 19
 20     // サービスに送信するメッセージ文字列の組み立て
 21     char szMessage[1024];
 22     char szSeparator[4];
 23     strcpy (szMessage, "");
 24     strcpy (szSeparator, "");
 25
 26     for (int i = 1; i < numArgs; i++) {
 27         strncat (szMessage, szSeparator, sizeof (szMessage) - strlen (szMessage) - 1);
 28         strncat (szMessage, ppArgs[i], sizeof (szMessage) - strlen (szMessage) - 1);
 29         strcpy (szSeparator, " ");
 30     }
 31
 32     // サービスにメッセージを送信
 33     DWORD bytesBeenWritten;
 34     BOOL bOK = WriteFile (hPipe,
 35         szMessage, strlen (szMessage), &bytesBeenWritten, NULL);
 36     if (! bOK) {
 37         printf (" コマンドを送信できません.\n");
 38         CloseHandle (hPipe);
 39         return 2;
 40     }
 41
 42     // 名前付きパイプを閉じる
 43     CloseHandle (hPipe);
 44
 45     return 0;
 46 }
11行目のCreateFile関数呼び出しで名前付きパイプをオープンしている。このプログラムは,サービスが稼動するのと同じマシンでの使用を前提として,名前付きパイプの名前の先頭に「\\.」の記法を用いている。
 
ここで使っているCreateFileは主にファイルをオープンするときに使われるAPIであるが,メールスロットや名前付きパイプをオープンするのにも使う。オープンされて得られた名前付きパイプのハンドルはファイルのハンドルなどとほぼ同様に扱うことができ,Win32 APIの入出力関数を使った読み出し・書き込み操作で名前付きパイプの通信が行える。
 
このプログラムでは,与えられた引数を21 〜 30 行目で一つの文字列に仕立てて,34 行目でその文字列を名前付きパイプに書き込んでいる。最後に名前付きパイプのハンドルをクローズし,クライアントプログラムはこれで終わりである。このCommander プログラムを次のような形,
    C:\> Commander コマンド引数...
で実行すると,「コマンド引数 ...」の部分がサービスプロセスCommanderSvcに送られ,サーバ側でsystem関数を使って実行される。
インパーソネーションと子プロセス
サーバのスレッドがインパーソネーションしているので,system 関数で呼び出された子プロセスもクライアント側のアカウントで稼動してくれると期待してよいだろうか? 実はダメなのである。まずそれを確かめてみよう。
 
リスト3に示すのは,与えられた引数をメッセージボックスにエコー表示するとともに,そのプロセスが動作しているアカウントのユーザ名をタイトルバーに表示する,Popupという簡単なC++プログラムだ。このプログラムをPATH 環境変数で呼び出せる場所に配置しておき,一般ユーザアカウントfoo から次のようなコマンド,
    C:\> Commander Popup alpha bravo charlie
を投入して表示されたのが画面1である。このウィンドウのタイトルバーが示すように,子プロセスはSYSTEMアカウントで稼動している。
リスト3 引数をエコーし,現在のユーザ名をタイトルバーに表示するプログラムPopup.cpp
  1 #include <windows.h>
  2
  3 int WINAPI
  4 WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
  5     // 現在のアカウントのユーザ名を取得
  6     char szUserName[100];
  7     DWORD dwSize = sizeof (szUserName);
  8     BOOL bOK = GetUserName (szUserName, &dwSize);
  9
 10     // ウィンドウタイトルにユーザ名を表示
 11     // メッセージボックスにこのプログラムへの引数をエコー表示
 12     MessageBox (NULL, lpCmdLine, szUserName, MB_OK);
 13
 14     return 0;
 15 }
画面1 サービスの子プロセスがSYSTEM アカウントで動作
 画面1 サービスの子プロセスがSYSTEM アカウントで動作
Win32 API の世界では,スレッドがインパーソネーションで他のアカウント権限で動作していても,一般のAPI を使っている限り起動した子プロセスには引き継がれないのである。起動される新しいプロセスは,インパーソネーションしていないときのように,親プロセスと同じアカウント権限で動作する(図4)。
図4 インパーソネーション・スレッドからの子プロセス起動
 図4 インパーソネーション・スレッドからの子プロセス起動
バックドアの危険
サービスプロセスの子プロセスがSYSTEM権限で動作する問題を見逃していると,高い権限でシステムを操作できる「権限昇格」の機会を一般ユーザに与えてしまうおそれがある。悪くすると,ネットワーク経由で他のコンピュータから部外者に指令を送り込まれてシステムに干渉されることもあり得る。
 
SYSTEM権限のまま子プロセスを起動してしまうサービスプロセスは,そのコンピュータのバックドア(「裏口」)となる危険を隠し持っているのである。
CreateProcessAsUser
インパーソネーションしている現在のクライアントのアカウント権限を持たせて子プロセスを起動したいときには,通常プロセスの起動に使われるCreateProcess関数の代わりにCreateProcesAsUserというAPIを使う必要がある。ライブラリ関数system は前者のCreateProcess を内部で呼び出しているために上記のような現象が起きるのだ。
 
現在のインパーソネーションのアカウントで子プロセスを起動する処理の例をリスト4に示す。実行するコマンドラインを引数として受け取る,MyInvokeCommand というC++ の関数の形にまとめてある
リスト4 インパーソネーションのアカウントで子プロセスを起動する関数MyInvokeCommand
  1 // MyInvokeCommand - インパーソネーション(偽装)のアカウントでコマンドを実行する
  2 void
  3 MyInvokeCommand (char* szCommandLine) {
  4     // 現在のスレッドハンドルをhThread に得る
  5     HANDLE hThread = GetCurrentThread ();
  6
  7     // 現在のスレッドのトークン(権限情報)のハンドルをhToken に得る
  8     // これはインパーソネーション(偽装)トークンである
  9     HANDLE hToken;
 10     BOOL bOK = OpenThreadToken (hThread,
 11         TOKEN_QUERY|TOKEN_DUPLICATE|TOKEN_ASSIGN_PRIMARY, TRUE, &hToken);
 12
 13     // インパーソネーション(偽装)トークンと同じ内容を持つ
 14     // プライマリ(本人)トークンを生成しハンドルをhNewToken に得る
 15     HANDLE hNewToken;
 16     bOK = DuplicateTokenEx (hToken, 0, NULL,
 17         SecurityIdentification, TokenPrimary, &hNewToken);
 18
 19     // 新しいプロセスに指定する起動時情報として
 20     // 現在のプロセスの起動時情報をコピーしstartupinfo に得る
 21     STARTUPINFO startupinfo;
 22     GetStartupInfo (&startupinfo);
 23
 24     // プロセスが起動されたときに情報が帰ってくる構造体processinfo
 25     PROCESS_INFORMATION processinfo;
 26
 27     // 新しく作ったプライマリ(本人)トークンを指定して新しいプロセスを起動する
 28     // szCommandLine の内容を実行するコマンドラインとして与えている
 29     bOK = CreateProcessAsUser (hNewToken, NULL, szCommandLine, NULL,
 30         NULL, FALSE, 0, NULL, "C:\\test", &startupinfo, &processinfo);
 31
 32     // 起動したプロセスが終了するのを待つ
 33     WaitForSingleObject (processinfo.hProcess, INFINITE);
 34
 35     // プロセスハンドルが開かれたままになっているので閉じる
 36     CloseHandle (processinfo.hProcess);
 37 }
ポイントは29行目のCreateProcessAsUser呼び出しである。ここには通常のCreateProcessの引数に加えて最初の引数で「トークン」というもののハンドルを渡している。hNewToken というのがそうである。Win32 ではプロセスおよびスレッドのアクセス権限はこのトークンというもので取り扱われており,ここには目的のアカウントのアクセス権限をもつトークンを指定するのである。
 
プログラム前半のそこまでのステップの多くはこのトークンを得るためのものである。
 
5行目で現在のスレッドのハンドルを得,そこからスレッドのトークンをハンドルの形でhToken得ている(10行目)。ここで得られるトークンはインパーソネーションをしているいわば借り物のトークンなので,これをいわゆる本物の「プライマリトークン」という形にする必要がある。それを行っているのが,16 行目のDuplicateTokenEx 関数の呼び出しだ。19〜25行目のコードは新しいプロセスを起動する際に必要なパラメタを整えているものである。
 
system関数の代わりにこのMyInvokeCommandを使ってコマンドを起動するようリスト1の30行目を書き直して,一般ユーザアカウントfoo からもう一度次のようなコマンド,
    C:\> Commander Popup delta echo foxtrot
を投入してみた結果が画面2である。今度はクライアントのアカウ ントで子プロセスを実行させることができた。
画面2 サービスの子プロセスが一般ユーザアカウントで動作
 画面2 サービスの子プロセスが一般ユーザアカウントで動作
まとめ
フロントエンドのモジュールがサービスプロセスと通信する形でアプリケーションソフトウェアを構築する際にはWin32 の名前付きパイプが有用である。インパーソネーション機能を使って,クライアントから過剰な要求が投入された場合でもシステムリソースへのアクセスに歯止めをかけることができる。ただし,子プロセスを起動する際にはインパーソネーションが自動で引き継がれないので注意が必要だ。
参考文献
『Win32 システムサービスプログラミング』,マーシャル・ブレイン著,郡司芳昭訳,三田典玄監修,プレンティスホール出版