EternalWindows
セキュリティコンテキスト / 偽装とログオン

これまで取り上げてきたトークングループや特権リストは、 プロセスのセキュリティコンテキストを大きく左右する存在といえます。 トークングループにAdministratorsが含まれていれば、 アクセス可能なオブジェクトは増えることになりますし、多くの特権が割り当てられていれば、 呼び出すことのできる関数も増えることになるからです。 しかし、逆にこうした要素が制限されたトークンが存在すれば、 プロセスのセキュリティコンテキストは降格することになりますから、 このようなトークンを参照する方法が欲しいものです。 偽装とは、このような制限付きトークンや特定のユーザーのトークンを スレッドの新しいトークンとして採用するメカニズムです。

偽装は、プロセスではなくスレッド単位で行われます。 一般にトークンはプロセス単位で割り当てられますが、 偽装したトークンは偽装関数を呼び出したスレッドに割り当てられます。 つまり、そのスレッドのセキュリティコンテキストだけが変更されます。 次に、偽装を行うまでの主な方法を示します。

主な方法 説明
ユーザーのログオン LogonUserでユーザーをログオンさせ、そのトークンを偽装する。
接続指向 通信相手とコネクションを確立して相手のトークンを偽装する。 通信に使用したAPIが名前付きパイプならImpersonateNamedPipeClient、 SSPIならImpersonateSecurityContextになる。
制限付きトークン CreateRestrictedTokenやSaferComputeTokenFromLevelで作成したトークンを偽装する。

上記の表の最初2つの方法は、特定のユーザーを偽装します。 よって、特定のユーザーとしてコードを実行したい場合は、これらの方法を利用することになります。 3つ目の方法は、既存のユーザーに特定の制限を加えて動作させたい場合に利用します。

アプリケーションがユーザーをログオンさせるというのは、 少し不思議に思えるところがあるかもしれません。 ログオンというのはPCを起動して、これからユーザーがWindowsを利用するときのみに 必要となるものではないでしょうか。 確かにこれはログオンの代表的な例ですが、実際のところログオンはいつでも行ってよいのです。 ユーザー名とパスワードを認証する目的でログオンを実行しても構いませんし、 特定のユーザーを表すトークンを取得するためにログオンを実行しても構いません。 ユーザーをログオンさせるには、次に示すLogonUserを呼び出すことになります。

BOOL LogonUser(
  LPTSTR lpszUsername,
  LPTSTR lpszDomain,
  LPTSTR lpszPassword,
  DWORD dwLogonType,
  DWORD dwLogonProvider,
  PHANDLE phToken
);

lpszUsernameは、ログオンさせたいユーザー名を指定します。 lpszDomainは、lpszUsernameに指定したユーザー名を検索するためのドメイン名、 またはサーバー名を指定します。 ユーザー名がローカルコンピュータ上に保存されている場合は、NULLを指定します。 lpszPasswordは、ログオンさせたいユーザーのパスワードを指定します。 dwLogonTypeは、ログオンタイプとして定義されている定数を指定します。 dwLogonProviderは、基本的にLOGON32_PROVIDER_DEFAULTを指定します。 ログオンプロバイダとはSSP(Security Support Provider)のことであり、 この定数を指定した場合はNTLM認証が行われます。 phTokenは、ログオンされたユーザーのトークンを受け取る変数のアドレスを指定します。

基本的にdwLogonTypeは、LOGON32_LOGON_INTERACTIVEまたはLOGON32_LOGON_NETWORKを指定します。 前者は、ローカルコンピュータ上のアプリケーションやユーザーから取得した認証情報、 またはアプリケーション内で定義した認証情報でログオンする場合に指定します。 後者は、ネットワーク上での通信相手から受け取った認証情報でログオンする場合に指定します。 NETWORKと記述されていますが、あくまでローカルコンピュータへのログオンとなります。 この方法を使用する場合は、ネットワーク上から認証情報を受け取る関係上、 セキュリティの面で様々な対策が必要になります。 このため、できれば通信相手とコネクションを確立し、 偽装関数を通じてトークンを取得することが望まれます。 指定したログオンタイプによるログオンが成功するかどうかは、 ログオンするユーザーまたはメンバであるグループに、 適切なユーザー権利(特権のことではありません)が割り当てられている必要があります。 LOGON32_LOGON_INTERACTIVEの場合はSE_INTERACTIVE_LOGON_NAME、 LOGON32_LOGON_NETWORKの場合はSE_NETWORK_LOGON_NAMEが必要となります。

