使用访问控制列表保护对象访问






4.81/5 (22投票s)
如何保护您的对象。
引言
我们大多数人习惯于在那些需要 SECURITY_ATTRIBUTES
结构体的 WIN32 API 调用中传递 NULL 作为指针。当我们创建文件时调用 CreateFile
,并传递一个 NULL 指针。传递 NULL 指针会让 Windows 为我们的对象提供一个默认的安全描述符。我可以在这里写很多关于默认安全描述符的描述,但 Raymond Chen[^] 比我能做得更好。
然而,Windows NT 及其后续操作系统如果使用得当,可以提供相当好的安全级别。Windows NT 安全模型基于访问控制列表 (ACL)。系统中的每个对象都有一个与之关联的安全描述符;每个安全描述符要么有一个 ACL,其中指定了用户/组列表以及授予或拒绝每个用户的权限,要么根本没有 ACL,在这种情况下,对象会获得默认的安全描述符。您可以选择指定谁可以或不可以访问您的对象。您系统上的其他帐户可以指定他们自己的访问标准,其中一些帐户甚至可能拒绝系统管理员的访问。
详细说明
ACL 有两种。系统 ACL (SACL) 用于提供访问给定对象的尝试者的审计跟踪,而自主访问控制列表 (DACL) 用于指定谁可以或不可以访问给定对象。每个 ACL 又由 ACE(访问控制条目)列表组成。每个 ACE 指定一个特定的 SID(安全标识符,代表系统上的特定用户或组)、授予该 SID 的访问权限以及这是一个拒绝 ACE 还是允许 ACE。我这里做了简化;还有其他类型的 ACE,但我这里提供的类不包含它们,所以我也不会介绍。
拒绝 ACE 顾名思义,就是拒绝访问。它根据添加到 ACL 时传递的权限集,拒绝指定 SID 对对象的访问。相反,允许 ACE 根据...的集授予指定 SID 对对象的访问权限。嗯,您明白意思了。
将 ACE 添加到 ACL 的顺序很重要。所有拒绝 ACE 都应该在添加允许 ACE 之前添加到 ACL。Larry Osterman 在 这篇文章[^] 中对此进行了很好的描述。
创建 ACL 后,将其添加到安全描述符,将安全描述符嵌入安全属性结构,然后将该结构传递给创建您要控制的对象的相应系统调用,然后就可以开始了。注意我说的是“创建”?如果您访问的对象是由该对象的创建者设置的安全描述符和 ACL 控制的现有对象;您对现有对象的访问受创建者指定的标准的约束,并且没有 API 可以调用来覆盖它。
如果您阅读了 MSDN 关于该主题的文档,您可能会认为我遗漏了一些内容。对象的无 DACL 与拥有一个空的 DACL 之间存在重要区别。如果您创建一个 ACL 但不向其中添加任何 ACE,您将得到一个空的 ACL,这将导致系统拒绝所有访问该对象的尝试。如果您甚至不创建 DACL 而只是传递 NULL,那么对象根本没有 DACL,这意味着所有访问请求都将被授予。这回到了文章开头说的;我们都习惯于在那些期望 SECURITY_ATTRIBUTES
结构指针的 WIN32 API 调用中传递 NULL。所有期望 SECURITY_ATTRIBUTES
结构指针的 WIN32 API 调用都将 NULL 解释为默认安全描述符。
繁琐的细节
乍一看,处理 ACL 和 ACE 的 API 集似乎令人困惑。例如,有人创建了一个 ACL,但没有创建要添加到 ACL 的 ACE。相反,有人调用函数,这些函数接受指向 ACL 和其他一些信息的指针,并将 ACE 添加到 ACL。这些函数假定 ACL 是在您分配的内存缓冲区中创建的,但正如名称所示,它是一个列表,因此您必须确保分配了足够大的缓冲区来容纳完整的 ACL,包括所有 ACE。如果您提前知道要将多少 ACE 添加到 ACL 中,您可以进行编译时计算并分配正确的内存量;否则,您必须采用更困难的方式。您认为您多久会在编译时知道要添加多少 ACE?我想也是……困难的方式是分配足够的内存来容纳 ACL,并在每次要添加 ACE 时重新分配内存块(并进行复制)。
或者您可以采取我的方法,即延迟创建 ACL,直到需要为止。您创建一个 CAccessControlList
实例,并将用户名或组名添加到其中,指定该名称允许或拒绝的访问权限,完成后,当您需要 ACL 时,它就会被创建。该类的外观如下。
class CAccessControlList
{
enum eAceType
{
eAllow,
eDeny,
};
class CAccessEntry
{
public:
CAccessEntry(eAceType type, PSID pSid, UINT uiRights);
~CAccessEntry();
UINT m_uiRights;
PSID m_pSid;
eAceType m_type;
};
public:
CAccessControlList();
virtual ~CAccessControlList();
PACL operator&();
bool Allow(LPCTSTR pszName,
UINT uiRights = STANDARD_RIGHTS_ALL | GENERIC_ALL,
LPCTSTR pszServer = NULL);
bool Deny(LPCTSTR pszName,
UINT uiRights = STANDARD_RIGHTS_ALL | GENERIC_ALL,
LPCTSTR pszServer = NULL);
private:
void AddAces(eAceType type);
list<CAccessEntry *> m_lAce;
PACL m_pAcl;
UINT m_uiAclSize;
};
有一个 UINT
成员,在构造函数中初始化为 sizeof(ACL)
。当我们向类添加帐户名或组时,我们会添加相关 ACE 及其 SID 的长度。到创建 ACL 时,我们就知道需要分配多少内存。
您可以通过调用 Allow()
和 Deny()
来添加帐户名或组。您还可以指定作为第二个参数允许或拒绝的访问权限;默认值为 STANDARD_RIGHTS_ALL | GENERIC_ALL
。Allow()
和 Deny()
函数会检索指定用户或组名的 SID,然后创建一个包含帐户或组名、权限和类型(允许或拒绝)的 CAccessEntry
实例,并将该类实例添加到 m_lAce
列表中。此时我们还不能创建 ACE,因为没有独立于 ACL 创建 ACE 的 API,并且如前所述,此时我们不知道 ACL 将包含多少 ACE,因此我们不知道为它分配多少内存。用于将条目添加到列表并计算要添加到总 ACL 大小的代码如下。
bool CAccessControlList::Allow(LPCTSTR pszName, UINT uiRights,
LPCTSTR pszServer)
{
assert(pszName);
PSID pSid = GetSid(pszName, pszServer);
if (pSid != PSID(NULL) && IsValidSid(pSid))
{
m_uiAclSize += sizeof(ACCESS_ALLOWED_ACE) +
GetLengthSid(pSid) - sizeof(DWORD);
m_lAce.push_back(new CAccessEntry(eAllow, pSid, uiRights));
return true;
}
return false;
}
Deny()
函数几乎相同。Allow()/Deny()
函数中唯一真正有趣的是调用 GetSid()
,它封装了查询 SID 所需缓冲区大小、分配内存和查询 SID 本身的过程。与许多 Windows API 一样,您需要调用两次相同的 API,一次将缓冲区长度设置为零,这将返回所需大小,第二次执行实际工作。
那么 SID 是什么?
SID 是安全标识符;它代表系统上的用户或组。如果我们只处理本地计算机,用户名或组名就足够了。但您可能已经注意到,上面 Allow()
和 Deny()
函数接受第三个参数,即服务器名称,默认为 NULL。NULL 当然意味着在本地计算机上查找此用户或组名。但是,正是用户或组帐户可能定义在本地计算机之外这一事实使得 SID 有用;SID 包含足够的信息,可以告诉本地安全机构,如果 SID 指向本地计算机,它就可以信任本地系统帐户信息;否则,本地安全机构必须转到域控制器进行最终裁决。无论哪种方式,SID 都是一个唯一标识帐户的数据块。
拥有 SID 并不赋予任何特殊权限。您可以获取代表域系统管理员的 SID,而您拥有的只是一个代表域系统管理员的 SID!您不能使用 SID 来提升您的权限;您能做的就是为该特定 SID 设置对您对象的访问权限。
GetSid()
函数如下所示
PSID GetSid(LPCTSTR pszName, LPCTSTR pszServer)
{
PSID pSid = PSID(NULL);
TCHAR ptszDomainName[256];
DWORD dwSIDSize = 0,
dwDomainNameSize = sizeof(ptszDomainName);
SID_NAME_USE snuType = SidTypeUnknown;
LookupAccountName(pszServer, pszName, NULL, &dwSIDSize, NULL,
&dwDomainNameSize, &snuType);
if (dwSIDSize)
{
// Success, now allocate our buffers
pSid = (PSID) new BYTE[dwSIDSize];
if (!LookupAccountName(NULL, pszName, pSid, &dwSIDSize,
ptszDomainName, &dwDomainNameSize,
&snuType))
{
// failed, delete the SID buffer
delete pSid;
pSid = PSID(NULL);
}
}
return pSid;
}
这是一个全局函数而不是类成员,原因有两个。第一个是它确实不属于任何类;它不触及属于类的任何特定于实例的数据;第二个原因是稍后我将在第二个类中使用它。其中有一些陷阱。MSDN 文档指出,如果您调用 LookupAccountName()
API,并将 NULL 作为 SID 结构指针传递,并将结构大小设置为 0,则该函数会返回所需大小。确实如此,但其措辞暗示函数返回值是所需大小。它不是那样返回的。该函数返回 0,但会将 dwSIDSize
指向的 DWORD
设置为所需大小。这就是为什么我不检查函数返回值。
通过解引用对象来访问 ACL(并创建它)。operator&
重载会删除 m_pAcl
成员并重新创建它。函数如下所示
PACL CAccessControlList::operator&()
{
delete m_pAcl;
m_pAcl = (PACL) new BYTE[m_uiAclSize];
// Change the ACL_REVISION_DS constant to ACL_REVISION if you want to
// support Windows NT 4.0 or earlier
InitializeAcl(m_pAcl, m_uiAclSize, ACL_REVISION_DS);
AddAces(eDeny);
AddAces(eAllow);
return m_pAcl;
}
没有什么特别的,除了我们确实必须迭代我们的帐户列表两次。第一次遍历添加拒绝 ACE,第二次遍历添加允许 ACE。请注意,对类调用重载的 & 运算符价格不菲;每次调用运算符时,ACL 都会被删除并重新创建;如果您知道允许和拒绝用户列表没有改变,那么缓存 ACL 指针可能是一个好主意。
现在我有了 ACL,该怎么做?
这就是我们在与 CreateFile()
等调用相关的函数中看到的安全性属性发挥作用的地方。您可以在内存中创建一个 SECURITY_ATTRIBUTES
结构,对其进行初始化,然后创建一个 SECURITY_DESCRIPTOR
结构,将您的 ACL 存储在 SECURITY_DESCRIPTOR
结构中,然后……嗯,如果您想传递比 NULL 更多的东西作为该参数,那么将其包装在一个类中确实很有意义。
CAccessAttributes
是一个类,至少可以用来替换那个无处不在的 NULL 指针。如果您确实想将 NULL 指针传递给 SECURITY_ATTRIBUTES
结构,那么就直接使用 NULL;但如果您想做更多的事情,可以使用 CAccessAtributes
实例,它会为您提供更多控制。
该类看起来像这样
class CAccessAttributes
{
public:
CAccessAttributes(PACL pDacl = NULL);
virtual ~CAccessAttributes();
LPSECURITY_ATTRIBUTES operator&() { return &m_sa; }
void SetDACL(PACL pAcl);
void SetSACL(PACL pAcl);
bool SetOwner(LPCTSTR pszOwner = NULL,
LPCTSTR pszServer = NULL);
bool SetGroup(LPCTSTR pszGroup, LPCTSTR pszServer = NULL);
private:
SECURITY_ATTRIBUTES m_sa;
SECURITY_DESCRIPTOR m_sd;
PSID m_pOwnerSid,
m_pGroupSid;
};
该类封装了一个 SECURITY_ATTRIBUTES
结构,该结构被初始化为包含一个空的 DACL。operator&()
成员返回 SECURITY_ATTRIBUTES
结构的地址。如果您只做实例化 CAccessAttributes
类,并使用 operator&()
将其传递给 WIN32 API,那么您就使您的对象对系统上的所有人完全不可访问。这是因为在 operator&()
调用中创建的 ACL 是通过 InitializeAcl()
API 初始化并设置为具有空 ACL 的。如果您至少一次没有在对象上调用 Allow()
,那么将没有允许 ACE,因此系统将不会授予任何人访问权限。这与传递 NULL 作为 SECURITY_ATTRIBUTES
结构的情况截然不同,在这种情况下,您会获得默认的安全描述符。或者,您可以创建 CAccessControlList
的实例或从其他来源获取 ACL,调用 SetDACL()
或 SetSACL()
并传递 ACL,然后您就拥有了一个 SECURITY_ATTRIBUTES
结构,该结构设置了使用类实例创建的任何对象的权限。
构造函数如下所示
CAccessAttributes::CAccessAttributes(PACL pAcl)
{
// Initialise our security attributes to give access to anyone.
memset(&m_sa, 0, sizeof(m_sa));
m_sa.nLength = sizeof(m_sa);
InitializeSecurityDescriptor(&m_sd, SECURITY_DESCRIPTOR_REVISION);
m_pGroupSid = m_pOwnerSid = PSID(NULL);
SetOwner();
SetDACL(pAcl);
}
没有什么特别的,除了调用 SetOwner()
。该函数所做的就是检索代表当前线程运行的帐户的 SID,并将其设置为 SECURITY_DESCRIPTOR
的所有者。这是真的,但您可能已经注意到 SetOwner()
函数默认所有者为 NULL;如果为 NULL,我们则获取当前用户名并将其设置为所有者。您可以通过在对象实例化后调用 SetOwner()
并传入任何用户/组名来覆盖构造函数。
使用代码
很难构建一个有意义的示例项目来演示这些类的使用。我有这样的项目,但它们不是我想公之于众的项目。我也无法创建一个能够在一无所有系统中明确工作的项目,除非规定您必须创建用户某某和组如此这般,并使用户 X 成为组 Y 的成员。所以您只能相信我。尽管如此,这里有一种使用这些类的方法。只需接受任意的用户和组名。// Create our DACL
CAccessControlList acl;
acl.Allow(_T("EveryOne"));
acl.Deny(_T("stan"));
// Now create a SECURITY ATTRIBUTES object and
// set the DACL to the ACL we just created.
CAccessAttributes aa(&acl);
::CreateFile(filename, desiredAccess, shareMode, &aa, ...);
这会创建一个 ACL,允许访问“Everyone”(一个代表可以连接到系统的所有人的组),但拒绝访问“stan”。然后我们创建一个 CAccessAttributes
对象,并将 ACL 传递给它,该 ACL 将被设置为对象的 DACL。然后我们创建一个具有各种访问模式和共享模式的文件,但传递我们的 SECURITY_ATTRIBUTES
对象。假设文件创建成功,则该文件现在可供“Everyone”访问,但“stan”将被拒绝访问。- 初始版本 - 2004 年 11 月 30 日。
- 2004 年 12 月 1 日 - 更新以纠正一些错误的陈述(我错了!)。感谢 Nemanja Trifunovic 和发帖者(匿名且胆小 :-))