EternalWindows
セキュリティコンテキスト / 制限付きトークン

プロセスを管理者として動作させることが危険という言葉は、 多くのプロセスには管理者として動作する必要がないという事実を裏付けています。 また、そうしたプロセスには最小限の特権しか必要ないということも意味しています。 この最小限の特権というのは、強力な特権を含まないのはもちろんのこと、 Administratorsのような強力なグループも無効にしており、 一般プロセスはこのような最小限の特権しか持たないユーザーとして動作することが望まれます。 このようなユーザーは、LUA(Least-Privileged User Account)と呼ばれ、 制限付きトークンによって実現されています。 制限付きトークンとは、既存のトークンから強力なグループや特権を無効、または削除して作成したトークンであり、 既存のトークンと比べてセキュリティ操作が制限されることになります。 制限付きトークンを作成するには、CreateRestrictedTokenを呼び出します。

BOOL WINAPI CreateRestrictedToken(
  HANDLE ExistingTokenHandle,
  DWORD Flags,
  DWORD DisableSidCount,
  PSID_AND_ATTRIBUTES SidsToDisable,
  DWORD DeletePrivilegeCount,
  PLUID_AND_ATTRIBUTES PrivilegesToDelete,
  DWORD RestrictedSidCount,
  PSID_AND_ATTRIBUTES SidsToRestrict,
  PHANDLE NewTokenHandle
);

ExistingTokenHandleは、制限付きトークンを作成するための元にするトークンを指定します。 Flagsは、定義されている定数を指定します。 DisableSidCountは、SidsToDisableの要素数を指定します。 SidsToDisableは、SID_AND_ATTRIBUTES構造体の配列を指定します。 ここで指定したSIDは無効化され、アクセスチェックに使用されなくなります。 DeletePrivilegeCountは、PrivilegesToDeleteの要素数を指定します。 PrivilegesToDeleteは、LUID_AND_ATTRIBUTES構造体の配列を指定します。 ここで指定した特権は、作成された制限付きトークンに割り当てらません。 RestrictedSidCountは、SidsToRestrictの要素数を指定します。 SidsToRestrictは、SID_AND_ATTRIBUTES構造体の配列を指定します。 ここで指定したSIDの数だけがアクセスチェックの回数が増えます。 NewTokenHandleは、作成された制限付きトークンを受け取る変数のアドレスを指定します。

ExistingTokenHandleからどのような情報を制限するかを表すのが、 SidsToDisableとPrivilegesToDelete、そしてSidsToRestrictになります。 これらの引数の詳細を次に示します。

制限と引数 説明
SIDの無効化
SidsToDisable
SIDの無効化とは、そのSIDをアクセスチェックで使用しないことである。 SIDを無効にしても、そのSIDは依然としてユーザーやグループを表す。 Administratorsグループを無効にしたとしても、 GetTokenInformationはAdministratorsグループを返す。 しかし、このときSID_AND_ATTRIBUTESのAttributesメンバには SE_GROUP_USE_FOR_DENY_ONLYフラグが付け加えられている。
特権の削除
PrivilegesToDelete
特権を削除するとは文字通りの意味で、 トークンに割り当てられた特権が取り除かれることである。 特権を無効にするAdjustTokenPrivilegesを呼び出すこととは、 全く意味が異なることに注意すること。
制限付きSIDの追加
SidsToRestrict
制限付きSIDの追加はアクセスチェックを複数回行わせることと等価である。 オブジェクトへのアクセスを試みたとき、トークンユーザーとトークングループの SIDを考慮してアクセスの成否が決定されるが、 ここでアクセスの成功した場合、今度は制限付きSIDを用いて アクセスチェックが行われることになる。 ここでのアクセスも成功した場合、オブジェクトにアクセスできるようになる。

次に、制限付きSIDを追加する例を示します。 特権やSIDは無効にしないものとします。

sidAttribute.Sid        = pSid;
sidAttribute.Attributes = 0;
CreateRestrictedToken(hToken, 0, 0, NULL, 0, NULL, 1, &sidAttribute, &hTokenRestricted);