システムに存在する全てのユーザーには、パスワードが設定されていることが望まれます。 そうでなければ、どのようなアプリケーションでもパスワードをNULLにしてLogonUserを呼び出し、 ログオンさせたユーザーのトークンを取得できてしまいます。 特に、そのユーザーがAdministratorsグループのメンバである場合は、 偽装を通じてセキュリティコンテキストを昇格させることができてしまいます。 実際のところ、システムに既定で存在するAdministratorアカウントには デフォルトでパスワードが設定されていないのですが、 次のレジストリキーによって保護されています。

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\LimitBlankPasswordUse

この値が1である場合、コンソールへのログオンを除いて、 空のパスワードでログオンすることはできません。 デフォルトでは、この値は1になっています。 値を0にした場合は、空のパスワードでログオンすることができるようになります。 Guestアカウントに関しては、このレジストリキーの値に関係なく、 空のパスワードでログオンすることができます。

トークンを取得すれば、ImpersonateLoggedOnUserを呼び出すことで偽装を行うことができます。 これにより、スレッドのセキュリティコンテキストは、トークンで表されるものに変化します。

BOOL WINAPI ImpersonateLoggedOnUser(
  HANDLE hToken
);

hTokenは、偽装したいトークンを指定します。

偽装したスレッドが元のセキュリティコンテキストに戻りたい場合は、RevertToSelfを呼び出します。

BOOL WINAPI RevertToSelf(void);

この関数を呼び出すことによって、スレッドは偽装をする前の状態に戻ります。

