EternalWindows
セキュリティコンテキスト / プロセスの昇格

プロセスのセキュリティコンテキストを昇格させるという行為は、慎重に行われるべきといえます。 たとえば、UACであれば昇格時にダイアログを表示するようにし、 実際に昇格を許可するかどうかの判断をユーザーに任せています。 これは当然といえば当然の話であり、プロセスが自動でセキュリティコンテキストを 昇格させることができるのであれば、何のために制限ユーザーとして実行しているのか分かりません。 しかしながら、プロセスが既に管理者として動作している場合は、 プロセスのセキュリティコンテキストをより強力なSYSTEMに昇格させることができます。 この方法を理解しておけば、たとえばWindows XPでSE_TCB_NAME特権が必要になった場合、 それをAdministratorsに割り当てて一度ログオフする必要はありません。 プロセスをSYSTEMとして実行することにより、SE_TCB_NAME特権を手に入れることができます。

プロセスをSYSTEMとして動作させることは、概念的にはそれほど複雑ではありません。 まず、既存のシステムプロセスのハンドルを取得し、次にそれを基にシステムプロセスのトークンを取得、 そして最後にCreateProcessAsUserを呼び出せば、プロセスはSYSTEMとして起動されることになります。 複雑なのはこれらを成功するために必要なコードと特権の問題です。 たとえば、CreateProcessAsUserはSE_ASSIGNPRIMARYTOKEN_NAMEを必要とするため、 Administratorsにこの特権を事前に割り当てておかなければなりません。 Windows Vistaでは、CreateProcessWithTokenWを呼び出すことができるため、 SE_ASSIGNPRIMARYTOKEN_NAMEは必須ではないといえますが、 ある状況においてSE_TCB_NAMEというまた別の特権が必要となります。 こうした特権の事情からも分るように、プロセスをSYSTEMとして動作させることは、 どのような環境でも可能というわけではありません。 特権を自由に割り当てられる自分のPC以外では実行に失敗するため、 悪意を持って昇格を狙うことはできません。

今回のプログラムは、前述の通りプロセスをSYSTEMとして実行します。

#include <windows.h>
#include <tlhelp32.h>
#include <aclapi.h>
#include <userenv.h>

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

