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

プロセスが特権を必要とする関数を呼び出すためには、 まずその特権がトークンの特権リストに格納されていなければなりません。 これを満たすためには、目的の特権をLsaAddAccountRightsでユーザーまたはグループに割り当て、 そのユーザーとして再度ログオンすることになります。 そして次に、特権リストに格納されている目的の特権を有効にしなければなりません。 関数によっては特権を内部的に有効にする場合もありますが、 そうでない関数も存在するため、特権を有効にする方法を理解しておく必要があります。 特権を有効にするには、AdjustTokenPrivilegesを呼び出します。

BOOL WINAPI AdjustTokenPrivileges(
  HANDLE TokenHandle,
  BOOL DisableAllPrivileges,
  PTOKEN_PRIVILEGES NewState,
  DWORD BufferLength,
  PTOKEN_PRIVILEGES PreviousState,
  PDWORD ReturnLength
);

TokenHandleは、トークンのハンドルを指定します。 このハンドルには、TOKEN_ADJUST_PRIVILEGESアクセス権が割り当てられている必要があります。 DisableAllPrivilegesは、全ての特権を無効にするかどうかを表すBOOL値を指定します。 NewStateは、特権の有効や無効、もしくは削除に関する情報を格納したTOKEN_PRIVILEGES構造体のアドレスを指定します。 BufferLengthは、NewStateのサイズを指定します。 PreviousStateは、特権が変更される前の状態を受け取るバッファを指定します。 不要な場合はNULLを指定します。 ReturnLengthは、PreviousStateのサイズを格納した変数のアドレスを指定します。 PreviousStateがNULLの場合は、必要なサイズが返ることになります。 不要な場合はNULLを指定します。

TOKEN_PRIVILEGES構造体は、次のように定義されています。

