EternalWindows
アクセスコントロール / アクセスチェック

既に述べてきたように、アカウントがオブジェクトへのアクセスに成功するかは、 DACLの中に要求したアクセス権を満たすACEが存在するかどうかで決定します。 次に、アクセスチェックの仕組みをステップ単位で示します。

1. 汎用アクセス権を要求した場合、標準アクセス権と特殊アクセス権にマッピングされる。

2. WRITE_OWNERアクセス権を要求した場合、 SE_TAKE_OWNERSHIP_NAME特権が有効であれば、その時点でアクセスチェックは成功となる。 ACCESS_SYSTEM_SECURITYアクセス権を要求する場合、 SE_SECURITY_NAME特権が有効でなければアクセスチェックは失敗となる。

3. 呼び出し元のトークン内のSIDが、オブジェクトの所有者SIDと一致するかを比較する。 所有者でかつ要求したアクセス権が READ_CONTROL, WRITE_DACL, WRITE_OWNERのいずれかの場合、アクセスチェックは成功となる。

4. オブジェクトのセキュリティ記述子にDACLが存在しない場合、アクセスは成功する。 DACLは存在するが、その中身が空であるならばアクセスチェックは失敗となる。

5. 呼び出し元のトークンのSIDが、ACEのSIDと一致するかどうかを比較する。 一致しなかった場合、次のACEを調べる(つまり、このステップを繰り返す)。 一致した場合、次のステップに進む。

6. そのACEがアクセス拒否ACEで、要求したアクセス権のいずれかがアクセスマスク内で満たされている場合、 アクセスチェックは失敗となる。 このとき、後続のACEが調べられることはない。 満たされていない場合は、次のACEを調べる(5に戻る)。 そのACEがアクセス許可ACEで、 要求したアクセス権の全てがアクセスマスク内で満たされている場合、 アクセスチェックは成功となる。 満たされていない場合は、次のACEを調べる(5に戻る)。

7. 全てのACEを走査した結果、呼び出し元にアクセスを許可するACEが存在しなかった場合、 アクセスチェックは失敗となる。

トークンというのは、プロセス(またはスレッド)に割り当てられているカーネルオブジェクトです。 トークンは、プロセスがどのユーザーとして実行しているかを表す情報を持っており、 この中のトークンユーザーSIDとトークングループSIDが、 ACEのSIDと照合されることになります。 トークンユーザーSIDは、実行しているユーザーを表したSIDであり、 トークングループSIDは、そのユーザーがメンバとなっているグループのSIDです。

上記したアクセスチェックは、具体的にいつ行われることになるのでしょうか。 答えは、オブジェクトのハンドルを取得するときです。 たとえば、ファイルハンドルを返すCreateFileは内部でアクセスチェックを行い、 これが成功した場合は関数に指定されたアクセス権をハンドルに割り当てます。 この仕様により、ReadFileやWriteFileでは、ハンドルに割り当てられたアクセス権から 読み取りや書き込みを許可するかの判定を行うことが可能になり、 何度もアクセスチェックが発生する事態(パフォーマンスの低下)を防ぐことができます。 ハンドルに割り当てられたアクセス権を調べるという関係上、 次のようなコードには注意する必要があります。

hFile = CreateFile(szFileName, FILE_GENERIC_READ, 0, 0, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE)
	return;
	
WriteFile(hFile, szBuf, dwSize, &dwResult, NULL);

このコードでは、CreateFileにFILE_GENERIC_READを指定しているため、 この定義に含まれるアクセス権を許可するACEが存在すれば、 指定したアクセス権が割り当てられたハンドルが返ることになります。 WriteFileは、ハンドルにFILE_GENERIC_WRITEが割り当てられている場合に書き込みを許可するため、 FILE_GENERIC_READだけが割り当てられたハンドルでは、WriteFileを呼び出すことではできません。 ここで注意しなければならないのは、実際に書き込みを許可するACEが存在したとしても、 ハンドルにFILE_GENERIC_WRITEが割り当てられていなければ失敗するという点です。 よって、書き込めることが予め分かっているファイルに対しても、 適切なアクセス権を指定する必要があります。

アクセス権には、最大許可ビットと呼ばれるMAXIMUM_ALLOWEDがあります。 このアクセス権を指定した場合、関数側はアクセスを拒否したり許可したりしません。 内部でアクセスチェックを行い、アカウントに許可されているアクセス権をハンドルに割り当てるだけです。 このとき割り当てられるアクセス権は、最大アクセス権と呼ばれます。 MAXIMUM_ALLOWEDを指定すれば、オブジェクトのハンドルはほぼ確実に取得できますが、 実際に操作を行う関数の呼び出しが成功するかどうかは、 やはりハンドルに割り当てられたアクセス権に依存します。 最大アクセス権を取得するには、GetEffectiveRightsFromAclを呼び出します。

DWORD WINAPI GetEffectiveRightsFromAcl(
  PACL pacl,
  PTRUSTEE pTrustee,
  PACCESS_MASK pAccessRights
);

