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

Windows Vistaから追加された様々な機能の中で、特にユーザーの興味をひきつけたのは、 UAC(User Account Control)ではないかと思われます。 この機能は、管理者としてログオンした場合でも、制限ユーザーとしてプロセスを実行するというものであり、 仮にそのようなプロセスで悪意のあるコードが実行されたとしても、処理が失敗しやすくなるという利点があります。 ただし、実際にこの機能について深く考えていくと、いくつかの疑問が生じてくると思われます。 たとえば、制限ユーザーとしてプロセスが実行されるならば、 そもそも管理者としてログオンしたことにはならないのではないでしょうか。 また、制限ユーザーとしてコードが実行されることにより、 いくつかのプロセスでは関数呼び出しに失敗することが予想されますが、 こうした問題はどう対処することになるのでしょうか。 今回は、こうしたUACの疑問を解消するために、リンクトークンと仮想化について説明します。

ユーザーがシステムにログオンする場合、LSA(Local Security Authority)を基点にトークンを作成するための一連の処理が行われます。 ログオンが成功した場合、そのログオンしたユーザーを表すトークンとログオンセッションが作成されることになりますが、 ここでUACが有効になっている場合は、作成されたトークンがLSAによって調べられることになります。 もし、トークンが昇格した特権を持っている場合は、そのトークンを基にフィルタ処理されたトークンが作成され、 このフィルタ処理されたトークンを基に2回目のログオンが実行されます。 そして、最終的には、フィルタ処理されたトークンがシェルに割り当てられることになります。 フィルタ処理というのは、昇格した特権とそれを持つグループをトークン内で無効にすることであり、 制限付きトークンを作成する要領とほぼ同一です。 このような制限付きトークンがシェルに割り当てられることにより、 シェルから起動される各プロセスが制限ユーザーとして実行されることになります。