今回のプログラムは、特定のユーザーとしてログオンし、偽装する例を示しています。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR  szUserName[] = TEXT("Guest");
	TCHAR  szPassword[] = TEXT("");
	TCHAR  szBuf[256];
	DWORD  dwSize; 
	HANDLE hToken;

	if (!LogonUser(szUserName, NULL, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken)) {
		if (GetLastError() == ERROR_LOGON_FAILURE)
			MessageBox(NULL, TEXT("ユーザー名またはパスワードが正しくありません。"), NULL, MB_ICONWARNING);
		else if (GetLastError() == ERROR_ACCOUNT_RESTRICTION)
			MessageBox(NULL, TEXT("アカウントのログオンは制限されています。"), NULL, MB_ICONWARNING);
		else if (GetLastError() == ERROR_LOGON_TYPE_NOT_GRANTED)
			MessageBox(NULL, TEXT("必要なユーザー権利が割り当てられていません。"), NULL, MB_ICONWARNING);
		else if (GetLastError() == ERROR_ACCOUNT_DISABLED)
			MessageBox(NULL, TEXT("アカウントが無効になっています。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("ログオンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	ImpersonateLoggedOnUser(hToken);

	dwSize = sizeof(szBuf) / sizeof(TCHAR);
	GetUserName(szBuf, &dwSize);

	RevertToSelf();
	
	MessageBox(NULL, szBuf, TEXT("ユーザー名"), MB_OK);

	CloseHandle(hToken);

	return 0;
}

szUserNameにログオンさせたいユーザーの名前を指定し、szPasswordにそのユーザーのパスワードを指定します。 この例ではGuestアカウントをログオンさせようとしていますが、 このためには事前にコントローパネルでGuestアカウントを有効にしておく必要があります。 LogonUserの呼び出しは、次のようになっています。

if (!LogonUser(szUserName, NULL, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken)) {
	if (GetLastError() == ERROR_LOGON_FAILURE)
		MessageBox(NULL, TEXT("ユーザー名またはパスワードが正しくありません。"), NULL, MB_ICONWARNING);
	else if (GetLastError() == ERROR_ACCOUNT_RESTRICTION)
		MessageBox(NULL, TEXT("アカウントのログオンは制限されています。"), NULL, MB_ICONWARNING);
	else if (GetLastError() == ERROR_LOGON_TYPE_NOT_GRANTED)
		MessageBox(NULL, TEXT("必要なユーザー権利が割り当てられていません。"), NULL, MB_ICONWARNING);
	else if (GetLastError() == ERROR_ACCOUNT_DISABLED)
		MessageBox(NULL, TEXT("アカウントが無効になっています。"), NULL, MB_ICONWARNING);
	else
		MessageBox(NULL, TEXT("ログオンに失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

ERROR_LOGON_FAILUREは、ユーザー名またはパスワードが不正であった場合に返ります。 ERROR_ACCOUNT_RESTRICTIONは、たとえばAdministratorアカウントを空のパスワードでログオンさせようとした場合に返ります。 ERROR_LOGON_TYPE_NOT_GRANTEDは、指定したユーザーまたはグループに適切なユーザー権利が割り当てられていない場合に返ります。 たとえば、Guestアカウントにはデフォルトでネットワークログオンを許可するユーザー権利が割り当てられていないため、 この場合にLOGON32_LOGON_NETWORKを指定するとこのエラーが返ります。

ログオンしたユーザーのトークンを取得すれば、ImpersonateLoggedOnUserでそのユーザーに偽装できるようになります。 今回の例ではGuestアカウントをログオンさせたので、 偽装したスレッドはGuestアカウントとしてコードを実行することになります。

ImpersonateLoggedOnUser(hToken);

dwSize = sizeof(szBuf) / sizeof(TCHAR);
GetUserName(szBuf, &dwSize);

RevertToSelf();

MessageBox(NULL, szBuf, TEXT("ユーザー名"), MB_OK);

Guestアカウントに偽装した場合、ここでGetUserNameが返すユーザー名はGusetとなります。 これは正に、スレッドに割り当てられたトークンのトークンユーザーがGusetであることを示しています。 逆に、GetUserNameの呼び出しを偽装の前に行った場合は、 プロセスがどのユーザーとしてコードを実行しているかが分かります。 RevertToSelfによって偽装を終了してから、MessageBoxを呼び出しているのは重要な意味を持ちます。 それは、ログオンしたユーザーのトークンに含まれるログオンSIDが、 現在のデスクトップに設定されているログオンSIDと異なるからです。 このため、Guestアカウントの状態でMessageBoxを呼び出しても、 それは正しく表示されません。 基本的に特定のユーザーとして偽装する目的は、 そのユーザーとしてファイルなどのリソースにアクセスする点にあるため、 それ以外の場合は元のセキュリティコンテキストで動作するべきといえます。

LogonUserExとLogonUserExExW

ユーザーをログオンさせる関数にはLogonUserの他に、LogonUserExやLogonUserExExWがあります。 これらの関数は基本的にLogonUserと同じですが、LogonUserと比べてより多くの情報を返すことができます。 まず、LogonUserExの呼び出し方を確認します。

PSID         pLogonSid;
PVOID        pProfileBuffer;
DWORD        dwProfileLength;
QUOTA_LIMITS quotaLimits;

LogonUserEx(szUserName, NULL, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken, &pLogonSid, &pProfileBuffer, &dwProfileLength, "aLimits);

LogonUserExの第7引数は、ログオンSIDを受け取ります。 ログオンSIDはGetTokenInformationで取得することもできます。 第8引数は、MSV1_0_INTERACTIVE_PROFILE構造体のデータを格納したバッファへのアドレスが格納されます。 この構造体には、ログオン回数やパス情報が格納されています。 第9引数は、第8引数のバッファのサイズが格納されます。 第10引数は、QUOTA_LIMITS構造体のデータが格納されます。 この構造体には、ログオンしたユーザーが使用可能なページプールの上限や、 ワーキングセットサイズの上限が格納されています。 基本的にこうした情報が必要ない場合は、LogonUserを使用するべきであると思われます。

LogonUserExExWは、LogonUserExのように多くの引数を受け取るほか、TOKEN_GROUPS構造体を受け取ります。 この構造体に指定したSIDは実際にトークンのトークングループに追加されるため、 ログオンしたユーザーは本来のセキュリティコンテキストよりも昇格することになります。 こうしたことから、TOKEN_GROUPS構造体を指定してLogonUserExExWを呼び出す場合は、 呼び出し側プロセスのトークンにSE_TCB_NAME特権が割り当てられていなければなりません。 LogonUserExExWの定義はヘッダーファイルに存在しないため、明示的にリンクすることになります。

#include <windows.h>

typedef BOOL (WINAPI *LPFNLOGONUSEREXEXW)(LPTSTR lpszUsername, LPTSTR lpszDomain, LPTSTR lpszPassword, DWORD dwLogonType, PTOKEN_GROUPS pTokenGroups, DWORD dwLogonProvider,
					PHANDLE phToken, PSID *ppLogonSid, PVOID *ppProfileBuffer, LPDWORD pdwProfileLength, PQUOTA_LIMITS pQuotaLimits);

BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR              szUserName[] = TEXT("Guest");
	TCHAR              szPassword[] = TEXT("");
	HANDLE             hToken;
	HANDLE             hFile;
	DWORD              dwSidSize;
	PSID               pSidGroup;
	TOKEN_GROUPS       tokenGroups;
	HMODULE            hmod;
	LPFNLOGONUSEREXEXW lpfnLogonUserExExW;
	
	if (!EnablePrivilege(SE_TCB_NAME, TRUE)) {
		MessageBox(NULL, TEXT("特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	hmod = LoadLibrary(L"advapi32.dll");
	if (hmod == NULL)
		return 0;
		
	lpfnLogonUserExExW = (LPFNLOGONUSEREXEXW)GetProcAddress(hmod, (LPSTR)"LogonUserExExW");
	if (lpfnLogonUserExExW == NULL) {
		FreeLibrary(hmod);
		return 0;
	}
	
	dwSidSize = SECURITY_MAX_SID_SIZE;
	pSidGroup = (PSID)LocalAlloc(LPTR, dwSidSize);
	if (!CreateWellKnownSid(WinBuiltinAdministratorsSid, NULL, pSidGroup, &dwSidSize)) {
		LocalFree(pSidGroup);
		FreeLibrary(hmod);
		return 0;
	}

	tokenGroups.GroupCount           = 1;
	tokenGroups.Groups[0].Sid        = pSidGroup;
	tokenGroups.Groups[0].Attributes = SE_GROUP_ENABLED;

	if (!lpfnLogonUserExExW(szUserName, NULL, szPassword, LOGON32_LOGON_INTERACTIVE, &tokenGroups, LOGON32_PROVIDER_DEFAULT, &hToken, NULL, NULL, NULL, NULL)) {
		if (GetLastError() == ERROR_INVALID_PARAMETER)
			MessageBox(NULL, TEXT("パラメータが正しくありません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("ログオンに失敗しました。"), NULL, MB_ICONWARNING);
		LocalFree(pSidGroup);
		FreeLibrary(hmod);
		return 0;
	}
	
	MessageBox(NULL, TEXT("ログオンに成功しました。"), TEXT("OK"), MB_OK);

	CloseHandle(hToken);
	LocalFree(pSidGroup);
	FreeLibrary(hmod);

	return 0;
}

BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable)
{
	BOOL             bResult;
	LUID             luid;
	HANDLE           hToken;
	TOKEN_PRIVILEGES tokenPrivileges;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
		return FALSE;
	
	if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) {
		CloseHandle(hToken);
		return FALSE;
	}

	tokenPrivileges.PrivilegeCount           = 1;
	tokenPrivileges.Privileges[0].Luid       = luid;
	tokenPrivileges.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
	
	bResult = AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	
	CloseHandle(hToken);

	return bResult && GetLastError() == ERROR_SUCCESS;
}

LogonUserExExWにTOKEN_GROUPS構造体を指定するものとし、最初にSE_TCB_NAME特権を有効にしています。 追加するグループSIDはAdministratorsとし、CreateWellKnownSidでこれを作成しています。 その後、TOKEN_GROUPS構造体にSIDを加えてLogonUserExExWを呼び出すことになるのですが、 何故かこれはERROR_INVALID_PARAMETERを返して失敗するようです。 TOKEN_GROUPS構造体の引数にNULLを指定した場合は成功することから、 他の引数の指定が間違っているようには思えず、 具体的な原因を突き止めることができませんでした。 本来、トークンに独自のグループSIDを追加するには、LsaLogonUserを呼び出す必要があるのですが、 これからもそれは変わらないのかもしれません。



戻る