プロセスが管理者として実行されていれば、多くのアクセスチェックが成功するのは周知の通りである。 たとえば、インストーラーの場合だと、%ProgramFiles%以下に書き込むことができる。 こうしたアクセスが成功するのは、そのフォルダが管理者に書き込みアクセスを許可しているに他ならないが、 "管理者に書き込みアクセスを許可"といった情報はどのように管理されているのだろうか。 以下に示すように、その答えはセキュリティ記述子である。
トークンは、ユーザーの資格情報を識別するものですが、これはオブジェクトのセキュリティ方程式の一部に過ぎません。 この方程式のもう1つの部分は、オブジェクトに関連付けられたセキュリティ情報であり、 そのオブジェクトに対して誰が何の操作を実行できるのかを指定します。 この情報のためのデータ構造体は、「セキュリティ記述子(Security Descriptor)」と呼ばれます。
(「インサイドWindows 第7版 上」p.727より引用)
トークンは、そのプロセス(またはスレッド)がどのアカウントとして実行されているかを示すものだった。 これに対し、セキュリティ記述子とは、ファイルなどのオブジェクトが、どのアカウントにアクセスを許可するなどを定義する。 一部例外はあるが、基本的にWindowsにおけるアクセスチェックとは、プロセスのトークンとオブジェクトのセキュリティ記述子の比較で決定すると考えてよい。
プログラミング上では、セキュリティ記述子はどのような型で扱われるのだろうか。また、セキュリティ記述子はどのようにして取得できるのだろうか。
セキュリティ記述子は、さまざまな関数を使用して取得できます。 GetSecurityInfo、GetKernelObjectSecurity、GetFileSecurity、GetNamedSecurityInfo、および他のより難解な関数を使用できます。
(「インサイドWindows 第7版 上」p.729より引用)
GetNamedSecurityInfoは扱いやすい関数なので、その定義を見てみよう。
GetNamedSecurityInfoW(
_In_ LPCWSTR pObjectName,
_In_ SE_OBJECT_TYPE ObjectType,
_In_ SECURITY_INFORMATION SecurityInfo,
_Out_opt_ PSID * ppsidOwner,
_Out_opt_ PSID * ppsidGroup,
_Out_opt_ PACL * ppDacl,
_Out_opt_ PACL * ppSacl,
_Out_ PSECURITY_DESCRIPTOR * ppSecurityDescriptor
);
最終引数のPSECURITY_DESCRIPTORがセキュリティ記述子を識別する。 セキュリティ記述子に格納されている主な情報は、所有者SID、グループSID、DACL、SACLであり、これらはppsidOwnerからppSaclに相当する。 GetNamedSecurityInfoはセキュリティ記述子そのものだけでなく、セキュリティ記述子に含まれる情報も同時に取得できるのが特徴といえる。
セキュリティ記述子に格納されている情報の中で、アクセスチェックにとりわけ関わるのがDACLである。
「アクセス制御リスト(Access Control List:ACL)」は、ヘッダーと0個以上のアクセス制御エントリ(ACE)構造体で構成されます。 ACLには、DACLとSACLの2つの種類があります。DACLでは、各ACEは1つのSIDと1つのアクセスマスク(および後述するフラグのセット)を含み、 これらは通常、SIDの保有者に許可または拒否されるアクセス権(読み取り、削除など)を指定しています。
(「インサイドWindows 第7版 上」p.729より引用)
DACLは複数のACEを内包するリストの役割を果たし、個々のACEがアクセスを制御している。 アクセス許可のACEは次のように定義されている。
typedef struct _ACCESS_ALLOWED_ACE {
ACE_HEADER Header;
ACCESS_MASK Mask;
DWORD SidStart;
} ACCESS_ALLOWED_ACE;
Maskがアクセスマスクであり、読み取りや書き込みを示すフラグがセットされる。 そして、 SidStartがアクセスを許可するアカウントを識別している。
現在ログオンしているユーザーがaliceだったとして、何らかのファイルを開けたならば、 そのファイルはaliceに読み取りアクセスを許可している。 この事実をアクセスコントロールの単語を使用して言い換えるならば、 ファイルに設定されたセキュリティ記述子のDACLには、aliceに読み取りを許可するACEを含んでいるという事になる。 コードであれば次のように記述できる。
// DACL_SECURITY_INFORMATIONを指定すれば、第6引数にDACLが返る
GetNamedSecurityInfo(lpszPath, SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, &pDacl, NULL, &pSecurityDescriptor);
// DACLの情報を取得。aclInformation.AceCountを参照すれば、何個のACEが含まれているか分かる
GetAclInformation(pDacl, &aclInformation, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation);
for (i = 0; i < aclInformation.AceCount; i++) {
// DACLからi番目のACEを取得
GetAce(pDacl, i, (LPVOID*)& pAce);
if (EqualSid((PSID)&pAce->SidStart, pSidUser))
break;
}
この例では、pSidUserで識別されるアカウントを示すACEがDACLに含まれるか調べている。 EqualSidがtrueを返した場合は、オブジェクトへのアクセスは成功するはずである。
DACL内のACEがアクセスの成否に関わる事は分かったが、もしACEが1つも存在しない場合はどうなるのだろうか。また、DACLが設定されていない場合は、どうなるのだろうか。
セキュリティ記述子にDACLが存在しない(NULL DACL)場合、誰もがそのオブジェクトに対するフルアクセスを持ちます。 空のDACL(つまり0個のACE)の場合、そのオブジェクトに対するアクセスを持つユーザーはいません。
(「インサイドWindows 第7版 上」p.730より引用)
空のDACLにアクセスが失敗する事実は、以下のように確認できる。
BYTE dacl[1024];
PACL pDacl = (PACL)dacl;
// メモリを空のACLとして初期化
InitializeAcl(pDacl, 1024, ACL_REVISION);
// ミューテックス(カーネルオブジェクト)に空のDACLを設定する
SetNamedSecurityInfo((LPWSTR)MUTEX_NAME, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, pDacl, NULL);
// ミューテックスのオープンを試みる
return TestMutexOpen();
InitializeAclを呼び出す事で、空のACLが完成する。 これをSetNamedSecurityInfoに指定すれば、オブジェクトに対して空のDACLを設定したことになる。 TestMutexOpenは、ミューテックスをオープンする自作関数だが、 空のDACLが設定されていると失敗する。
DACLが設定されてないケースは、NULL DACLと呼ばれ、誰でもアクセスが成功するという。 これを確認すべく、最も権限が弱いとされる匿名アカウントで検証を行った。
// 第6引数にNULLを指定することで、NULL DACLを設定する
SetNamedSecurityInfo((LPWSTR)MUTEX_NAME, SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, NULL, NULL, NULL, NULL);
// Windows Vista以降は、DACLの検証の前に整合性レベルの検証が入るので、オブジェクトの整合性レベルを下げておく
SetUntrustedLabel();
// 現在スレッドを匿名アカウントとして実行する
ImpersonateAnonymousToken(GetCurrentThread());
return TestMutexOpen();
SetNamedSecurityInfoの第6引数にNULLを指定すれば、DACLが存在しないNULL DACLの状態になる。 ImpersonateAnonymousTokenを呼び出せば、スレッドは匿名アカウントとして実行されることになるが、 NULL DACLの状態であるためオブジェクトへのオープンは成功する。
- Null DACLs and Empty DACLs NULL DACLと空のDACLの説明。