アプリケーションのプライベート オブジェクトをセキュリティ保護する手法

Kenny Kerr
Software Consultant

March 2004

適用対象 :
Microsoft® Windows® セキュリティ認証
C++ 言語

要約 : Windows オペレーティングシステムの豊富なセキュリティ機能をアプリケーションに取り入れる方法について、興味をお持ちですか。この記事では、Windows セキュリティ承認の基礎と、ユーザー独自のセキュリティ記述子の作成方法について解説します。

サンプル コード SecuringPrivateObjects.exe をダウンロードしてご参照ください。

目次

はじめに
トークンによるユーザーの識別
セキュリティ記述子の基礎
メモリ管理
プライベート セキュリティ記述子
アクセス許可
アクセス制御リスト
アクセス制御エディタ
まとめ

はじめに

Windows オペレーティングシステムの豊富なセキュリティ機能をアプリケーションに取り入れる方法について知りたいと思われたことはあるでしょうか。Windows ファイル システムのセキュリティ エディタを見て、これと同レベルのセキュリティをビジネス オブジェクトでも実現したいとお考えですか。この記事では、まず Windows セキュリティ認証の基礎について解説します。その後、セキュリティ記述子の操作、固有のセキュリティ記述子を作成する方法、およびそれらの編集方法についてふれていきます。この記事を読み終えると、ご自身で開発されるアプリケーションにこれらの手法を適用できるようになります。

この記事の目的の 1 つは、セキュリティに関連するプログラミングを実践的で取り組みやすいものにすることです。そのため、特定の関数について常に詳しく説明することはしません。むしろ、読者の方々のセキュリティ コードを堅牢で管理しやすいものにするヘルパー関数やヘルパー クラスを多数紹介していくことにします。ヘルパー関数やサンプル コードでは、さまざまなセキュリティ関数の使用方法を示すだけでなく、例外やその他のエラー条件が発生した際に安全かつ信頼できる方法でこれらの関数を使用する方法についても示します。

トークンによるユーザーの識別

この記事では、アクセス制御管理 (アクセス認証とも呼ばれます) のすべてについて説明します。アクセス制御の説明に入る前に、セキュリティ保護されたリソースにアクセスしようとするユーザーを識別する方法が必要になります。この識別を実現するのがトークンです。トークンは、コンピュータのログオン セッションを表します。ログオン セッションは、ユーザーが対話的に、またはリモート接続でコンピュータにアクセスした際に作成されます。トークンは、ログオン セッションの問い合わせや操作の際に使用するハンドルです。トークンを取得すれば、そのログオン セッションを使用しているユーザーを識別し、セキュリティ保護されたリソースに対するアクセスをそのユーザーに許可すべきかどうかを決定できます。

すべてのアプリケーションは特定のログオン セッションのコンテキストで動作するため、アプリケーションを実行しているユーザーを示す何らかのトークンが常に存在します。少々厄介なのは、同時に 1 つ以上の異なるトークン、つまりセキュリティ コンテキストが存在し得るという点です。各トークンは異なるユーザーを表します。少なくとも、特定のプロセスには 1 つのトークンが接続されています。このトークンを取得するには、OpenProcessToken 関数を使用します。

CHandle token;
Helpers::CheckError(::OpenProcessToken(::GetCurrentProcess(),
TOKEN_QUERY,
&token.m_h));

****CHandle は、Active Template Library (ATL) によって提供されるラッパー クラスです。このクラスを使用すると、トークンがスコープから出たときに CloseHandle 関数が呼び出され、 ハンドルがクローズされます。****CheckError は、筆者が作成した Helpers 名前空間内のヘルパー関数です。 CheckError は、発生したエラーを記述する HRESULT をスローします。Windows C プログラミングではさまざまな方法でエラーを表現できますが、この記事では HRESULT を使用する方法に統一することにします。Helpers 名前空間は、この記事のダウンロード ファイルに含まれています。****GetCurrentProcess 関数は、現在のプロセスを表す擬似ハンドルを返します。これは本物のハンドルではないため、返された HANDLE を CloseHandle 関数を呼び出して解放する必要はありません。

もう 1 つのトークンとして、スレッド トークンがあります。スレッド トークンを取得するには、OpenThreadToken 関数を使用します。

CHandle token;
Helpers::CheckError(::OpenThreadToken(::GetCurrentThread(),
TOKEN_QUERY,
TRUE, // open as self
&token.m_h));

