EternalWindows
GINA / ワークステーションのロック

WlxLoggedOnSASで返すことのできる定数は非常に多く存在しますが、 大抵の場合、ユーザーにとって重要となるのはWLX_SAS_ACTION_LOCK_WKSTAだけでしょう。 この値を返すとWinlogonはワークステーションをロックし、 デスクトップを不正に使うことができないようにします。 この機能により、たとえばパソコンの前からしばらく席を外すようなときは、 ワークステーションをロックすることで、他のユーザーからデータを知らない間に 盗み見られるような事態を未然に防ぐことができるようになります。

GINAの世界では、「ワークステーションのロック」と似たような言葉がいくつか存在します。 まず、それらの言葉が厳密に何を意味しているのかを考えたいと思います。

言葉 意味
ディスプレイのロック(コンピュータのロック) デスクトップを使えないようにすること。 このときアクティブなデスクトップはwinlogonデスクトップだが、 実際にwinlogonデスクトップで何らかの操作を行えるのは、 SASの入力を終えてからになる。 つまり、SASの入力を終えなければ実質上何もできないため、 ディスプレイの内容は変化することがなく、 コンピュータに備わっている他の機能も利用できない。
ワークステーションのロック(デスクトップのロック) defaultデスクトップを使えないようにすること。 このときアクティブなデスクトップはwinlogonデスクトップであり、 多くの場合、そこで提供されているUIに解除情報を入力することで、 ロックを解除する(defaultデスクトップに戻る)。

各言葉の意味を大まかに定義した上で、本当に理解しておかなければならないのは、 ワークステーションのロックにディスプレイのロックという操作も含まれるという点です。 GINAがエクスポートする関数には、ワークステーションがロックされたときに 呼ばれるWlxWkstaLockedSASという関数がありますが、この関数が制御を返すのは、 ユーザーからの認証情報を入力を受けとった場合に限られるはずです。 つまり、winlogonデスクトップでのセキュリティ操作が生じることから、 事前にディスプレイをロックしてSASの入力を促す必要があります。 このディスプレイのロック時には、WlxDisplayLockedNoticeが呼ばれます。

VOID WlxDisplayLockedNotice(
  PVOID pWlxContext
);

pWlxContextは、コンテキストのアドレスが格納されています。 この関数に戻り値はありません。

WlxDisplayLockedNoticeが制御を返すと、 その次にはWlxWkstaLockedSASが呼ばれます。

int WlxWkstaLockedSAS(
  PVOID pWlxContext,  
  DWORD dwSasType     
);

pWlxContextは、コンテキストのアドレスです。 dwSasTypeは、生じたSASのタイプです。 戻り値として返すことのできる定数を次に示します。

定数 意味
WLX_SAS_ACTION_NONE ワークステーションのロックを維持する。
WLX_SAS_ACTION_UNLOCK_WKSTA ワークステーションのロックを解除する。
WLX_SAS_ACTION_FORCE_LOGOFF ワークステーションをロックしたユーザーとは異なるユーザーにロックの解除を認めるものの、 その異なるユーザーにdefaultデスクトップでの作業を許可したくない場合は、 ロックしたユーザーを強制的にログオフさせることになる。

独自のSASを実装するとは、その独自に定義しているSASの入力を検出し、 WlxSasNotifyを呼び出すことであると説明してきましたが、 WlxDisplayLockedNoticeではWlxSasNotifyを呼び出す必要はありません。 これは、一度ユーザーがログオンしてからのWlxSasNotifyの呼び出しが、 WlxLoggedOnSASの呼び出しへと解釈されるためです。 この動作は、ユーザーがdefaultデスクトップで作業している場合に意味を持ちますから、 WlxDisplayLockedNoticeでWlxLoggedOnSASが呼ばれる必要はどこにもありません。

