鷲ノ巣

C# とか PowerShell とか。当ブログの記事は(特に公開直後は)頻繁に改定される場合があることをご了承ください。

セキュリティ記述子の中身を見てみる

前回のおさらい

前回は、ファイルのセキュリティ情報としてどんなものがあるか、ということを概観しました。
ざっとおさらいしますと、

  • ファイルのセキュリティ情報は「セキュリティ記述子(Security Descriptor)」という領域に記録されている。
  • セキュリティ記述子には、アクセス制御のための「DACL(随意アクセス制御リスト)」、監査のための「SACL(システム アクセス制御リスト)」と、ファイルの所有者の情報がある
  • DACL と SACL の中身は「ACE(アクセス制御エントリ)」というもの
  • ACE や所有者情報は、アカウント名の代わりに「SID(セキュリティ ID)」という形式で記録されている

という感じでしたね。

今回はコードを書いて、ファイルのセキュリティ記述子の中身を見ていきます。

サンプル コードは GitHub にあるので、適宜参照しながらお読みください。*1
github.com

セキュリティ記述子の取得

まず GetNamedSecurityInfo 関数で、ファイルのセキュリティ記述子を取得します。

DWORD dwResult = GetNamedSecurityInfo(
	pszFile,
	SE_FILE_OBJECT,
	DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION,
	nullptr, nullptr, nullptr, nullptr, &pSD);

第一引数がファイル名です。
この関数は、ファイル以外にも、レジストリ キーやサービスなど、多数の対象に対応しているので、第二引数で、対象がファイルであることを明示しています。
第三引数は、取得したい情報です。ここでは、DACL、オーナー、プライマリ グループを指定しています。
SACL を取得するにはもう一手間必要なので、それは後回し。
第四~第七引数は、それぞれ、オーナー、プライマリ グループ、DACL、SACL を受け取る場合に、それらを指す個別のポインターです。今回はここでは取得しないので nullptr にしています。
最後の第八引数がセキュリティ記述子へのポインタです。この関数は、関数側でメモリを確保して返すので、呼び出し側で LocalFree で開放する必要があります。

プライマリ グループ?

前回、最後にこんなことを書きました。

DACL、SACL、所有者の情報をまとめて格納しているのが、セキュリティ記述子(Security Descriptor)です。

https://tech.blog.aerie.jp/entry/2017/12/16/012207

今回いきなり出てきたプライマリ グループというのは何かと言うと…セキュリティ記述子に含まれてはいるのですが、Windows では基本的に使われないものです。*2

Windows のアクセス制御の仕組みは POSIX という Unix の仕様をベースにしています。*3
Unix のファイルのアクセス権というのは、こんなフォーマットで表しますよね。

rwxr-xr--

最初の 3 文字(rwx)が、このファイルの所有者の権限。
次の 3 文字(r-x)が、このファイルの所有グループに属するユーザーに与えられた権限。
最後の 3 文字(r--)が、その他のユーザーに与えられた権限です。

このように、Unix には、ファイルの所有グループという概念があります。
Windows でも、これとの互換性を持つために、ファイルにプライマリ グループという属性があるのではないかと思われます。
ただし、Windows では Unix のように、所有者に対する権限とか、プライマリ グループに対する権限というものはありません(アカウントと権限の対応は固定的であり、所有者が変わったからといって、追随して変わったりはしません)*4
そのため、一応記録されてはいますが、ほとんど使われることのない属性です。

制御フラグの取得

最初に GetSecurityDescriptorControl 関数で、制御フラグの取得をしています。
この内容は、後で詳しくやります。

BOOL bResult = GetSecurityDescriptorControl(pSD, &control, &revision);

所有者情報とプライマリ グループ情報の取得

セキュリティ記述子から所有者の情報を取得するには GetSecurityDescriptorOwner 関数、プライマリ グループの情報を取得するには
GetSecurityDescriptorGroup 関数を使います。

これらは、既に GetNamedSecurityInfo 関数によって確保されているメモリ内の位置を示すポインターですので、これらを取得するために改めてメモリを確保したり開放したりする必要はありません。
また、これらは GetNamedSecurityInfo 関数でセキュリティ記述子と同時に取得することもできますが、今回は別個に取得しています。

BOOL bResult = GetSecurityDescriptorOwner(pSD, &pSidOwner, &bOwnerDefauled);
BOOL bResult = GetSecurityDescriptorGroup(pSD, &pSidGroup, &bGroupDefaulted);

SID から名前を取得

次に、所有者とプライマリ グループの名前を表示しています。
前回も言ったように、セキュリティ記述子の中では、ユーザーは SID という形式で記録されています。

この SID というのは、文字列で表記すれば、こんな感じです。

S-1-5-21-2078972769-103758036-1019910339-1006

これでは誰だかわからないので、人間が分かる名前に変換します。
そのために使うのが LookupAccountSid 関数です。