typedef struct _TOKEN_PRIVILEGES {
  DWORD               PrivilegeCount;
  LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES,  *PTOKEN_PRIVILEGES;

PrivilegeCountは、Privilegesの要素数を指定します。 Privilegesは、LUID_AND_ATTRIBUTES構造体の配列を指定します。 この構造体のLuidメンバに目的の特権のLUIDを指定し、 Attributesメンバに変更する属性を指定します。 たとえば、特権を有効にする場合はSE_PRIVILEGE_ENABLEDを指定し、 無効にする場合は0を指定します。 また、Windows XP SP2からはSE_PRIVILEGE_REMOVEDを指定することで、 特権を特権リストから削除することもできます。 削除された特権は存在しない特権になるため、有効にすることができません。

TOKEN_PRIVILEGES構造体は特権をLUIDで要求するため、 アプリケーションは特権名から適切なLUIDを取得する必要があります。 これには、LookupPrivilegeValueを呼び出します。

BOOL WINAPI LookupPrivilegeValue(
  LPCTSTR lpSystemName,
  LPCTSTR lpName,
  PLUID lpLuid
);

lpSystemNameは、特権名を検索するシステムの名前を指定します。 NULLを指定した場合は、ローカルシステムが対象となります。 lpNameは、SE_DEBUG_NAMEなどの特権名を指定します。 lpLuidは、特権名に関連するLUIDを受け取る変数のアドレスを指定します。

目的の特権が、特定のアカウントに既定で割り当てられているかどうかは非常に重要です。 たとえば、SE_DEBUG_NAMEという特権は既定でAdministratorsに割り当てられているため、 このメンバであるユーザーとしてプロセスが実行されている場合は、 SE_DEBUG_NAMEを有効にできるという前提で動作することができます。 一方、SE_ASSIGNPRIMARYTOKEN_NAMEやSE_TCB_NAMEは既定で割り当てられていないため、 このような特権を必要とするアプリケーションは、 特権を有効にできるという前提で動作することはできません。 こうしたアプリケーションを外部に公開するのは難しいといえるでしょう。

今回のプログラムは、SE_DEBUG_NAMEを有効にしています。 この特権は、プロセスのオープン時におけるアクセスチェックをパスするというものであり、 これを有効にすることでシステムプロセスのハンドルを取得するようなことも可能になります。

#include <windows.h>

BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable);
BOOL GetLsaProcessId(LPVOID lpData, DWORD dwSize);
BOOL ConvertSidToName(PSID pSid, LPTSTR lpszName, DWORD dwSizeName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR       szUserName[256];
	DWORD       dwProcessId;
	HANDLE      hProcess;
	HANDLE      hToken;
	DWORD       dwLength;
	PTOKEN_USER pTokenUser;
	
	if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) {
		MessageBox(NULL, TEXT("特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	GetLsaProcessId(&dwProcessId, sizeof(DWORD));
	hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);
	if (hProcess == NULL) {
		MessageBox(NULL, TEXT("プロセスのオープンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	if (!OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hProcess);
		return 0;
	}
	
	GetTokenInformation(hToken, TokenUser, NULL, 0, &dwLength);
	pTokenUser = (PTOKEN_USER)LocalAlloc(LPTR, dwLength);
	GetTokenInformation(hToken, TokenUser, pTokenUser, dwLength, &dwLength);

	ConvertSidToName(pTokenUser->User.Sid, szUserName, sizeof(szUserName) / sizeof(TCHAR));

	MessageBox(NULL, szUserName, TEXT("トークンユーザー"), MB_OK);

	LocalFree(pTokenUser);
	CloseHandle(hToken);
	CloseHandle(hProcess);

	return 0;
}

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

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &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;
}

BOOL GetLsaProcessId(LPVOID lpData, DWORD dwSize)
{
	HKEY hKey;
	LONG lResult;

	lResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SYSTEM\\CurrentControlSet\\Control\\Lsa"), 0, KEY_QUERY_VALUE, &hKey);
	if (lResult == ERROR_SUCCESS) {
		lResult = RegQueryValueEx(hKey, TEXT("LsaPid"), NULL, NULL, (LPBYTE)lpData, &dwSize);
		RegCloseKey(hKey);
		return TRUE;
	}
	else
		return FALSE;
}

BOOL ConvertSidToName(PSID pSid, LPTSTR lpszName, DWORD dwSizeName)
{
	TCHAR        szDomainName[256];
	DWORD        dwSizeDomain = sizeof(szDomainName) / sizeof(TCHAR);
	SID_NAME_USE sidName;

	return LookupAccountSid(NULL, pSid, lpszName, &dwSizeName, szDomainName, &dwSizeDomain, &sidName);
}

このプログラムは、GetLsaProcessIdという自作関数でLSA(lsass.exe)のプロセスIDを取得し、 これを基にOpenProcessでLSAプロセスのハンドルを取得します。 LSAはシステムプロセスであり、本来ならばOpenProcessは失敗することになりますが、 EnablePrivilegeという自作関数で事前にSE_DEBUG_NAMEを有効にしているため、 プロセスのハンドルを取得することができます。 LSAがシステムプロセスであるかどうかは、プロセスハンドルからトークンを取得し、 そのトークンユーザーを表示する後続の処理からも明らかです。 EnablePrivilegeの内部は、次のようになっています。

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;
}

まず、OpenProcessTokenにGetCurrentProcessの戻り値を指定して、現在のプロセスのトークンを取得します。 このとき、AdjustTokenPrivilegesの呼び出しが必要になる関係上、TOKEN_ADJUST_PRIVILEGESを指定しています。 続いて、LookupPrivilegeValueで特権名から関連するLUIDを取得し、 これをTOKEN_PRIVILEGES構造体に指定します。 指定する特権の数は1つであるため、PrivilegeCountは1であり、 bEnableにはTRUEが設定されているため、AttributesにはSE_PRIVILEGE_ENABLEDが指定されます。 これにより、AdjustTokenPrivilegesの呼び出しで特権が有効になります。 ただし、少し注意しなければならないのは、 AdjustTokenPrivilegesの戻り値が特権を実際に有効にできたかどうかを表わさないという点です。 この関数は、引数や構造体の指定に問題がなければTRUEを返すため、 特権を有効にできたかどうかはGetLastErrorを通じて確認する必要があります。


戻る