上記の解釈には少し不確定さが混じっており、実はWlxDisplayLockedNoticeで WlxSasNotifyを呼び出さなかった場合、関数が制御を返した時点でデスクトップが スクリーンセーバーとなり、それをアクティブにすると再びWlxDisplayLockedNoticeが 呼ばれるという奇怪な現象が繰り返されてしまうことがあります。 WindowsUpdate等をしたためか、筆者はいつのまにかこの現象に遭遇することは なくなりましたが、GINAを配布する際などは軽視できる問題ではありません。 しかし、だからといってWlxSasNotifyを呼び出してはWlxLoggedOnSASが 呼ばれることになるため、この方法は解決策には至りません。 WlxLoggedOnSASで特定のSASタイプを処理しないよう設計すれば、 そのSASタイプを指定したWlxSasNotifyは無効にできそうに思えますが、 WlxLoggedOnSASが制御を返した時点でデスクトップがdefaultになってしまい、 WlxWkstaLockedSASで表示したログオンダイアログは確認できないことになります。

さて、それではどのような方法を取ればよいでしょうか。考えられる方法としては、 以下の2つが挙げられます。

1. WlxSasNotifyを呼ばずに、WlxDisplayLockedNoticeを実装する。
2. ワークステーションのロックそのものを無効にし、
WlxDisplayLockedNoticeとWlxWkstaLockedSASを実装しない。

1の場合は、いわば普通に実装するということです。 WlxDisplayLockedNoticeが制御が返したとき、スクリーンセーバーの問題が 発生しなければ、WlxWkstaLockedSASが呼ばれますから、 そこでロックを解除するためのダイアログを表示することになります。2の場合は、 WlxIsLockOkというロックの許可を決める関数でFALSEを返すようにします。 この方法であれば、WlxDisplayLockedNoticeとWlxWkstaLockedSASの実装の他、 WlxLoggedOnSASでWLX_SAS_ACTION_LOCK_WKSTAを返す必要もなくなります。

BOOL WlxIsLockOk(
  PVOID pWlxContext
);

pWlxContextは、コンテキストのアドレスです。 戻り値はロックを許可する場合はTRUE、しない場合はFALSEを返します。 次に、ワークステーションのロックを許可しない場合のWlxDisplayLockedNotice、 WlxWkstaLockedSAS、そしてWlxIsLockOkのコード例を示します。

VOID WINAPI WlxDisplayLockedNotice(PVOID pWlxContext)
{
}

int WINAPI WlxWkstaLockedSAS(PVOID pWlxContext, DWORD dwSasType)
{
	return WLX_SAS_ACTION_UNLOCK_WKSTA;
}

BOOL WINAPI WlxIsLockOk(PVOID pWlxContext)
{
	return FALSE;
}

WlxIsLockOkでFALSEを返すようにすれば、 LockWorkStationでWlxDisplayLockedNoticeが呼ばれることはなくなり、 それに併せてWlxWkstaLockedSASも呼ばれません。 そのため、特に何も実装する必要はありません。

WlxWkstaLockedSASの実装

msgina.dllのワークステーションのロックを確認してみると分かることですが、 ロックの解除時にはログオンしていたユーザーのパスワードが求められます。 このことから、WlxWkstaLockedSASの処理というのは、多くの点において、 WlxLoggedOutSASと似ているように思えますが、実際にはもう少し複雑です。 まず、ユーザーは既にログオンしているわけですから、 LogonUserを呼び出して認証を行うのは適切であるといえるのでしょうか。 また、ワークステーションのロックを解除できるのは、 既にログオンしているユーザーでなくても、管理者アカウントとされている ユーザーならばロックを解除できなければなりません。 次に示すコードは、これらの処理を全てダイアログで行おうとしています。

int WINAPI WlxWkstaLockedSAS(PVOID pWlxContext, DWORD dwSasType)
{
	LPGINACONTEXT lpgc = (LPGINACONTEXT)pWlxContext;

	return lpgc->pDispatchTable->WlxDialogBoxParam(lpgc->hWlx, lpgc->hinstDll, MAKEINTRESOURCE(ID_LOCKDIALOG), NULL, LockProc, (LPARAM)lpgc);
}

