EternalWindows
セキュリティコンテキスト / 別ユーザーのプロセス

特定のユーザーとしてコードを実行するにあたって、偽装は便利なメカニズムです。 これによりスレッドにトークンが割り当てられ、 スレッドが特定のユーザーとしてコードを実行できるようになります。 それでは、プロセスに任意のトークンを割り当てるにはどうすればよいでしょうか。 プロセスにトークンが割り当てられるのは、プロセスの作成時です。 CreateProcessで作成したプロセスは、呼び出し元のプロセスのトークンのコピーが割り当てられますが、 CreateProcessAsUserやCreateProcessWithLogonW、そしてCreateProcessWithTokenWでは、 任意のトークンを割り当てられます。 よって、今回はこれらの関数について見ていきます。 多くの引数はCreateProcessと同じ意味を持つため、重要な部分のみを取り上げます。

BOOL WINAPI CreateProcessAsUser(
  HANDLE hToken,
  LPCTSTR lpApplicationName,
  LPTSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCTSTR lpCurrentDirectory,
  LPSTARTUPINFO lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

hTokenは、プロセスに割り当てたいトークンのハンドルを指定します。 STARTUPINFO.lpDesktopにNULLを指定した場合、 新しいプロセスは呼び出し側と同じウインドウステーション上のデスクトップに作成されます。 一方、""を指定した場合は、新しいウインドウステーション上にデスクトップが作成されます。 ただし、このようなデスクトップの事情は、Windowsのバージョンによって一定でないようにも思えます。 この関数は、呼び出し側のセキュリティコンテキストで実行ファイルにアクセスします。 この関数を呼び出すには、呼び出し側のトークンにSE_ASSIGNPRIMARYTOKEN_NAMEとSE_INCREASE_QUOTA_NAMEが割り当てられている必要があります。 ただし、hTokenに指定したトークンが制限付きトークンである場合は、 SE_ASSIGNPRIMARYTOKEN_NAMEは必要ありません。

次に示すCreateProcessWithLogonWは、指定したユーザーをログオンし、 そのユーザーのトークンをプロセスに割り当てます。

BOOL WINAPI CreateProcessWithLogonW(
  LPCWSTR lpUsername,
  LPCWSTR lpDomain,
  LPCWSTR lpPassword,
  DWORD dwLogonFlags,
  PCWSTR lpApplicationName,
  LPWSTR lpCommandLine,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCWSTR lpCurrentDirectory,
  LPSTARTUPINFOW lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInfo
);

lpUsernameは、ログオンさせたいユーザー名を指定します。 lpDomainは、ユーザーが存在するドメインまたはサーバーを指定します。 lpPasswordは、ユーザーのパスワードを指定します。 dwLogonFlagsは、LOGON_WITH_PROFILEという定数を指定することができます。 この定数を指定した場合、ユーザーのログオンと共にそのユーザーの ユーザープロファイルがロードされます。 この動作が不要な場合は、0を指定します。 STARTUPINFO.lpDesktopにNULLまたは""を指定した場合、 新しいプロセスは呼び出し側と同じウインドウステーション上のデスクトップに作成されます。 この関数は、ログオンしたユーザーのセキュリティコンテキストで実行ファイルにアクセスします。 また、この関数はログオンしたユーザーが対話ウインドウステーション上のデスクトップにアクセスできるよう、 適切な処理を行います。

次に、CreateProcessWithTokenWの定義を示します。 他の2つの関数はWindows 2000から使用できますが、 この関数はWindows Vistaから使用できます。

BOOL WINAPI CreateProcessWithTokenW(
  HANDLE hToken,
  DWORD dwLogonFlags,
  LPCWSTR lpApplicationName,
  LPWSTR lpCommandLine,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCWSTR lpCurrentDirectory,
  LPSTARTUPINFOW lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInfo
);

hTokenは、プロセスに割り当てたいトークンのハンドルを指定します。 dwLogonFlagsは、CreateProcessWithLogonWのdwLogonFlagsと同じ意味を持ちます。 STARTUPINFO.lpDesktopにNULLまたは""を指定した場合、 新しいプロセスは呼び出し側と同じウインドウステーション上のデスクトップに作成されます。 この関数は、トークンで表されるユーザーのセキュリティコンテキストで実行ファイルにアクセスします。 この関数を呼び出すには、呼び出し側のトークンにSE_IMPERSONATE_NAMEが割り当てられている必要があります。

アプリケーションがユーザー名とパスワードを持っており、 そのユーザーをログオンさせて新しいプロセスを作成したい場合は、 必ずCreateProcessWithLogonWを呼び出すようにします。 これは言い換えれば、LogonUserを呼び出してトークンを取得し、 それをCreateProcessAsUserやCreateProcessWithTokenWに指定すべきではないということですが、 その理由は一体何なのでしょうか。 たとえば、CreateProcessAsUserやCreateProcessWithTokenWは特権を必要としますし、 特にWindows 2000ではLogonUserの呼び出しにSE_TCB_NAME特権が必要になるため、 関数を呼び出しにくいという欠点があります。 これに対して、CreateProcessWithLogonWは特権を必要としませんから、 制限されたユーザーでも呼び出すことができます。 ただし、こうした特権の事情は、CreateProcessWithTokenWを勧める小さな要素に過ぎません。 この関数を呼び出すべき最大の理由は、ウインドウステーションとデスクトップへのアクセスを可能にするためです。

まず、デスクトップについてですが、これは1つの画面(ウインドウの集合)を持ったオブジェクトのことです。 デスクトップは複数存在することが可能であり、その中の現在アクティブであるデスクトップは、 実際にPCのディスプレイから見ることができます。 一方、ウインドウステーションは複数のデスクトップを維持するオブジェクトであり、 こちらも複数存在することが可能になっています。 ウインドウステーションには対話と非対話という概念があり、 対話ウインドウステーション上のデスクトップはアクティブになることが許可されています。 システムに存在するデフォルトの対話ウインドウステーションはwinsta0という名前を持ち、 デフォルトでアクティブになるデスクトップはdefaultという名前を持っています。 私達が普段から操作しているエクスプローラやメモ帳などは、 実際にPCのディスプレイから見ることができますから、 これらのプロセスはwinsta0\default(winsta0のdefaultデスクトップ)に存在しているということになります。

新しく作成されるプロセスがUIなどを表示してユーザーと対話する場合、 指定されたウインドウステーションとデスクトップにアクセスが許可されている必要があります。 具体的には、トークンに含まれるログオンSIDを持ったACEが、ウインドウステーションとデスクトップのDACLに追加されている必要があるのですが、 LogonUserを呼び出してもこのような処理は行われません。 実を言うと、CreateProcessWithLogonWもこうした追加処理を行っていないのですが、 この関数は呼び出し側スレッドのトークンに含まれるログオンSIDを、 ユーザーがログオンして作成されるトークンに追加するため問題はありません。 つまり、呼び出し側がウインドウステーションとデスクトップにアクセスできるのならば、 新しいプロセスもアクセスすることが保障されるわけです。 ただし、サービスのような非対話のウインドウステーションでこの関数を呼び出すと、 新しいプロセスは非対話のウインドウステーションでアクセスが許可されるだけであり、 対話ウインドウステーションでは見えない可能性があります。 よって、この場合ではCreateProcessWithLogonWの代わりに、 LogonUserとCreateProcessAsUserかLogonUserとCreateProcessWithTokenWを呼び出します。 これらの関数は特権を必要としますが、サービスとして動作している場合は特に問題ないでしょう。 ACEの追加処理は、プロセスを作成する前に行っておきます。

ACEの追加処理を行わずに、CreateProcessAsUserやCreateProcessWithTokenWを呼び出すと具体的にどうなるのでしょうか。 たとえば、CreateProcessWithTokenWのリファレンス上には、 ウインドウステーションとデスクトップを明示的に指定しない場合、 指定されたユーザーがアクセスできるような処理が行われるという記述があります。 しかし、実際にこれらを明示的に指定せず、LogonUserで取得したトークンを指定したところ、 次のようなダイアログが表示されてしまいます。

このエラーコードだけでは失敗の原因を特定できませんが、 ダイアログの形状に注目してください。 このダイアログはWindows Vistaのテーマが有効である状態で表示されたものですが、 テーマが正しく反映されていないことが分かります。 こうした現象は、デスクトップがトークンのログオンSIDにアクセスを許可していない場合に 発生するものであり、上記の症状もこの例に該当すると考えられます。 恐らくはこのアクセスの失敗により、user32.dllの初期化が失敗したのではないかと思われます。 ちなみに、CreateProcessAsUserでは上記のようなダイアログが表示されることはありませんでしたが、 やはりアクセスが許可されていないことにはプロセスの起動は失敗します。

次に、CreateProcessWithLogonWを呼び出す例を示します。 各WCHAR型変数に適切な文字列を指定してください。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR               szUserName[] = L"";
	WCHAR               szPassword[] = L"";
	WCHAR               szApplicationName[] = L"";
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;

	ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
	startupInfo.cb = sizeof(STARTUPINFO);
	
	if (!CreateProcessWithLogonW(szUserName, NULL, szPassword, 0, szApplicationName, NULL, 0, NULL, NULL, &startupInfo, &processInformation)) {
		if (GetLastError() == ERROR_ACCESS_DENIED)
			MessageBox(NULL, TEXT("アクセスが拒否されました。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("プロセスの作成に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
	
	return 0;
}

STARTUPINFO構造体は、cbメンバを除いて0に初期化して問題ありません。 特に、lpDesktopメンバに明示的に"winsta0\\default"を指定すると関数が失敗するので、注意してください。 CreateProcessWithLogonWの第4引数に0を指定しているため、 ログオンしたユーザーのユーザープロファイルはロードされません。 関数が失敗した場合ではアクセスの拒否についてだけ特別に処理していますが、 これは比較的発生しやすいのではないかと思われます。 szApplicationNameに指定したファイルパスは、ログオンしたユーザーのセキュリティコンテキストでアクセスされるため、 呼び出し側のセキュリティコンテキストで参照可能なパスを指定しても、 ログオンしたユーザーではアクセスが拒否される可能性があります。 よって、ログオンしたユーザーがアクセス可能なファイルパスを指定する必要があります。 ちなみに、CreateProcessAsUserは、呼び出し側のセキュリティコンテキストでファイルを実行します。

CreateProcessWithLogonWなどのlpEnvironment引数にNULLを指定した場合、 新しく作成されるプロセスは呼び出し側プロセスの環境変数を継承することになります。 環境変数とは、%APPDATA%や%USERNAME%といった文字列であり、 環境変数を展開する関数に指定することで、ユーザーに応じた値を取得することができます。 当然ながら、このユーザーというのは、 新しく作成されたプロセスのユーザーでなければならないため、 呼び出し側プロセスの環境変数を継承するのは適切とはいえません。 よって、これからログオンするユーザーの環境変数を指定するべきといえます。

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

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR               szUserName[] = L"";
	WCHAR               szPassword[] = L"";
	WCHAR               szApplicationName[] = L"";
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;
	HANDLE              hToken;
	LPVOID              lpEnvironment;
	WCHAR               szUserProfile[256];
	DWORD               dwSize;
	PROFILEINFOW        profileInfo;

	if (!LogonUser(szUserName, NULL, szPassword, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &hToken)) {
		MessageBox(NULL, TEXT("ユーザーのログオンに失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	ZeroMemory(&profileInfo, sizeof(PROFILEINFO));
	profileInfo.dwSize     = sizeof(PROFILEINFO);
	profileInfo.lpUserName = szUserName;

	if (!LoadUserProfileW(hToken, &profileInfo)) {
		if (GetLastError() == ERROR_PRIVILEGE_NOT_HELD)
			MessageBox(NULL, TEXT("必要な特権が割り当てられていません。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("ユーザープロファイルのロードに失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}
	
	if (!CreateEnvironmentBlock(&lpEnvironment, hToken, TRUE)) {
		MessageBox(NULL, TEXT("環境ブロックの作成に失敗しました。"), NULL, MB_ICONWARNING);
		UnloadUserProfile(hToken, profileInfo.hProfile);
		CloseHandle(hToken);
		return 0;
	}
	
	dwSize = sizeof(szUserProfile);
	if (!GetUserProfileDirectoryW(hToken, szUserProfile, &dwSize)) {
		MessageBox(NULL, TEXT("プロファイルディレクトリの取得に失敗しました。"), NULL, MB_ICONWARNING);
		UnloadUserProfile(hToken, profileInfo.hProfile);
		DestroyEnvironmentBlock(lpEnvironment);
		CloseHandle(hToken);
		return 0;
	}

	ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
	startupInfo.cb = sizeof(STARTUPINFO);
	
	if (!CreateProcessWithLogonW(szUserName, NULL, szPassword, 0, szApplicationName, NULL, CREATE_UNICODE_ENVIRONMENT, lpEnvironment, szUserProfile, &startupInfo, &processInformation)) {
		if (GetLastError() == ERROR_ACCESS_DENIED)
			MessageBox(NULL, TEXT("アクセスが拒否されました。"), NULL, MB_ICONWARNING);
		else
			MessageBox(NULL, TEXT("プロセスの作成に失敗しました。"), NULL, MB_ICONWARNING);
		DestroyEnvironmentBlock(lpEnvironment);
		UnloadUserProfile(hToken, profileInfo.hProfile);
		CloseHandle(hToken);
		
		return 0;
	}
	
	DestroyEnvironmentBlock(lpEnvironment);
	CloseHandle(hToken);
	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
	
	return 0;
}

LoadUserProfileを呼び出している理由については後述します。 CreateEnvironmentBlockは、第2引数に指定されたトークンから環境ブロックを作成し、 そのアドレスを第1引数に返します。 環境ブロックとは、一連の環境変数を含んだデータのことです。 CreateProcessWithLogonWでユーザーをログオンさせるのも関わらず、 事前にLogonUserを呼び出すのは少し妙に思えるかもしれませんが、 これは仕方ありません。 GetUserProfileDirectoryは環境変数とは関係のない関数ですが、 CreateProcessWithLogonWの呼び出しをよりよくするために呼び出しています。 この関数は第1引数で表されるユーザーのプロファイルディレクトリのパスを取得する関数で、 上記コードではこれをプロセスのカレントディレクトリとして指定しています。 NULLを指定した場合は、system32フォルダがカレントディレクトリとなるため、 これは適切ではないという判断です。 CreateEnvironmentBlockで作成した環境ブロックを指定する場合は、 CREATE_UNICODE_ENVIRONMENTを指定します。

CreateEnvironmentBlockは指定したユーザーの環境ブロックを作成しますが、 予めそのユーザーのユーザープロファイルがロードされていなければ、 ユーザーの環境ブロックは正しく設定されません。 CreateProcessWithLogonWにはLOGON_WITH_PROFILEという定数を指定することができますが、 この時点でユーザープロファイルをロードしても、 指定した環境ブロックに変化が生じるようなことはないため、 LoadUserProfiledで明示的にロードする必要があるのです。 ちなみに、CreateEnvironmentBlockの第3引数ですが、 これはユーザープロファイルがロードされていない場合に意味を持つのではないかと思われます。 TRUEの場合は現在のプロセスの環境変数を継承し、 FALSEの場合はデフォルトの環境変数が設定されます。 当然ながら、ユーザープロファイルをロードしている場合は、 第2引数が示すユーザーの環境変数が設定されます。

CreateProcessAsUserとCreateProcessWithTokenW

これまでの話により、CreateProcessAsUserとCreateProcessWithTokenWの第1引数に、 LogonUserで取得したトークンを指定すべきではないことが分かりました。 しかし、そうなるとこれらの関数にはどのようなトークンを指定すればよいのでしょうか。 たとえば、サーバーがサービスとして実装されており、 クライアントと何らかの通信を行っていたとします。 そしてある段階で、サーバーがクライアントと同じアカウントのプロセスを作成したくなったような場合、 CreateProcessでは自分と同じSYSYEMプロセスを作成することになりますから、 CreateProcessAsUserかCreateProcessWithTokenWを呼び出すことになります。

HANDLE              hTokenImpersonation, hTokenPrimary;
STARTUPINFO         startupInfo;
PROCESS_INFORMATION processInformation;

ImpersonateNamedPipeClient(hPipe);
OpenThreadToken(GetCurrentThread(), TOKEN_DUPLICATE, FALSE, &hTokenImpersonation);
RevertToSelf();

DuplicateTokenEx(hTokenImpersonation, TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_ASSIGN_PRIMARY, NULL, SecurityIdentification, TokenPrimary, &hTokenPrimary);
CloseHandle(hTokenImpersonation);

ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
startupInfo.cb        = sizeof(STARTUPINFO);
startupInfo.lpDesktop = TEXT("winsta0\\default");
CreateProcessAsUser(hTokenPrimary, szApplicationName, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation);

CloseHandle(processInformation.hThread);
CloseHandle(processInformation.hProcess);
CloseHandle(hTokenPrimary);

サーバーがクライアントと名前付きパイプで通信している場合、 ImpersonateNamedPipeClientを呼び出すことでクライアントのアカウントに偽装できます。 ただし、このためには最低一回、クライアントからのデータを受信している必要があります。 偽装している状態でOpenThreadTokenを呼び出すと、スレッドに割り当てられた偽装トークンを取得できます。 これを取得したら、クライアントのアカウントで動作し続ける必要はないため、 RevertToSelfで元のアカウントに戻っても問題ありません。 偽装トークンは、CreateProcessAsUserに直接指定することはできないため、 DuplicateTokenExでプライマリトークンに変換する作業が必要です。 このとき、第5引数にはTokenPrimaryを指定します。 第2引数は、トークンに割り当てるアクセス権であり、 CreateProcessAsUserで必要となる値を指定します。 CreateProcessAsUserの呼び出す場合は、ACEの追加処理が必要になる場合がありますが、 通信しているクライアントが現在対話ログオンしている場合は不要です。

CreateProcessAsUserやCreateProcessWithTokenWは、管理者として動作するプロセスが、 制限されたプロセスを作成したい場合にも使用できます。 たとえば、自作のインストーラーが管理者として動作しており、 インストール終了後にアプリケーションを起動したいとします。 この場合、CreateProcessを呼び出すとそのアプリケーションが管理者として動作することになるため、 CreateProcessAsUserやCreateProcessWithTokenWを呼び出すべきといえます。

#include <windows.h>

HANDLE GetNormalUserToken(HANDLE hToken);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE              hToken;
	HANDLE              hTokenNormal;
	WCHAR               szApplicationName[] = L"";
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	hTokenNormal = GetNormalUserToken(hToken);
	if (hTokenNormal == NULL) {
		MessageBox(NULL, TEXT("制限されたトークンの取得に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}

	ZeroMemory(&startupInfo, sizeof(STARTUPINFO));
	startupInfo.cb = sizeof(STARTUPINFO);

	if (!CreateProcessWithTokenW(hTokenNormal, 0, szApplicationName, NULL, 0, 0, NULL, &startupInfo, &processInformation)) {
		CloseHandle(hTokenNormal);
		CloseHandle(hToken);
		return 0;
	}

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
	CloseHandle(hTokenNormal);
	CloseHandle(hToken);
	
	return 0;
}

このコードでは、CreateProcessAsUserではなくCreateProcessWithTokenWを呼び出していますが、 もちろんCreateProcessAsUserの呼び出しでも問題はありません。 この2つの違いは、必要とする特権と実行ファイルへアクセスする際のセキュリティコンテキストぐらいですから、 それを気にしない場合はどちらを呼び出しても問題ないと思われます。 GetNormalUserTokenは、通常のユーザー(制限されたユーザー)のトークンの取得する自作関数ですが、 こうしたトークンの取得方法にはいくつか種類があります。 しかし、今回のように制限されたプロセスの作成という目的で使用する場合は、 下記の方法を選択するしかないと思われます。

HANDLE GetNormalUserToken(HANDLE hTokenFull)
{
	HWND    hwndShell;
	DWORD   dwProcessId;
	HANDLE  hProcess;
	HANDLE  hTokenNormal, hTokenDuplicate;

	hwndShell = GetShellWindow();
	GetWindowThreadProcessId(hwndShell, &dwProcessId);

	hProcess = OpenProcess(MAXIMUM_ALLOWED, FALSE, dwProcessId);
	if (hProcess == NULL)
		return NULL;

	if (!OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, &hTokenNormal)) {
		CloseHandle(hProcess);
		return NULL;
	}

	DuplicateTokenEx(hTokenNormal, MAXIMUM_ALLOWED, NULL, SecurityDelegation, TokenPrimary, &hTokenDuplicate);

	CloseHandle(hProcess);
	CloseHandle(hTokenNormal);

	return hTokenDuplicate;
}

UACが有効な場合は、制限されたユーザーで動作しているプロセスが存在しているはずであり、 そうしたプロセスのトークンを参照してCreateProcessWithTokenWに指定すれば、 プロセスは制限されたユーザーで動作することが期待できそうです。 このためには、その対象とするプロセスがどのような環境でも存在していることが望まれますから、 シェルプロセス(explorer.exe)を対象にするのがちょうどよいところでしょう。 GetShellWindowを呼び出せばシェルのウインドウハンドルを取得でき、 GetWindowThreadProcessIdを呼び出せばウインドウハンドルを作成したプロセスのIDを取得できます。 これはシェルのプロセスIDですから、OpenProcessを呼び出せばシェルのプロセスハンドルを取得でき、 これを基にOpenProcessTokenを呼び出せば、シェルのトークンを取得できたことになります。 これをそのまま返して使用すると、CreateProcessWithTokenWでERROR_TOKEN_ALREADY_IN_USEが返るため、 DuplicateTokenExで事前に複製しておくようにします。

プロセスが制限されたユーザーで動作しているということは、 そのプロセスに制限付きトークンが割り当てられているということです。 よって、管理者プロセスで制限付きトークンを明示的に作成し、 それをCreateProcessWithTokenWに指定すれば、 プロセスは制限されたユーザーで動作するように思えます。 制限されたユーザーのトークンは、CreateRestrictedTokenにLUA_TOKENを指定して作成できますが、 この方法ではトークンの整合性レベルがHighになってしまい、 制限されたユーザーのトークンを完璧に取得できたとはいえません。 よって、SaferComputeTokenFromLevelを使用して、制限付きトークンを取得する方法が考えられます。 Safer系の関数を呼び出す場合は、事前にwinsafer.hをインクルードしておいてください。

HANDLE GetNormalUserToken(HANDLE hTokenFull)
{
	HANDLE             hTokenNormal;
	SAFER_LEVEL_HANDLE hLevel;

	SaferCreateLevel(SAFER_SCOPEID_USER, SAFER_LEVELID_NORMALUSER, 0, &hLevel, NULL);
	SaferComputeTokenFromLevel(hLevel, hTokenFull, &hTokenNormal, 0, NULL);
	SaferCloseLevel(hLevel);

	return hTokenNormal;
}

SaferCreateLevelにSAFER_LEVELID_NORMALUSERを指定すれば、 SaferComputeTokenFromLevelで基本ユーザーのトークンを取得できます。 この基本ユーザーのトークンも制限付きトークンであり、整合性レベルもMediumで問題ないのですが、 何故かこれをCreateProcessWithTokenW(CreateProcessAsUserでも同一)に指定すると、 作成されたプロセスのトークンの整合性レベルはMediumにならず、Highになってしまうようです。 こうしたことから、制限付きトークンを明示的に作成する方法は、今回の場合では不向きであるといえます。

最後に紹介する方法は、リンクトークンの仕組みに注目したものです。 UACが有効な環境では、管理者のトークンと制限されたトークンが互いにリンクしており、 管理者のトークンから制限されたトークンを、制限されたトークンから管理者のトークンを取得できるようになっています。

HANDLE GetNormalUserToken(HANDLE hTokenFull)
{
	HANDLE               hTokenNormal;
	TOKEN_LINKED_TOKEN   linkedToken;
	TOKEN_ELEVATION_TYPE tokenElevationType;
	DWORD                dwLength;
	
	GetTokenInformation(hTokenFull, TokenElevationType, &tokenElevationType, sizeof(TOKEN_ELEVATION_TYPE), &dwLength);
	if (tokenElevationType == TokenElevationTypeFull) {
		GetTokenInformation(hTokenFull, TokenLinkedToken, &linkedToken, sizeof(TOKEN_LINKED_TOKEN), &dwLength);
		hTokenNormal = linkedToken.LinkedToken;
	}
	else
		hTokenNormal = NULL;

	return hTokenNormal;
}

GetTokenInformationにTokenElevationTypeを指定して、その結果がTokenElevationTypeFullである場合は、 トークンが管理者のトークンであるということになります。 この場合、GetTokenInformationにTokenLinkedTokenを指定して取得できるリンクトークンは、 制限されたトークンを識別していますから、これをCreateProcessWithTokenWに指定すればよいように思えます。 しかし、これは何故かERROR_BAD_IMPERSONATION_LEVELが返ることになり、プロセスの作成は成功しません。

UACが有効な状態でも、シェルは通常ユーザーとして実行しているからそのトークンは応用できるという考え方は、 ある1つのひらめきを与えてくれます。 それは、管理者プロセスがプロセスの起動を行うのではなく、シェルに対してプロセスの起動を依頼するという方法です。 つまり、シェルが通常ユーザーとして実行しているのであれば、 そのシェル自体にプロセスの起動を任せれば、プロセスは通常ユーザーとして動作するだろうという狙いです。 事実、Windows SDKにはExecInExplorerというサンプルがあり、 このサンプルを昇格して実行させても、新プロセスは通常ユーザーとして動作することを確認できます。 この方法を使用した場合は、親プロセスがシェルになることや、PROCESS_INFORMATION構造体を取得できない点がありますが、 そうした点が気にならない場合は使用するのも面白いといえるでしょう。 ExecInExplorerは、Windows 7のSDKから付属していますが、Vistaでも問題なく動作します。



戻る