制限付きSIDは第8引数に相当するため、 SID_AND_ATTRIBUTES構造体を第8引数に指定します。 たとえば、pSidがGuestアカウントのSIDを表しているのであれば、 hTokenRestrictedを偽装したスレッドは、 Guestアカウントにアクセスを許可するオブジェクトのみアクセスできることになります。

CreateRestrictedTokenによる制限付きトークンの作成は、 無効にするSIDを指定できるなど、非常に柔軟性があります。 しかし、どのような情報を制限するのが最も適切かどうかは、 なかなか分かりにくいと思われます。 Flagsに次に示す定数を指定すれば、直観的な制限が可能になります。

定数 意味
DISABLE_MAX_PRIVILEGE SE_CHANGE_NOTIFY_NAMEを除く全ての特権を削除する。 特権が削除されるため、特権を有効にすることもできなくなる。
LUA_TOKEN Administratorsを無効にし、多くの特権を削除する。 つまり、LUAとして動作するためのトークンが作成されることになる。 Windows Vista以降で指定可能。

CreateRestrictedTokenで作成した制限付きトークンは、整合性レベルが制限されていません。 たとえば、管理者のトークンの整合性レベルはデフォルトでHighとなっていますが、 これを元にした場合は制限付きトークンの整合性レベルもHighになります。 このため、制限付きトークンを偽装したスレッド、もしくは割り当てられたプロセスは、 PostMessageなどを使用して通常通りメッセージの送信を行えることになります。 もし、整合性レベルが低くなっているのであれば、UIPIの制限によって メッセージの送信は失敗することになります。 基本的に、制限されたスレッドやプロセスがメッセージを送信する必要はありませんから、 整合性レベルを下げる処理を別途行うべきといえるかもしれません。

今回のプログラムは、制限付きトークンを作成してそれを偽装します。 これにより、オブジェクトへのアクセスチェックがどのように変化するかを確認します。

