EternalWindows
GINA / ログオンダイアログ

WlxDisplaySASNoticeがWlxSasNotifyを呼び出して制御を返したら、 その次にはWlxLoggedOutSASが呼び出されることになります。 この関数は、実際にユーザーを認証する機能を表示し、 その認証過程で得られた情報をWinlogonに返します。

int WlxLoggedOutSAS(
  PVOID pWlxContext,
  DWORD dwSasType,
  PLUID pAuthenticationId,
  PSID pLogonSid,
  PDWORD pdwOptions,
  PHANDLE phToken,
  PWLX_MPR_NOTIFY_INFO pNprNotifyInfo,
  PVOID *pProfile
);

pWlxContextは、コンテキストのアドレスです。 dwSasTypeは、WlxDisplaySASNoticeのWlxSasNotifyで指定したSASタイプです。 pAuthenticationIdは、認証IDを格納するアドレスです。 ユーザーの認証に成功した場合、このアドレスにIDを返します。 pLogonSidは、ログオンSIDを格納するアドレスです。 ユーザーの認証に成功した場合、このアドレスにSIDを返します。 pdwOptionsは、ユーザープロファイルに関するオプションを返します。 phTokenは、ログオンしたユーザーを表すトークンハンドルを返します。 pMprNotifyInfoは、ログオンしたユーザーの認証情報を返します。 pProfileは、ロードしたいユーザープロファイルのパスを返します。 戻り値は、認証が成功した場合にはWLX_SAS_ACTION_LOGON、 失敗した場合はWLX_SAS_ACTION_NONE、コンピュータをシャットダウンする場合は、 WLX_SAS_ACTION_SHUTDOWNを返します。

この関数のpAuthenticationId以降の引数は、全て関数側が適切に初期化しなくてはなりません。 これらの引数は実際にユーザーの認証が終了していなければ、 つまりログオンしたユーザーのトークンがなければ初期化できないものも含まれるため、 まず先にトークンの取得までのコードを説明し、そのうえで個々の引数の初期化について 説明することにします。 次のコードは、ログオンダイアログボックスを表示する例を示しています。

lpgc->pMprNotifyInfo = pMprNotifyInfo;

nResult = lpgc->pDispatchTable->WlxDialogBoxParam(lpgc->hWlx, lpgc->hinstDll, MAKEINTRESOURCE(ID_DIALOG), NULL, LogonProc, (LPARAM)lpgc);
if (nResult == -1)		
	return WLX_SAS_ACTION_SHUTDOWN;

このコードは、GINAにID_DIALOGで示されるダイアログリソースが存在することを前提にしています。 もし、指定されたリソースが存在しなかったり、引数の指定に誤りがある場合は、 -1が返ることになっているため、このときにはWLX_SAS_ACTION_SHUTDOWNを返すようにします。 WLX_SAS_ACTION_NONEを返すとWlxDisplaySASNoticeが呼ばれ、 最終的には再びWlxLoggedOutSASが呼び出されることになるため、 半永久的にWlxDialogBoxParamの失敗が繰り返されることになってしまいます。 pMprNotifyInfoのアドレスをコンテキストのメンバに格納しているのは、 このpMprNotifyInfoを初期化するための情報がLogonProcで取得できるからです。

typedef struct _WLX_MPR_NOTIFY_INFO {
  PWSTR    pszUserName;
  PWSTR    pszDomain;
  PWSTR    pszPassword;
  PWSTR   pszOldPassword;
} WLX_MPR_NOTIFY_INFO;

LogonProcで呼び出すことになるLogonUserはWLX_MPR_NOTIFY_INFO構造体と同じように、 ユーザー名やパスワードといった認証情報を必要とします。 そのため、LogonProcで取得した情報を無理にWlxLoggedOutSASに返すよりも、 pMprNotifyInfoのアドレスをコンテキストに指定し、LogonProcでpMprNotifyInfoを 初期化したほうが効率の面で優れているといえるでしょう。

