EternalWindows
LSA / 監査の仕組み

システム管理者にとって、これまでシステム上でどのような操作が行われてきたかを把握することは重要であるといえます。 こうした一連の操作を記録するための機構としてイベントログがあり、 各種アプリケーションは専用のログに操作内容をイベントして格納しています。 イベントログにはセキュリティという名前を持ったログが存在しますが、 これは監査によって生成されたイベントが格納されます。 監査とは、監査ポリシーによって定義された条件が満たされた時に、 システムがセキュリティイベントログにイベントを書き込む機能です。 システム管理者は、自分にとって有用である監査ポリシーを設定することで、 特定のイベントのみを監視することができるようになります。

存在する監査ポリシーの確認と設定は、ローカルセキュリティポリシーで行うことができます。

たとえば、ログオン イベントの監査というポリシーがありますが、 これを成功に設定した場合は、ユーザーがログオンに成功した場合に監査イベントが生成されることになります。 つまり、セキュリティイベントログがシステムによって更新されます。 一方、失敗に設定した場合は、ログオンの失敗時に監査イベントが生成されます。 監査しないに設定した場合は、監査ポリシーが定義している条件が満たされた場合でも、 監査イベントが生成されることはありません。

アプリケーションから監査ポリシーを設定するには、LsaSetInformationPolicyを呼び出します。

NTSTATUS LsaSetInformationPolicy(
  LSA_HANDLE PolicyHandle,
  POLICY_INFORMATION_CLASS InformationClass,
  PVOID Buffer
);

PolicyHandleは、ポリシーオブジェクトのハンドルを指定します。 InformationClassは、設定する情報の種類を表すPOLICY_INFORMATION_CLASS型の定数を指定します。 監査ポリシーを設定する場合は、PolicyAuditEventsInformationを指定します。 Bufferは、設定したい情報を格納したバッファを指定します。 監査ポリシーの場合は、POLICY_AUDIT_EVENTS_INFO構造体のアドレスを指定します。 この構造体は、次のように定義されています。

typedef struct _POLICY_AUDIT_EVENTS_INFO {
  BOOLEAN                     AuditingMode;
  PPOLICY_AUDIT_EVENT_OPTIONS EventAuditingOptions;
  ULONG                       MaximumAuditEventCount;
} POLICY_AUDIT_EVENTS_INFO,  *PPOLICY_AUDIT_EVENTS_INFO;

AuditingModeは、監査そのものを有効にするかを指定します。 TRUEの場合は、監査が有効になります。 EventAuditingOptionsは、POLICY_AUDIT_EVENT_OPTIONS列挙型の配列を指定します。 MaximumAuditEventCountは、EventAuditingOptionsの要素数を指定します。 この値は、POLICY_AUDIT_EVENT_TYPE型の定義数と一致します。

typedef enum _POLICY_AUDIT_EVENT_TYPE {
  AuditCategorySystem,
  AuditCategoryLogon,
  AuditCategoryObjectAccess,
  AuditCategoryPrivilegeUse,
  AuditCategoryDetailedTracking,
  AuditCategoryPolicyChange,
  AuditCategoryAccountManagement,
  AuditCategoryDirectoryServiceAccess,
  AuditCategoryAccountLogon 
} POLICY_AUDIT_EVENT_TYPE,  *PPOLICY_AUDIT_EVENT_TYPE;

個々の定数の意味は、次のようになります。

POLICY_AUDIT_EVENT_TYPE 対応する監査ポリシー 意味
AuditCategorySystem システムイベント シャットダウンやWindowsの起動、認証パッケージの読み込み、 LsaRegisterLogonProcessの呼び出しなどで発生する。
AuditCategoryLogon ログオン/ログオフ ログオンまたはログオフが終了したときに発生する。
AuditCategoryObjectAccess オブジェクトアクセス SACL(システムアクセス制御リスト)を維持しているオブジェクトにアクセスを試みたときに発生することがある。
AuditCategoryPrivilegeUse 特権の使用 ユーザーのログオン時に作成されるトークンに特権が追加されたときに発生する。 このイベントは有効でない場合も記録されることがある。 たとえば、PrivilegedServiceAuditAlarmのような関数を呼べば明示的に監査イベントを生成することも可能となる。
AuditCategoryDetailedTracking 詳細追跡 プロセスの作成や失敗、ハンドルの複製やプロセスへのトークン割り当てを行ったときに発生する。 この監査イベントが有効であると相当な量のイベントが記録されるので、 失敗の監査のみを有効にするべきである。
AuditCategoryPolicyChange ポリシーの変更 LsaSetInformationPolicyによる監査ポリシーの変更や、 LsaAddAccountRightsによる特権の追加で発生する。
AuditCategoryAccountManagement アカウント管理 ユーザーアカウントの作成やパスワードの変更で発生する。
AuditCategoryDirectoryServiceAccess ディレクトリサービスアクセス アクティブディレクトリにアクセスすることで発生する。 アクティブディレクトリはドメイン内のユーザーやグループを 管理しているが、これらの情報を変更した場合は、 アカウントログオンイベントも発生することになる。
AuditCategoryAccountLogon アカウント ログオン ログオンを試みたときに発生する。 アカウントやパスワードの不正で失敗した場合、 ログオン/ログオフイベントは発生しない。