以上の事から分かるように、UACが有効である場合でも管理者としてのログオンは成立しています。 話が複雑になるのは、フィルタ処理されたトークンがシェルに割り当てられ、 完全なトークン(管理者のログオンで作成されたトークン)がプロセスから隠蔽されているからなのですが、 実際にはいくつかの確認方法があります。 たとえば、LsaEnumerateLogonSessionsを呼び出せば、 同じユーザー名のログオンセッションが2つ作成されていることが分かりますが、 これは完全なトークンとフィルタ処理されたトークンの2つが作成されているからに他なりません。 また、完全なトークンとフィルタ処理されたトークンは互いにリンクしており、 片方のトークンからもう片方トークンのハンドルを取得することが可能になっています。 このようなリンクトークンを取得する例を次に示します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE             hToken;
	DWORD              dwLength;
	TOKEN_ELEVATION    tokenElevation;
	TOKEN_LINKED_TOKEN linkedToken;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!GetTokenInformation(hToken, TokenLinkedToken, &linkedToken, sizeof(TOKEN_LINKED_TOKEN), &dwLength)) {
		MessageBox(NULL, TEXT("リンクトークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}
	
	if (!GetTokenInformation(linkedToken.LinkedToken, TokenElevation, &tokenElevation, sizeof(TOKEN_ELEVATION), &dwLength)) {
		MessageBox(NULL, TEXT("昇格情報の取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(linkedToken.LinkedToken);
		CloseHandle(hToken);
		return 0;
	}

	if (tokenElevation.TokenIsElevated)
		MessageBox(NULL, TEXT("リンクトークンは昇格されています。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("リンクトークンは昇格されていません。"), TEXT("OK"), MB_OK);

	CloseHandle(linkedToken.LinkedToken);
	CloseHandle(hToken);

	return 0;
}

リンクトークンを取得するには、GetTokenInformationにTokenLinkedTokenを指定します。 UACが有効でない場合はこのようなトークンは作成されていないため、関数は失敗することになります。 2回目のGetTokenInformationに指定しているTokenElevationは、トークンが昇格されているかを確認するためのもので、 ここではリンクトークンを対象にしています。 この結果、リンクトークンが昇格しているならば、現在プロセスに割り当てられているトークンはフィルタ処理されたトークンであり、 リンクトークンが昇格されていないならば、現在プロセスに割り当てられているトークンは完全なトークンであると分かります。 なお、リンクトークンが完全なトークンであったとしても、 それを基に偽装を行ったり、プロセスを作成したりすることは当然できません。 このような操作を実行した場合は、ERROR_PRIVILEGE_NOT_HELDが返ることになります。

続いて、UACにおける仮想化について説明します。 これは、ある特定のストア(ファイルパスやレジストリキー)に対して書き込みや作成が失敗する場合、 他のストアを代わりにして書き込みや作成を行うというものです。 次に、仮想化が適応されるストアを示します。

仮想化が適応されるストア リダイレクトされる仮想ストア
%ProgramFiles% %LocalAppData%VirtualStore\Program Files
%WinDir% %LocalAppData%VirtualStore\Windows
HKLM\Software HKCU\Software\Classes\VirtualStore\Machine\Software

上記に示すストアは、アプリケーションのインストール時などに利用されますが、 制限されたユーザーでは、このようなパスに対しての書き込みや作成はアクセスが拒否されます。 これでは、アプリケーションが正常に動作することができなくなってしまうため、 代わりとしてアクセス可能な場所(仮想ストア)を対象にします。 たとえば、%ProgramFiles%にファイルを作成しようとすると、 %LocalAppData%VirtualStore\Program Filesにファイルが作成されることになります。 読み取りアクセスでは、まず%LocalAppData%VirtualStore\Program Filesを確認し、 目的のファイルが存在しない場合に%ProgramFiles%を確認します。

UACが有効になっている場合、プロセスのトークンはデフォルトで仮想化が有効になっていますが、 これは実行時に無効することができます。 トークンには仮想化を有効に示す値が含まれているため、これを変更すればよいことになります。 トークンの情報を変更するには、SetTokenInformationを呼び出します。

BOOL WINAPI SetTokenInformation(
  HANDLE TokenHandle,
  TOKEN_INFORMATION_CLASS TokenInformationClass,
  LPVOID TokenInformation,
  DWORD TokenInformationLength
);

TokenHandleは、トークンのハンドルを指定します。 TokenInformationClassは、設定する情報の種類を表すTOKEN_INFORMATION_CLASS型の定数を指定します。 TokenInformationは、設定する情報を格納したバッファを指定します。 TokenInformationLengthは、TokenInformationに指定したバッファのサイズを指定します。

次に、SetTokenInformationを呼び出して仮想化を無効にする例を示します。 この結果、仮想ストアへのリダイレクトが発生しないため、 ファイルへの書き込みや作成は失敗に終わることになります。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE          hToken;
	HANDLE          hFile;
	DWORD           dwEnabled;
	DWORD           dwLength;
	TOKEN_ELEVATION tokenElevation;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_ADJUST_DEFAULT, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!GetTokenInformation(hToken, TokenElevation, &tokenElevation, sizeof(TOKEN_ELEVATION), &dwLength)) {
		MessageBox(NULL, TEXT("昇格情報の取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}
	
	if (tokenElevation.TokenIsElevated == 0) {
		dwEnabled = 0;
		if (!SetTokenInformation(hToken, TokenVirtualizationEnabled, &dwEnabled, sizeof(DWORD)))
			MessageBox(NULL, TEXT("Virtualizationの無効化に失敗しました。"), NULL, MB_ICONWARNING);
	}
	
	hFile = CreateFile(TEXT("C:\\Program Files\\sample.txt"), GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
	if (hFile == INVALID_HANDLE_VALUE) {
		if (GetLastError() == ERROR_ACCESS_DENIED)
			MessageBox(NULL, TEXT("アクセスが拒否されました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}

	MessageBox(NULL, TEXT("ファイルを作成しました。"), TEXT("OK"), MB_OK);

	CloseHandle(hFile);
	CloseHandle(hToken);

	return 0;
}

UACが有効である場合でも、管理者として実行されている場合は仮想化が既に無効になっているため、 この場合は無効にする処理が不要となります。 よって、TokenElevationを通じてトークンが昇格されているかを確認する必要があります。 昇格されていない場合は、仮想化が有効になっているため、 TokenVirtualizationEnabledを0に設定することで、仮想化を無効にすることになります。 これにより、CreateFileの呼び出しは失敗することになります。 逆にSetTokenInformationの呼び出しを省略した場合は、仮想ストアにファイルが作成されていることが確認できます。 当然ながら、管理者として実行した場合は、CreateFileに指定したパスにファイルは作成されます。 また、管理者として実行しているトークンに対して、仮想化を有効にすることはできません。

シールドアイコンと昇格

プロセスを制限されたユーザーで実行することは好ましいといえますが、 場合によっては管理者としてコードを実行しなければならないこともあります。 このような場合、勝手に管理者としての動作に入ろうとしては、唐突に昇格ダイアログが表示されてしまうため、 動作を開始するためのボタンを用意し、そこにシールドアイコンを表示するのが一般的です。 こうすればユーザーは、そのボタンを押すことで昇格を促すダイアログが表示されると理解できます。 ダイアログが表示される仕組みは、管理者でなければ実行できない処理を持ったプロセスを用意し、 それを意図的に昇格させて実行することで成立しています。 つまり、ボタンが押されたプロセスが昇格するのではなく、 ボタンが押されたプロセスが別のプロセスを起動しているのです。 次に、コード例を示します。

#include <windows.h>
#include <commctrl.h>
#include <psapi.h>

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

#define ID_BUTTON 100

void EnumProcessName(HWND hwndListBox);
BOOL IsTokenElevation();
BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable);
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: {
		HWND hwndButton;
		
		hwndListBox = CreateWindowEx(0, TEXT("LISTBOX"), NULL, WS_CHILD | WS_VISIBLE | WS_VSCROLL, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		EnumProcessName(hwndListBox);

		if (!IsTokenElevation()) {
			hwndButton = CreateWindowEx(0, TEXT("BUTTON"), TEXT("全てのプロセスを列挙"), WS_CHILD | WS_VISIBLE, 30, 20, 200, 30, hwnd, (HMENU)ID_BUTTON, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
			SendMessage(hwndButton, BCM_SETSHIELD, 0, TRUE);
		}
		
		return 0;
	}

	case WM_COMMAND:
		if (LOWORD(wParam) == ID_BUTTON) {
			TCHAR szModulePath[MAX_PATH];

			GetModuleFileName(NULL, szModulePath, sizeof(szModulePath) / sizeof(TCHAR));
			ShellExecute(NULL, TEXT("runas"), szModulePath, NULL, NULL, SW_SHOWNORMAL);
			if (GetLastError() == ERROR_SUCCESS)
				PostMessage(hwnd, WM_CLOSE, 0, 0);
		}
		return 0;
	
	case WM_SIZE:
		MoveWindow(hwndListBox, 0, 70, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

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

void EnumProcessName(HWND hwndListBox)
{
	DWORD  i, dwCount;
	DWORD  dwProcessId[1024], dwSize;
	HANDLE hProcess;
	TCHAR  szImageName[MAX_PATH];
	
	EnablePrivilege(SE_DEBUG_NAME, TRUE);

	EnumProcesses(dwProcessId, sizeof(dwProcessId), &dwSize);
	dwCount = dwSize / sizeof(DWORD);

	for (i = 0; i < dwCount; i++) {
		hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, dwProcessId[i]);
		if (hProcess != NULL) {
			dwSize = sizeof(szImageName) / sizeof(TCHAR);
			QueryFullProcessImageName(hProcess, 0, szImageName, &dwSize);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szImageName);
			CloseHandle(hProcess);
		}
	}
}

BOOL IsTokenElevation()
{
	HANDLE          hToken;
	DWORD           dwLength;
	TOKEN_ELEVATION tokenElevation;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))
		return FALSE;

	if (!GetTokenInformation(hToken, TokenElevation, &tokenElevation, sizeof(TOKEN_ELEVATION), &dwLength)) {
		CloseHandle(hToken);
		return FALSE;
	}
	
	CloseHandle(hToken);

	return tokenElevation.TokenIsElevated;
}

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

このプログラムはプロセスを列挙することが目的ですが、 制限されたユーザーとして実行されている場合は、全てのプロセスを列挙できません。 理由はSE_DEBUG_NAME特権の有効化に失敗するからであり、 これによりシステムプロセスに対するOpenProcessが失敗するからです。 そこで、こうしたシステムプロセスも列挙したい場合は、 "全てのプロセスを列挙"というボタンを押下します。 このときは、昇格するための処理を行うべきであるため、 昇格して実行させたいEXEのフルパスが必要になりますが、 これは自プロセスのもので問題ありません。 つまり、自分と同じアプリケーションを昇格して実行させ、これが成功した場合に自分自身を終了させるのです。 昇格させて実行することはCreateProcessではできませんが、 ShellExecuteならばRunAsという文字列を指定することで可能です。 この場合は昇格のためのダイアログが表示され、 ユーザーが「許可」を選択した場合はERROR_SUCCESSが返ってプロセスが起動し、 「キャンセル」を選択した場合はERROR_CANCELLEDが返ります。

制限されたプロセスが終了し、昇格されたプロセスが存在し続けるというのは、必ずしも良い選択とは限りません。 たとえば、あるレジストリキーへの書き込みを行うために昇格が必要になったとします。 この場合は、その昇格されたプロセスがレジストリへのアクセスを終えたら終了すればよいだけであり、 制限されたプロセスが終了する必要はどこにもありません。 昇格が必要な処理を終えたプロセスが、依然として昇格されて実行されることは危険であるといえますから、 昇格されたプロセスは可能な限り素早く終了したほうがよいでしょう。 次に、一例を示します。

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR szKey[] = TEXT("write-registry");

	if (lstrcmp(GetCommandLine(), szKey) != 0) {
		// レジストリへの書き込み
		return 0;
	}

	// アプリケーション本来のコード

	return 0;
}

プロセスが自分と同じアプリケーションを昇格して起動する際には、 "write-registry"というパラメータを指定し、自分自身は終了しないようにします。 起動されたプロセスはコマンドラインを確認し、 これが"write-registry"という文字列であるならば、 レジストリへの書き込みを行って終了します。 これにより、昇格されたプロセスはわずか一瞬の間しか存在しないことになります。



戻る