LogonProcの目的は、ユーザーを認証する機能を表示することです。 この認証機能というのは最もGINAの個性が出るところだと思われますが、 今回のシリーズで扱うのは単純なログオンダイアログボックスに留めておきます。 このダイアログには、ユーザー名、パスワード、ドメインの入力を受け取る 3つのエディットボックスが存在し、それぞれのIDはID_USERNAME、 ID_PASSWORD、ID_DOMAINであるとします。 また、他のコントロールとしてログオン開始するOKボタン(IDOK)、 キャンセルをするキャンセルボタン(IDCANCEL)、シャットダウンをするための シャットダウンボタン(ID_SHUTDOWN)が配置されているものと仮定します。 ではまず、WM_INITDIALOGのコードを書いてみます。

static LPGINACONTEXT lpgc = NULL;

switch (uMsg) {

case WM_INITDIALOG: {
	BOOL  bDontDisplayLastUserName = FALSE;
	WCHAR szUserName[256];

	lpgc = (LPGINACONTEXT)lParam;

	GetWinlogonRegistryData(L"DontDisplayLastUserName", &bDontDisplayLastUserName, sizeof(BOOL));
	if (!bDontDisplayLastUserName) {
		GetWinlogonRegistryData(L"DefaultUserName", szUserName, sizeof(szUserName));
		SetDlgItemText(hwndDlg, ID_USERNAME, szUserName);
	}

	return TRUE;
}

WlxDialogBoxParamの第6引数でコンテキストのアドレスを指定したため、 WM_INITDIALOGではlParam経由でそのアドレスを取得することができます。 WinlogonのレジストリにはDefaultUserNameと呼ばれるエントリが存在し、 ここに格納されている文字列は同レジストリのDontDisplayLastUserNameが1でない場合に限り、 DefaultUserNameのデータをエディットボックスに表示してもよいことになっています。 よって、まずDontDisplayLastUserNameのデータを取得し、 それが1でない場合はDefaultUserNameのデータを取得して表示します。 DefaultUserNameは、前回ログオンしたユーザー名が格納されることになっているため、 これを予めエディットボックスに表示しておくことで、ユーザー名の入力を省略することができます。 続いて、WM_COMMANDの処理を書いてみます。

case WM_COMMAND:
switch (LOWORD(wParam)) {

case IDOK: {
	WCHAR szUserName[256];
	WCHAR szPassword[256];
	WCHAR szDomain[256];

	GetDlgItemText(hwndDlg, ID_USERNAME, szUserName, sizeof(szUserName) / sizeof(WCHAR));
	GetDlgItemText(hwndDlg, ID_PASSWORD, szPassword, sizeof(szPassword) / sizeof(WCHAR));
	GetDlgItemText(hwndDlg, ID_DOMAIN, szDomain, sizeof(szDomain) / sizeof(WCHAR));

	if (LogonUser(szUserName, szDomain, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &lpgc->hToken)) {
		lpgc->pMprNotifyInfo->pszUserName    = (PWSTR)LocalAlloc(LPTR, (lstrlen(szUserName) + 1) * sizeof(WCHAR));
		lpgc->pMprNotifyInfo->pszPassword    = (PWSTR)LocalAlloc(LPTR, (lstrlen(szPassword) + 1) * sizeof(WCHAR));
		lpgc->pMprNotifyInfo->pszDomain      = (PWSTR)LocalAlloc(LPTR, (lstrlen(szDomain) + 1)  * sizeof(WCHAR));
		lpgc->pMprNotifyInfo->pszOldPassword = NULL;

		lstrcpy(lpgc->pMprNotifyInfo->pszUserName, szUserName);
		lstrcpy(lpgc->pMprNotifyInfo->pszPassword, szPassword);
		lstrcpy(lpgc->pMprNotifyInfo->pszDomain, szDomain);
		
		SetWinlogonRegistryData(L"DefaultUserName", REG_SZ, szUserName, (lstrlen(szUserName) + 1) * sizeof(WCHAR));

		EndDialog(hwndDlg, WLX_SAS_ACTION_LOGON);
	}
	else {
		lpgc->pDispatchTable->WlxMessageBox(lpgc->hwlx, hwndDlg, L"ログオンに失敗しました。", NULL, MB_ICONWARNING);
		EndDialog(hwndDlg, WLX_SAS_ACTION_NONE);
	}

	return TRUE;
}

GetDlgItemTextでそれぞれのエディットボックスから認証情報を取得し、 それをLogonUserに指定します。 このとき、返されるトークンはコンテキストのhTokenメンバで受け取ります。 pMprNotifyInfoの個々のメンバは、最初から文字列を格納できるだけのメモリを 指しているわけではないので、まず必要なサイズ分のメモリを確保することになります。 このとき、サイズの指定でlstrlenの戻り値に+1しているのは'\0'文字を考慮するためであり、 また、lstrlenは文字列の文字の数を返すことからそれをバイト単位に変換するために、 sizeof(WCHAR)で乗算しなければなりません。 SetWinlogonRegistryDataはWinlogonのレジストリのデータを変更する自作関数で、 ここではDefaultUserNameにログオンしたユーザー名を設定しています。 これにより、次回のログオンダイアログボックスのエディットボックスには、 今回ログオンしたユーザー名が表示されることになります。 EndDialogはダイアログプロシージャの戻り値を決定する関数で、 ここではログオンの成功を示すWLX_SAS_ACTION_LOGONを指定しています。 また、ログオンに失敗したときにはWLX_SAS_ACTION_NONEを指定します。 キャンセルボタンとシャットダウンボタンの押下の処理は、次のようになっています。

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

case ID_SHUTDOWN:
	EndDialog(hwndDlg, WLX_SAS_ACTION_SHUTDOWN);
	return TRUE;

WLX_SAS_ACTION_XXXといった定数はWlxLoggedOutSASが返すべき値であるため、 EndDialogでこの定数をWlxDialogBoxParamの戻り値とすれば、 その値をそのままWlxLoggedOutSASの戻り値にすることができるようになります。

nResult = lpgc->pDispatchTable->WlxDialogBoxParam(lpgc->hWlx, lpgc->hinstDll, MAKEINTRESOURCE(ID_DIALOG), NULL, LogonProc, (LPARAM)lpgc);
if (nResult == -1)		
	return WLX_SAS_ACTION_SHUTDOWN;
else if (nResult == WLX_SAS_ACTION_SHUTDOWN || nResult == WLX_SAS_ACTION_NONE)
	return nResult;
else if (nResult == WLX_SAS_ACTION_LOGON)
	;
else
	return WLX_SAS_ACTION_NONE;

関数が失敗したときには-1が返るため、処理を続行できないことからシャットダウンをするようにします。 WLX_SAS_ACTION_LOGONが返ったときはログオンの成功を意味しますから、 このときはまだ制御は返さず、WlxLoggedOutSASの引数の初期化処理を行うことになります。 LogonProcのEndDialogで指定した定数がWLX_SAS_ACTION_LOGON、 WLX_SAS_ACTION_NONE、WLX_SAS_ACTION_SHUTDOWNの3つであったことから、 else文が実行されるようなことは考えられないように思えますが、 SASメッセージに対してFALSEを返している場合は、その限りではありません。

たとえば、ログオンダイアログボックスが表示されているときにCtrl+Alt+Delを押下すると、 wParamをWLX_SAS_TYPE_CTRL_ALT_DELとしたWLX_WM_SASが送られます。 SASメッセージに対してFALSEを返すと表示しているダイアログボックスは破棄され、 WlxDialogBoxParamの戻り値がWLX_DLG_XXXになりますから、 このような値が返るときにはelse文が実行されることになります。 また、ダイアログはWinlogonによってタイムアウトが適応されることがあり、 そのようなときは無条件にダイアログが閉じられ、WlxDialogBoxParamは0を返します。 このタイムアウトでは、wParamをWLX_SAS_TYPE_TIMEOUTとした WLX_WM_SASが送られることを予想していたのですが、実際にはWLX_WM_SAS自体が 送られていないため、タイムアウトを無効に手段がないようにも思えます。 しかし、ディスパッチテーブルのWlxSetTimeoutを呼び出せば、 デフォルトである2分というタイムアウトを変更することは可能です。 WLX_WM_SASではこの他、スクリーンセーバーの起動を意味する WLX_SAS_TYPE_SCRNSVR_TIMEOUTという定数がありますが、 WlxLoggedOutSASではこのSASタイプが送られることはないようです。

自動ログオンについて

GINAを独自に開発する目的は、標準でない認証方法を提供することに あると思われますが、そのような場合においても自動ログオンはサポートして おきたいものです。自動ログオンとは、Winlogonのレジストリに書き込まれている ユーザー名やパスワードを取得し、ユーザーインターフェースの表示を行わずに 自動でログオンする仕組みのことです。自動ログオンに関係するレジストリエントリの 一部を次に示します。

エントリ 説明
AutoAdminLogon 値が1のとき、自動ログオンを実行する。
AutoLogonCount AutoAdminLogonが1のときにシャットダウン時において値を確認する。 これは、自動ログオンする回数を表すものであり、 シャットダウンする毎に減算することになる。 値が0のときは、自動ログオンを無効にするためにAutoAdminLogonを0にし、 AutoLogonCountとDefaultPasswordのエントリ自体を削除する。 AutoLogonCountが存在しない場合は、自動ログオンの回数に制限はない。
DefaultUserName AutoAdminLogonが1のときに自動ログオンするユーザー名。
DefaultDomainName AutoAdminLogonが1のときに自動ログオン時に参照されるドメイン。
DefaultPassword AutoAdminLogonが1のときに自動ログオンするユーザーのパスワード。

レジストリのタイプはAutoLogonCountがREG_DWORDで、それ以外がREG_SZとなります。 デフォルトでは、AutoLogonCountとDefaultPasswordは存在していないと思われますから、 これらに関してはエントリから作成することになるでしょう。 自動ログオンに関するコードが実行されなければならないのは、 AutoAdminLogonが1のときであり、それを確認しなければならない状態というのは、 ログオン時とシャットダウン時の2つです。 ログオン時に呼ばれる関数は、今回説明したWlxLoggedOutSASであり、 実際の処理はLogonProcというダイアログプロシージャで行っていました。 よって、ここでAutoAdminLogonの値を確認します。

case WM_INITDIALOG: {
	BOOL  bDontDisplayLastUserName = FALSE;
	WCHAR szUserName[256];
	WCHAR szAutoAdminLogon[2];

	lpgc = (LPGINACONTEXT)lParam;

	szAutoAdminLogon[0] = '0';
	szAutoAdminLogon[1] = '\0';
	GetWinlogonRegistryData(L"AutoAdminLogon", szAutoAdminLogon, sizeof(szAutoAdminLogon));
	
	if (szAutoAdminLogon[0] == '1') {
		WCHAR szUserName[256];
		WCHAR szPassword[256];
		WCHAR szDomain[256];
		
		GetWinlogonRegistryData(L"DefaultUserName", szUserName, sizeof(szUserName));
		GetWinlogonRegistryData(L"DefaultPassword", szPassword, sizeof(szPassword));
		GetWinlogonRegistryData(L"DefaultDomainName", szDomain, sizeof(szDomain));
		
		if (LogonUser(szUserName, szDomain, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &lpgc->hToken)) {
			lpgc->pMprNotifyInfo->pszUserName    = (PWSTR)LocalAlloc(LPTR, (lstrlen(szUserName) + 1) * sizeof(WCHAR));
			lpgc->pMprNotifyInfo->pszPassword    = (PWSTR)LocalAlloc(LPTR, (lstrlen(szPassword) + 1) * sizeof(WCHAR));
			lpgc->pMprNotifyInfo->pszDomain      = (PWSTR)LocalAlloc(LPTR, (lstrlen(szDomain) + 1)  * sizeof(WCHAR));
			lpgc->pMprNotifyInfo->pszOldPassword = NULL;

			lstrcpy(lpgc->pMprNotifyInfo->pszUserName, szUserName);
			lstrcpy(lpgc->pMprNotifyInfo->pszPassword, szPassword);
			lstrcpy(lpgc->pMprNotifyInfo->pszDomain, szDomain);
			
			SetWinlogonRegistryData(L"DefaultUserName", REG_SZ, szUserName, (lstrlen(szUserName) + 1) * sizeof(WCHAR));

			EndDialog(hwndDlg, WLX_SAS_ACTION_LOGON);
			return TRUE;
		}
		else
			lpgc->pDispatchTable->WlxMessageBox(lpgc->hWlx, hwndDlg, L"ログオンに失敗しました。", NULL, MB_ICONWARNING);
	}

	GetWinlogonRegistryData(L"DontDisplayLastUserName", &bDontDisplayLastUserName, sizeof(BOOL));
	if (!bDontDisplayLastUserName) {
		GetWinlogonRegistryData(L"DefaultUserName", szUserName, sizeof(szUserName));
		SetDlgItemText(hwndDlg, ID_USERNAME, szUserName);
	}

	return TRUE;
}

自動ログオンをする際には、ダイアログが表示されてはいけませんから、 ダイアログの初期化段階であるWM_INITDIALOGにコードを記述することになります。 AutoAdminLogonのレジストリタイプはREG_SZであるため、文字列から数値を参照します。 その値が1である場合、WlxLoggedOutSASと同じようにユーザーをログオンさせることになりますが、 ユーザー名等の取得はエディットボックスからではなく、 DefaultUserName等のレジストリエントリを対象とします。 ログオンが成功した場合は、認証用のダイアログが表示される必要はないため、 EndDialogでダイアログを閉じることになります。 続いて、シャットダウン時の処理を見てみます。

VOID WINAPI WlxShutdown(PVOID pWlxContext, DWORD dwShutdownType)
{
	WCHAR szAutoAdminLogon[2];
	
	szAutoAdminLogon[0] = '0';
	szAutoAdminLogon[1] = '\0';
	GetWinlogonRegistryData(L"AutoAdminLogon", szAutoAdminLogon, sizeof(szAutoAdminLogon));
	
	if (szAutoAdminLogon[0] == '1') {
		int nAutoLogonCount = -1;
		
		GetWinlogonRegistryData(L"AutoLogonCount", &nAutoLogonCount, sizeof(int));
	
		if (nAutoLogonCount > 0) {
			nAutoLogonCount--;
			SetWinlogonRegistryData(L"AutoLogonCount", REG_DWORD, &nAutoLogonCount, sizeof(int));
		}
		else if (nAutoLogonCount == 0) {
			szAutoAdminLogon[0] = '0';
			szAutoAdminLogon[1] = '\0';
			SetWinlogonRegistryData(L"AutoAdminLogon", REG_SZ, szAutoAdminLogon, sizeof(szAutoAdminLogon));
			
			DeleteWinlogonRegistryEntry(L"AutoLogonCount");
			DeleteWinlogonRegistryEntry(L"DefaultPassword");
		}
		else
			;
	}
}

nAutoLogonCountという変数を-1で初期化しているのは、 AutoLogonCountエントリが存在しなかった場合の対策です。 この際には、nAutoLogonCountの値が変更されませんから、何も処理は行われません。 nAutoLogonCountが0より大きい場合は、ログオン回数を1つ減らしてAutoLogonCountエントリに設定します。 nAutoLogonCountが0に達している場合は、これ以上自動ログオンしてはいけませんから、 AutoAdminLogonエントリに1を設定し、AutoLogonCountエントリとDefaultPasswordエントリを削除します。

自動ログオンでは、上記で紹介した以外にもいくつか確認すべきエントリがあります。 たとえば、上記のサンプルでは自動ログオンの有効時にログオフをした場合、 再度自動でログオンすることになりますが、本来ならばこの動作は、 ForceAutoLogonというエントリが1であるときのみに実行するべきです。 また、msgina.dll特有の処理かもしれませんが、WlxLoggedOutSASでログオン処理を 実行する以前においてShiftキーが押下されている場合、 自動ログオンを強制的に無効にし、本来のログオンインターフェースを表示することがあります。 ただし、この機能はIgnoreShiftOverrideが1のときは無効とするようです。 これらのことからも分かるように、GINAを開発する場合はエクスポート関数の詳細のみならず、 Winlogonのレジストリに関する知識も必要であるといえます。



戻る