たとえば、オブジェクトへアクセスした際に監査が行われるようにしたい場合は、 次のようなコードを記述します。

pAuditInfo->EventAuditingOptions[AuditCategoryObjectAccess] = POLICY_AUDIT_EVENT_SUCCESS | POLICY_AUDIT_EVENT_NONE;

オブジェクトへのアクセスは、AuditCategoryObjectAccessとして定義されているため、 これをEventAuditingOptionsのインデックスとして指定することになります。 EventAuditingOptionsに指定できる定数を次に示します。

EventAuditingOptions 意味
POLICY_AUDIT_EVENT_UNCHANGED イベントは発生しない
POLICY_AUDIT_EVENT_SUCCESS 成功の監査
POLICY_AUDIT_EVENT_FAILURE 失敗の監査
POLICY_AUDIT_EVENT_NONE LsaSetInformationPolicyで使用することになる。

たとえば、成功の監査を設定した場合はPOLICY_AUDIT_EVENT_SUCCESSを指定することになりますが、 この場合はPOLICY_AUDIT_EVENT_NONEも指定する必要があります。 POLICY_AUDIT_EVENT_SUCCESSを単一で指定した場合は、追加という意味で解釈されるため、 仮に失敗の監査が既に設定されていた場合は、 成功の監査と失敗の監査の両方が設定されることになってしまいます。

今回のプログラムは、オブジェクトアクセスの監査ポリシーを有効にします。 このポリシーは、SACLを持っているオブジェクトのアクセス時に監査イベントを生成するため、 ポリシーの効果を知るためにはSACLが設定されたオブジェクトが必要になります。 今回のプログラムでは、カレントディレクトリに存在するsample.txtに対してこの処理を行っています。

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

BOOL SetAuditPolicy(POLICY_AUDIT_EVENT_TYPE eventType, DWORD dwEventOption);
BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	PACL        pSacl;
	DWORD       dwSaclSize;
	DWORD       dwResult;
	HANDLE      hToken;
	PTOKEN_USER pTokenUser;
	DWORD       dwLength;

	if (!EnablePrivilege(SE_SECURITY_NAME, TRUE)) {
		MessageBox(NULL, TEXT("SE_SECURITY_NAME特権の有効に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken);
	GetTokenInformation(hToken, TokenUser, NULL, 0, &dwLength);
	pTokenUser = (PTOKEN_USER)LocalAlloc(LPTR, dwLength);
	GetTokenInformation(hToken, TokenUser, pTokenUser, dwLength, &dwLength);

	dwSaclSize = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pTokenUser->User.Sid) - sizeof(DWORD);
	pSacl = (PACL)LocalAlloc(LPTR, dwSaclSize);
	InitializeAcl(pSacl, dwSaclSize, ACL_REVISION); 

	if (!AddAuditAccessAce(pSacl, ACL_REVISION, GENERIC_ALL, pTokenUser->User.Sid, TRUE, TRUE)) {
		MessageBox(NULL, TEXT("ACEの追加に失敗しました。"), NULL, MB_ICONWARNING);
		LocalFree(pSacl);
		return 0;
	}

	dwResult = SetNamedSecurityInfo(TEXT("sample.txt"), SE_FILE_OBJECT, SACL_SECURITY_INFORMATION, NULL, NULL, NULL, pSacl);
	if (dwResult == ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("SACLを設定しました。"), TEXT("OK"), MB_OK);
		if (!SetAuditPolicy(AuditCategoryObjectAccess, POLICY_AUDIT_EVENT_SUCCESS | POLICY_AUDIT_EVENT_NONE)) {
			MessageBox(NULL, TEXT("監査ポリシーの設定に失敗しました。"), NULL, MB_ICONWARNING);
			return 0;
		}
	}
	else
		MessageBox(NULL, TEXT("SACLの設定に失敗しました。"), NULL, MB_ICONWARNING);
	
	CloseHandle(hToken);
	LocalFree(pTokenUser);
	LocalFree(pSacl);

	return 0;
}