このWlxWkstaLockedSASでは、ログオン用のダイアログボックスを表示し、 LockProcというプロシージャでロックを解除するための処理を全て行います。 WlxDialogBoxParamの戻り値を関数の戻り値としていることから、 LockProcは最終的にWlxWkstaLockedSASで許可されている定数を返します。 LockProcの実装は、次のようになっています。

INT_PTR CALLBACK LockProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static LPGINACONTEXT lpgc = NULL;
	
	switch (uMsg) {

	case WM_INITDIALOG:
		lpgc = (LPGINACONTEXT)lParam;
		return TRUE;

	case WM_COMMAND:
	switch (LOWORD(wParam)) {

	case IDOK: {
		int    nResult;
		WCHAR  szUserName[256];
		WCHAR  szPassword[256];
		WCHAR  szDomain[256];
		HANDLE hTokenUnlock;

		GetDlgItemText(hwndDlg, ID_LOCK_USERNAME, szUserName, sizeof(szUserName));
		GetDlgItemText(hwndDlg, ID_LOCK_PASSWORD, szPassword, sizeof(szPassword));
		GetDlgItemText(hwndDlg, ID_LOCK_DOMAIN, szDomain, sizeof(szDomain));

		if (LogonUser(szUserName, szDomain, szPassword, LOGON32_LOGON_UNLOCK, LOGON32_PROVIDER_DEFAULT, &hTokenUnlock)) {
			if (IsEqualTokenUser(hTokenUnlock, lpgc->hToken))
				nResult = WLX_SAS_ACTION_UNLOCK_WKSTA;
			else if (IsTokenForAdmin(hTokenUnlock))
				nResult = WLX_SAS_ACTION_FORCE_LOGOFF;	
			else {
				lpgc->pDispatchTable->WlxMessageBox(lpgc->hWlx, hwndDlg, L"ロックの解除に失敗しました。", NULL, MB_ICONWARNING);
				nResult = WLX_SAS_ACTION_NONE;			
			}
			CloseHandle(hTokenUnlock);
			EndDialog(hwndDlg, nResult);
		}
		else {
			lpgc->pDispatchTable->WlxMessageBox(lpgc->hWlx, hwndDlg, L"ロックの解除に失敗しました。", NULL, MB_ICONWARNING);
			EndDialog(hwndDlg, WLX_SAS_ACTION_NONE);
		}

		return TRUE;
	}
		
	case IDCANCEL:
		EndDialog(hwndDlg, WLX_SAS_ACTION_NONE);
		return TRUE;

	default:
		break;

	}
	break;
	
	default:
		break;

	}

	return FALSE;
}

WM_COMMANDでwParamがIDOKとなるのは、ユーザーが認証情報を入力し終えたときであると仮定します。 そして、それらの情報を取り出してLogonUserを呼び出し、 入力されたユーザーのトークンを取得します。 このようにしなければならない理由は、トークンを取得しなければ、 入力されたユーザーと既にログオンしているユーザーが同一であることを証明できないからです。 つまり、あくまでロック解除用のログオンになるのですが、これは許容することにします。 ただし、LogonUserの第4引数であるログオンタイプに LOGON32_LOGON_UNLOCKを指定している点は非常に重要です。 これは、GINAにおいてロックを解除するために用意された専用のログオンタイプです。