paclは、ACLを指定します。 pTrusteeは、アカウントを表すTRUSTEE構造体のアドレスを指定します。 pAccessRightsは、最大アクセス権を受け取るACCESS_MASK構造体のアドレスを指定します。

アカウント名を基にTRUSTEE構造体を初期化する場合は、BuildTrusteeWithNameを呼び出します。

VOID WINAPI BuildTrusteeWithName(
  PTRUSTEE pTrustee,
  LPTSTR pName
);

pTrusteeは、TRUSTEE構造体のアドレスを指定します。 pNameは、アカウント名を指定します。

今回のプログラムは、最大アクセス権を取得することによって、 実際にファイルを作成することなく、ファイルが作成可能かどうかを調べます。 対象とするフォルダは、C:\Program Filesとします。

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR                szAccountName[256];
	DWORD                dwSize;
	PACL                 pDacl;
	PSECURITY_DESCRIPTOR pSecurityDescriptor;
	TRUSTEE              trustee;
	ACCESS_MASK          accessMask;
	
	if (GetNamedSecurityInfo(TEXT("C:\\Program Files"), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &pDacl, NULL, &pSecurityDescriptor) != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("セキュリティ記述子の取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	dwSize = sizeof(szAccountName) / sizeof(TCHAR);
	GetUserName(szAccountName, &dwSize);
	BuildTrusteeWithName(&trustee, szAccountName);
	
	if (GetEffectiveRightsFromAcl(pDacl, &trustee, &accessMask) != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("最大アクセス権の取得に失敗しました。"), NULL, MB_ICONWARNING);
		LocalFree(pSecurityDescriptor);
		return 0;
	}

	if (accessMask & FILE_GENERIC_WRITE)
		MessageBox(NULL, TEXT("ファイルを作成することができます。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("ファイルを作成することはできません。"), NULL, MB_ICONWARNING);

	LocalFree(pSecurityDescriptor);

	return 0;
}

まず、GetNamedSecurityInfoでオブジェクトのDACLを取得します。 今回のオブジェクトはフォルダであるため、第2引数にSE_FILE_OBJECTを指定し、 DACLを取得するため、第3引数にはDACL_SECURITY_INFORMATIONを指定します。 DACLを取得したら、GetEffectiveRightsFromAclの呼び出しに必要なTRUSTEE構造体を初期化します。 GetUserNameで現在のユーザー名を指定し、 これをBuildTrusteeWithNameに指定すれば、TRUSTEE構造体とszUserNameが関連付けられます。 szUserNameがTRUSTEE構造体にコピーされるわけではないので、 szUserNameは常に有効なアドレスでなければなりません。 ユーザー名を指定した場合は、それがメンバであるグループも考慮されることになります。 GetEffectiveRightsFromAclの呼び出しは、次のようになっています。

if (GetEffectiveRightsFromAcl(pDacl, &trustee, &accessMask) != ERROR_SUCCESS) {
	MessageBox(NULL, TEXT("最大アクセス権の取得に失敗しました。"), NULL, MB_ICONWARNING);
	LocalFree(pSecurityDescriptor);
	return 0;
}

if (accessMask & FILE_GENERIC_WRITE)
	MessageBox(NULL, TEXT("ファイルを作成することができます。"), TEXT("OK"), MB_OK);
else
	MessageBox(NULL, TEXT("ファイルを作成することはできません。"), NULL, MB_ICONWARNING);

GetEffectiveRightsFromAclの呼び出しに成功すれば、 第2引数に指定したアカウントに許可されている最大のアクセスが第3引数に返ります。 この最大のアクセスにFILE_GENERIC_WRITEが含まれている場合は、 FILE_GENERIC_WRITEを指定してCreateFileを呼び出すことができることを意味しています。 今回のオブジェクトはフォルダであり、 フォルダに対してFILE_GENERIC_WRITEを指定することは、 そのフォルダにファイルを作成することを意味するため、 FILE_GENERIC_WRITEが含まれている場合は、 ファイルの作成が可能であると判断することができます。 GENERIC_WRITEのような汎用アクセス権は、アクセスマスク内でビットが立たないため、 これが含まれているかどうかで判断してはいけません。

GetEffectiveRightsFromAclとAccessCheck

GetEffectiveRightsFromAclは、最大アクセス権を取得する関数であり、 特定のアクセスが成功するかどうかを確認する関数ではないことに注意してください。 これは、GetEffectiveRightsFromAclが呼び出し側プロセス(またはスレッド)のトークンを参照しないからであり、 正規のアクセスチェックの仕組みと若干の違いが生じるためです。 つまり、GetEffectiveRightsFromAclによって取得したアクセスマスクを基に判定を行っても、 その結果は必ずしも信頼できるわけではありません。 特定のアクセスが成功するかどうかを確認するには、 トークンのハンドルを要求するAccessCheckを呼び出す必要があります。

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	HANDLE               hToken;
	HANDLE               hTokenImpersonatation;
	DWORD                dwDesiredAccess = FILE_GENERIC_WRITE;
	DWORD                dwGrantedAccess;
	DWORD                dwSize;
	BOOL                 bAccessStatus;
	GENERIC_MAPPING      genericMapping;
	PRIVILEGE_SET        privilegeSet;
	PSECURITY_DESCRIPTOR pSecurityDescriptor;
	
	if (GetNamedSecurityInfo(TEXT("C:\\Program Files"), SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, NULL, NULL, NULL, NULL, &pSecurityDescriptor) != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("セキュリティ記述子の取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}
	
	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY | TOKEN_DUPLICATE, &hToken)) {
		MessageBox(NULL, TEXT("トークンのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		LocalFree(pSecurityDescriptor);
		return 0;
	}
	
	if (!DuplicateTokenEx(hToken, GENERIC_ALL, NULL, SecurityImpersonation, TokenImpersonation, &hTokenImpersonatation)) {
		MessageBox(NULL, TEXT("トークンの複製に失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hToken);
		LocalFree(pSecurityDescriptor);
		return 0;
	}	
	
	genericMapping.GenericRead    = FILE_GENERIC_READ;
	genericMapping.GenericWrite   = FILE_GENERIC_WRITE;
	genericMapping.GenericExecute = FILE_GENERIC_EXECUTE;
	genericMapping.GenericAll     = FILE_ALL_ACCESS;
	MapGenericMask(&dwDesiredAccess, &genericMapping);

	dwSize = sizeof(PRIVILEGE_SET);
	privilegeSet.PrivilegeCount = 0;
	if (!AccessCheck(pSecurityDescriptor, hTokenImpersonatation, dwDesiredAccess, &genericMapping, &privilegeSet, &dwSize, &dwGrantedAccess, &bAccessStatus)) {
		MessageBox(NULL, TEXT("アクセスチェックに失敗しました。"), NULL, MB_ICONWARNING);
		CloseHandle(hTokenImpersonatation);
		CloseHandle(hToken);
		LocalFree(pSecurityDescriptor);
		return 0;
	}

	if (bAccessStatus)
		MessageBox(NULL, TEXT("ファイルを作成することができます。"), TEXT("OK"), MB_OK);
	else
		MessageBox(NULL, TEXT("ファイルを作成することはできません。"), NULL, MB_ICONWARNING);

	CloseHandle(hTokenImpersonatation);
	CloseHandle(hToken);
	LocalFree(pSecurityDescriptor);

	return 0;
}

AccessCheckに指定するトークンは偽装トークンでなければならないため、 OpenProcessTokenで取得したトークンをDuplicateTokenExで偽装トークンに複製しています。 このとき、第5引数にTokenImpersonationという定数を指定するのが重要です。 MapGenericMaskという関数は、汎用アクセス権を特殊アクセス権と標準アクセス権にマッピングします。 たとえば、dwDesiredAccessがGENERIC_WRITEを格納しているとGENERIC_MAPPING構造体のGenericWriteメンバが参照され、 dwDesiredAccessにFILE_GENERIC_WRITEが格納されることになります。 ただし、今回ではdwDesiredAccessに汎用アクセス権を指定していないため、 MapGenericMaskの呼び出しは必須ではありません。 AccessCheckは名前の通りアクセスチェックを行う関数で、 関数が成功した場合は、第7引数に許可されたアクセス権が格納され、 第8引数にアクセスチェックの結果が格納されます。 これがTRUEである場合はアクセスが成功したことを意味するため、 FILE_GENERIC_WRITEを指定している今回では、ファイルの作成を行えることになります。

トークンを考慮しないアクセスチェックにどのような問題があるのかを考えると、 アクセスチェックについての理解がより一層深まります。 いくつかの例を挙げてみましょう。 まず、トークンを参照できないと、特権を利用することができません。 たとえば、WRITE_OWNERアクセス権の指定は、SE_TAKE_OWNERSHIP_NAMEが有効であれば無条件に成功しますが、 GetEffectiveRightsFromAclではこれは考慮されません。 また、トークンのトークングループには、 ユーザーがメンバとなっているグループのSID以外に、 INTERACTIVEやAuthenticated UsersなどのSIDが含まれますが、 GetEffectiveRightsFromAclではこうしたSIDまで考慮することはできません。 そして、恐らく最も問題なのは、呼び出し側プロセスに制限付きトークンが割り当てられている場合です。 たとえば、UACが有効な場合は制限付きトークンによってAdministratorsのSIDが無効にされますが、 GetEffectiveRightsFromAclではこれを考慮しませんから、 本来ならばアクセスに失敗するにも関わらず、アクセスを可能と判断してしまうことになります。 実際に、UACを有効にした状態で管理者としてログオンし、 先に示した2つのプログラムを実行すれば、 GetEffectiveRightsFromAclの結果が間違っていることが分かります。



戻る