GetCurrentProcess と同様、GetCurrentThread も擬似ハンドルを返すので、CloseHandle を呼び出してハンドルを解放する必要はありません。****OpenThreadToken は OpenProcessToken とは異なり、現在のスレッドに関連付けられているトークンが存在しないと ERROR_NO_TOKEN を返して異常終了することがあります。

場合によっては、さらに別のセキュリティ コンテキストを表す 3 番目のトークンが存在することもあります。たとえば ASP.NET では、クライアントの偽装をオフにすることができますが、その場合は HttpContext クラスを使用することで明示的にクライアントの識別情報を取得できます。

トークンを取得したので、次にトークンを使用してできる処理について考えてみます。最も簡単なのは、ログオン セッションに関する情報をトークンに問い合わせる処理です。それには、GetTokenInformation 関数を使用します。**** GetTokenInformation はさまざまな情報のクラスを問い合わせるのに使用できるため、使い方がやや面倒です。そこで、簡単に使えるようにラッパー関数テンプレートを作成してみました。次の例では、トークンにユーザー情報を問い合わせています。

CLocalMemoryT<PTOKEN_USER> tokenUser(Helpers::GetTokenInformation<TOKEN_USER>(token,
TokenUser));
CComBSTR string = Helpers::ConvertSidToStringSid(tokenUser->User.Sid);

ConvertSidToStringSid ヘルパー関数は、バイナリ形式のセキュリティ識別子 (SID) を、人間が読み取れる文字列に変換します。SID は、コンピュータが判読可能な形式でユーザー アカウントを表現するのに使用します。これらのラッパー関数のしくみに興味がある方は、この記事のソース コードをダウンロードしてご参照ください。****CLocalMemoryT クラス テンプレートについては、「メモリ管理」で説明します。

セキュリティ記述子の基礎

ユーザーの識別方法を理解できたら、次はさまざまなユーザーに与える各種のアクセス許可を管理する方法が必要になります。そこでセキュリティ記述子の出番です。セキュリティ記述子には、さまざまな情報が含まれています。中でも興味深いのは、所有者セキュリティ識別子 (SID) と2 つのアクセス制御リスト (ACL) です。所有者 SID は、オブジェクトを所有するユーザーを識別します。2 つの ACL とは、随意 ACL とシステム ACL を指します。システム ACL はアクセス制御とは何の関係もないので、この記事では随意 ACL (DACL) についてのみ説明します。

セキュリティ記述子は、SECURITY_DESCRIPTOR 構造体によって表されます。この構造体については正規のドキュメントに記載がないので、直接操作するのは避けるべきです。マイクロソフトは、ファイルやレジストリ キーなどの組み込みオブジェクトのセキュリティ記述子に対する問い合わせや操作を実行する、便利な関数を数多く提供しています。次の例は、筆者のコンピュータ上の Program Files ディレクトリの所有者 SID と DACL を取得する方法を示しています。

CLocalMemory securityDescriptor;
PSID sid = 0;
PACL dacl = 0;

Helpers::CheckError(::GetNamedSecurityInfo(_T("C:\\Program Files"),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
&sid,
0, // group
&dacl,
0, // sacl
&securityDescriptor.m_ptr));

GetNamedSecurityInfo は用途の広い関数です。****この関数を使用すると、組み込みのセキュリティ保護されたオブジェクトの大半に問い合わせを出すことができます。最初のパラメータは、問い合わせ先オブジェクトの名前またはパスです。第 2 パラメータは、オブジェクトの型を示します。この例では、ファイル システム オブジェクトに対して問い合わせを出しています。これを、たとえば SE_REGISTRY_KEY に変更すれば、レジストリ オブジェクトに問い合わせを出すことができます。 次に、SECURITY_INFORMATION 列挙を使って、取得する情報を指定します。その後の 4 つのパラメータは、セキュリティ記述子の主な 4 つの部分へのポインタです。取得する必要のない部分には "0" を渡します。最後のパラメータは、セキュリティ記述子自体へのポインタです。 これは、実際には LocalFree 関数を使って解放する必要のあるセキュリティ記述子のコピーです。

この他に、組み込みオブジェクトのセキュリティ記述子の更新に使用する SetNamedSecurityInfo という関数もあります。 機能はほぼ同じなので、ここではこの詳細については言及しません。****

