アプリケーションがファイルなどにアクセスする際には、 本当にアクセスが許可されるかどうかの確認、すなわちアクセスチェックが欠かせない。 重要なファイルが誰にでも削除できたら大変なのは容易に想像がつく。 この章では、Windowsにおけるアクセスチェックが「どのようなタイミング」で発生しているかを取り上げる。
Windowsのセキュリティモデルでは、スレッドがオブジェクトを開く時点で、オブジェクトに対して実行したい操作の種類を、スレッドが前もって指定する必要があります。
(「インサイドWindows 第7版 上」p.695より引用)
まずは、スレッドについて簡単に整理しておこう。 何らかのアプリケーション(たとえばメモ帳)を起動すると、プロセスが作成される。 これはタスクマネージャーにメモ帳という項目が増えることから実感できる。 そして、プロセスの中には最低でも1つのスレッドが存在しており、 そのスレッドがプログラムコードを実行している。 これを実感するために、以下のコードを検証する。
int main()
{
printf("3秒待機します。");
Sleep(3000);
printf("3秒待機しました。");
Sleep(3000);
return 0;
}
アプリケーションのエントリポイントであるmain関数に、プログラムのコードが記述されているが、 このコードを実行するのがスレッドである。 メッセージは二度表示されるが、2回目のメッセージは少し遅れて実行される。 これは、Sleep関数を呼び出すことで、スレッドの進行を3秒(3000ミリ秒)待機させているからである。
スレッドが何らかのオブジェクトを使用するためには、まずそのオブジェクトをオープンしなければならない。 たとえば、ファイルを読み込むならば、ファイルをオープンするということである。
DWORD dwAccessMask = FILE_READ_ACCESS;
HANDLE hFile = CreateFile(szFileName, dwAccessMask, ...);
ReadFile(hFile, buffer, ...);
printf("%s", buffer);
オープンの際には、オブジェクトに対して実行したい操作を指定しなければならなかった。 それがFILE_READ_ACCESS(読み取り要求)である。
アクセスが許可されている場合、スレッドのプロセスにハンドルが割り当てられ、スレッド(またはプロセス内の他のスレッド)はそのオブジェクトに対するさらなる操作を実行できます。
(「インサイドWindows 第7版 上」p.695-696より引用)
オープンに成功すれば、ハンドルを使用してオブジェクトに対して操作を実行できると述べている。 この操作が上記コードであればReadFile関数の呼び出しに相当する。
セキュリティ設定可能なオブジェクトは、ファイル以外にもレジストリキーやプロセスなど様々である。 ここでは、プロセスについて見ていく。
ここで一例を挙げましょう。あるスレッドが、特定のプロセスが終了(または何らかの方法で強制終了)するときを知りたいとします。 それには、Windows APIのOpenProcess関数を呼び出して対象プロセスに対するハンドルを取得し、2つの重要な引数を渡す必要があります。 その2つとは、一意のプロセスID(そのプロセスIDを知っているか、何らかの方法で取得済みであると仮定します)、 およびスレッドが返されたハンドルを使用して実行したい操作を示すアクセスマスクです。
(「インサイドWindows 第7版 上」p.697より引用)
この文章は、コードで以下のように記述できる。
// オブジェクトに対して実行したい操作の種類を定義
DWORD dwAccessMask = PROCESS_ALL_ACCESS;
// MSペイントのプロセスID
DWORD dwProcessId = GetProcessIdFromWindowClass(TEXT("MsPaintApp"));
// スレッドがオブジェクトを開く。
// 内部でアクセスチェックが発生する。
HANDLE hProcess = OpenProcess(dwAccessMask, FALSE, dwProcessId);
if (hProcess == NULL) {
// スレッドはオブジェクトへのアクセスが許可されなかった。
return 0;
}
WaitForSingleObject(hProcess, 2000);
このコードは、OpenProcessでMSペイントのハンドルを取得し、 WaitForSingleObjectでMSペイントが終了するまで待機している。 しかし、このコードはアクセスマスクの指定という点で1つのミスをしている。
怠惰な開発者なら、アクセスマスクとして単にPROCESS_ ALL_ ACCESSを渡し、そのプロセスに対して可能なすべてのアクセス権を指定するかもしれません。 その結果、次の2つのうち、いずれかが起こります。
(「インサイドWindows 第7版 上」p.697より引用)
アクセスマスクとは、「ハンドルを使用して実行したい操作」ということだったが、 そもそもプロセスに対してできる操作はどのようなものがあるのだろうか。 幸いなことに、PROCESS_ALL_ACCESSで検索すると、その近くに次のような定義が見つかった。
#define PROCESS_TERMINATE (0x0001)
#define PROCESS_CREATE_THREAD (0x0002)
#define PROCESS_SET_SESSIONID (0x0004)
#define PROCESS_VM_OPERATION (0x0008)
#define PROCESS_VM_READ (0x0010)
#define PROCESS_VM_WRITE (0x0020)
#define PROCESS_DUP_HANDLE (0x0040)
#define PROCESS_CREATE_PROCESS (0x0080)
#define PROCESS_SET_QUOTA (0x0100)
#define PROCESS_SET_INFORMATION (0x0200)
#define PROCESS_QUERY_INFORMATION (0x0400)
#define PROCESS_SUSPEND_RESUME (0x0800)
#define PROCESS_QUERY_LIMITED_INFORMATION (0x1000)
#if (NTDDI_VERSION >= NTDDI_VISTA)
#define PROCESS_ALL_ACCESS (STANDARD_RIGHTS_REQUIRED | SYNCHRONIZE | \
0xFFFF)
PROCESS_TERMINATEからPROCESS_QUERY_LIMITED_INFORMATIONがプロセスに対して行える操作である。 たとえば、アクセスマスクとしてPROCESS_TERMINATEを指定したら、 スレッドはプロセスを強制終了させるTerminateProcessという関数を呼び出すことができる。 しかし、こうしたアクセスマスクを1つずつ把握するのは面倒なので、 プロセスに対して何でも行えるPROCESS_ALL_ACCESSを指定すれば楽なように思える。 つまり、何をするかなどは事前に考えず、とにかくハンドルが欲しいわけである。 これを怠惰とする理由が2つ挙げられている。
スレッドは次に、WaitForSingleObject関数を呼び出して、そのプロセスが終了するのを待機します。しかし、おそらく少ない特権しか持たないプロセス内の別のスレッドも、 プロセスで他の操作をするために同じハンドルを使用することができます。 例えば、TerminateProcess関数を使用して、通常に終了する前にプロセスを強制終了したりできます。 なぜなら、そのハンドルがプロセスに対するすべての可能な操作を許可しているからです。
(「インサイドWindows 第7版 上」p.697より引用)
この文章を理解するために、1つの例を挙げる。 先のコードでは、プロセスのハンドルを取得した後にWaitForSingleObjectで待機していたが、 その前に以下のコードを実行したとする。
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, hProcess, 0, NULL);
CreateThreadは新規スレッドを作成する関数である。 なぜ、この関数を呼び出すかというと、WaitForSingleObjectを呼び出した時点でスレッドが何もできなくなるため、 事前に別スレッドを作成して何かの作業を行わせるためである。 上記コードでは新規スレッドに対して、先程取得したプロセスハンドルを渡しているが、これによって以下のような事が起きる可能性がある。
// 新規スレッドの処理
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
HANDLE hProcess = lpParameter;
TerminateProcess(hProcess, 0);
if (GetLastError() == ERROR_ACCESS_DENIED)
printf("アクセスが拒否された\n");
return 0;
}
PROCESS_ALL_ACCESSのようなアクセスマスクを指定することで2つの結果が生じると述べたが、 その1つが、別スレッドによって想定していない操作(上記のTerminateProcessなど)が実行されるというものである。 この問題の解決は簡単で、スレッドが行いたい動作のみをアクセスマスクで指定すればよい。 たとえば、スレッドがWaitForSingleObjectしか呼び出すことがないのであれば、 PROCESS_ALL_ACCESSではなくSYNCHRONIZEを指定すればよい。
DWORD dwAccessMask = SYNCHRONIZE;
このようにすれば、TerminateProcessの呼び出しは失敗することになる。
PROCESS_ALL_ACCESSのようなアクセスマスクを指定することのもう1つの問題は、 アクセスチェックそのものが失敗し、ハンドルを取得できない確率が上がるというものである。
呼び出し側のスレッドが、すべての可能なアクセスが付与されるのに十分な特権を持たず、結果が無効なハンドルである場合、 その呼び出しは失敗する可能性があります。つまり、プロセスへのアクセスがないことを意味します。これは残念なことです。 そのスレッドはSYNCHRONIZEアクセスマスクを要求するだけで済むからです。その場合、PROCESS_ALL_ACCESSアクセスマスクを要求するよりも、成功する機会がかなり増えます。
(「インサイドWindows 第7版 上」p.697より引用)
この問題の解決も先程と同じように、アクセスマスクとしてSYNCHRONIZEを指定すればよいだけである。 プロセスに対して要求する操作が増えれば増えるほど、さらにそれがセキュリティ絡みであればあるほど、アクセスチェックのパスは厳しくなるから、 最低限のアクセスマスクを指定するほうが成功しやすくなる。 つまり、話をまとめると以下になる。
ここでの簡単な議論は、スレッドが正確なアクセスを要求する必要があるということです。
(「インサイドWindows 第7版 上」p.697より引用)
アクセスマスクを含めば含むほど、プロセスに強固なセキュリティが設定されている場合に、 ハンドルを取得できる確率は減るから、最低限のアクセスマスクだけ指定すべきということである。
アクセスチェックが発生するのは、オブジェクトのオープン時だけではない
あるプロセスに対して、SYNCHRONIZE(WaitForSingleObject)とPROCESS_TERMINATE(TerminateProcess)のどちらも許可されていても、 OpenProcessの際にSYNCHRONIZEのみを指定したならば、TerminateProcessは呼び出せなくなる。 この事実は、アクセスチェックというものがオブジェクトのオープン時だけでなく、ハンドルを使用する際にも行われていることを示唆している。
セキュリティアクセス検証を実施する別のイベントは、プロセスが既存のハンドルを使用してオブジェクトを参照するときです。 そのような参照はプロセスがオブジェクトを操作するためにWindows APIを呼び出して、オブジェクトのハンドルを渡すとき、間接的に起こることがよくあります。 例えば、ファイルを開くスレッドは、ファイルに対して読み取りアクセス許可を要求できます。
(「インサイドWindows 第7版 上」p.697より引用)
アクセスチェックがハンドルを取得する直接的な段階だけでなく、 ハンドルを使用して何かを行う間接的な段階でも発生する事を述べている。 なぜ、最初にチェックしたのに、また行う仕組みになっているのだろうか。 ファイルという単語が出てきたので、ファイルを例に考えてみよう。 読み取りアクセスは許可するが、書き込みアクセスは許可しないファイルがあったとして、 次のようなコードを実行したとする。
// 書き込みのアクセスマスクを指定する
hFile = CreateFile(path, FILE_WRITE_ACCESS, ...);
if (hFile == INVALID_HANDLE_VALUE) {
// ファイルは書き込みを許可していないから、CreateFileの呼び出しは失敗する。
return;
}
// ファイルに書き込む関数だが、ここまで到達しない
WriteFile(hFile, ...);
このコードの要点は、CreateFileの時点で失敗するから、WriteFileは呼ばれないというものだ。 それでは、次のコードだとどうなるだろう。
// 本当は書き込みアクセスする予定だが、読み取りアクセスを指定することで、アクセスチェックをくぐり抜ける
hFile = CreateFile(path, FILE_READ_ACCESS, ...);
if (hFile == INVALID_HANDLE_VALUE) {
// ファイルは読み取りは許可するから、ここは実行されない。
return;
}
// ハンドルさえ取得したら、書き込みも自由自在に成功?
WriteFile(hFile, ...);
言うまでもないが、このWriteFileの呼び出し失敗する。 書籍Windows Internalsでは非公開APIで動作手順を説明しているが、 以下の部分だけ見ても意味はつかめるはずである。
ObReferenceObjectByHandleルーチンは、書き込み操作が失敗することを示すことがあります。その理由は、ファイルが開かれたとき、呼び出し元は書き込みアクセスを取得していないからです。
(「インサイドWindows 第7版 上」p.698より引用)
つまり、ファイルハンドルを取得した段階でハンドルにアクセス権が付与されており、 WriteFileの呼び出しはそれがチェックされているということである。
-
Kernel Objects ユーザーモードから作成可能なオブジェクト一覧。
-
Process Security and Access Rights プロセスに指定可能なアクセス権を確認できる。TerminateProcessを呼び出すには、PROCESS_TERMINATEが必要と記載されている。