EternalWindows
セキュリティコンテキスト / 特権リスト

プロセスに割り当てられたトークンが参照される例は、主に2通り考えられます。 1つは、オブジェクトへのアクセスを行う場合で、 オブジェクトのACEのSIDと、トークンユーザー及びトークングループのSIDが比較されます。 そしてもう1つは、特権を必要とする関数を呼び出した場合です。 特権とは、ある特定の操作や関数を呼び出す際に必要な権利であり、 どれだけ昇格したセキュリティコンテキストでそのような関数を呼び出しても、 トークンに特権が割り当てられて有効になっていなければ、関数の呼び出しは失敗することになります。 定義されている全ての特権は、次のURLから確認することができます。

http://msdn.microsoft.com/en-us/library/bb530716(VS.85).aspx

次に、特権とそれを必要とする関数を示します。 この表は完全なものではないため、特に目を通す必要はありません。

特権 必要とする関数
SE_ASSIGNPRIMARYTOKEN_NAME ・CreateProcessAsUser
SE_AUDIT_NAME ・PrivilegedServiceAuditAlarm
・AuthzReportSecurityEvent
SE_BACKUP_NAME ・CreateFile(FILE_FLAG_BACKUP_SEMANTICS指定時)
・BackupEventLog
SE_CHANGE_NOTIFY_NAME ・NetUserChangePassword
SE_DEBUG_NAME ・DebugActiveProcess
・GetThreadWaitChain
SE_INCREASE_QUOTA_NAME ・CreateProcessAsUser
SE_INC_WORKING_SET_NAME ・SetProcessWorkingSetSize
・SetSystemFileCacheSize
SE_LOCK_MEMORY_NAME ・AllocateUserPhysicalPages
SE_MACHINE_ACCOUNT_NAME ・NetUserAdd(UF_WORKSTATION_TRUST_ACCOUNT指定時)
SE_MANAGE_VOLUME_NAME ・SetFileValidData
SE_REMOTE_SHUTDOWN_NAME ・InitiateSystemShutdownEx(リモートシャットダウン時)
SE_RESTORE_NAME ・CreateFile(FILE_FLAG_BACKUP_SEMANTICS指定時)
・RegLoadKey
SE_SECURITY_NAME ・SetNamedSecurityInfo(SACL指定時)
・AuditQuerySystemPolicy
SE_SYSTEMTIME_NAME ・SetSystemTime
SE_SHUTDOWN_NAME ・ExitWindowsEx
・InitiateSystemShutdownEx(ローカルシャットダウン時)
SE_TCB_NAME ・LogonUser(Windows 2000)
・LogonUserExExW(TOKEN_GROUPS指定時)
・LsaRegisterLogonProcess
・WTSQueryUserToken
・BroadcastSystemMessage(BSM_ALLDESKTOPS指定時)
SE_TIME_ZONE_NAME ・SetDynamicTimeZoneInformation

たとえば、ExitWindowsExという関数を呼び出すのであれば、 SE_SHUTDOWN_NAMEという特権がトークンに割り当てられている必要があります。 また、その特権は有効になっておく必要もあります。 ただし、関数によっては内部的に特権を有効にすることもあるため、 明示的に有効にする必要があるかどうかは関数によって異なります。

特権はログオン時にトークンに割り当てられ、特権リストとしてトークンに格納されています。 ただし、各特権はLUIDで識別されているため、この値だけではそれがどのような特権なのかを 判定するのは難しいものがあります。 次に示すLookupPrivilegeNameを呼び出せば、LUIDから特権名(SE_SHUTDOWN_NAMEなど)を 表す文字列を取得できます。

BOOL WINAPI LookupPrivilegeName(
  LPCTSTR lpSystemName,
  PLUID lpLuid,
  LPTSTR lpName,
  LPDWORD cchName
);

lpSystemNameは、特権名を検索するシステムの名前を指定します。 NULLを指定した場合は、ローカルシステムが対象となります。 lpLuidは、特権のLUIDを格納した構造体のアドレスを指定します。 lpNameは、特権名を受け取るバッファを指定します。 cchNameは、lpNameに指定したバッファの文字数単位のサイズを格納した変数のアドレスを指定します。