bResult = LookupAccountSid(nullptr, pSid, name.get(), &cchName, domain.get(), &cchDomain, &sidType);

DACL の取得

DACL を取得するには GetSecurityDescriptorDacl 関数を使います。
所有者やプライマリ グループと同様に、GetNamedSecurityInfo で取得することも可能です。

BOOL bResult = GetSecurityDescriptorDacl(pSD, &bDaclPresent, &pAcl, &bDaclDefauted);

DACL の先頭部分は ACL 構造体です。
この AceCount が、この DACL に含まれる ACE の数を表します。

ACE の取得と判別

GetAce 関数で ACE を取得します。

bResult = GetAce(pAcl, i, &pAce);

ACE にはいくつかの種別があり、種別によって構造も異なるのですが、どの種別であっても、先頭部分には ACE_HEADER 構造体があります。
この構造体の AceType が、ACE の種別を表します。
そのため、ACE_HEADER にキャストして、種別(に対応する名前)を取得しています。

auto ace_header = static_cast<ACE_HEADER *>(pAce);
auto ace_type = ace_header->AceType;

auto ace_type_name = GetAceType(ace_type);

その後のところで、種別が ACCESS_ALLOWED_ACE_TYPE と ACCESS_DENIED_ACE_TYPE 以外は弾いています。
これは、ファイルの ACE のほとんどが、この 2 つの種別だからです。
ACCESS_ALLOWED_ACE_TYPE はアクセス許可、ACCESS_DENIED_ACE_TYPE はアクセス拒否を示す ACE 種別です。
他のタイプについては別の機会に紹介します。

if (ace_type != ACCESS_ALLOWED_ACE_TYPE &&
    ace_type != ACCESS_DENIED_ACE_TYPE)
{
	std::cout << "\tUnsupported" << std::endl;
	continue;
}

続いて、ACE のフラグを表示しています。
これも後で詳しくやります。

ShowAceFlags(ace_header->AceFlags);

次に ACE に含まれる SID をユーザー名に変換して表示しています。
先程、ACE は種別ごとに構造が異なると言いましたが、ACCESS_ALLOWED_ACEACCESS_DENIED_ACE は同じ構造なので、ACCESS_ALLOWED_ACE にキャストしています。
SID は可変長ですが、その先頭の 4 バイトが SidStart です。そのため、このポインタを PSID 型にキャストして、先程使った LookupAccountSid 関数でユーザー名を得ます。

ACCESS_ALLOWED_ACE * pAAAce = static_cast<ACCESS_ALLOWED_ACE *>(pAce);
PSID pSid = reinterpret_cast<PSID>(&pAAAce->SidStart);
auto user_name = GetUserName(pSid);
std::wcout << L"\tUser: " << user_name << std::endl;

最後に、このユーザーに対して許可(または拒否)されているアクセスを一覧表示しています。
ACCESS_ALLOWED_ACE 構造体の Mask がアクセス権を表します。
各ビットの意味は File Access Rights Constants を見てください。
1 つのビットにつき 2 つの名前があるのは、同じビットでも、ファイルの場合とディレクトリの場合で意味が異なるものがあるからです。

また、DELETE ~ SYNCHRONIZE までは Standard Access Rights に一覧があります。

アクセス権は 32 ビット整数で表されますが、そのうち下位 16 ビットが、リソースの種類(ファイル、レジストリ キー、プロセス、etc...)に個別の値です。
そのため、同じ値であっても、リソースの種類が異なれば意味が変わります。
今回のサンプルコードでは、対象をファイル(とディレクトリ)に決め打ちしているので、ファイル(とディレクトリ)の定数名を表示しています。

上位 16 ビットのうち、16 ~ 23 ビットが、リソースの種類に依存しない(どのリソースであっても同じ意味を持つ)標準アクセス権です。
24 ビット目から上は特殊な値なのですが、ファイル等、リソースの属性として保存されるものではありません。

アクセス権の構造は Access Mask Format を見てください。

これについても別途、詳しくやる予定です。

その他の API

SID の代わりに TRUSTEE 構造体、ACE の代わりに EXPLICIT_ACCESS 構造体を使う高レベルな API 系もあるのですが、こちらは扱いやすい分、本当の姿を隠蔽してしまうので、この連載では取り扱いません。

最後に

次回は ACL に ACE を追加してみます。

目次

シリーズの目次はこちら
tech.blog.aerie.jp

*1:C++ わかんないなりに頑張りました! ネーミングが C++ 風と Win32 風でちゃんぽんですが勘弁してください。

*2:ファイル作成時の DACL に CREATOR GROUP というのを含めておくと使われます。

*3:Windows NT をアメリカの政府機関で使ってもらうために、POSIX に準拠する必要があったためだとか。

*4:ただし、所有者であれば「DACL を設定する権限」を持っていなくても DACL を設定できるという特別扱いはあります。そのため DACL は「随意」なのです。