メモリ管理

次に進む前に、メモリ管理について説明しておきます。メモリ管理、およびリソース管理は一般に、セキュリティ保護された堅牢なコードを記述するために極めて重要となる要素です。メモリ管理コードは、できれば書かないのが最良です。まず、自分が扱うさまざまな関数で使われているメモリ管理手法を理解すること、次に、それをいくつかのクラスでラップしたうえで適切なタイミングで正しくクリーンアップが行われるようにすることが重要です。これを怠ると、メモリをはじめとするリソースのリークが発生し、さらにはバグを発生させた結果、セキュリティ保護されたリソースへのアクセスを侵入者に許してしまうことになります。

前出のサンプル コードで CLocalMemoryT クラス テンプレートを紹介しましたが、そこではその詳しい説明を行いませんでした。 セキュリティ記述子に関連する大半のセキュリティ関数は、ローカル メモリを使用しています。ローカル メモリという概念は、メモリ管理が極めて複雑だった 16 ビット Windows の時代に考え出されたものです。****LocalAlloc や LocalFree といったローカル メモリ関数は、16 ビット アプリケーションや、セマンティクスの一部として 16 ビット アプリケーションに依存している古い API 関数との下位互換性を保つために現在でも残されています。

そこで、ローカル メモリを簡単に扱えるように、ローカル メモリ ポインタをラップする簡単なクラスを書いてみました。CLocalMemoryT は、賢いポインタ クラスと考えることができます。というのは、メンバ選択演算子 (operator ->) をオーバーロードしているからです。これが実現できるのは、CLocalMemoryT がクラス テンプレートであるため、ポインタの指すメモリの型、つまり構造体をテンプレート パラメータで指定できるからです。CLocalMemoryT を使用して新規のローカル メモリ ブロックを作成することもできますが、通常は、関数によって返される既存のメモリ ブロックに接続するという使い方をします。デストラクタは、LocalFree を呼び出してメモリを解放します。セキュリティ関数によって使われるデータ構造体の一部は不透明です。そこで、このような構造体を使いやすくするために、CLocalMemory ヘッダ ファイルの最後に次のような typedef 文を定義しました。

typedef CLocalMemoryT<PVOID> CLocalMemory;

これは、たとえば SECURITY_DESCRIPTOR オブジェクト用のメモリ管理に便利です。****

プライベート セキュリティ記述子

GetNamedSecurityInfo と SetNamedSecurityInfo は、組み込みオブジェクトに対して使用するには非常に便利です。しかし、ご自身で開発されたアプリケーションのビジネス ロジックなど、プライベート オブジェクトで使用する場合はどうでしょうか。これらのセキュリティ関数は、プライベート オブジェクトでも使用できるように拡張が可能でしょうか。残念ながら、答えはノーです。ファイル システムやレジストリといった各リソースは、それぞれセキュリティ記述子を保存するための独自の方法を定義します。そのため、これらのセキュリティ関数からは、ユーザーが作成したプライベート オブジェクトのセキュリティ情報についての問い合わせや設定は不可能です。しかし、幸いなことに解決策があります。

まず、プライベート セキュリティ記述子を作成する必要があります。セキュリティ記述子を新たに一から作成するには、CreatePrivateObjectSecurityEx 関数を使用します。 これは用途の広い関数ですが、主に新規のセキュリティ記述子の作成と、既存のセキュリティ記述子の継承の更新という 2 つの使われ方をします。この関数のプロトタイプは次のとおりです。

BOOL CreatePrivateObjectSecurityEx(PSECURITY_DESCRIPTOR parentDescriptor,
PSECURITY_DESCRIPTOR defaultDescriptor,
PSECURITY_DESCRIPTOR* newDescriptor,
GUID* type,
BOOL isContainer,
ULONG autoInheritFlags,
HANDLE token,
PGENERIC_MAPPING genericMapping);

parentDescriptor は、ACL を継承する親オブジェクトの指定に使用します。親オブジェクトが存在しない場合は、0 を渡します。defaultDescriptor の主な使用目的は、parentDescriptor の変更後に既存のセキュリティ記述子を継承可能なアクセス制御エントリ (ACE) で更新することです。 ただし、通常は newDescriptor パラメータによって返される新規のセキュリティ記述子を作成し、元のセキュリティ記述子を解放するという方法をとります。 オブジェクトの ACL の明示的なエントリを引き継ぐには、既存のセキュリティ記述子を defaultDescriptor パラメータとして渡します。

