EternalWindows
アクセスコントロール / プロセスの保護

これまで、ファイルやフォルダに対してセキュリティを設定してきましたが、 アクセスコントロール適応可能なオブジェクトは、この他にも数多く存在します。 たとえば、SetSecurityInfoの第1引数にプロセスのハンドルを指定して、 第2引数にSE_KERNEL_OBJECTを指定すれば、 プロセスのセキュリティ記述子のDACLを変更することができます。 プロセスに設定されているDACLがどのように機能しているかを調べるには、 次のようなコードを実行してみると分かりやすいでしょう。

hProcess = OpenProcess(PROCESS_TERMINATE, FALSE, dwProcessId);
if (hProcess == NULL) {
	MessageBox(NULL, TEXT("ハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
	return 0;
}

TerminateProcess(hProcess);

OpenProcessは、第3引数のプロセスIDで表されるプロセスのハンドルを取得する関数です。 このようなハンドルを取得する関数は内部でアクセスチェックを行い、 指定したアクセス権がACEのアクセスマスクに含まれている場合は、 そのアクセス権をハンドルに割り当てて返します。 TerminateProcessはプロセスを強制終了させる関数であり、 この関数はハンドルにPROCESS_TERMINATEが割り当てられていなければ失敗するため、 ハンドルを取得する段階でPROCESS_TERMINATEを指定しておく必要があります。 次に、プロセスのアクセス権とそれを必要とする関数の一部を示します。

アクセス権 必要とする関数
PROCESS_TERMINATE TerminateProcess
PROCESS_CREATE_THREAD CreateRemoteThread
PROCESS_VM_READ ReadProcessMemory
PROCESS_VM_WRITE WriteProcessMemory
PROCESS_SET_QUOTA SetProcessWorkingSetSize
PROCESS_SET_INFORMATION SetPriorityClass
PROCESS_QUERY_INFORMATION GetProcessTimes

このように、プロセスのハンドルを必要とする関数は数多く存在しますが、 自プロセスを保護したいという視点に立った場合、 中には呼ばれてほしくない関数も存在すると思われます。 たとえば、ReadProcessMemoryを呼び出せば、プロセスの特定のアドレスに格納されているデータを取得できますし、 TerminateProcessを呼び出せば、プロセスを強制終了することもできます。 自プロセスに対して、こうした外部からの関数呼び出しを失敗させるには、 そのような関数が必要とするアクセス権をACEのアクセスマスクから取り除けばよいでしょう。 このようにすれば外部プロセスは、OpenProcessの呼び出しでアクセス拒否のエラーが返ることになります。 自プロセスでは、OpenProcessではなくGetCurrentProcessでハンドルを取得するため、 アクセスが拒否されることはありません。

今回のプログラムは、特定のアクセスを許可しないプロセスを作成する例を示しています。

#include <windows.h>
#include <aclapi.h>

void RestartProcess(LPTSTR lpszKey, DWORD dwRemoveAccess);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	TCHAR szKey[] = TEXT("restart-key");

	if (lstrcmp(GetCommandLine(), szKey) != 0) {
		MessageBox(NULL, TEXT("再起動します。"), TEXT("OK"), MB_OK);
		RestartProcess(szKey, PROCESS_TERMINATE);
		return 0;
	}

	MessageBox(NULL, TEXT("終了します。"), TEXT("OK"), MB_OK);

	return 0;
}

void RestartProcess(LPTSTR lpszKey, DWORD dwRemoveAccess)
{
	TCHAR               szAccountName[256];
	TCHAR               szModuleName[MAX_PATH];
	PACL                pDacl;
	DWORD               dwSize;
	EXPLICIT_ACCESS     explicitAccess;
	SECURITY_ATTRIBUTES securityAttributes;
	SECURITY_DESCRIPTOR securityDescriptor;
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;
	
	dwSize = sizeof(szAccountName) / sizeof(TCHAR);
	GetUserName(szAccountName, &dwSize);
	BuildExplicitAccessWithName(&explicitAccess, szAccountName, PROCESS_ALL_ACCESS & ~dwRemoveAccess, GRANT_ACCESS, 0);
	SetEntriesInAcl(1, &explicitAccess, NULL, &pDacl);
	
	InitializeSecurityDescriptor(&securityDescriptor, SECURITY_DESCRIPTOR_REVISION);
	SetSecurityDescriptorDacl(&securityDescriptor, TRUE, pDacl, FALSE);
	securityAttributes.nLength              = sizeof(SECURITY_ATTRIBUTES);
	securityAttributes.lpSecurityDescriptor = &securityDescriptor;
	securityAttributes.bInheritHandle       = FALSE;

	GetModuleFileName(NULL, szModuleName, MAX_PATH);

	startupInfo.dwFlags = 0;
	GetStartupInfo(&startupInfo);
	CreateProcess(szModuleName, lpszKey, &securityAttributes, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation);

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
}

外部プロセスのOpenProcessを失敗させるためには、 当然ながらプロセスのセキュリティ記述子を独自に設定しておく必要があります。 ただし、エクスプローラーのようなアプリケーションが、プロセスを作成するCreateProcessに こちらが望むセキュリティ記述子を指定してくれるようなことはありませんから、 プロセス内部で明示的にCreateProcessを呼び出すことになります。

TCHAR szKey[] = TEXT("restart-key");

if (lstrcmp(GetCommandLine(), szKey) != 0) {
	MessageBox(NULL, TEXT("再起動します。"), TEXT("OK"), MB_OK);
	RestartProcess(szKey, PROCESS_VM_READ);
	return 0;
}

プロセスが明示的にCreateProcessを呼び出して再起動するという関係上、 今回起動されたプロセスが再起動によるものかを確認する必要があります。 上記コードでは、GetCommandLineで取得した文字列が特定の値と一致するかによって、 このプロセスが再起動されたものであるかを確認しています。 一致しなかった場合は、現在のプロセスに独自のセキュリティ記述子が設定されていませんから、 RestartProcessでプロセスを再起動することになります。

void RestartProcess(LPTSTR lpszKey, DWORD dwRemoveAccess)
{
	TCHAR               szAccountName[256];
	TCHAR               szModuleName[MAX_PATH];
	PACL                pDacl;
	DWORD               dwSize;
	EXPLICIT_ACCESS     explicitAccess;
	SECURITY_ATTRIBUTES securityAttributes;
	SECURITY_DESCRIPTOR securityDescriptor;
	STARTUPINFO         startupInfo;
	PROCESS_INFORMATION processInformation;
	
	dwSize = sizeof(szAccountName) / sizeof(TCHAR);
	GetUserName(szAccountName, &dwSize);
	BuildExplicitAccessWithName(&explicitAccess, szAccountName, PROCESS_ALL_ACCESS & ~dwRemoveAccess, GRANT_ACCESS, 0);
	SetEntriesInAcl(1, &explicitAccess, NULL, &pDacl);
	
	InitializeSecurityDescriptor(&securityDescriptor, SECURITY_DESCRIPTOR_REVISION);
	SetSecurityDescriptorDacl(&securityDescriptor, TRUE, pDacl, FALSE);
	securityAttributes.nLength              = sizeof(SECURITY_ATTRIBUTES);
	securityAttributes.lpSecurityDescriptor = &securityDescriptor;
	securityAttributes.bInheritHandle       = FALSE;

	GetModuleFileName(NULL, szModuleName, MAX_PATH);

	startupInfo.dwFlags = 0;
	GetStartupInfo(&startupInfo);
	CreateProcess(szModuleName, lpszKey, &securityAttributes, NULL, FALSE, 0, NULL, NULL, &startupInfo, &processInformation);

	CloseHandle(processInformation.hThread);
	CloseHandle(processInformation.hProcess);
}

まず、セキュリティ記述子に格納するためのDACLをSetEntriesInAclで作成します。 BuildExplicitAccessWithNameには現在のユーザー名を指定しているため、 DACLに格納されるACEは現在のユーザーに対してアクセスを許可します。 どのようなアクセスが許可されるかは、PROCESS_ALL_ACCESSからdwRemoveAccessを除いたものになります。 今回はdwRemoveAccessにPROCESS_TERMINATEを指定しているため、 TerminateProcessの呼び出しはできないことになります。 DACLを作成したら、InitializeSecurityDescriptorでセキュリティ記述子を初期化し、 SetSecurityDescriptorDaclでセキュリティ記述子にDACLを設定します。 そして、これをSECURITY_ATTRIBUTES構造体に指定し、 再起動を表す文字列と共にCreateProcessへ指定すれば、 コマンドライン文字列がrestart-keyであり、独自のセキュリティ記述子が設定されたプロセスが作成されます。

実際に正しくセキュリティ記述子が設定されているかを確認するために、 プログラムを実行してみましょう。 まず、再起動する旨を示すメッセージが表示されますが、 これはRestartProcessの呼び出しを明確にするのが目的であり、 実際の開発では不要です。 メッセージに応答したら、今度は終了する旨を示すメッセージが表示されますが、 このときには再起動によって正しいセキュリティ記述子が設定されます。 よって、タスクマネージャでプロセスを強制終了しようと試みると、 次のようにエラーが発生することになります。

プロセスが強制終了しない理由は、プロセスのセキュリティ記述子に設定されたDACLのACEが、 PROCESS_TERMINATEを許可していないためです。 この結果からも分かるように、RestartProcessに指定するアクセス権を変更すれば、 任意の関数の呼び出しを失敗させることができます。


戻る