EternalWindows
LSA / ポリシーオブジェクト

前節で述べたように、LSAはログオンに関係する情報をポリシーデータベースなどに格納し、 必要に応じてこれを参照するようにしています。 ポリシーデータベースは、次のレジストリキーで表されます。

HKEY_LOCAL_MACHINE\\Security

LSAが最初に実行されたとき、ポリシーデータベースの内容を反映したポリシーオブジェクトが作成されます。 ポリシーオブジェクトはシステムに1つだけ存在し、次に示すオブジェクトを含んでいます。

オブジェクト 意味
ポリシーオブジェクト デフォルトのメモリクォータや監査ポリシー、プライマリドメイン情報を維持する。
信頼されたドメインオブジェクト 信頼されたドメインのSIDや名前を維持する。
アカウントオブジェクト アカウントのSIDやアカウント権利、及び特権を維持する。
プライベートデータオブジェクト LSAシークレットを維持する。

ポリシーオブジェクトがポリシーオブジェクトを含むというのは少し妙に思えますが、実際にはそのようになっています。 オブジェクトといっても、アプリケーションが使用するのはハンドルですから、 どのようなオブジェクトが存在するかを正確に理解しておく必要はありません。 LSA関数を呼び出せば、上記に示したデータを扱えると考えるだけで十分です。

アプリケーションからポリシーオブジェクトにアクセスできれば、 LSAが参照している各種データを知ることができます。 ポリシーオブジェクトのハンドルを取得するには、LsaOpenPolicyを呼び出します。

NTSTATUS LsaOpenPolicy(
  PLSA_UNICODE_STRING SystemName,
  PLSA_OBJECT_ATTRIBUTES ObjectAttributes,
  ACCESS_MASK DesiredAccess,
  PLSA_HANDLE PolicyHandle
);

SystemNameは、ポリシーオブジェクトを取得するシステムの名前を指定します。 ローカルシステムを対象とする場合は、NULLで構いません。 ObjectAttributesは、LSA_OBJECT_ATTRIBUTES構造体のアドレスを指定します。 DesiredAccessは、ポリシーオブジェクトに要求するアクセス権を指定します。 PolicyHandleは、ポリシーオブジェクトのハンドルを受け取る変数のアドレスを指定します。

LSA関数の戻り値はNTSTATUS型であり、値がERROR_SUCCESSの場合は関数が成功したことを意味します。 ERROR_SUCCESSでない場合は、LsaNtStatusToWinErrorでエラーの詳細を取得することができます。

ULONG LsaNtStatusToWinError(
  NTSTATUS Status
);

Statusは、LSA関数の戻り値を指定します。 戻り値は、GetLastErrorで返される値と同じ意味を持ちます。

ポリシーオブジェクトのハンドルを取得すれば、 LsaQueryInformationPolicyでポリシーオブジェクトの情報を取得することができます。

NTSTATUS LsaQueryInformationPolicy(
  LSA_HANDLE PolicyHandle,
  POLICY_INFORMATION_CLASS POLICY_INFORMATION_CLASS,
  PVOID *Buffer
);

PolicyHandleは、ポリシーオブジェクトのハンドルを指定します。 InformationClassは、POLICY_INFORMATION_CLASS型として定義されている定数を指定します。 Bufferは、取得した情報を受け取るバッファを指定します。

LsaQueryInformationPolicyの第2引数にPolicyPrimaryDomainInformationを指定した場合、 第3引数にPOLICY_ACCOUNT_DOMAIN_INFO構造体が返ることになります。 この構造体は、プライマリドメインの名前とSIDを格納しています。

typedef struct _POLICY_ACCOUNT_DOMAIN_INFO {
  LSA_UNICODE_STRING DomainName;
  PSID               DomainSid;
} POLICY_ACCOUNT_DOMAIN_INFO,  *PPOLICY_ACCOUNT_DOMAIN_INFO;

DomainNameは、プライマリドメインの名前が格納されます。 ドメインが構成されていない環境の場合は、ワークグループの名前が格納されます。 DomainSidは、プライマリドメインのSIDが格納されます。 ドメインが構成されていない環境の場合は、NULLになります。

LSA関数では、文字列をLSA_UNICODE_STRING構造体で表すことになっています。 この構造体は、次のように定義されています。

typedef struct _LSA_UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} LSA_UNICODE_STRING,  *PLSA_UNICODE_STRING;