BOOL OpenSystemProcess(HANDLE *phProcessSystem);
BOOL OpenSystemProcessToken(HANDLE hProcessSystem, HANDLE *phTokenSystem, BOOL bWindowsVistaLater);
BOOL ModifyTokenSecurity(HANDLE hToken, DWORD dwAccessMask);
BOOL RunAsSystem(HANDLE hTokenSystem, LPTSTR lpszApplicationName);
BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable);
BOOL IsWindowsVistaLater(void);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR  szApplicationName[] = TEXT("C:\\Windows\\notepad.exe");
	HANDLE hProcessSystem;
	HANDLE hTokenSystem;
	BOOL   bWindowsVistaLater;
	
	if (!EnablePrivilege(SE_DEBUG_NAME, TRUE)) {
		MessageBox(NULL, TEXT("SE_DEBUG_NAME特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	if (!OpenSystemProcess(&hProcessSystem)) {
		MessageBox(NULL, TEXT("システムプロセスのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	bWindowsVistaLater = IsWindowsVistaLater();
	
	if (!OpenSystemProcessToken(hProcessSystem, &hTokenSystem, bWindowsVistaLater)) {
		MessageBox(NULL, TEXT("システムプロセスのトークンの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hProcessSystem);
		return 0;
	}

	if (bWindowsVistaLater) {
		DWORD dwSessionId = 1;
		
		if (!EnablePrivilege(SE_TCB_NAME, TRUE)) {
			MessageBox(NULL, TEXT("SE_TCB_NAME特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
			CloseHandle(hTokenSystem);
			CloseHandle(hProcessSystem);
			return 0;
		}

		if (!SetTokenInformation(hTokenSystem, TokenSessionId, &dwSessionId, sizeof(DWORD))) {
			MessageBox(NULL, TEXT("セッションIDの変更に失敗しました。"), NULL, MB_ICONWARNING);
			CloseHandle(hTokenSystem);
			CloseHandle(hProcessSystem);
			return 0;
		}
	}

	if (!RunAsSystem(hTokenSystem, szApplicationName)) {
		MessageBox(NULL, TEXT("プロセスの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hTokenSystem);
		CloseHandle(hProcessSystem);
		return 0;
	}

	CloseHandle(hTokenSystem);
	CloseHandle(hProcessSystem);

	return 0;
}

BOOL OpenSystemProcess(HANDLE *phProcessSystem)
{
	HANDLE         hSnapshot;
	DWORD          dwProcessId;
	PROCESSENTRY32 pe;
	
	hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
	if (hSnapshot == INVALID_HANDLE_VALUE)
		return FALSE;

	pe.dwSize = sizeof(PROCESSENTRY32);
	Process32First(hSnapshot, &pe);

	dwProcessId = 0;
	do {
		if (lstrcmp(pe.szExeFile, TEXT("lsass.exe")) == 0) {
			dwProcessId = pe.th32ProcessID;
			break;
		}
	} while (Process32Next(hSnapshot, &pe));
	
	CloseHandle(hSnapshot);
	
	*phProcessSystem = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId);

	return *phProcessSystem != NULL;
}

BOOL OpenSystemProcessToken(HANDLE hProcessSystem, HANDLE *phTokenSystem, BOOL bWindowsVistaLater)
{
	HANDLE hToken;

	if (bWindowsVistaLater) {
		HANDLE hTokenDuplicate;

		if (!OpenProcessToken(hProcessSystem, TOKEN_DUPLICATE, &hToken))
			return FALSE;

		DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, 0, SecurityIdentification, TokenPrimary, &hTokenDuplicate);
		*phTokenSystem = hTokenDuplicate;
		CloseHandle(hToken);
	}
	else {
		DWORD dwAccessMask = TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY;

		if (!OpenProcessToken(hProcessSystem, dwAccessMask, &hToken)) {
			if (!OpenProcessToken(hProcessSystem, READ_CONTROL | WRITE_DAC, &hToken))
				return FALSE;
			
			if (!ModifyTokenSecurity(hToken, dwAccessMask)) {
				CloseHandle(hToken);
				return FALSE;
			}
			
			CloseHandle(hToken);

			if (!OpenProcessToken(hProcessSystem, dwAccessMask, &hToken))
				return FALSE;
		}
		
		*phTokenSystem = hToken;
	}
	
	return *phTokenSystem != NULL;
}

BOOL ModifyTokenSecurity(HANDLE hTokenSystem, DWORD dwAccessMask)
{
	TCHAR                szUserName[256];
	DWORD                dwBufferSize;
	DWORD                dwResult;
	PACL                 pDacl;
	PACL                 pDaclNew;
	EXPLICIT_ACCESS      explicitAccess;
	PSECURITY_DESCRIPTOR pSecurityDescriptor;
	
	if (GetSecurityInfo(hTokenSystem, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &pDacl, NULL, &pSecurityDescriptor) != ERROR_SUCCESS)
		return FALSE;

	dwBufferSize = sizeof(szUserName) / sizeof(TCHAR);
	GetUserName(szUserName, &dwBufferSize);
	BuildExplicitAccessWithName(&explicitAccess, szUserName, dwAccessMask, GRANT_ACCESS, 0);
	SetEntriesInAcl(1, &explicitAccess, pDacl, &pDaclNew);

	dwResult = SetSecurityInfo(hTokenSystem, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pDaclNew, NULL);
	
	LocalFree(pDaclNew);
	LocalFree(pSecurityDescriptor);

	return dwResult == ERROR_SUCCESS;
}

BOOL RunAsSystem(HANDLE hTokenSystem, LPTSTR lpszApplicationName)
{
	LPVOID              lpEnvironment;
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;

	ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
	startupInfo.cb        = sizeof(STARTUPINFO);
	startupInfo.lpDesktop = TEXT("winsta0\\default");

	CreateEnvironmentBlock(&lpEnvironment, hTokenSystem, TRUE);

	if (!CreateProcessAsUser(hTokenSystem, lpszApplicationName, NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &startupInfo, &processInformation)) {
		DestroyEnvironmentBlock(lpEnvironment);
		return FALSE;
	}

	DestroyEnvironmentBlock(lpEnvironment);
	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);

	return TRUE;
}

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

BOOL IsWindowsVistaLater(void)
{
	DWORDLONG       dwlConditionMask;
	OSVERSIONINFOEX versionInfo;

	ZeroMemory(&versionInfo, sizeof(OSVERSIONINFOEX));
	versionInfo.dwMajorVersion      = 6;
	versionInfo.dwMinorVersion      = 0;
	versionInfo.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);

	dwlConditionMask = 0;
	dwlConditionMask = VerSetConditionMask(dwlConditionMask, VER_MAJORVERSION, VER_GREATER_EQUAL);
	dwlConditionMask = VerSetConditionMask(dwlConditionMask, VER_MINORVERSION, VER_GREATER_EQUAL);

	return VerifyVersionInfo(&versionInfo, VER_MAJORVERSION | VER_MINORVERSION, dwlConditionMask);
}

自作関数のOpenSystemProcessは、システムプロセスのハンドルを取得します。 対象とするプロセスはlsass.exe(他でも当然可)とし、プロセスを列挙するProcess32First/Process32Nextでこれを探します。 一致したならば、取得したプロセスIDをOpenProcessに指定し、 プロセスのハンドルを取得できることになります。 システムプロセスをオープンする関係上、SE_DEBUG_NAME特権を有効にしておく必要があります。

続いて行うべき操作は、システムプロセスのトークンの取得です。 これを成功させるために必要な手順は、Windows VistaとXPで異なるため、 事前にIsWindowsVistaLaterという自作関数を呼び出すことで現在の環境を確認しています。 まず、Windows VistaにおけるOpenSystemProcessTokenの処理を確認します。

if (bWindowsVistaLater) {
	HANDLE hTokenDuplicate;

	if (!OpenProcessToken(hProcessSystem, TOKEN_DUPLICATE, &hToken))
		return FALSE;

	DuplicateTokenEx(hToken, TOKEN_ALL_ACCESS, 0, SecurityIdentification, TokenPrimary, &hTokenDuplicate);
	*phTokenSystem = hTokenDuplicate;
	CloseHandle(hToken);
}

OpenProcessTokenでプロセスのトークンを取得するのは当然といえますが、 その後にDuplicateTokenExでトークンを複製しています。 これは、既存のトークン(今回の場合lsass.exeのトークン)に対して、 変更を加えないようにするための処理です。 Windows Vistaでは「セッション 0 の分離」と呼ばれる機能により、 システムプロセスがセッション0で動作しており、 トークンのセッションIDも0が指定されています。 このトークンをセッション1で動作するプロセスに割り当てる場合は、 事前にセッションIDを1に変更しておかなければなりませんが、 既存のトークンに対してこれを行うわけにはいきません。 よって、トークンを複製し、そのトークンに対してセッションIDを設定することになります。

if (bWindowsVistaLater) {
	DWORD dwSessionId = 1;
	
	if (!EnablePrivilege(SE_TCB_NAME, TRUE)) {
		MessageBox(NULL, TEXT("SE_TCB_NAME特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hTokenSystem);
		CloseHandle(hProcessSystem);
		return 0;
	}

	if (!SetTokenInformation(hTokenSystem, TokenSessionId, &dwSessionId, sizeof(DWORD))) {
		MessageBox(NULL, TEXT("セッションIDの変更に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hTokenSystem);
		CloseHandle(hProcessSystem);
		return 0;
	}
}

セッションIDを設定するには、SetTokenInformationにTokenSessionIdを指定します。 これを成功させるためには、SE_TCB_NAME特権を有効にする必要があります。

次に、Windows XPにおけるOpenSystemProcessTokenの処理を示します。

else {
	DWORD dwAccessMask = TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY;

	if (!OpenProcessToken(hProcessSystem, READ_CONTROL | WRITE_DAC, &hToken))
		return FALSE;

	if (!ModifyTokenSecurity(hToken, dwAccessMask)) {
		CloseHandle(hToken);
		return FALSE;
	}
	
	CloseHandle(hToken);

	if (!OpenProcessToken(hProcessSystem, dwAccessMask, &hToken))
		return FALSE;

	*phTokenSystem = hToken;
}

最終的にCreateProcessAsUserに指定するトークンは、 dwAccessMaskで表されるアクセス権を用いてオープンされている必要があります。 しかしながら、Windows XPではTOKEN_DUPLICATEとTOKEN_ASSIGN_PRIMARY(これはVistaでも許可されない)が管理者に対して許可されていないため、 dwAccessMaskを直ちに指定しても失敗することになります。 そこで、トークンのDACLを書き換えるという手段を用いることになります。 つまり、現在のユーザーに対してdwAccessMaskを許可するACEをDACLに対して追加し、 このDACLをトークンに設定するようにします。 そしてこれが成功した後に、dwAccessMaskを指定してOpenProcessTokenを呼び出すのです。 DACLの書き換え時に指定しているREAD_CONTROLはセキュリティ記述子の読み取りを意味し、 WRITE_DACはDACLの書き換えを意味します。 WRITE_DACは呼び出し側がオブジェクト(今回の場合トークン)の所有者であれば、 アクセスチェックがパスされるので、これは成功することになります。 DACLを書き換えるModifyTokenSecurityの内部は、次のようになっています。

if (GetSecurityInfo(hTokenSystem, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &pDacl, NULL, &pSecurityDescriptor) != ERROR_SUCCESS)
	return FALSE;

dwBufferSize = sizeof(szUserName) / sizeof(TCHAR);
GetUserName(szUserName, &dwBufferSize);
BuildExplicitAccessWithName(&explicitAccess, szUserName, dwAccessMask, GRANT_ACCESS, 0);
SetEntriesInAcl(1, &explicitAccess, pDacl, &pDaclNew);

dwResult = SetSecurityInfo(hTokenSystem, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pDaclNew, NULL);

まず、GetSecurityInfoにDACL_SECURITY_INFORMATIONを指定して、トークンのDACLを取得します。 トークンはカーネルオブジェクトであるため、第2引数にはSE_KERNEL_OBJECTを指定します。 続いて、GetUserNameで現在のユーザー名を取得し、 これにdwAccessMaskを許可するための情報をBuildExplicitAccessWithNameで作成します。 第4引数に指定されているGRANT_ACCESSが、アクセスの許可を意味しています。 次に、トークンの元のDACLと先に作成した情報をSetEntriesInAclに指定します。 これにより、元のDACLにACEが格納された新しいDACLが第4引数に返ります。 後はこれをSetSecurityInfoに指定すれば、トークンのDACLは変更されることになります。

適切なトークンを取得すれば、それをCreateProcessAsUserに指定して、 SYSTEMとして動作するプロセスを作成することができます。 RunAsSystemの内部は、次のようになっています。

ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
startupInfo.cb        = sizeof(STARTUPINFO);
startupInfo.lpDesktop = TEXT("winsta0\\default");

CreateEnvironmentBlock(&lpEnvironment, hTokenSystem, TRUE);

if (!CreateProcessAsUser(hTokenSystem, lpszApplicationName, NULL, NULL, NULL, FALSE, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, NULL, &startupInfo, &processInformation)) {
	DestroyEnvironmentBlock(lpEnvironment);
	return FALSE;
}

Windows VistaではstartupInfo.lpDesktopにNULLを指定しても問題ありません。 この場合、呼び出し側プロセスのウインドウステーションとデスクトップを継承することになります。 一方、Windows XPではNULLを指定すると非対話のウインドウステーション上にプロセスが作成されるため、 この点を踏まえて明示的に対話ウインドウステーションとデスクトップを指定しています。 ウインドウステーションとデスクトップとはSYSTEMに対してアクセスを許可しているため、 ACEを追加するなどの特別な処理は必要ありません。 CreateEnvironmentBlockは、作成されるプロセスの環境変数をSYSTEMのものにするために呼び出しています。 ちなみに、CreateProcessAsUserは内部でSE_ASSIGNPRIMARYTOKEN_NAMEを有効にするため、 EnablePrivilegeにSE_ASSIGNPRIMARYTOKEN_NAMEを指定しておく必要はありません。

Windows XPにおけるOpenSystemProcessTokenの処理は、既存のトークンを変更しているという点を十分に注意すべきです。 一度目の実行でトークンのDACLは書き換えられるため、二度目の実行に関しては書き換え処理を行わなくても、 トークンのハンドルを取得することができます。 このため、他のプロセスからでもトークンのハンドルを取得できることになります。 この点を踏まえた場合、ハンドルをオープンするプロセスについてもよく検討する必要がありますが、 どのプロセスなら問題が少ないかといったことは一概には分らないのが現状です。 なお、Windows XPでは、プロセス名にSystemという文字列を指定して判定を行うこともできます。


戻る