【C++】滅多に使わない ACL 操作の関数をまとめた話

# 目的
Win32API に SetEntriesInAcl という関数がある。これらは、様々な関数や構造体を組み合わせて、ファイルの ACL (Access Control List) を制御するためにあるが、ネット上にはそれを使用したプログラムのサンプルが少ない (調べた限り)。**どうやら全く使われていないらしい。**
この前に使う機会があったが、**コーディングが地味に長くて結構めんどくさかった。**
そこで、誰も使わないであろう関数達をラップし、次回また使うとき (多分ない) にコピペだけで対応できるようにここにソースをおいておこうというのが目的だ。

:::note warn
これらのクラスは、**随意アクセス許可リスト (DACL) のみ使用可能**
また、ファイルだけではなく、レジストリキー等の操作も可能
:::

# 使用環境
* IDE: [Visual Studio 2022](https://visualstudio.microsoft.com/ja/vs/)
* バージョン: [C++20](https://cpprefjp.github.io/lang/cpp20.html)

# ソース
<details><summary>ヘッダーファイル</summary>

```c++:LUsr.h
#pragma once

#include <Windows.h>
#include <sddl.h>
#include <aclapi.h>

#include <iostream>
#include <tuple>
#include <bit>
#include <unordered_map>
#include <stdexcept>

struct _HLOCAL_deleter {
	void operator()(HLOCAL _ptr) const noexcept {
		::LocalFree(_ptr);
	}
};

enum class AccessType {
	Allow = ACCESS_MODE::GRANT_ACCESS,
	Deny = ACCESS_MODE::DENY_ACCESS,
	AllowNM = ACCESS_MODE::SET_ACCESS
};

class Principal {
private:
	std::wstring Name;
	std::unique_ptr<::_SID, _HLOCAL_deleter> BinarySID;
	std::wstring StringSID;

	std::wstring Domain;

public:
	Principal(
		_In_ std::wstring_view Name
	);

	Principal(
		_In_ ::PSID lpSid
	);

	const std::wstring& GetName() const noexcept {
		return Name;
	}
	const std::wstring& GetStringSID() const noexcept {
		return StringSID;
	}
	const std::wstring& GetDomain() const noexcept {
		return Domain;
	}

	::SID* GetSid() const {
		return BinarySID.get();
	}

	bool operator==(Principal& _pr) {
		if (
			this->Name == _pr.Name && 
			this->StringSID == _pr.StringSID && 
			this->Domain == _pr.Domain) return true;
			
		return false;
	}

	virtual ~Principal();
};

class AccessControlList {
private:
	SE_OBJECT_TYPE Type;
	std::wstring Name;

	std::unique_ptr<Principal> Owner;

	std::unique_ptr<::SECURITY_DESCRIPTOR, _HLOCAL_deleter> lpSecurityDescriptor;
	::PSID lpOwner;
	::PACL lpAcl;

	std::unordered_map<
		std::shared_ptr<Principal>,
		std::tuple<
			ACCESS_MODE,
			ACCESS_MASK,
			DWORD
		>
	> Aces;

public:
	AccessControlList(
		_In_ std::wstring_view ObjectName,
		_In_ ::SE_OBJECT_TYPE ObjectType
	);

	void Update();

	void Add(
		_In_ Principal Principal,
		_In_ AccessType Type,
		_In_ ACCESS_MASK Permissions,
		_In_ DWORD InheritFlag,
		_In_ bool NotBinding = false
	);
		
	void SetOwner(
		_In_ Principal Principal
	);

	void Revoke(
		_In_ Principal Principal,
		_In_ DWORD InheritFlag = NO_INHERITANCE
	);

	const std::unordered_map<
		std::shared_ptr<Principal>,
		std::tuple<
			ACCESS_MODE,
			ACCESS_MASK,
			DWORD
		>
	>& EnumEntries();

	const std::tuple<
		ACCESS_MODE,
		ACCESS_MASK,
		DWORD
	>& GetEntry(
		_In_ Principal Principal
	);

	const Principal& GetOwner();

    virtual ~AccessControlList();
};
```
</details>
<details><summary>ソースファイル</summary>

```C++:LUsr.cpp
#include "LUsr.h"

Principal::Principal(
	_In_ std::wstring_view Name
) {
	this->Name = Name;
	
	BOOL bRet;
	
	DWORD dwcbComputerNameLength = MAX_COMPUTERNAME_LENGTH + 1;
	auto szComputerName = std::make_unique<wchar_t[]>(dwcbComputerNameLength);

	if (!GetComputerNameW(szComputerName.get(), &dwcbComputerNameLength)) {
		szComputerName.reset();
	}

	DWORD dwcbSIDSize = 0, dwcbDomainLength = 0;

	SID_NAME_USE SNU;

	LookupAccountNameW(
		szComputerName.get(),
		this->Name.c_str(),
		nullptr,
		&dwcbSIDSize,
		nullptr,
		&dwcbDomainLength,
		&SNU
	);

	auto lpDomainName = std::make_unique<wchar_t[]>(dwcbDomainLength);

	::PSID pSid = LocalAlloc(LPTR, dwcbSIDSize);
	if (!pSid) {
		throw std::runtime_error("SID 構造体に必要なヒープ領域の確保に失敗しました");

		return;
	}

	bRet = LookupAccountNameW(
		szComputerName.get(),
		this->Name.c_str(),
		pSid,
		&dwcbSIDSize,
		lpDomainName.get(),
		&dwcbDomainLength,
		&SNU
	);
	if (!bRet || !pSid) {
		throw std::runtime_error("SID からアカウント名の取得に失敗しました");

		return;
	}
	
	this->BinarySID.reset(static_cast<::_SID*>(pSid));
	this->Domain = lpDomainName.get();
		
	LPWSTR lpStringSID = nullptr;
	bRet = ConvertSidToStringSidW(this->BinarySID.get(), &lpStringSID);
	if (!bRet || !lpStringSID) {
		throw std::runtime_error("構造体の SID から文字列の SID への変換に失敗しました");

		return;
	}

	this->StringSID = lpStringSID;

	LocalFree(lpStringSID);
}

Principal::Principal(
	_In_ ::PSID lpSid
) {
	BOOL bRet;

	DWORD dwcbComputerNameLength = MAX_COMPUTERNAME_LENGTH + 1;
	auto szComputerName = std::make_unique<wchar_t[]>(dwcbComputerNameLength);

	if (!GetComputerNameW(szComputerName.get(), &dwcbComputerNameLength)) {
		szComputerName.reset();
	}

	DWORD dwcbNameLength = 0, dwcbDomainLength = 0;

	SID_NAME_USE SNU;

	LookupAccountSidW(
		szComputerName.get(),
		lpSid,
		nullptr,
		&dwcbNameLength,
		nullptr,
		&dwcbDomainLength,
		&SNU
	);
	
	auto lpDomainName = std::make_unique<wchar_t[]>(dwcbDomainLength);
	auto lpAccountName = std::make_unique<wchar_t[]>(dwcbNameLength);

	bRet = LookupAccountSidW(
		szComputerName.get(),
		lpSid,
		lpAccountName.get(),
		&dwcbNameLength,
		lpDomainName.get(),
		&dwcbDomainLength,
		&SNU
	);
	if (!bRet || !lpAccountName) {
		throw std::runtime_error("SID からアカウント名の取得に失敗しました");

		return;
	}

	DWORD dwcbSidSize = 0;

	LookupAccountNameW(
		szComputerName.get(),
		lpAccountName.get(),
		nullptr,
		&dwcbSidSize,
		nullptr,
		&dwcbDomainLength,
		&SNU
	);

	PSID pSid = LocalAlloc(LPTR, dwcbSidSize);
	if (!pSid) {
		throw std::runtime_error("SID 構造体に必要なヒープ領域の確保に失敗しました");

		return;
	}

	bRet = LookupAccountNameW(
		szComputerName.get(),
		lpAccountName.get(),
		pSid,
		&dwcbSidSize,
		lpDomainName.get(),
		&dwcbDomainLength,
		&SNU
	);

	if (!bRet || !pSid) {
		throw std::runtime_error("SID からアカウント名の取得に失敗しました");

		return;
	}

	this->BinarySID.reset(static_cast<::_SID*>(pSid));
	this->Domain = lpDomainName.get();
	this->Name = lpAccountName.get();

	LPWSTR lpStringSID = nullptr;

	bRet = ConvertSidToStringSidW(this->BinarySID.get(), &lpStringSID);
	if (!bRet || !lpStringSID) {
		throw std::runtime_error("構造体の SID から文字列の SID への変換に失敗しました");

		return;
	}

	this->StringSID = lpStringSID;

	LocalFree(lpStringSID);
}

Principal::~Principal() {}

AccessControlList::AccessControlList(
	_In_ std::wstring_view ObjectName,
	_In_::SE_OBJECT_TYPE ObjectType
) {
	this->Name = ObjectName;
	this->Type = ObjectType;
	
	Update();
}

void AccessControlList::Update() {
	this->Aces.clear();
	this->lpAcl = nullptr;
	this->lpOwner = nullptr;
	this->lpSecurityDescriptor.reset();

	PSECURITY_DESCRIPTOR _lpSD = nullptr;
	
	DWORD dwRet = GetNamedSecurityInfoW(
		this->Name.c_str(),
		this->Type,
		OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
		&(this->lpOwner),
		nullptr,
		&(this->lpAcl),
		nullptr,
		&_lpSD
	);
	if (dwRet) {
		throw std::runtime_error("オブジェクトのセキュリティ修飾子の取得に失敗しました");

		return;
	}

	this->lpSecurityDescriptor.reset(static_cast<SECURITY_DESCRIPTOR*>(_lpSD));

	// 所有者
	{
		this->Owner = std::make_unique<Principal>(this->lpOwner);
	}

	// ACE 
	{
		PEXPLICIT_ACCESSW lpExpAces = nullptr;
		ULONG ulExpAcesCount = 0;

		dwRet = GetExplicitEntriesFromAclW(
			this->lpAcl,
			&ulExpAcesCount,
			&lpExpAces
		);
		if (dwRet) {
			throw std::runtime_error("ACL 情報からエントリ情報を取得できませんでした");

			return;
		}

		for (ULONG i = 0; i < ulExpAcesCount; i++) {
			TRUSTEE_W& lpExpTrustee = lpExpAces[i].Trustee;

			switch (lpExpTrustee.TrusteeForm) {
			case TRUSTEE_FORM::TRUSTEE_IS_NAME:
			{
				auto lpPrincipal = std::make_shared<Principal>(lpExpTrustee.ptstrName);

				this->Aces[lpPrincipal] = std::tuple(
					lpExpAces[i].grfAccessMode,
					lpExpAces[i].grfAccessPermissions,
					lpExpAces[i].grfInheritance
				);

				break;
			}
			case TRUSTEE_FORM::TRUSTEE_IS_SID:
			{
				auto lpPrincipal =
					std::make_shared<Principal>(std::bit_cast<PSID>(lpExpTrustee.ptstrName));

				this->Aces[lpPrincipal] = std::tuple(
					lpExpAces[i].grfAccessMode,
					lpExpAces[i].grfAccessPermissions,
					lpExpAces[i].grfInheritance
				);

				break;
			}
			case TRUSTEE_FORM::TRUSTEE_IS_OBJECTS_AND_NAME:
			{
				auto lpPrincipal =
					std::make_shared<Principal>(std::bit_cast<POBJECTS_AND_NAME_W>(lpExpTrustee.ptstrName)->ptstrName);

				this->Aces[lpPrincipal] = std::tuple(
					lpExpAces[i].grfAccessMode,
					lpExpAces[i].grfAccessPermissions,
					lpExpAces[i].grfInheritance
				);

				break;
			}
			case TRUSTEE_FORM::TRUSTEE_IS_OBJECTS_AND_SID:
			{
				auto lpPrincipal =
					std::make_shared<Principal>(std::bit_cast<POBJECTS_AND_SID>(lpExpTrustee.ptstrName)->pSid);

				this->Aces[lpPrincipal] = std::tuple(
					lpExpAces[i].grfAccessMode,
					lpExpAces[i].grfAccessPermissions,
					lpExpAces[i].grfInheritance
				);

				break;
			}

			default:
				break;
			}
		}

		LocalFree(lpExpAces);
	}
}

void AccessControlList::Add(
	_In_ Principal _pr,
	_In_ AccessType Type,
	_In_ ACCESS_MASK Permissions,
	_In_ DWORD InheritFlag,
	_In_ bool NotBinding
) {
	ACCESS_MODE __desired_access_mode;
	switch (Type) {
	case AccessType::Allow:
	{
		__desired_access_mode = ACCESS_MODE::GRANT_ACCESS;
		break;
	}
	case AccessType::Deny:
	{
		__desired_access_mode = ACCESS_MODE::DENY_ACCESS;
		break;
	}
	case AccessType::AllowNM:
	{
		__desired_access_mode = ACCESS_MODE::SET_ACCESS;
		break;
	}
	
	default:
		__desired_access_mode = ACCESS_MODE::NOT_USED_ACCESS;
		break;

	}

	EXPLICIT_ACCESS_W ExpAce{};
	SecureZeroMemory(&ExpAce, sizeof(ExpAce));

	BuildExplicitAccessWithNameW(
		&ExpAce,
		const_cast<LPWSTR>(_pr.GetName().c_str()),
		Permissions,
		__desired_access_mode,
		InheritFlag
	);

	PACL lpExistingAcl = nullptr;

	if (!NotBinding) {
		lpExistingAcl = this->lpAcl;
	}

	PACL RpNewAcl = nullptr;

	std::unique_ptr<::ACL, _HLOCAL_deleter> pNewAcl = nullptr;

	DWORD dwRet = SetEntriesInAclW(
		1,
		&ExpAce,
		lpExistingAcl,
		&RpNewAcl
	);
	if (dwRet) {
		throw std::runtime_error("既存の ACL に新規の ACE のマージに失敗しました");

		return;
	}

	pNewAcl.reset(RpNewAcl);

	dwRet = SetNamedSecurityInfoW(
		const_cast<LPWSTR>(this->Name.c_str()),
		this->Type,
		DACL_SECURITY_INFORMATION,
		nullptr,
		nullptr,
		pNewAcl.get(),
		nullptr
	);

	if (dwRet) {
		throw std::runtime_error("オブジェクトの ACL の変更に失敗しました");

		return;
	}

	this->Update();

	return;
}

void AccessControlList::SetOwner(
	_In_ Principal _pr
) {
	DWORD dwRet = SetNamedSecurityInfoW(
		const_cast<LPWSTR>(this->Name.c_str()),
		this->Type,
		OWNER_SECURITY_INFORMATION,
		_pr.GetSid(),
		nullptr,
		nullptr,
		nullptr
	);

	if (dwRet) {
		throw std::runtime_error("オブジェクトの ACL の変更に失敗しました");

		return;
	}

	this->Update();

	return;
}

void AccessControlList::Revoke(
	_In_ Principal _pr,
	_In_ DWORD InheritFlag
) {
	EXPLICIT_ACCESS_W ExpAces{};
	SecureZeroMemory(&ExpAces, sizeof(ExpAces));

	BuildExplicitAccessWithNameW(
		&ExpAces,
		const_cast<LPWSTR>(_pr.GetName().c_str()),
		GENERIC_ALL,
		ACCESS_MODE::REVOKE_ACCESS,
		InheritFlag
	);

	PACL RpNewAcl = nullptr;

	std::unique_ptr<::ACL, _HLOCAL_deleter> pNewAcl = nullptr;

	DWORD dwRet = SetEntriesInAclW(
		1,
		&ExpAces,
		this->lpAcl,
		&RpNewAcl
	);
	if (dwRet) {
		throw std::runtime_error("既存の ACL に新規の ACE のマージに失敗しました");

		return;
	}

	pNewAcl.reset(RpNewAcl);

	dwRet = SetNamedSecurityInfoW(
		const_cast<LPWSTR>(this->Name.c_str()),
		this->Type,
		DACL_SECURITY_INFORMATION,
		nullptr,
		nullptr,
		pNewAcl.get(),
		nullptr
	);

	if (dwRet) {
		throw std::runtime_error("オブジェクトの ACL の変更に失敗しました");

		return;
	}

	this->Update();

	return;
}

const std::unordered_map<
	std::shared_ptr<Principal>,
	std::tuple<
	ACCESS_MODE,
	ACCESS_MASK,
	DWORD
	>
>& AccessControlList::EnumEntries() {
	this->Update();

	return this->Aces;
}

const std::tuple<
	ACCESS_MODE,
	ACCESS_MASK,
	DWORD
>& AccessControlList::GetEntry(
	_In_ Principal _principal
) {
	this->Update();

	for (auto& [_pr, _ace] : this->Aces) {
		if (*_pr != _principal) continue;

		return _ace;
	}

	throw std::runtime_error("ACL の中に、引数として渡された Principal クラスが示すユーザーが存在しませんでした");
	return {};
}

const Principal& AccessControlList::GetOwner() {
	return *Owner;
}

AccessControlList::~AccessControlList() {}
```
</details>

# 概要
Win32API の関数を直接使用し、ファイルの ACL を制御する時は、以下のような処理を行う。

| 関数 | 処理 |
|:-:|:-:|
| GetNamedSecurityInfo | ファイルから既存の ACL を取得 |
| BuildExplicitAccessWithName | EXPLICIT_ACCESS 構造体 (個々の ACE を表現) をユーザー名・グループ名から初期化|
| SetEntriesInAcl | 既存の ACL に初期化した EXPLICIT_ACCESS 構造体が示す ACE を追加・削除 |
| SetNamedSecurityInfo | ファイルの ACL を更新 |

※ 処理の順番は上から下に

これらの処理だけでは 既存の ACL に含まれる ACE や 所有者の解析が含まれないため、単純だが、それらを行う場合は、SID 等を扱うため、どうしても処理が複雑になってしまう。特に LookupAccountName 等の関数は、**LocalAlloc** と呼ばれる関数 (malloc の Win32API 版) を使用するケースが多いため (多分)、下手に使うと**バグの温床**となってしまう。

そのため、このソースには、3つのクラス・構造体と 1つの列挙型を定義し、メモリ管理をスマートポインタに委ねることで、メモリリーク等のバグの防止や、コードの使いまわしを実現する。

|名前|型|概要|
|:-:|:-:|:-:|
| Principal| クラス |**ユーザー・グループを表現。** アカウント名、SID (構造体)、SID (文字列) の情報を持たせる。|
| AccessControlList | クラス | **ACL を表現。** ACE の操作や、所有者の変更などのメソッドを提供する。|
| _HLOCAL_deleter | 構造体 | LocalAlloc 関数により確保されたヒープメモリ領域の開放を行うデリータ。 (スマートポインタ)|
| AccessType | 列挙型 | ACE のアクセスの種類 (許可・拒否) を示す。値については[こちら](https://learn.microsoft.com/ja-jp/windows/win32/api/accctrl/ne-accctrl-access_mode)を参照。|

またファイルの ACL が変更された場合を想定して、クラス内の ACL と**同期**するため、メソッドを呼び出す際に、ACL 内の ACE と、所有者をもう一度取得し直す。(AccessControlList::Update メソッド)
この Update メソッドは任意のタイミングでの呼び出しも可能。

# サンプル
## C:\hoge\foo.txt の ACE に すべての権限 (F) (許可) を Everyone に付与するプログラム
```c++:wWinMain.cpp
#include "LUsr.h"

INT APIENTRY wWinMain(
	_In_		HINSTANCE	hCurInst,
	_In_opt_	HINSTANCE	/* hPrvInst = nullptr */,
	_In_		LPWSTR		lpCmdLine,
	_In_		INT			nCmdShow
) {
   AccessControlList acl(L"C:\\hoge\foo.txt", SE_OBJECT_TYPE::SE_FILE_OBJECT);

	acl.Add(
		Principal(L"Everyone"),
		AccessType::Allow,
		GENERIC_ALL,
		NO_INHERITANCE
	);

	return 0;
}
``` 

## HKLM\\SOFTWARE\\Policies の ACE に 読み取り権限 (R) (拒否) を Administrators に付与し、下層にあるすべてのコンテナ・オブジェクトに継承させ、SYSTEM のアクセス許可を削除するプログラム
```c++:wWinMain.cpp
#include "LUsr.h"

INT APIENTRY wWinMain(s
	_In_		HINSTANCE	hCurInst,
	_In_opt_	HINSTANCE	/* hPrvInst = nullptr */,
	_In_		LPWSTR		lpCmdLine,
	_In_		INT			nCmdShow
) {
   AccessControlList acl(L"MACHINE\\SOFTWARE\\Policies", SE_OBJECT_TYPE::SE_REGISTRY_KEY);

	acl.Add(
		Principal(L"Administrators"),
		AccessType::Deny,
		GENERIC_READ,
        SUB_CONTAINERS_AND_OBJECTS_INHERIT
	);

    acl.Revoke(Principal("SYSTEM"));

	return 0;
}
``` 
## C:\fuga\baz.exe の所有者を Everyone に変更する
```c++:wWinMain.cpp
#include "LUsr.h"

INT APIENTRY wWinMain(
	_In_		HINSTANCE	hCurInst,
	_In_opt_	HINSTANCE	/* hPrvInst = nullptr */,
	_In_		LPWSTR		lpCmdLine,
	_In_		INT			nCmdShow
) {
   AccessControlList acl(L"C:\\fuga\\baz.exe", SE_OBJECT_TYPE::SE_FILE_OBJECT);

	acl.SetOwner(Principal(L"Everyone"));

	return 0;
}
```

:::note alert
所有者の変更は、**有効なアクセス許可が存在しないと実行できません。**
アクセス許可がない場合は、SeTakeOwnershipPrivilege 特権を有効にし、所有者を Administrators に設定して、有効なアクセス許可を追加してから、変更する必要があります。
:::

# 最後に
ラップしている以上、**すべての ACL 操作系の関数に対応した操作を行うことはできない。** そのため、もし使う場合、足りない点があれば、これらのクラスに自由に付け加えたりしてほしい。また、ACL は **使い方次第では、Windows のセキュリティが低くなってしまう可能性がある。** ある特定の操作を行えば、システムファイル等へのアクセスが誰でも出来るようにもなってしまう (管理者権限がある中でもし実行してしまった場合に限る)。そのため、確実な安全を考慮しながら使用してもらいたい。