0%

Windows文件系统概述(四)安全描述符

本文将介绍Windows文件系统中安全描述符(Security Descriptor)

Security Desciptor与Linux ACL

Windows安全描述符和Linux ACL(访问控制列表)都是操作系统用于管理文件和目录权限的机制。尽管它们的目的相似,但它们的实现和功能有所不同。以下是 Windows安全描述符和Linux ACL之间的一些主要区别:

  • 操作系统:Windows安全描述符用于Microsoft Windows操作系统,而Linux ACL用于基于Linux的操作系统。
  • 数据结构:Windows安全描述符使用一个复杂的数据结构,包括安全标识符(SID)、访问控制列表(ACL)和一组其他属性。Linux ACL由一系列的访问控制项(ACE)组成,每个ACE都包含一个标识符(用户或组)、权限和权限类型。
  • 继承:Windows安全描述符可以支持多层次的权限继承,文件和目录可以从父目录继承权限。Linux ACL也支持继承,但实现方式略有不同,使用默认ACL来定义新文件和目录的权限。
  • 权限管理:Windows安全描述符提供了详细的权限管理,包括读取、写入、执行、删除等多种操作。Linux ACL也提供了类似的权限管理,包括读取、写入和执行(rwx),但相对来说较为简单。
  • 权限类型:Windows安全描述符包括两种类型的 ACL,即访问控制列表(DACL)和系统访问控制列表(SACL)。DACL用于控制文件和目录的访问权限,而 SACL用于记录对象的访问事件。Linux ACL只提供一种 ACL类型,用于控制访问权限。
  • 用户和组:Windows和Linux都使用用户和组来管理权限,但它们的实现方式不同。Windows使用安全标识符(SID)来表示用户和组,而Linux使用用户 ID(UID)和组 ID(GID)。
  • 工具和命令:Windows和Linux使用不同的工具和命令来管理文件和目录的权限。Windows中常用的工具包括图形界面和命令行工具,如“icacls”和“cacls”。在Linux系统中,可以使用命令如“chmod”、“chown”和“setfacl”等来管理权限。

尽管 Windows安全描述符和Linux ACL在实现和功能上有所不同,但它们的核心目标是相同的,即为操作系统提供一种灵活、安全的权限管理机制。

ACE字符串

Windows访问控制项(ACE)字符串是用于表示访问控制列表(ACL)中每个访问控制项的文本表示。它用于指定特定安全主体(如用户或组)的权限。ACE 字符串的结构包括以下几个组成部分:

  • 信任主体(Trustee):表示安全主体的安全标识符(SID)或名称,通常是一个用户、组或计算机。信任主体可以是域用户、本地用户、内置用户(如 “SYSTEM” 或 “NETWORK SERVICE”)等。
  • 访问权限:指定允许或拒绝的访问权限。这些权限通常包括读取、写入、执行、删除、修改权限等。权限可以用缩写表示,例如,”R” 代表读取权限,”W” 代表写入权限,”X” 代表执行权限等。
  • 访问控制类型(Access Control Type):表示授予或拒绝权限的类型。通常有两种类型,即允许(Allow,用 “A” 表示)和拒绝(Deny,用 “D” 表示)。
  • 继承标志(Inheritance Flags):指示ACE是否应用于子对象(如子文件夹和文件)。常用的继承标志包括 “OI”(仅对象继承)、”CI”(仅容器继承)和 “IO”(继承只)。这些标志可以组合使用,例如 “OI|CI” 表示ACE应用于子对象和容器。
  • 附加选项(Optional Flags):ACE 字符串可以包含附加选项,如继承属性 “NP”(不传播继承)或 “ID”(继承的)。这些选项有时用于控制更复杂的继承和权限行为。

下面是一个示例ACE字符串:

D:(A;OICI;FA;;;Administrator)

  • “D” 代表它是一个访问控制项的描述符(Discretionary)。
  • “A” 表示允许(Allow)权限。
  • “OICI” 是继承标志,表示ACE应用于子对象(Object Inherit)和容器(Container Inherit)。
  • “FA” 代表完全访问(Full Access)权限。
  • “Administrator” 是信任主体,指示这个ACE应用于 “Administrator” 用户。