サンプルコードでは、ユーザーがログオンしたら、そのユーザーが既にログオンしている ユーザーかどうかをIsEqualTokenUserという自作関数で調べようとしています。 もし同一であれば、ロックをかけたユーザーと同じユーザーが解除をしようとしているという ことなので、ロックの解除を示すWLX_SAS_ACTION_UNLOCK_WKSTAを戻り値とします。 同一でない場合は、ログオンしたユーザーが管理者アカウントかを調べる IsTokenForAdminという自作関数を呼び出しています。 このとき、同一であるならば、既にログオンしているユーザーを強制的にログオフさせます。 この結果、ワークステーションのロックは解除され、 再びWlxLoggedOutSASでユーザーを認証し直すことになります。 結果がいずれであろうとも、解除用のためにログオンしたhTokenUnlockは、 直ぐにCloseHandleの呼び出しによってログオフすることになります。

紹介した2つの自作関数を簡単に説明します。 まず、IsEqualTokenUserです。

BOOL IsEqualTokenUser(HANDLE hToken1, HANDLE hToken2)
{
	BOOL        bResult;
	DWORD       dwLength;
	PTOKEN_USER pTokenUser1, pTokenUser2;

	GetTokenInformation(hToken1, TokenUser, NULL, 0, &dwLength);
	pTokenUser1 = (PTOKEN_USER)LocalAlloc(0, dwLength);
	GetTokenInformation(hToken1, TokenUser, pTokenUser1, dwLength, &dwLength);

	GetTokenInformation(hToken2, TokenUser, NULL, 0, &dwLength);
	pTokenUser2 = (PTOKEN_USER)LocalAlloc(0, dwLength);
	GetTokenInformation(hToken2, TokenUser, pTokenUser2, dwLength, &dwLength);

	bResult = EqualSid(pTokenUser1->User.Sid, pTokenUser2->User.Sid);

	LocalFree(pTokenUser1);
	LocalFree(pTokenUser2);

	return bResult;
}

この関数は引数によって指定されたトークンから GetTokenInformationを通じて、TOKEN_USER構造体を取得しようとします。 この構造体にはユーザーのSIDが含まれていますから、 これをEqualSidで等しいかどうかを確認し、一致しているならば、 トークンが表すユーザー、即ちログオンしたユーザーは同一であること分かります。 続いて、IsTokenForAdminのコードを確認します。

BOOL IsTokenForAdmin(HANDLE hToken)
{
	BOOL   bResult;
	HANDLE hTokenImpersonate;

	bResult = DuplicateTokenEx(hToken, TOKEN_QUERY, 0, SecurityIdentification, TokenImpersonation, &hTokenImpersonate);
	if (bResult) {
		BOOL                     bMember;
		PSID                     pSidAdministrators;
		SID_IDENTIFIER_AUTHORITY sidIdentifier = SECURITY_NT_AUTHORITY;
		
		AllocateAndInitializeSid(&sidIdentifier, 2, SECURITY_BUILTIN_DOMAIN_RID, DOMAIN_ALIAS_RID_ADMINS,
			0, 0, 0, 0, 0, 0, &pSidAdministrators);

		if (CheckTokenMembership(hTokenImpersonate, pSidAdministrators, &bMember))
			bResult = bMember;

		CloseHandle(hTokenImpersonate);	
		FreeSid(pSidAdministrators);
	}

	return bResult;
}

この関数の最終的な目的は、CheckTokenMembershipの呼び出しを通じて、 トークンに管理者グループのSIDが含まれているかを確認することです。 DuplicateTokenExとAllocateAndInitializeSidの呼び出しは、 それを満たすために呼び出しているだけに過ぎません。まず、 CheckTokenMembershipは受け取るトークンが偽装トークンでなければ ならないことから、DuplicateTokenExの第5引数にTokenImpersonationを指定することで、 第6引数を通じて偽装トークンを取得しようとしています。 これが成功すれば、後は管理者グループのSIDがあれば、 CheckTokenMembershipを呼び出せますから、AllocateAndInitializeSidで それを作成します。CheckTokenMembershipのbMemberがTRUEである場合は、 指定したトークンに指定したSIDが含まれるということですから、 判定結果を関数の戻り値にすることができます。



戻る