Active Directory オブジェクトのセキュリティを扱う際には、type を使用します。セキュリティ記述子が表すオブジェクトが他のセキュリティ保護オブジェクトのコンテナかどうかを示すためには、isContainer を使用します。****継承可能な各 ACE が新規に作成されたセキュリティ記述子に継承される方法を制御するには、autoInheritFlags を使用します。****SEF_DACL_AUTO_INHERIT を渡すと、継承可能な ACE がすべて継承されます。親セキュリティ記述子から継承する ACE を変更する場合は、SEF_AVOID_PRIVILEGE_CHECK および SEF_AVOID_OWNER_CHECK の各フラグも設定して、ユーザーに対するアクセス制限を回避する必要があります。ただし、継承だけを更新する場合は、これらのフラグは不要です。 ****ACL の継承の管理についての詳細は、Keith Brown 著『Programming Windows Security』 (英語) を参照してください。

オブジェクトを作成しているユーザーを識別するには、token を使って新規のセキュリティ記述子のデフォルト値 (所有者 SID など) を取得します。 ユーザー トークンが明示的に渡されると、偽装を行う必要がなくなり、サーバー アプリケーションにとっては好都合です。

最後の genericMapping は、明示的なオブジェクト固有のアクセス許可を 4 つの汎用アクセス許可 (読み取り、書き込み、実行、すべて) にマッピングする際に必要な情報を提供するために使用します。**** アクセス許可については、次に説明します。

セキュリティ記述子が不要になったら、DestroyPrivateObjectSecurity を呼び出してセキュリティ記述子が占有しているメモリ領域を解放します。

これで、自分のセキュリティ記述子を作成できるようになりました。次に、それらのセキュリティ記述子に対して問い合わせや変更を行う方法について説明します。GetNamedSecurityInfo および SetNamedSecurityInfo はプライベート オブジェクトには使用できませんが、これらと同等の機能をプライベート オブジェクトに対して実行する関数が用意されています。プライベート セキュリティ記述子を変更するには、SetPrivateObjectSecurityEx 関数を使用します。**** この関数のプロトタイプは次のとおりです。

BOOL SetPrivateObjectSecurityEx(SECURITY_INFORMATION securityInformation,
PSECURITY_DESCRIPTOR modificationDescriptor,
PSECURITY_DESCRIPTOR* securityDescriptor,
ULONG autoInheritFlags,
PGENERIC_MAPPING genericMapping,
HANDLE token);

securityDescriptor パラメータは、入力に際して有効なセキュリティ記述子をポイントしている必要があります。しかしドキュメントでは、本来 [in, out] であるべき属性が、[out] と誤って記述されています。 **** SetPrivateObjectSecurityEx は、必要に応じて LocalReAlloc を呼び出し、新規の情報を設定するためのメモリ領域を確保します。**** セキュリティ記述子を指すポインタへのポインタが必要になる理由は、ここにあります。SetPrivateObjectSecurityEx を呼び出した後で、securityDescriptor がポイントするメモリ位置が変わる可能性があるからです。

ご覧のとおり、SetPrivateObjectSecurityEx は SetNamedSecurityInfo と違ってセキュリティ記述子の各部分を設定する個々のパラメータがありません。その代わり、値のコピー元となる既存のセキュリティ記述子を指定する必要があります。幸いなことに、セキュリティ記述子オブジェクトをスタック上に構築して、プライベート セキュリティ記述子にコピーする情報をその中に格納する処理は、非常に簡単です。以下に例を示します。

CWellKnownSid adminSid = CWellKnownSid::Administrators();

SECURITY_DESCRIPTOR templateDescriptor = { 0 };

Helpers::CheckError(::InitializeSecurityDescriptor(&templateDescriptor,
SECURITY_DESCRIPTOR_REVISION));

Helpers::CheckError(::SetSecurityDescriptorOwner(&templateDescriptor,
&adminSid,
false));

Helpers::CheckError(::SetPrivateObjectSecurityEx(OWNER_SECURITY_INFORMATION,
&templateDescriptor,
&securityDescriptor,
SEF_AVOID_PRIVILEGE_CHECK,
&genericMapping,
                                                 0));