此示例表示允许 “Administrator” 用户对文件或文件夹及其子对象具有完全访问权限。

Security Descriptor WIN32 API

获取安全描述符

在C++中,你可以使用WindowsAPI来获取文件的完整安全描述符信息,包括SACL(系统访问控制列表)、DACL(访问控制列表)、属组(Group)和属主(Owner)。以下是一个示例,演示如何获取这些信息:

#include <Windows.h>
#include <iostream>
#include <Aclapi.h>
#include <string>
#include <optional>

std::optional<std::wstring> GetSecurityDescriptorW(const std::wstring& wPath)
{
PSECURITY_DESCRIPTOR pSecurityDescriptor = nullptr;
PACL pDacl = nullptr;
PACL pSacl = nullptr;
PSID psidOwner = nullptr;
PSID psidGroup = nullptr;
LPWSTR wSddlStr = nullptr;

DWORD result = ::GetNamedSecurityInfoW(
wPath.c_str(),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION,
&psidOwner,
&psidGroup,
&pDacl,
&pSacl,
&pSecurityDescriptor);
if (result != ERROR_SUCCESS) {
return std::nullopt;
}
bool ret = ::ConvertSecurityDescriptorToStringSecurityDescriptorW(
pSecurityDescriptor,
SDDL_REVISION_1,
DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION,
&wSddlStr,
nullptr);
::LocalFree(pSecurityDescriptor);
if (!ret) {
return std::nullopt;
}
std::wstring res(wSddlStr);
::LocalFree(wSddlStr);
return std::make_optional<std::wstring>(res);
}

该函数实现了两件事:

  1. 使用GetNamedSecurityInfoW从文件中获取安全描述符指针PSECURITY_DESCRIPTOR pSecurityDescriptor
  2. ConvertSecurityDescriptorToStringSecurityDescriptorWPSECURITY_DESCRIPTOR pSecurityDescriptor中存放的所有信息(DACL、SACL、Owner和Group)序列化成ACE字符串。

psidOwnerpsidGrouppDaclpSacl分别指向文件的Owner、Group、DACL、SACL。pSecurityDescriptor指向文件的安全描述符信息,pSecurityDescriptor可以看成包含了上述所有信息。在使用完pSecurityDescriptor后需要用LocalFree()释放掉pSecurityDescriptor指向的资源。psidOwnerpsidGrouppDaclpSacl如果不需要使用可以传入nullptr,且不需要释放资源。

GetNamedSecurityInfoW的第二个参数DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION用bitmap的形式声明要获取的安全描述符类型为DACL、Owner和Group。这里没有传入SACL_SECURITY_INFORMATION是因为SACL的获取需要进程有管理员权限,且需要开启SE_SECURITY_NAME权限,有关权限提升这部分内容后续章节再详细说明。

之前使用GetNamedSecurityInfoW成功获取了pSecurityDescriptor指针后就可以用ConvertSecurityDescriptorToStringSecurityDescriptorW将它其中的内容序列化ACE字符串了,这里直接传入DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION序列化所有信息即可。

设置安全描述符

Windows提供SetNamedSecurityInfo API用于设置安全描述符。设置安全描述符需要提供指向Owner、Group、DACL、SACL的指针,即上文GetNamedSecurityInfoW获得的psidOwnerpsidGrouppDaclpSacl。而对于上文序列化为字符串的安全描述符,则需要用ConvertStringSecurityDescriptorToSecurityDescriptor先将它解析成pSecurityDescriptor指针,再从中获得psidOwnerpsidGrouppDaclpSacl。以下是从ACE字符串中设置安全描述符的具体实现:

bool SetSecurityDescriptorW(const std::wstring& wPath, const std::wstring& wSddlStr)
{
PSECURITY_DESCRIPTOR pSecurityDescriptor = nullptr;
PACL pDacl = nullptr;
PACL pSacl = nullptr;
PSID psidOwner = nullptr;
PSID psidGroup = nullptr;
SECURITY_INFORMATION flags = DACL_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION;

/* Get SACL need to enable privilege SE_SECURITY_NAME */
if (!EnablePrivilege(SE_SECURITY_NAME)) {
flags |= SACL_SECURITY_INFORMATION;
}

/* convert sddlStr back to pSecurityDescriptor */
if (!::ConvertStringSecurityDescriptorToSecurityDescriptor(
wSddlStr.c_str(),
SDDL_REVISION_1,
&pSecurityDescriptor,
nullptr)) {
return false;
}

/* Get Owner from pSecurityDescriptor */
BOOL ownerDefaulted = false;
if (!::GetSecurityDescriptorOwner(pSecurityDescriptor, &psidOwner, &ownerDefaulted)) {
::LocalFree(pSecurityDescriptor);
return false;
}w

/* Get Group from pSecurityDescriptor */
BOOL groupDefaulted = false;
if (!::GetSecurityDescriptorGroup(pSecurityDescriptor, &psidGroup, &groupDefaulted)) {
::LocalFree(pSecurityDescriptor);
return false;
}

/* Get DACL from pSecurityDescriptor */
BOOL bDaclPresent = false;
BOOL bDaclDefaulted = false;
if (!::GetSecurityDescriptorDacl(pSecurityDescriptor, &bDaclPresent, &pDacl, &bDaclDefaulted) || !bDaclPresent) {
::LocalFree(pSecurityDescriptor);
return false;
}

/* Get SACL from pSecurityDescriptor */
BOOL bSaclPresent = false;
BOOL bSaclDefaulted = false;
if (!::GetSecurityDescriptorSacl(pSecurityDescriptor, &bSaclPresent, &pSacl, &bSaclDefaulted) || !bSaclPresent) {
::LocalFree(pSecurityDescriptor);
return false;
}

// set pSecurityDescriptor to file
if (::SetNamedSecurityInfo(
const_cast<LPWSTR>(wPath.c_str()),
SE_FILE_OBJECT,
flags,
psidOwner,
psidGroup,
pDacl,
pSacl) != ERROR_SUCCESS) {
::LocalFree(pSecurityDescriptor);
return false;
}

if (pSecurityDescriptor) {
::LocalFree(pSecurityDescriptor);
}
return true;
}

以上代码虽然能编译成功,但是执行时会失败。首先执行上述代码需要管理员权限,其次,即使再管理员权限下运行,SetNamedSecurityInfo依然可能返回ERROR_INVALID_OWNER错误。可能ACE字符串解析出的Owner无效或不正确,可以用IsValidSid检测Owner是否合法。也可能当前进程没有分配SeRestorePrivilege特权。

权限提升

首先定义权限提升方法:

static bool EnablePrivilege(const wchar_t* privilegeName)
{
HANDLE hToken;
char buf[sizeof(TOKEN_PRIVILEGES) * 2];
TOKEN_PRIVILEGES& tkp = *((TOKEN_PRIVILEGES*) buf);

if (!::OpenProcessToken(
GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
&hToken)) {
return false;
}

/* enable SeBackupPrivilege, SeRestorePrivilege */
if (!::LookupPrivilegeValue(
nullptr,
privilegeName,
&tkp.Privileges[0].Luid)) {
return false;
}

if (!::LookupPrivilegeValue(
nullptr,
SE_RESTORE_NAME,
&tkp.Privileges[1].Luid)) {
return false;
}

tkp.PrivilegeCount = 2;
tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
tkp.Privileges[1].Attributes = SE_PRIVILEGE_ENABLED;

::AdjustTokenPrivileges(hToken, false, &tkp, sizeof(tkp), nullptr, nullptr);
return true;
}

然后在调用上文提到的API之前调用EnablePrivilege(SE_RESTORE_NAME)提升进程权限:

int main() {
// To get SACL
if (!EnablePrivilege(SE_SECURE_NAME)) {
std::cerr << "Error enabling SeSecurePrivilege" << std::endl;
return -1;
}
// Set ACL
if (!EnablePrivilege(SE_RESTORE_NAME)) {
std::cerr << "Error enabling SeRestorePrivilege" << std::endl;
return -1;
}
// invoke GetSecurityDescriptorW or SetSecurityDescriptorW here ...
return 0;
}

Disqus评论区没有正常加载,请使用科学上网