オブジェクトへのアクセスチェックは、2つの仕組みで成り立っている。 1つはこれまで見てきた随意アクセスチェックであり、DACL内のACEがアカウントにアクセスを許可するという方式である。 そしてもう1つは、整合性レベルの章でも取り上げた必須整合性チェックであり、呼び出し元の整合性レベルがオブジェクトと同等以上か調べる方式である。
プロセスがオブジェクトを開こうとすると、カーネルのSeAccessCheck関数でWindows標準の随意アクセス制御リスト(DACL)のチェックの前に、 必須整合性チェックが実施されます。その理由は、DACLチェックよりも必須整合性チェックのほうが高速であり、完全な随意アクセスチェックを実施する必要性をすばやく排除できるからです。
(「インサイドWindows 第7版 上」p.737より引用)
このような仕組みである事から、オブジェクトへのアクセスが失敗する際は、2つの視点からその理由を探る必要がある。 つまり、整合性レベルが原因で失敗しているのか、DACLベースで失敗しているのかという事である。 逆に言えば、オブジェクトへのアクセスに成功したという事は、整合性レベルベースでも、DACLベースでもチェックをパスしたという事である。
アクセストークンに含まれる既定の必須ポリシー(この章の「7.4.2 セキュリティ識別子(SID)」の「トークン」の項で説明したTOKEN_MANDATORY_POLICY_NO_WRITE_UPとTOKEN_MANDATORY_POLICY_NEW_PROCESS_MIN)の指定により、 プロセスの整合性ポリシーがオブジェクトの整合性レベル以上であり、DACLでもプロセスが希望するアクセスが許可されている場合、 プロセスはオブジェクトを書き込みアクセスで開くことができます。
(「インサイドWindows 第7版 上」p.737-738より引用)
この引用が示している通り、DACLの検証の前には必須整合性チェックが入るわけだが、 必須整合性チェックの有無は必須ポリシーに依存するという。 もし、必須ポリシーにTOKEN_MANDATORY_POLICY_OFFを設定するとどうなるのだろうか。
TOKEN_MANDATORY_POLICY policy;
policy.Policy = TOKEN_MANDATORY_POLICY_OFF;
SetTokenInformation(hToken, TokenMandatoryPolicy, &policy, sizeof(TOKEN_MANDATORY_POLICY));
if (GetLastError() == ERROR_PRIVILEGE_NOT_HELD) {
printf("SE_TCB_NANE特権が有効でないため、必須整合性チェックを無効にできない。");
}
プログラムが必須整合性チェックを自在に無効化できるなら、整合性レベルの存在意義も事実上なくなるため、 当然ながらこの呼び出しは失敗する。 SE_TCB_NAME特権を有効にしていれば成功するが、この特権は既定でAdministratorsに割り当てられていない。 特権が理由で関数が失敗しているため、戻り値はERROR_PRIVILEGE_NOT_HELDになる。
GetEffectiveRightsFromAclとAccessCheck
随意アクセスチェックの本論に入る前に、随意アクセスチェックのアルゴリズムというのは、2種類用意されていることを確認しておきたい。
必須整合性チェックが完了し、呼び出し元の整合性レベルに基づいて必須ポリシーがアクセスを許可していると見なされると、 次の2つのアルゴリズムの1つを使用してオブジェクトに対する随意アクセスチェックが実施され、アクセスチェックの結果が決まります。
(「インサイドWindows 第7版 上」p.739より引用)
随意アクセスチェックでは、使用する関数などで、アクセスチェックに使用されるアルゴリズムが異なっている。 その関数は以下である。
■そのオブジェクトに対して許可される最大のアクセスを決定し、その形式がAuthZ APIまたは古いGetEffectiveRightsFromAcl関数を使用してユーザーモードにエクスポートされます。 これは、プログラムがMAXIMUM_ALLOWEDの希望するアクセスを指定するときに使用されます。
(「インサイドWindows 第7版 上」p.739-740より引用)
■特定の希望するアクセスが許可されているかどうかを決定します。これは、Windows APIのAccessCheck関数またはAccessCheckByType関数によって行われます。
(「インサイドWindows 第7版 上」p.740より引用)
2種類用意されているアルゴリズムをAとBと命名するならば、 プログラムがGetEffectiveRightsFromAclを呼び出す場合や、OpenMutexなどのハンドル取得関数にMAXIMUM_ALLOWEDを指定した場合はアルゴリズムAが使用される。 一方、ハンドル取得関数にMAXIMUM_ALLOWEDを指定しない場合や、AccessCheck関数などを呼び出す場合はアルゴリズムBが使用される。 具体的にAとBのアルゴリズムにどのような差異があるかは取り上げないが、結果が異なる例を取り上げたい。 注目すべきは関数の引数である。
// 現在スレッドがどのアカウントで実行されているかを取得する
GetUserName(szAccountName, &dwSize);
// アカウント名からTRUSTEE構造体を構築
BuildTrusteeWithName(&trustee, szAccountName);
// アカウント名をベースに、アクセスチェックの結果を受け取る
if (GetEffectiveRightsFromAcl(pDacl, &trustee, &accessMask) != ERROR_SUCCESS) {
GetEffectiveRightsFromAclは第2引数のアカウントが第1引数のDACLに対して許可されているアクセス権を返す。 TRUSTEE構造体をユーザー名で初期化した場合、GetEffectiveRightsFromAclはユーザーとユーザーが属するグループをアクセスチェックに使用するが、 グループの有効/無効は調べられないことに注意したい。 これを示唆する記述が以下である。
上記の説明は、アルゴリズムのカーネルモード方式にのみ適用されます。GetEffectiveRightsFromAcl関数により実装されたWindowsバージョンは、ステップ2を実施しない点と、アクセストークンではなく、 単一のユーザーまたはグループSIDを考慮する点が異なります。
(「インサイドWindows 第7版 上」p.740より引用)
グループの有効/無効という情報はトークンには格納されているが、GetEffectiveRightsFromAclにはトークンを指定する引数がないので、有効/無効は確認できない。 つまり、現在のプロセスが本来ならばアクセスできないオブジェクトにも、GetEffectiveRightsFromAclはアクセス権を返してしまうことがある。 たとえば、UACが有効な際はAdministratorsが無効にされているので、%ProgramFiles%に書き込みアクセスはできないが、GetEffectiveRightsFromAclはそれが可能と見なしてしまう。 トークンを指定しないということは特権も参照できないということだから、後述するステップ2のSeTakeOwnershipPrivilegeもチェックできない。
if (!AccessCheck(pSecurityDescriptor, hTokenImpersonatation, dwDesiredAccess, &genericMapping, &privilegeSet, &dwSize, &dwGrantedAccess, &bAccessStatus)) {
こちらはAccessCheck関数のケースだが、第2引数にトークンを指定しているので、有効/無効の確認は可能になる。
随意アクセスチェックとは、呼び出し側プロセスのトークンユーザーとトークングループが、対象オブジェクトのDACL内のACEで許可されているかをチェックする方式である。 ただし、このACE検証に至るまでにいくつかのステップ(例外)があるため、それを見ていこう。
- そのオブジェクトがDACLを持たない場合(NULL DACL)、そのオブジェクトは保護されず、セキュリティシステムはすべてのアクセスを許可します。
(「インサイドWindows 第7版 上」p.741より引用)
SetNamedSecurityInfoのDACL引数にNULLを指定した場合、そのオブジェクトはNULL DACLの状態になり、 どのようなアカウントでもオブジェクトにアクセス可能となる。 ただし、既に述べてきたように、あくまで必須整合性チェックをパスしている場合での話である。
SetNamedSecurityInfoでDACLをNULLにできるのであれば、 システムファイルに対してその呼び出しを行えば、 実質的にシステムファイルのDACLを無意味なものにできそうだが、当然ながらそのような設定は失敗する。 なぜなら、DACLの書き換えには、呼び出し側にWRITE_DACアクセス権が許可されていなければならず、 システムファイルは標準ユーザーにそれを許可しないからである。
bResult1 = CheckSidAndAccessMask(szDirectoryPath, pTokenUser->User.Sid, READ_CONTROL);
bResult2 = CheckSidAndAccessMask(szDirectoryPath, pTokenUser->User.Sid, WRITE_DAC);
int nExitCode = -1;
if (bResult1 && bResult2) {
bResult1 = CheckSidAndAccessMask(L"C:\\Program Files", pUsersSid, READ_CONTROL);
// 標準ユーザーは以下の呼び出しに失敗する
bResult2 = CheckSidAndAccessMask(L"C:\\Program Files", pUsersSid, WRITE_DAC);
CheckSidAndAccessMaskという自作関数は、第3引数のアクセス権が許可されているかを調べる。 READ_CONTROLはセキュリティ記述子を読み取る権利であり、デスクトップ以下のディレクトリに対しては成立する。 デスクトップ以下のファイルはDACL書き換えできるから、WRITE_DACも成立する。 しかし、対象ファイルが%ProgramFiles%の場合は、WRITE_DACは成立しなくなる。 つまり、標準ユーザーでは%ProgramFiles%以下のファイルにNULL DACLは設定できない。
随意アクセスチェックのステップの話に戻ろう。NULL DACLかのチェックの後は次のステップが実行される。
- 呼び出し元が「ファイルとその他のオブジェクトの所有権の取得」特権(SeTakeOwnershipPrivilege)を持つ場合、 セキュリティシステムはDACLを調べる前に、所有権の書き込み(WRITE_OWNER)アクセスを付与します
(「インサイドWindows 第7版 上」p.741より引用)
通常、オブジェクトをオープンする際は、PROCESS_TERMINATEなどのオブジェクト専用アクセス権を指定し、 それがACEのアクセスマスクに含まれるか照合される。 しかし、WRITE_OWNERを指定した場合は、SeTakeOwnershipPrivilege特権が有効であれば、その時点でアクセス成功と見なされる。
- 呼び出し元がそのオブジェクトの所有者である場合、システムはOWNER_RIGHTSのセキュリティ識別子(SID)を探し、 そのSIDを次にステップのためのSIDとして使用します。所有者でない場合、読み取り制御(READ_CONTROL)およびDACLの書き込み(WRITE_DAC)アクセスが付与されます。
(「インサイドWindows 第7版 上」p.741より引用)
この3番目は、極めて重要なステップである。 実はプロセスのトークンユーザーがアクセス先のオブジェクトの所有者ならば、たとえDACLベースでREAD_CONTROLとWRITE_DACが許可されていなくても、許可されたものとしてみなされる。 オブジェクトの所有者かどうかは、以下のようにして確認できる。
// OWNER_SECURITY_INFORMATIONを指定して、所有者SIDを取得できる
if (GetNamedSecurityInfo(lpszFileName, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION, &pSidOwner, ..., &pSecurityDescriptor) != ERROR_SUCCESS)
return FALSE;
PTOKEN_USER pTokenUser = GetTokenUser();
// SIDが一致するか調べる
bResult = EqualSid(pTokenUser->User.Sid, pSidOwner);
GetNamedSecurityInfoにOWNER_SECURITY_INFORMATIONを指定すれば、オブジェクトの所有者のSIDを取得できる。 このSIDとプロセスのトークンユーザーが一致するならば、プロセスはオブジェクトの所有者であるといえる。
オブジェクトの所有者にREAD_CONTROLとWRITE_DACが暗黙的に許可される事で生じる効果は以下である。
実際、オブジェクトの所有者には、オブジェクトに対するDACLの書き込み(WRITE_DAC)アクセスが付与されますが、 これはユーザーが自分が所有するオブジェクトのアクセスを決して阻まれることがないことを意味しています。 いくつかの理由のため、オブジェクトが空のDACL(アクセスなし)を持つ場合、 所有者は依然としてDACLの書き込み(WRITE_DAC)アクセスでそのオブジェクトを開くことができ、希望するアクセスのアクセス許可を用いて新しいDACLを適用できます。
(「インサイドWindows 第7版 上」p.743より引用)
空のDACLを設定してしまった場合、当然ながらWRITE_DACを含むACEなど存在しないから、2度とDACLを変更できないように思えるが、 オブジェクトの所有者ならばそれが可能ということになる。
ところで、所有者ならばWRITE_DACが付与されるということは、システムファイルの所有者を書き換え、結果としてDACLも書き換えることができそうだが、当然そうはならない。 標準ユーザーは、ACEレベルでWRITE_OWNERが許可されないからである。 ただし、ステップ2で示したようにSeTakeOwnershipPrivilege特権がある場合は例外となる。 この特権については、後の章で取り上げる。
現在ログオンしているユーザーに、書き込みを許可しないファイルを用意したいと仮定しよう。 しかし、そのログオンしているユーザーがファイルの所有者であれば、 どのようなセキュリティを設定しても書き換えられるわけだから、実質的にセキュリティ設定は意味をなくしてしまう。 何か対策はないだろうか。
オブジェクトの所有者は、必ず付与される読み取り制御(READ_CONTROL)および随意アクセス制御リスト(DACL)の書き込み(WRITE_DAC)によって、 オブジェクトのセキュリティを上書きできるため、この動作を制御する専用の方法がWindowsによって公開されています。 それが、OWNER_RIGHTS SIDです。
(「インサイドWindows 第7版 上」p.741より引用)
オブジェクトのACEがアカウントしてOWNER_RIGHTS SIDを持つ場合、そのACEのアクセスマスクの値が、オブジェクトの所有者ができることそのものになる。 つまり、アクセスマスクとしてWRITE_DACを取り除いておけば、オブジェクトの所有者であっても、本当にDACLの書き換えができなくなる。
// FILE_ALL_ACCESSは、ファイルに対してすべてのアクセスを許可するが、WRITE_DACなどは取り除いておく
BuildExplicitAccessWithName(&explicitAccess[0], (LPWSTR)L"OWNER RIGHTS", FILE_ALL_ACCESS & ~WRITE_DAC & ~WRITE_OWNER & ~READ_CONTROL, GRANT_ACCESS, 0);
BuildExplicitAccessWithName(&explicitAccess[1], (LPWSTR)L"SYSTEM", FILE_ALL_ACCESS, GRANT_ACCESS, 0);
SetEntriesInAcl(sizeof(explicitAccess) / sizeof(explicitAccess[0]), explicitAccess, NULL, &pDacl);
// PROTECTED_DACL_SECURITY_INFORMATIONを指定することで、コンテナーのACEを継承しない
SetNamedSecurityInfo((LPWSTR)lpszFileName, SE_FILE_OBJECT, PROTECTED_DACL_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, NULL, NULL, pDacl, NULL);
DACLの構築は、InitializeAclからAddAccessAllowedAceExで追加していくパターン以外に、BuildExplicitAccessWithNameとSetEntriesInAclを使う方法もある。 後者はアカウントをSIDではなく文字列で指定できるのが便利である(CreateWellKnownSidは、OWNER RIGHTS SIDを作れない)。 BuildExplicitAccessWithNameでは、FILE_ALL_ACCESSからWRITE_DACを除去しているため、 これにより所有者はDACLを書き換えられなくなる。 ただし、この対策は標準ユーザーからの書き換えを防ぐものであり、 昇格したプロセスからは書き換え可能であることに注意したい。 理由は、昇格したプロセスがSeTakeOwnershipPrivilege特権を有効にできるためである。