templateDescriptor は、スタックベースのセキュリティ記述子です。初期値としてゼロを設定している点に注意してください。****InitializeSecurityDescriptor はリビジョン レベルを設定します。この設定を行わないと、セキュリティ記述子は空のままになります。SetSecurityDescriptorOwner は、所有者 SID を、よく知られたローカルの Administrators グループに設定します。これには、CWellKnownSid クラス (この記事からダウンロード可能) を使用します 。****最後に SetPrivateObjectSecurityEx を呼び出します。所有者情報を、テンプレート記述子から securityDescriptor に格納されているプライベート セキュリティ記述子にコピーします。

これらの関数を使用してプライベート セキュリティ記述子の各部分を直接設定できないかと思われた方がいらっしゃるかもしれません。セキュリティ記述子には、2 つの異なるメモリ フォーマットがあります。絶対セキュリティ記述子では、セキュリティ情報へのポインタが格納されます。このメモリ領域は、セキュリティ記述子構造体を格納するためのメモリ領域とは別に割り当てられます。一方、自己相対セキュリティ記述子では、連続するメモリ ブロックにすべての情報が格納されます。このセキュリティ記述子には、ポインタの代わりに、メモリへのオフセットが格納されます。

このように、セキュリティ記述子をメモリ内で表現する別の方法があることを理解すると、上記の疑問も解けるはずです。プライベート セキュリティ記述子は常に自己相対的です。プライベート セキュリティ記述子を扱う関数が複雑になるのは、そのためです。スタック上に簡単に作成できるのは、絶対セキュリティ記述子だけです。絶対セキュリティ記述子であれば、SetSecurityDescriptorOwner などの関数は、スタックベースのセキュリティ記述子内にポインタ値を設定するだけで済むからです。

幸いなことに、プライベート セキュリティ記述子への問い合わせは簡単です。GetSecurityDescriptorOwner や GetSecurityDescriptorDacl など標準の関数を使用して、セキュリティ記述子内の各部分を取得できます。 ****GetPrivateObjectSecurity という関数もありますが、セキュリティ記述子の問い合わせにはあまり役立ちません。 むしろ、アクセス制御エディタを操作する際に便利です。アクセス制御エディタについては後述します。

アクセス許可

さて、アクセス許可 (アクセス権ともいいます) について説明する準備がようやく整いました。アクセス許可の話は、メモリ管理と同程度に興味深い内容です。単に興味深いということにとどまらず、プライベート オブジェクトを扱うには、アクセス許可を設計する方法を理解しておくことが必要不可欠です。アクセス許可は、セキュリティ保護されたリソースへのアクセスを提供する関数を呼び出すごとに使われます。たとえば、よく知られた CreateFile 関数は、アクセス ビットマスクをパラメータとします。ビットマスクの各ビットが、特定のアクセス許可を表します。

ファイル システムが FILE_APPEND_DATA や FILE_TRAVERSE といった固有のアクセス許可を定義しているように、プライベート オブジェクトにも固有のアクセス許可を定義する必要があります。**** 32 ビットのアクセスマスクのうち、16 ビットを固有のアクセス許可に使用できます。作成中のオブジェクトに固有のアクセス許可を定義したら、それらを 4 つの汎用アクセス許可に分類する必要があります。その 4 つとは、GENERIC_READ、GENERIC_WRITE、GENERIC_EXECUTE、および GENERIC_ALL です。**** このようにすることで、プログラマは単純に読み取りアクセスを要求するだけで、読み取りアクセスに論理的にマッピングされたアクセス許可が適用されます。しかし、セキュリティ関数は、ユーザー固有のアクセス許可を汎用のアクセス許可にマッピングできません。このため、GENERIC_MAPPING 構造体を設定して、多くのセキュリティ関数に渡す必要があります。****

アクセス許可の基本を理解したところで、特定のウィジェット リソースに対する次のアクセス許可を定義します。

namespace Permissions
{
const DWORD AddWidget       = 0x0001;
const DWORD ListWidgets     = 0x0002;
const DWORD RenameWidget    = 0x0004;
const DWORD ReadWidgetData  = 0x0008;
const DWORD WriteWidgetData = 0x0010;

const DWORD GenericRead     = STANDARD_RIGHTS_READ |
ListWidgets |
ReadWidgetData;

const DWORD GenericWrite    = STANDARD_RIGHTS_WRITE |
AddWidget |
RenameWidget | 
WriteWidgetData;

const DWORD GenericExecute  = STANDARD_RIGHTS_EXECUTE;

const DWORD GenericAll      = STANDARD_RIGHTS_REQUIRED |
GenericRead |
GenericWrite |
GenericExecute;
}