BOOL SetAuditPolicy(POLICY_AUDIT_EVENT_TYPE eventType, DWORD dwEventOption)
{
	NTSTATUS                  ns;
	LSA_HANDLE                hPolicy;
	LSA_OBJECT_ATTRIBUTES     objectAttributes;
	PPOLICY_AUDIT_EVENTS_INFO pAuditInfo;

	ZeroMemory(&objectAttributes, sizeof(LSA_OBJECT_ATTRIBUTES));
	objectAttributes.Length = sizeof(LSA_OBJECT_ATTRIBUTES);

	ns = LsaOpenPolicy(NULL, &objectAttributes, POLICY_VIEW_AUDIT_INFORMATION | POLICY_SET_AUDIT_REQUIREMENTS, &hPolicy);
	if (LsaNtStatusToWinError(ns) != ERROR_SUCCESS)
		return FALSE;

	LsaQueryInformationPolicy(hPolicy, PolicyAuditEventsInformation, (LPVOID *)&pAuditInfo);

	pAuditInfo->EventAuditingOptions[eventType] = dwEventOption;
	LsaSetInformationPolicy(hPolicy, PolicyAuditEventsInformation, (LPVOID)pAuditInfo);

	LsaFreeMemory(pAuditInfo);
	LsaClose(hPolicy);

	return TRUE;
}

BOOL EnablePrivilege(LPTSTR lpszPrivilege, BOOL bEnable)
{
	BOOL             bResult;
	LUID             luid;
	HANDLE           hToken;
	TOKEN_PRIVILEGES tokenPrivileges;

	if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
		return FALSE;
	
	if (!LookupPrivilegeValue(NULL, lpszPrivilege, &luid)) {
		CloseHandle(hToken);
		return FALSE;
	}

	tokenPrivileges.PrivilegeCount           = 1;
	tokenPrivileges.Privileges[0].Luid       = luid;
	tokenPrivileges.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
	
	bResult = AdjustTokenPrivileges(hToken, FALSE, &tokenPrivileges, sizeof(TOKEN_PRIVILEGES), NULL, NULL);
	
	CloseHandle(hToken);

	return bResult && GetLastError() == ERROR_SUCCESS;
}

SACLは、オブジェクト(ファイルなど)に設定されているセキュリティ記述子の一要素です。 セキュリティ記述子には、DACLとSACLを含めることができ、 DACLはオブジェクトにアクセスできるアカウントを制御します。 一方、SACLは監査対象とするアカウントを定義し、 そのアカウントがオブジェクトにアクセスを行った場合は、 監査イベントが生成されることになります。 SACLを初期化しているのは、次のコードです。

dwSaclSize = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) + GetLengthSid(pTokenUser->User.Sid) - sizeof(DWORD);
pSacl = (PACL)LocalAlloc(LPTR, dwSaclSize);
InitializeAcl(pSacl, dwSaclSize, ACL_REVISION); 

if (!AddAuditAccessAce(pSacl, ACL_REVISION, GENERIC_ALL, pTokenUser->User.Sid, TRUE, TRUE)) {
	MessageBox(NULL, TEXT("ACEの追加に失敗しました。"), NULL, MB_ICONWARNING);
	LocalFree(pSacl);
	return 0;
}

DACLやSACLには、1つ以上のACEを含めることができます。 1つのACEは1つの情報であり、DACLにおけるACEは1人のアカウントを制御し、 SACLにおけるACEは1人の監査対象アカウントを定義します。 SACLのサイズは、格納するACEのサイズを考慮しなければならないため、 それを考慮したサイズをdwSaclSizeに指定します。 そして、メモリを確保してInitializeAclでSACLを初期化し、 AddAuditAccessAceでSACLにACEを格納します。 この関数の第4引数は、監査対象とするアカウントのSIDであり、 今回のプログラムでは現在のユーザーとなります。 第5引数はアクセスの成功時に監査イベントを生成するかを表し、 第6引数はアクセスの失敗時に監査イベントを生成するかを表します。 今回のプログラムではどちらにもTRUEを指定しているため、 アクセス時には必ず監査イベントが生成されることになります。 SACLが完成したら、SetNamedSecurityInfoでオブジェクトに設定します。

dwResult = SetNamedSecurityInfo(TEXT("sample.txt"), SE_FILE_OBJECT, SACL_SECURITY_INFORMATION, NULL, NULL, NULL, pSacl);