#include <windows.h>

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE hToken;
	HANDLE hTokenRestricted;
	HANDLE hFile;
	
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!CreateRestrictedToken(hToken, LUA_TOKEN, 0, NULL, 0, NULL, 0, NULL, &hTokenRestricted)) {
		MessageBox(NULL, TEXT("制限付きトークンの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		return 0;
	}
	
	ImpersonateLoggedOnUser(hTokenRestricted);

	hFile = CreateFile(TEXT("C:\\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);
		else
			MessageBox(NULL, TEXT("ファイルの作成に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hTokenRestricted);
		CloseHandle(hToken);
		return 0;
	}
	
	RevertToSelf();

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

	CloseHandle(hFile);
	CloseHandle(hTokenRestricted);
	CloseHandle(hToken);

	return 0;
}

CreateRestrictedTokenを呼び出すためには元とするトークンが必要になるため、 OpenProcessTokenでこれを取得します。 TOKEN_DUPLICATEはCreateRestrictedTokenを呼び出すために必要で、 TOKEN_QUERYは後で偽装を行うために必要です。 CreateRestrictedTokenでは第2引数にLUA_TOKENを指定しているので、 暗黙的に適切な制限が加えられることになります。 このため、他の引数を通じて明示的に制限内容を指定する必要はありません。 偽装とオブジェクトへのアクセスを確認します。

ImpersonateLoggedOnUser(hTokenRestricted);

hFile = CreateFile(TEXT("C:\\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(hTokenRestricted);
	CloseHandle(hToken);
	return 0;
}

ImpersonateLoggedOnUserにより、制限付きトークンがスレッドに割り当てられ、 セキュリティコンテキストが低くなります。 CreateFileではCドライブの直下にファイルを作成しようとしていますが、 これはAdministratorsのメンバであるであるユーザーでなければ成功しません。 現在のスレッドは、制限ユーザーとしてコードを実行しているため、 この処理には必ず失敗することになります。 なお、UACが有効な場合は、制限付きトークンを偽装する必要はありません。 この場合は、既にスレッドが制限ユーザーとしてコードを実行しているからです。

匿名トークンについて

スレッドを最も低いセキュリティコンテキストで動作させる1つの候補として、 匿名アカウントとしてコードを実行するというものがあります。 匿名アカウントは認証されていないユーザーであり、 それは匿名トークンで表されることになっています。 このトークンのトークンユーザーは、ANONYMOUS LOGONであり、 トークングループ(ログオンSID含む)や特権リストには利用可能データが存在しません。 このため、オブジェクトへのアクセスや特権が必要な関数呼び出しは原則失敗することになります。

#include <windows.h>

void GetTokenUser(HANDLE hToken, LPTSTR lpszName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR  szUserName[256];
	HANDLE hToken;
	HANDLE hThread;

	hThread = GetCurrentThread();

	if (!ImpersonateAnonymousToken(hThread)) {
		MessageBox(NULL, TEXT("匿名トークンの偽装に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!OpenThreadToken(hThread, TOKEN_ALL_ACCESS, FALSE, &hToken)) {
		MessageBox(NULL, TEXT("偽装トークンの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	GetTokenUser(hToken, szUserName);
	
	RevertToSelf();

	MessageBox(NULL, szUserName, TEXT("OK"), MB_OK);

	CloseHandle(hToken);

	return 0;
}

void GetTokenUser(HANDLE hToken, LPTSTR lpszName)
{
	DWORD                    dwLength;
	PTOKEN_USER              pTokenUser;
	PSID                     pSidAnonymous;
	SID_IDENTIFIER_AUTHORITY sidIdentifier = SECURITY_NT_AUTHORITY;
	
	GetTokenInformation(hToken, TokenUser, NULL, 0, &dwLength);
	pTokenUser = (PTOKEN_USER)LocalAlloc(LPTR, dwLength);
	GetTokenInformation(hToken, TokenUser, pTokenUser, dwLength, &dwLength);

	AllocateAndInitializeSid(&sidIdentifier, 1, SECURITY_ANONYMOUS_LOGON_RID, 0, 0, 0, 0, 0, 0, 0, &pSidAnonymous);
	
	if (EqualSid(pTokenUser->User.Sid, pSidAnonymous))
		lstrcpy(lpszName, TEXT("ANONYMOUS LOGON"));

	LocalFree(pTokenUser);
	FreeSid(pSidAnonymous);
}

ImpersonateAnonymousTokenを呼び出した場合、スレッドはシステムが維持する匿名トークンを偽装するようになります。 これにより、スレッドは匿名アカウントとしてコードを実行することになります。 この例では、匿名トークンのトークンユーザーを取得するために、 OpenThreadTokenでスレッドに割り当てられた匿名トークンを取得し、 それをGetTokenUserに指定しています。 本来ならば、LookupAccountSidを通じてSIDから関連する名前を取得したいところですが、 匿名アカウントとしてコードを実行している場合はアクセスが拒否されることになります。 よって、匿名アカウントを表すSID(S-1-5-7)と比較し、 一致すればANONYMOUS LOGONと見なしています。

オブジェクトに設定されたセキュリティ記述子のDACLがNULLである場合、 そのオブジェクトへのアクセスは制限されないことになります。 よって、こうしたオブジェクトならば匿名アカウントでもアクセスできるように思えますが、 実際にはそのように上手くいくとは限りません。 匿名トークンの整合性レベルはUntrustedとなっているため、 オブジェクトの整合性レベルもUntrustedでないことには、 書き込みアクセスが失敗することになります。 また、ファイルやレジストリキーのような階層を持ったオブジェクトについては、 アクセスの余地がありません。 これは、匿名トークンにSE_CHANGE_NOTIFY_NAMEが割り当てられていないためです。 この特権は、目的のオブジェトの上に存在するオブジェクトに対してはアクセスチェックを省略するというものですが、 この特権が割り当てられていない場合や無効の場合は、アクセスチェックが行われます。 つまり、目的のオブジェクトにセキュリティが設定されていなくても、 上に存在するオブジェクトにてアクセスが失敗することになり、 目的のオブジェクトまでたどり着くことができません。



戻る