このように定義することで、グローバル構造体 GENERIC_MAPPING を設定し、この構造体を必要とするすべての関数に対して、この構造体へのポインタを渡すことができます。これで、プログラマが GENERIC_WRITE などの汎用アクセス許可を指定すると、それが上記のウィジェットに対する Permissions::GenericRead アクセス許可にマッピングされます。 慎重なプログラマは、ユーザーがすべてのアクセス許可を持っていると仮定しません。この例では、ユーザーは Permissions::RenameWidget などの特定のアクセス許可を使用することもできます。**** もちろん、DELETE などの標準のアクセス許可に固有の意味を与えれば、それを使用することも可能です。

アクセス制御リスト

DACL は、セキュリティ記述子の中心です。アクセス制御リスト内の各アクセス制御エントリ (ACE) は、特定のユーザーまたはグループがリソースに対して持つアクセス許可を定義します。ACE には、許可型 ACE と拒否型 ACE があります。許可型 ACE は、特定のアクセス許可がユーザーまたはグループに与えられることを示します。拒否型 ACE は、特定のアクセス許可が与えられないことを示します。ACL 内に存在しないユーザーは、直接、またはグループのメンバとしても、すべてのアクセスを拒否されます。

ACL は、ACL 構造体とそれに続く順序付き ACE リストで構成される、連続したメモリ ブロックとして格納されます。次の例は、簡単な ACL を構築する方法を示しています。

CLocalMemoryT<PACL> acl(100);

Helpers::CheckError(::InitializeAcl(acl.m_ptr,
                                    100,
ACL_REVISION));

DWORD inheritFlags = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;

CWellKnownSid everyoneSid = CWellKnownSid::Everyone();

Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr,
ACL_REVISION,
inheritFlags,
GENERIC_READ,
&everyoneSid));

CLocalMemoryT<PSID> userSid(Helpers::GetUserSid(token));

Helpers::CheckError(::AddAccessAllowedAceEx(acl.m_ptr,
ACL_REVISION,
inheritFlags,
GENERIC_ALL,
userSid.m_ptr));

ACL は連続するメモリ領域に格納されるため、ACL ヘッダーとそのすべてのエントリを格納できるだけの大きさを持つメモリ ブロックを割り当てる必要があります。ただし、すべてのエントリを割り当てることができさえすれば、割り当てるメモリの大きさは重要ではありません。通常は、ACL を作成した後でリソース マネージャを呼び出すため、いずれにしても ACL のコピーは作成されます。この例では、AddAccessAllowedAceEx 関数を使用して、すべてのユーザーに読み取りアクセス許可を、トークンで表されるユーザーにすべてのアクセス許可を与えています。****

上のように簡単な ACL ではなく、より現実的で複雑な ACL を直接作成して編集するのは非常に難しく、エラーが発生しやすくなります。その理由として第一に挙げられるのは、リスト内の ACE の順番です。というのも、アクセス チェックの実行時に、リストは上から下に走査されるからです。リストの最後に拒否型 ACE を追加しても、リストの先頭の ACE でそのユーザーによるアクセスが許可されていると、アクセスを拒否されるユーザーでもアクセス チェックは通過することになります。アクセス チェックでは、このような短絡的な手法をとることで効率の向上を図っています。ACE が正しい順序で並んでいれば、こうした問題は起こりません。さらに問題を複雑にしているのは、Windows 2000 で導入された新しい ACL 継承モデルです。このモデルは旧モデルに比べて非常に強力ですが、実装には特に注意が必要です。とにかく ACL を直接操作するのを避けること、これが最善の策です。次に、その方法を示します。

アクセス制御エディタ