SetNamedSecurityInfoの第2引数はオブジェクトのタイプであり、 第1引数にファイル名を指定する場合はSE_FILE_OBJECTとなります。 第3引数は設定するセキュリティ情報の種類であり、 SACL_SECURITY_INFORMATIONを指定した場合は、第8引数にSACLを指定することができます。 よって、pSaclを指定して関数が成功した場合は、 オブジェクトのセキュリティ記述子にSACLが設定されたことになります。 なお、SACLを設定する場合は、SE_SECURITY_NAME特権を有効にしておかなければならないため、 自作関数のEnablePrivilegeで事前に行うようにしています。

SACLの設定が終了すれば、自作関数のSetAuditPolicyでオブジェクトアクセスのポリシーを有効にします。 これにより、sample.txtに対するアクセスが監査されるようになります。 関数の内部としては、LsaQueryInformationPolicyで現在の監査ポリシーを取得し、 その後にEventAuditingOptionsを更新してLsaSetInformationPolicyを呼び出します。 実際に、今回のプログラムの終了後に、sample.txtにアクセスをしてみたところ、 イベントビューアのセキュリティイベントログは次のようになっていました。

タスクのカテゴリから分るように、ファイルシステムを理由として監査イベントが生成されています。 また、ポリシーの変更の監査というイベントもありますが、 これはLsaSetInformationPolicyの呼び出しによって生成されたものです。

監査ポリシーの確認

監査ポリシーが現在、どのように設定されているか確認するコードを次に示します。 重要な点は、外部アプリケーションによって監査ポリシーが変更されても、 その変更内容を即座に反映しているところです。

#include <windows.h>
#include <ntsecapi.h>

#pragma comment (lib, "secur32.lib")

#define WM_AUDITPOLICYCHANGE WM_APP

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR      szAppName[] = L"sample";
	HWND       hwnd;
	MSG        msg;
	WNDCLASSEX wc;
	HANDLE     hEvent;

	wc.cbSize        = sizeof(WNDCLASSEX);
	wc.style         = 0;
	wc.lpfnWndProc   = WindowProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = hinst;
	wc.hIcon         = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	wc.hCursor       = (HCURSOR)LoadImage(NULL, IDC_ARROW, IMAGE_CURSOR, 0, 0, LR_SHARED);
	wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = szAppName;
	wc.hIconSm       = (HICON)LoadImage(NULL, IDI_APPLICATION, IMAGE_ICON, 0, 0, LR_SHARED);
	
	if (RegisterClassEx(&wc) == 0)
		return 0;

	hwnd = CreateWindowEx(0, szAppName, szAppName, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hinst, NULL);
	if (hwnd == NULL)
		return 0;

	ShowWindow(hwnd, nCmdShow);
	UpdateWindow(hwnd);
	
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	LsaRegisterPolicyChangeNotification(PolicyNotifyAuditEventsInformation, hEvent);

	for (;;) {
		if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
			if (GetMessage(&msg, NULL, 0, 0) > 0) {
				TranslateMessage(&msg);
				DispatchMessage(&msg);
			}
			else
				break;
		}
		else {
			if (MsgWaitForMultipleObjects(1, &hEvent, FALSE, INFINITE, QS_ALLINPUT) == WAIT_OBJECT_0)
				SendMessage(hwnd, WM_AUDITPOLICYCHANGE, 0, 0);
		}
	}

	LsaUnregisterPolicyChangeNotification(PolicyNotifyAuditEventsInformation, hEvent);
	CloseHandle(hEvent);

	return (int)msg.wParam;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	static HWND hwndListBox = NULL;

	switch (uMsg) {

	case WM_CREATE:
		hwndListBox  = CreateWindowEx(0, L"LISTBOX", NULL, WS_CHILD | WS_VISIBLE | LBS_NOTIFY, 0, 0, 0, 0, hwnd, (HMENU)1, ((LPCREATESTRUCT)lParam)->hInstance, NULL);
		SendMessage(hwnd, WM_AUDITPOLICYCHANGE, 0, 0);
		return 0;

	case WM_AUDITPOLICYCHANGE: {
		DWORD                     i;
		NTSTATUS                  ns;
		LSA_HANDLE                hPolicy;
		LSA_OBJECT_ATTRIBUTES     objectAttributes;
		PPOLICY_AUDIT_EVENTS_INFO pAuditInfo;
		WCHAR                     szEventType[256];
		WCHAR                     szBuf[256];
		LPWSTR                    lpszAuditPolicyName[] = {
			L"システムイベント", L"ログオン/ログオフ", 
			L"オブジェクトアクセス", L"特権の使用",
			L"詳細追跡", L"ポリシーの変更",
			L"アカウント管理", L"ディレクトリサービスアクセス",
			L"アカウント ログオン"
		};

		ZeroMemory(&objectAttributes, sizeof(LSA_OBJECT_ATTRIBUTES));
		objectAttributes.Length = sizeof(LSA_OBJECT_ATTRIBUTES);

		ns = LsaOpenPolicy(NULL, &objectAttributes, POLICY_VIEW_AUDIT_INFORMATION, &hPolicy);
		if (LsaNtStatusToWinError(ns) != ERROR_SUCCESS) {
			MessageBox(NULL, L"ポリシーオブジェクトのハンドルを取得できませんでした。", NULL, MB_ICONWARNING);
			return -1;
		}

		LsaQueryInformationPolicy(hPolicy, PolicyAuditEventsInformation, (LPVOID *)&pAuditInfo);

		SendMessage(hwndListBox, LB_RESETCONTENT, 0, 0);

		for (i = 0; i < pAuditInfo->MaximumAuditEventCount; i++) {
			if (pAuditInfo->EventAuditingOptions[i] == (POLICY_AUDIT_EVENT_SUCCESS | POLICY_AUDIT_EVENT_FAILURE))
				lstrcpy(szEventType, L"○×");
			else if (pAuditInfo->EventAuditingOptions[i] == POLICY_AUDIT_EVENT_SUCCESS)
				lstrcpy(szEventType, L"○");
			else if (pAuditInfo->EventAuditingOptions[i] == POLICY_AUDIT_EVENT_FAILURE)
				lstrcpy(szEventType, L"×");
			else if (pAuditInfo->EventAuditingOptions[i] == POLICY_AUDIT_EVENT_UNCHANGED)
				lstrcpy(szEventType, L"---");
			else
				lstrcpy(szEventType, L"");

			wsprintf(szBuf, L"%s %s", szEventType, lpszAuditPolicyName[i]);
			SendMessage(hwndListBox, LB_ADDSTRING, 0, (LPARAM)szBuf);
		}

		LsaFreeMemory(pAuditInfo);
		LsaClose(hPolicy);

		return 0;
	}

	case WM_SIZE:
		MoveWindow(hwndListBox, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE);
		return 0;

	case WM_DESTROY:
		PostQuitMessage(0);
		return 0;

	default:
		break;

	}

	return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