Lengthは、Bufferで表されるバッファに格納された文字列のサイズを指定します。 このサイズは、'\0'文字などを含んではいけません。 MaximumLengthは、Bufferで表されるバッファのサイズを指定します。 Bufferは、UNICODE文字列を指定します。 この文字列は、'\0'文字で終了していなくても構いません。

次に、LSA_UNICODE_STRING構造体を初期化する例を示します。

WCHAR              szBuf[] = L"sample";
LSA_UNICODE_STRING lsaString;

lsaString.Length        = (USHORT)(lstrlen(szBuf) * sizeof(WCHAR));
lsaString.MaximumLength = lsaString.Length + sizeof(WCHAR);
lsaString.Buffer        = szBuf;

Lengthには、Bufferに格納された文字列のサイズを指定します。 lstrlenの戻り値に+1をしていないため、'\0'文字のサイズは含まれません。 MaximumLengthは、Bufferのサイズを指定します。 これは、Lengthに'\0'文字のサイズを加算した値になります。 Bufferは、文字列を指定します。

LSA関数によって確保されたメモリは、LsaFreeMemoryで開放することになります。

NTSTATUS LsaFreeMemory(
  PVOID Buffer
);

Bufferは、開放したいメモリを指定します。

LSA関数によって取得したハンドルは、LsaCloseでクローズすることになります。

NTSTATUS LsaClose(
  LSA_HANDLE ObjectHandle
);

ObjectHandleは、クローズしたいハンドルを指定します。

