EternalWindows
サービス / サービスのクライアント

今回は、前節のサーバーのクライアントを作成します。 クライアントは名前付きパイプでサーバーに接続し、 サーバーがパイプに書き込んだ値を受信します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR  szData[256];
	DWORD  dwResult;
	HANDLE hPipe;

	hPipe = CreateFile(TEXT("\\\\.\\pipe\\GetComputer"), GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (hPipe == INVALID_HANDLE_VALUE) {
		if (GetLastError() == ERROR_ACCESS_DENIED)
			MessageBox(NULL, TEXT("パイプへのアクセスが拒否されました。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("パイプへの接続に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	ReadFile(hPipe, szData, sizeof(szData), &dwResult, NULL);
	
	MessageBox(NULL, szData, TEXT("サーバーのコンピュータ名"), MB_OK);

	CloseHandle(hPipe);

	return 0;
}

前節のプログラムが実行されており、さらにサービスが開始されていれば、 クライアントはサーバーに接続できることになります。

hPipe = CreateFile(TEXT("\\\\.\\pipe\\GetComputer"), GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

CreateFileはファイルのオープンだけではなく、 名前付きパイプのオープンにも利用することができます。 第1引数には必ず、サーバーで指定したパイプ名と同じ文字列を指定してください。 関数が成功したらパイプに接続できたということなので、 前節のOVERLAPPED構造体のhEventメンバはシグナル状態となります。

ReadFile(hPipe, szData, sizeof(szData), &dwResult, NULL);

サーバーからのデータを受信します。 CreateFileでFILE_FLAG_OVERLAPPEDを指定しなかったため、 ReadFileはサーバーからのデータを受信するまで制御を返しません。 しかし、前節のサーバーはクライアントとの接続を確認後、 直ちにWriteFileを呼びだしていたため、 結果的にReadFileは直ぐに制御を返すことになります。

今回のクライアントプログラムを見てみると分かることですが、 このプログラムにはサービス関連のコードが一切存在しません。 それもそのはずで、クライアントはサーバーとのデータを送受信するのが目的ですから、 SCPのようにサービスを開始したり停止したりすることはありませんし、 サーバーがサービスとして実装されているかどうかを把握する必要もありません。 つまり、クライアントを作成する際には、サービスの知識は必須ではないのです。

名前付きパイプのセキュリティ

名前付きパイプは、Windowsのアクセスコントロールに統合されているオブジェクトです。 つまり、パイプにセキュリティ記述子を設定することが可能で、 パイプに対するクライアントからの接続を制御できます。 一般にセキュリティ記述子を指定せずにオブジェクトを作成した場合、 その作成元のトークンのデフォルトDACLが割り当てられることになります。 サービスはローカルシステムアカウントで動作しており、 ローカルシステムアカウントのトークンのデフォルトDACLは、 自分自身(つまり、ローカルシステムアカウント)にフルアクセスを許可するACEと、 管理者グループに読み取りと実行を許可するACEで構成されています。 つまり、クライアントが管理者やシステムとして動作していなければ、 サーバーへの接続に失敗することが予想されます。

実際に上記した予想が当たっているかを確認するためには、 クライアントを降格したセキュリティコンテキストで実行しなければなりません。 たとえば、ImpersonateAnonymousTokenを呼び出すと、 クライアントは匿名アカウント(ANONYMOUS LOGON)として実行することができます。

ImpersonateAnonymousToken(GetCurrentThread());

このコードを実行した状態でCreateFileを呼び出したところ、 不思議な事にパイプへの接続は成功することになります。 この問題を考えるべく、サーバーが作成したパイプのハンドルからセキュリティ記述子を取得し、 DACL内のACEをファイルにダンプしてみたところ、次のような結果を得ることができました。

アカウント アクセス権
SYSTEM FILE_ALL_ACCESS
Administrators FILE_ALL_ACCESS
Administrators FILE_ALL_ACCESS
Everyone FILE_GENERIC_READ & ~FILE_READ_EA | FILE_CREATE_PIPE_INSTANCE
ANONYMOUS LOGONFILE_GENERIC_READ & ~FILE_READ_EA | FILE_CREATE_PIPE_INSTANCE

この結果を見て分かることは、パイプのセキュリティ記述子がトークンのデフォルトDACLを参照していないということです。 つまり、システムによって、Everyoneグループにアクセスを許可するACEや、 ANONYMOUS LOGONにアクセスを許可するACEが明示的に追加されているため、 こうしたアカウントでもパイプに接続できるのは当然といえば当然となります。 ただし、これではアクセスを全く制限できていないことになりますから、 しかるべき対策を取る必要があります。

PACL                pDacl;
EXPLICIT_ACCESS     explicitAccess[2];
SECURITY_ATTRIBUTES securityAttributes;
SECURITY_DESCRIPTOR securityDescriptor;

InitializeSecurityDescriptor(&securityDescriptor, SECURITY_DESCRIPTOR_REVISION);

BuildExplicitAccessWithName(&explicitAccess[0], TEXT("SYSTEM"), FILE_ALL_ACCESS, GRANT_ACCESS, 0);
BuildExplicitAccessWithName(&explicitAccess[1], TEXT("Administrators"), FILE_ALL_ACCESS, GRANT_ACCESS, 0);
SetEntriesInAcl(2, explicitAccess, NULL, &pDacl);

SetSecurityDescriptorDacl(&securityDescriptor, TRUE, pDacl, FALSE);

securityAttributes.nLength              = sizeof(SECURITY_ATTRIBUTES);
securityAttributes.lpSecurityDescriptor = &securityDescriptor;
securityAttributes.bInheritHandle       = FALSE;

hPipe = CreateNamedPipe(TEXT("\\\\.\\pipe\\GetComputer"), PIPE_ACCESS_OUTBOUND | FILE_FLAG_OVERLAPPED, 
	PIPE_TYPE_BYTE, 1, sizeof(szBuf), sizeof(szBuf), 1000, &securityAttributes);

このコードはセキュリティ記述子を明示的に指定することにより、 ローカルシステムアカウントと管理者グループのみにアクセスを許可しています。 一方、クライアントを一般のユーザーや匿名アカウントとして実行した場合、 パイプの接続に成功することはありません。 よって、サーバーはアクセス可能なクライアントを制限できたことになります。

さて、これで全てが解決したと思いたいところですが、 やはり、パイプのデフォルトセキュリティについて今一度考えておく必要があるでしょう。 ANONYMOUS LOGONにアクセスを許可するという点は、軽視できる問題ではありません。 たとえば、サーバーがクライアントからの接続の度にスレッドを作成して そこで処理するような設計(コンカレントモデル)であれば、 悪意のあるリモートコンピュータからの接続の連打を受け、 そのサーバーはリソースとスレッドスイッチングの極度な発生により、 ダウンを余儀なくされることでしょう。 このようなヌルセッションアクセス(つまり、認証なしのアクセス)を許可する場合には、 計り知れない潜在的な問題が起こる可能性があるわけです。

実は、パイプとヌルセッションに関係する情報は、次のレジストリキーに存在します。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\lanmanserver

上記キーのparametersサブキーには、次のようなエントリが含まれています。 (lanmanserverは、ネットワーク経由でのファイルや印刷、 さらにパイプの共有をサポートするサービスです。)

名前 データ
AdjustedNullSessionPipes 1
NullSessionPipes COMNAP COMNODE SQL\QUERY SPOOLSS LLSRPC browser

このNullSessionPipesに書き込まれているのは、パイプの名前です。 つまり、実際にCOMNAPやbrowserという名前を持つパイプを作成するアプリケーションが存在するということです。 恐らく、AdjustedNullSessionPipesが1であるときには、 これらのパイプのデフォルトセキュリティがヌルセッションアクセスを許可するのだろうと思われます。 しかし、実際にはNullSessionPipesにパイプ名を入れていないのにも関わらず、 パイプのデフォルトセキュリティはヌルセッションアクセスを許可していることから、 デフォルトセキュリティの謎は解明しきることができませんでした。 ちなみに、parametersキーにRestrictNullSessionAcessというエントリを追加して そのデータを0にした場合、コンピュータ上の全てのパイプと共有に対して、 ヌルセッションアクセスが可能となるようです。



戻る