リストボックスにそれぞれの監査ポリシーの名前が表示され、 その先頭には○や×などが表示されます。 ○の場合は、成功の監査が設定されていることを意味し、 ×の場合は失敗の監査、---の場合は監査されないことを意味します。 これらの記号は、監査ポリシーがローカルセキュリティポリシーなどの外部アプリケーションで 変更された場合でも適切に対応していますが、 これはWinMainの次の処理のおかげです。

hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
LsaRegisterPolicyChangeNotification(PolicyNotifyAuditEventsInformation, hEvent);

for (;;) {
	if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) {
		if (GetMessage(&msg, NULL, 0, 0) > 0) {
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}
		else
			break;
	}
	else {
		if (MsgWaitForMultipleObjects(1, &hEvent, FALSE, INFINITE, QS_ALLINPUT) == WAIT_OBJECT_0)
			SendMessage(hwnd, WM_AUDITPOLICYCHANGE, 0, 0);
	}
}

LsaUnregisterPolicyChangeNotification(PolicyNotifyAuditEventsInformation, hEvent);
CloseHandle(hEvent);

LsaRegisterPolicyChangeNotificationの第1引数にPolicyNotifyAuditEventsInformationを指定した場合、 監査ポリシーの変更を検出した際に第2引数のイベントオブジェクトがシグナル状態に変化します。 よって、これを検出することで、監査ポリシーをもう一度取得すべきタイミングを特定できます。 PeekMessageを呼び出して、メッセージキューにメッセージが存在しない場合は、 MsgWaitForMultipleObjectsで処理を待機するようにします。 この関数が制御を返すのは、第2引数のオブジェクトがシグナル状態になった場合か、 あるいはメッセージキューにメッセージがポストされた場合であり、 前者の場合は戻り値としてWAIT_OBJECT_0が返ります。 この場合は、自作メッセージをウインドウに送信することで、 監査ポリシーを改めて取得するようにしています。 ポリシーの検出が不要になった場合は、LsaUnregisterPolicyChangeNotificationを呼び出します。



戻る