今回のプログラムは、システムが所属しているドメインまたはワークグループ名を取得します。 LSA関数を呼び出すには、ntsecapi.hのインクルードが必要になります。

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

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	WCHAR                       szBuf[256];
	WCHAR                       szDomainName[256];
	NTSTATUS                    ns;
	LSA_HANDLE                  hPolicy;
	LSA_OBJECT_ATTRIBUTES       objectAttributes;
	PPOLICY_ACCOUNT_DOMAIN_INFO pDomainInfo;

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

	ns = LsaOpenPolicy(NULL, &objectAttributes, POLICY_VIEW_LOCAL_INFORMATION, &hPolicy);
	if (LsaNtStatusToWinError(ns) != ERROR_SUCCESS) {
		MessageBox(NULL, TEXT("ポリシーオブジェクトのハンドルの取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	LsaQueryInformationPolicy(hPolicy, PolicyPrimaryDomainInformation, (LPVOID *)&pDomainInfo);
	if (pDomainInfo->DomainSid != NULL)
		lstrcpy(szBuf, L"ドメイン");
	else
		lstrcpy(szBuf, L"ワークグループ");

	lstrcpyn(szDomainName,  pDomainInfo->DomainName.Buffer, (pDomainInfo->DomainName.Length / sizeof(WCHAR)) + 1);
	MessageBox(NULL, szDomainName, szBuf, MB_OK);

	LsaFreeMemory(pDomainInfo);
	LsaClose(hPolicy);

	return 0;
}

LsaOpenPolicyの第2引数に指定するLSA_OBJECT_ATTRIBUTES構造体は、 Lengthメンバを除いて0に初期化して問題ありません。 第3引数に指定しているPOLICY_VIEW_LOCAL_INFORMATIONは、 PolicyPrimaryDomainInformationを指定したLsaQueryInformationPolicyを 成功させるために必要なアクセス権です。 ポリシーオブジェクトのハンドルを取得すれば、 LsaQueryInformationPolicyでPOLICY_ACCOUNT_DOMAIN_INFO構造体を取得し、 DomainSidメンバがNULLかどうかで、システムの所属を確認します。 そして、ドメイン名またはワークグループ名を取得します。

lstrcpyn(szDomainName,  pDomainInfo->DomainName.Buffer, (pDomainInfo->DomainName.Length / sizeof(WCHAR)) + 1);
MessageBox(NULL, szDomainName, szBuf, MB_OK);

pDomainInfo->DomainNameはLSA_UNICODE_STRING型であり、Bufferメンバから文字列を参照することができます。 ただし、この文字列は'\0'文字で終了しているとは限りませんから、 lstrcpynを通じて専用のバッファに文字列をコピーしています。 このようにすれば、lstrcpynによってバッファの終端に'\0'文字が付加されるようになります。

ポリシーオブジェクトのセキュリティ記述子

ポリシーオブジェクトは、ファイルやカーネルオブジェクトと同じように、 セキュリティが設定可能なオブジェクトです。 つまり、オブジェクト時のアクセス時に、セキュリティ記述子の内容に基づいてアクセスチェックが行われ、 アクセスが許可された場合のみ関数の呼び出しに成功することになります。 LsaOpenPolicyの第3引数にどのようなアクセス権を指定できるかを確認するために、 ポリシーオブジェクトのセキュリティ記述子について考察していきたいと思います。

まず、ポリシーオブジェクトのセキュリティ記述子をどう取得するかですが、 これはレジストリのSECURITY\\Policy\\SecDescキーのエントリに格納されているので、 このキーへのアクセスが成功すれば取得することができます。 ただし、SECURITYキーのセキュリティ記述子は、 デフォルトでAdministratorsグループに読み取りアクセスすら許可していないため、 まずはSECURITYキーへのアクセス自体を可能にしなければなりません。 レジストリエディタなどで、HKEY_LOCAL_MACHINEに存在するSECURITYキーの上で右クリックをすれば、 アクセスを変更できるはずですから、そこでAdministratorsを選択して、 読み取りのチェックボックスにチェックを設定します。 これにより、SECURITYキー以下のレジストリキーを参照できるようになります。 次に示すプログラムは、ポリシーオブジェクトのセキュリティ記述子を取得し、 それに含まれるDACL内の個々のACEを表示します。

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

BOOL GetPolicyObjectSD(PSECURITY_DESCRIPTOR *ppSecurityDescriptor);
BOOL ConvertSidToName(PSID pSid, LPTSTR lpszName, DWORD dwSizeName);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	PACL                 pDacl;
	BOOL                 bDaclPresent;
	BOOL                 bDaclDefaulted;
	TCHAR                szBuf[1024];
	TCHAR                szAccountName[256];
	DWORD                i, j;
	PACCESS_ALLOWED_ACE  pAce;
	ACL_SIZE_INFORMATION aclInformation;
	PSECURITY_DESCRIPTOR pSecurityDescriptor;

	struct ACCESS_RIGHT {
		DWORD  dwAccessRight;
		LPTSTR lpszAccessRight;
	} accessRigtht [] = {
		POLICY_VIEW_LOCAL_INFORMATION, TEXT("POLICY_VIEW_LOCAL_INFORMATION"),
		POLICY_VIEW_AUDIT_INFORMATION, TEXT("POLICY_VIEW_AUDIT_INFORMATION"),
		POLICY_GET_PRIVATE_INFORMATION, TEXT("POLICY_GET_PRIVATE_INFORMATION"),
		POLICY_TRUST_ADMIN, TEXT("POLICY_TRUST_ADMIN"),
		POLICY_CREATE_ACCOUNT, TEXT("POLICY_CREATE_ACCOUNT"),
		POLICY_CREATE_SECRET, TEXT("POLICY_CREATE_SECRET"),
		POLICY_CREATE_PRIVILEGE, TEXT("POLICY_CREATE_PRIVILEGE"),
		POLICY_SET_DEFAULT_QUOTA_LIMITS, TEXT("POLICY_SET_DEFAULT_QUOTA_LIMITS"),
		POLICY_SET_AUDIT_REQUIREMENTS, TEXT("POLICY_SET_AUDIT_REQUIREMENTS"),
		POLICY_AUDIT_LOG_ADMIN, TEXT("POLICY_AUDIT_LOG_ADMIN"),
		POLICY_SERVER_ADMIN, TEXT("POLICY_SERVER_ADMIN"),
		POLICY_LOOKUP_NAMES, TEXT("POLICY_LOOKUP_NAMES"),
		POLICY_NOTIFICATION, TEXT("POLICY_NOTIFICATION"),
	};
	DWORD dwAccessRigthtCount = sizeof(accessRigtht) / sizeof(accessRigtht[0]);

	if (!GetPolicyObjectSD(&pSecurityDescriptor)) {
		MessageBox(NULL, TEXT("LSAポリシーオブジェクトのセキュリティ記述子の取得に失敗しました。"), NULL, MB_ICONWARNING);
		return 0;
	}

	GetSecurityDescriptorDacl(pSecurityDescriptor, &bDaclPresent, &pDacl, &bDaclDefaulted);
	GetAclInformation(pDacl, &aclInformation, sizeof(ACL_SIZE_INFORMATION), AclSizeInformation);
	
	for (i = 0; i < aclInformation.AceCount; i++) {
		GetAce(pDacl, i, (LPVOID *)&pAce);
		ConvertSidToName((PSID)&pAce->SidStart, szAccountName, sizeof(szAccountName) / sizeof(TCHAR));
		lstrcpy(szBuf, TEXT("\0"));
		for (j = 0; j < dwAccessRigthtCount; j++) {
			if (accessRigtht[j].dwAccessRight & pAce->Mask) {
				lstrcat(szBuf, accessRigtht[j].lpszAccessRight);
				lstrcat(szBuf, TEXT("\n"));
			}
		}
		
		MessageBox(NULL, szBuf, szAccountName, MB_OK);
	}

	LocalFree(pSecurityDescriptor);

	return 0;
}

BOOL GetPolicyObjectSD(PSECURITY_DESCRIPTOR *ppSecurityDescriptor)
{
	HKEY   hKey;
	LONG   lResult;
	LPVOID lpData;
	DWORD  dwSize;

	lResult = RegOpenKeyEx(HKEY_LOCAL_MACHINE, TEXT("SECURITY\\Policy\\SecDesc"), 0, KEY_QUERY_VALUE, &hKey);
	if (lResult == ERROR_SUCCESS) {
		RegQueryValueEx(hKey, NULL, NULL, NULL, NULL, &dwSize);
		lpData = LocalAlloc(LPTR, dwSize);
		RegQueryValueEx(hKey, NULL, NULL, NULL, (LPBYTE)lpData, &dwSize);
		*ppSecurityDescriptor = lpData;
		RegCloseKey(hKey);
		return TRUE;
	}
	else
		return FALSE;
}

BOOL ConvertSidToName(PSID pSid, LPTSTR lpszName, DWORD dwSizeName)
{
	TCHAR        szDomainName[256];
	DWORD        dwSizeDomain = sizeof(szDomainName) / sizeof(TCHAR);
	SID_NAME_USE sidName;

	return LookupAccountSid(NULL, pSid, lpszName, &dwSizeName, szDomainName, &dwSizeDomain, &sidName);
}

まず、GetPolicyObjectSDという自作関数で、ポリシーオブジェクトのセキュリティ記述子を取得します。 このセキュリティ記述子の形式は、自己相対形式です。 続いて、GetSecurityDescriptorDaclでセキュリティ記述子からDACLを取得し、 GetAclInformationでACEの個数を取得します。 その後、GetAceでACEを取得し、 ACEのアクセスマスクにaccessRigtht[j].dwAccessRightが含まれているかを確認します。 含まれている場合は、アクセス権を文字列としてバッファに追加し、 最終的に追加されたアクセス権がMessageBoxで表示されます。

実際に上記のプログラムを実行してみると分かりますが、 Administratorsには全てのアクセス権が許可されています。 よって、Administratorsのメンバであるユーザーとして実行している場合は、 LsaOpenPolicyの第3引数にPOLICY_ALL_ACCESSを指定しても関数は成功します。 一方、Everyoneには、POLICY_VIEW_LOCAL_INFORMATIONとPOLICY_LOOKUP_NAMESが許可されています。 これは、POLICY_EXECUTEに相当します。 全てのユーザーはEveryoneのメンバとなっているため、 Administratorsのユーザーでないメンバや、 AdministratorsのメンバであってもUACが有効な場合は、 Everyoneに許可されているアクセス権しか指定することができません。

ポリシーオブジェクトに対して、より多くのアクセスを行うようにするには、 独自のACEを追加したDACLをセキュリティ記述子に設定し、 これをSecDescキーに格納する方法が考えられます。 具体的には、SecDescキーから取得したセキュリティ記述子をMakeAbsoluteSDで絶対形式に変換し、 この形式の状態でDACLを設定します。 そして、MakeSelfRelativeSDで自己相対形式に戻し、これをSecDescキーに設定します。 これで、システムを再起動すれば、ポリシーオブジェクトのセキュリティ記述子は設定内容が反映されていると思われます。 一度、絶対形式に変換するのは、自己相対形式がデータを連続的に格納する形式であるからであり、 この形式では新しいDACLを追加するための空き領域が存在しません。 よって、データへのポインタを維持する絶対形式に変換し、 ポインタが示すアドレスを新しいDACLに設定します。



戻る