Windows シェルによって使用されるアクセス制御エディタは、プログラマがアプリケーションを開発するときにも使用できます。図 1 をご覧ください。アクセス制御エディタは極めて強力かつ柔軟なエディタで、セキュリティ記述子のあらゆる部分を操作する機能を備えています。アクセス制御エディタは、2 つの一見単純な関数によって公開されています。CreateSecurityPage は、おなじみのセキュリティ プロパティ ページを作成します。このページはご自身のプロパティ シートに追加できます。EditSecurity は、セキュリティ ページをプロパティ シートに反映させ、それを表示するためのヘルパー関数です。****一見、非常に簡単に見えますが、実は落とし穴があります。どちらの関数も、ISecurityInformation と呼ばれるかなり変則的な COM インターフェイスを実装する必要があるのです。 この COM インターフェイスが変則的なのは、標準の COM メモリ管理ルールに従っていないためです。その代わりに、大半のセキュリティ関数が使用している、グローバル メモリや LocalAlloc などに依存したメモリ管理機能や手法の使用を選択しています。このため、C# やその他のマネージ言語で実装するのは極めて困難です。ISecurityInformation は、見せかけだけであまり出来のよくない C のコールバック メカニズムと考えてください。

図1. アクセス制御エディタのセキュリティ プロパティ ページ

GetObjectInformation メソッドを使用すると、アクセス制御エディタのカスタマイズ方法を指定できます。 たとえば、標準のプロパティ ページよりも詳細なセキュリティ記述子エディタを提供する、[Advanced](詳細) ボタンの表示 / 非表示を切り替えることができます (図 2)。他にも、セキュリティ記述子の所有者 SID とシステム ACL の表示と変更をユーザーに許可するかどうかといったオプションを制御できます。

図 2. セキュリティの詳細設定を示すダイアログ

GetSecurity メソッドは、エディタが各種コントロールにセキュリティ記述子関連情報を設定する必要がある、さまざまなタイミングで呼び出されます。 ここで、前に少し触れた GetPrivateObjectSecurity 関数が役に立ちます。GetSecurity メソッドは、編集対象であるセキュリティ記述子の一部分のコピーを返す必要があります。これはまさに、GetPrivateObjectSecurity の機能です。

SetSecurity メソッドは、保存の必要がある変更をユーザーがエディタ内で行った後に呼び出されます。****SetSecurity は、前述の SetPrivateObjectSecurityEx 関数を使用すれば簡単に実装できます。

GetAccessRights メソッドは、編集対象であるオブジェクト型の固有および汎用のアクセス許可リストを取得する際に呼び出されます。このメソッドの実装では、SI_ACCESS 構造体の配列を作成します。 **** 各 SI_ACCESS 構造体は、固有または汎用のアクセス許可、およびそのビットマスクの各ビット、わかりやすい名前、その他のフラグで構成されます。この情報によりセキュリティ記述子のすべてのインスタンスが記述されるため、この配列は通常 static で宣言されます。

残りのメソッドの中で、唯一興味深いのは GetInheritTypes です。 このメソッドを呼び出すと、エディタを使用して ACE が継承される方法を決定できます。それには、継承フラグのさまざまな組み合わせを記述するために SI_INHERIT_TYPE 構造体の静的配列を作成する必要があります。**** 通常、継承をサポートしているオブジェクトでは、次のオプションを使用できます。

適用対象 フラグ
オブジェクトと子 CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE
子のみ CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE | INHERIT_ONLY_ACE
オブジェクトのみ 0

ISecurityInformation を実装する作業は、最初はかなり難しくなることもありますが、良い例を参考にしながら少し経験を積めばすぐに上達します。この記事のダウンロード ファイルには、有用なヘルパー関数が数多く含まれています。また、ACL 継承の作成、編集、管理を容易にする CSecurityDescriptor クラス (筆者作成) も含まれています。さらには、ISecurityInformation の実装例、およびこれらのクラスの簡単な使用例を提供する C++ プロジェクトも含まれています。

まとめ

Windows のセキュリティ モデルをプライベート オブジェクトに拡張するには、セキュリティ記述子全般と、プライベート セキュリティ記述子の管理に関連した関数を特によく理解する必要があります。Windows のアクセス制御エディタを使用できるようになれば、セキュリティ保護された高度なアプリケーションを構築できます。この記事を読み終えた開発者の方々は、ご自身でセキュリティ記述子についていろいろと実験し、この記事で説明されている手法を使用してオブジェクト中心のアクセス制御をご自身のアプリケーションで実現できるはずです。

Kenny Kerr は、分散型 Windows アプリケーションの設計と構築に長年携わってきました。セキュリティに関連したプログラミングにも意欲的に取り組んでいます。メールは kennykerr@hotmail.com までお送りください。Web サイトは次のとおりです。http://www.kennyandkarin.com/Kenny/ (英語)