65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (22投票s)

2004年11月30日

CPOL

11分钟阅读

viewsIcon

98531

downloadIcon

1447

如何保护您的对象。

引言

我们大多数人习惯于在那些需要 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_ALLAllow()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 和发帖者(匿名且胆小 :-))
© . All rights reserved.