特権名には、表示名と呼ばれる説明文が関連付けられています。 たとえば、SE_SHUTDOWN_NAMEであれば、「システムのシャットダウン」となります。 こうした表示名は、LookupPrivilegeDisplayNameで取得することができます。

BOOL WINAPI LookupPrivilegeDisplayName(
  LPCTSTR lpSystemName,
  LPCTSTR lpName,
  LPTSTR lpDisplayName,
  LPDWORD cchDisplayName,
  LPDWORD lpLanguageId
);

lpSystemNameは、特権の表示名を検索するシステムの名前を指定します。 NULLを指定した場合は、ローカルシステムが対象となります。 lpNameは、特権名を指定します。 lpDisplayNameは、特権の表示名を受け取るバッファを指定します。 cchDisplayNameは、lpDisplayNameに指定したバッファの文字数単位のサイズを格納した変数のアドレスを指定します。 lpLanguageIdは、言語識別子を受け取る変数のアドレスを指定します。

今回のプログラムは、トークンの特権リストを取得し、各特権をリストボックスに列挙します。

#include <windows.h>

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR      szAppName[] = TEXT("sample");
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	while (GetMessage(&msg, NULL, 0, 0) > 0) {
		TranslateMessage(&msg);
		DispatchMessage(&msg);
	}

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndListBox = NULL;
	
	switch (uMsg) {

	case WM_CREATE: {
		TCHAR             szBuf[1024];
		TCHAR             szDisplayName[256];
		TCHAR             szProgramName[256];
		DWORD             i;
		DWORD             dwLength;
		DWORD             dwLanguageId;
		BOOL              bEnabled;
		HANDLE            hToken;
		PTOKEN_PRIVILEGES pTokenPrivileges;
		
		hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		
		if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
			MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
			return -1;
		}
		
		GetTokenInformation(hToken, TokenPrivileges, NULL, 0, &dwLength);
		pTokenPrivileges = (PTOKEN_PRIVILEGES)LocalAlloc(LPTR, dwLength);
		GetTokenInformation(hToken, TokenPrivileges, pTokenPrivileges, dwLength, &dwLength);
		
		for (i = 0; i < pTokenPrivileges->PrivilegeCount; i++) {
			bEnabled = pTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED;

			dwLength = sizeof(szProgramName) / sizeof(TCHAR);
			LookupPrivilegeName(NULL, &pTokenPrivileges->Privileges[i].Luid, szProgramName, &dwLength);
			
			dwLength = sizeof(szDisplayName) / sizeof(TCHAR);
			LookupPrivilegeDisplayName(NULL, szProgramName, szDisplayName, &dwLength, &dwLanguageId);
			
			wsprintf(szBuf, TEXT("%s %s %s "), bEnabled ? TEXT("○") : TEXT("×"), szProgramName, szDisplayName);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
		}

		LocalFree(pTokenPrivileges);
		CloseHandle(hToken);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

列挙される特権の横には、○または×の文字が表示されています。 ○の場合は、その特権が有効であることを意味しています。 次の図は、UACによって制限されたユーザーの特権リストと、 管理者の特権リストを示しています。

制限ユーザーの特権リスト 管理者の特権リスト

ここで最も注視するべきところは、有効になっている特権の数ではありません。 特権の有効化はアプリケーションの実行時にAdjustTokenPrivilegesで行えるため、 初期状態で特権が無効になっていることは特に問題ではありません。 重要なのは列挙されている特権の数、つまりトークンに割り当てられた特権の数です。 割り当てられていない特権は有効にすることができないため、 たとえば制限されたユーザーは、SE_DEBUG_NAME(SeDebugPrivilege)を有効にすることはできません。 管理者が強力なアカウントであると言われるのは、このように特権が多く割り当てられていることが1つの要因といえるでしょう。 管理者といっても全ての特権が割り当てられているわけではありませんが、 特権をアカウントに割り当てることのできるLsaAddAccountRightsを呼び出せば、 全ての特権を割り当てることも事実上は可能となります。 アカウントに特権を割り当てれば、そのアカウントのトークンに特権が割り当てられます。

トークンの特権リストを取得するには、GetTokenInformationにTokenPrivilegesを指定します。 具体的な手順については、特に問題ないと思われます。 特権リストは、TOKEN_PRIVILEGES構造体で表され、PrivilegeCountメンバから特権の数、 Privilegesメンバから特権のLUIDと属性を参照できます。 属性を確認するコードは、次のようになっています。

bEnabled = pTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED;

SE_PRIVILEGE_ENABLEDが含まれている場合は、特権が有効であると判断できます。 属性にはこの他に、SE_PRIVILEGE_ENABLED_BY_DEFAULTがあります。 この定数が含まれている場合、プロセスの実行時から特権が有効ということになります。 次節で紹介するAdjustTokenPrivilegesで実行時に有効にされた特権には、 SE_PRIVILEGE_ENABLED_BY_DEFAULTが含まれていません。

権限とは何か

一般にプロセスのセキュリティコンテキストを表す場合、 Administrator権限やUser権限という言葉がよく使われていますが、 この権限というのはそもそも何を意味しているのでしょうか。 たとえば、Administrator権限をAdministratorsに割り当てられた特権として考えた場合、 こうした特権を他のアカウントに割り当てたら、 そのアカウントにもAdministrators権限があるといえるのでしょうか。 当然、そうではないはずです。 たとえ特権を割り当てられたとしても、オブジェクトへのアクセスに使われるのは トークン内のSIDですから、AdministratorsのグループSIDがないことには、 そのアカウントは管理者として動作していることにはなりません。 したがって、プロセスがAdministrator権限として動作しているという言葉は、 プロセスのトークンのトークングループにAdministratorsが含まれてそれが有効である、 という意味になります。

権限という思考で考え続けると、トークンのトークングループに目的のグループが 存在するかを確認をする作業は、非常に分かりにくいものです。 このため、トークンやSIDを隠蔽したIsUserAnAdminは、 直観的にも利用方法も分りやすく、よく使われているのだと思います。 ただし、トークンの存在は、決して忘れてよいものではありません。 以下に、注意しなければ例を示します。

#include <windows.h>
#include <lm.h>

#pragma comment (lib, "netapi32.lib")

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR         szUserName[256];
	DWORD         dwSize;
	LPUSER_INFO_1 lpUserInfo;

	dwSize = sizeof(szUserName) / sizeof(WCHAR);
	GetUserName(szUserName, &dwSize);
	
	NetUserGetInfo(NULL, szUserName, 1, (LPBYTE *)&lpUserInfo);

	if (lpUserInfo->usri1_priv == USER_PRIV_GUEST)
		MessageBox(NULL, TEXT("guest"), TEXT("OK"), MB_OK);
	else if (lpUserInfo->usri1_priv == USER_PRIV_USER)
		MessageBox(NULL, TEXT("user"), TEXT("OK"), MB_OK);
	else if (lpUserInfo->usri1_priv == USER_PRIV_ADMIN)
		MessageBox(NULL, TEXT("admin"), TEXT("OK"), MB_OK);
	else
		;

	NetApiBufferFree(lpUserInfo);

	return 0;
}

このコードは現在のスレッドのユーザー名を取得し、 それをNetUserGetInfoに指定してユーザー情報を取得しています。 USER_INFO_1.usri1_privには、そのユーザーの権限が格納されており、 これがUSER_PRIV_ADMINであればAdministrator権限、 つまりユーザーがAdministratorsグループのメンバであることになります。 この事から、現在のスレッドは管理者として動作していると判断できそうに思えますが、これは誤りです。 確かにスレッドのトークンには、Administratorsグループが含まれているはずですが、 それはUACなどの制限によって無効になっている可能性があります。 しかし、NetUserGetInfoはトークンを基に権限の判定を行いませんから、 この関数を通じてセキュリティコンテキストを確認してはいけません。



戻る