• 文章
  • 如何使用 Windows 授权
发布
2014年1月12日(最后更新:2014年1月19日)

如何使用 Windows 授权

评分:3.3/5(98票)
*****
本文旨在帮助读者对 discretionary access control lists (DACLs) 及其 access control entries (ACEs) 有中间程度的理解。读者应牢固掌握 C++,并对 Microsoft 的 Windows 平台及其 WinAPI 有一定经验,如果希望测试此代码,还需要能够访问运行 Windows 的系统。此代码在任何 POSIX 平台上都无法正常工作。请参阅页面底部的源代码链接。

DACL 是文件或文件夹属性页“安全”选项卡下的权限列表。它们用于管理用户或组对特定文件或目录的访问权限。没有 DACL 的文件允许 Everyone 完全访问,请注意,Everyone 是 Microsoft Windows 平台中的一个安全组,旨在包含所有用户、组和服务。如果存在 DACL,则仅允许它明确列出的用户或组的访问权限。没有列出权限的用户或组将被拒绝访问(文件或文件夹的所有者用户或组是例外),这意味着一个空白 DACL 的文件与没有 DACL 的文件不同,因为空白 DACL 会拒绝所有用户和组的任何访问。

以下代码将在可执行文件运行的当前工作目录中创建一个文件(名为“New.txt”),此目录中同名同扩展名的文件将被删除。首次创建时,文件将具有系统授予的默认安全权限以及从其父目录继承的任何权限。鼓励读者在第 111 行给出的暂停期间检查这些权限,并将输出(第 108 行)与其中列出的内容进行比较。代码中大部分使用的函数和结构都提供了指向相应 MSDN 条目的注释。此项目应链接 AdvApi32.lib 和 User32.lib(对于 MSVS 用户)或 libadvapi32.a 和 libuser32.a(对于 Mingw 用户)。

我们的代码执行的第一个实际工作是在第 27 行声明一个指向 SECURITY_DESCRIPTOR 结构的指针。该结构使用“new”运算符在内存中分配空间,并使用“InitializeSecurityDescriptor()”函数进行初始化。此函数将 SECURITY_DESCRIPTOR 结构的所有成员(安全修订级别除外)设置为空白。此时,SECUIRTY_DESCRIPTOR_REVISION 是此函数的“dwRevision”的唯一有效值。

接下来要做的就是根据可执行文件的当前工作目录计算要操作的文件名。这是通过“GetCurrentDirectory()”函数获取当前目录名,然后使用“sprinf_s()”函数在其末尾追加“\New.txt”来完成的,这样做只是为了让我们在运行此代码时少操心一件事。

接下来是声明和初始化我们的 EXPLICIT_ACCESS 结构数组。首先在此说明一点,本节以冗长的方式编写,以确保读者了解我们每一步在做什么,有一些事情可以节省您大量打字(如果您喜欢的话),但本文的重点是可读性而不是作者的便利性。您可以将它们视为实际的 ACE,然后由系统处理为可用格式,它们处于所谓的“自相对格式”中。为了使它们可供系统使用,拒绝任何用户或组访问的条目必须列在授予访问权限的条目之前。我们使用的“SetEntriesInAcl()”函数会自动为新条目添加到现有条目中,但不会为现有条目添加到新条目中。这不是“按用户”基础,而是所有权限的总体排序,因此:UserA:拒绝权限,UserA:授予权限,UserB:拒绝权限,UserB:授予权限;这样的模式是无效的。我们的第一个条目 (ExplicitAccess[0]) 被清零,然后使用“BuildExplicitAccessWithName()”函数进行初始化。此函数有效地与我为其他两个条目使用的初始化结构实例相同,就我们的目的而言,这两种方法提供的控制程度并没有更多或更少。我们首先通过将 ExplicitAccess[0] 按引用作为第一个参数传递来告诉函数我们的 EXPLICIT_ACCESS 结构存储在内存中的位置。然后我们告诉函数我们要为哪个帐户设置此条目,我选择了“Guest”帐户,因为它适用于所有版本的 Windows 操作系统,即使它被禁用(自 Windows XP 开始默认禁用),此设置仍然有效。第三个参数是我们希望处理的权限列表,每个权限都代表一个数字,我们可以使用按位 OR 运算符将它们组合成一个参数后再传递给函数。第四个参数告诉函数要设置哪种类型的 ACE,我们选择 DENY_ACCESS,因为这就是我们想要做的,并且请记住,所有拒绝访问的条目都必须先输入。最后一个参数有该函数文档中列出的多种可能性,我选择 NO_PROPAGATE_INHERIT_ACE 来稍后展示如何通过仅更改文件名并将“CreateFile()”替换为“CreateDirectory()”来将这些相同的方法应用于设置文件夹权限。

我们的 ExplicitAccess 数组中的另外两个条目是通过手动定义的;在这样做时,必须记住 WinAPI 是为 C 编写的,因此没有设置默认值的构造函数,未输入的任何内容在技术上都是未定义的。ExplicitAccess[1] 条目授予“Authenticated Users”组所有访问权限(GENERIC_ALL),这是一个 Windows 默认内置的组帐户,应该可以在您的系统上正常工作。这里唯一值得注意的条目是 Trustee 成员 pMultipleTrustee、MultipleTrusteeOperation 和 TrusteeType。在撰写本文时,前两个列表不受支持,必须设置为我在这里的值。第三个设置为 TRUSTEE_IS_GROUP,以告知系统您在此条目中设置权限的帐户是一个组,而不是用户或系统。如果您还没有猜到,Trustee 成员 pstrName 是您希望为其创建条目的用户或组的名称。ExplicitAccess[2] 设置为另一个与“Guest”帐户相关的条目。我这样做的原因有两个:首先是展示定义 EXPLICIT_ACCESS 结构的两种方法是可互换的,其次是展示条目所涉及的帐户可以按任何顺序进行,只要所有拒绝任何帐户权限的条目都放在前面。

现在我们进入 try catch 块,用于在发生错误时确保代码的正确清理。第 80 行的“DeleteFile()”命令是为了防止您多次运行此过程,并且您不想每次都删除它创建的文件。我注意到,即使在“CreateFile()”中设置了 CREATE_ALWAYS 标志,ACE 在文件被覆盖时仍然保留,因此为了看到所做的更改,必须每次删除目标文件。如果文件不存在,“DeleteFile()”将返回 FALSE,但我们在程序继续执行时会忽略该值。

首先,我们调用“CreateFile()”来实际创建我们将要操作的文件。这里唯一真正值得注意的是我们传递给 lpSecurityAttributes 的参数是 NULL。我们这样做是为了让系统使用您系统的默认安全权限来创建我们的文件,这些权限应该只包括运行程序的用户的权限(您)以及可能从父目录继承的任何安全权限。如果成功,此函数将创建我们的文件并返回一个句柄,我们从此处开始使用该句柄来引用文件本身;否则,将抛出并捕获一个错误,我们的程序会输出一个错误代码,您可以在此处查找:http://msdn.microsoft.com/en-us/library/windows/desktop/ms681381(v=vs.85).aspx

接下来,在第 91 行,我们调用“GetNamedSecurityInfo()”函数,正如您可能猜到的,该函数允许您获取命名对象的安全信息。在这种情况下,我们使用刚刚创建的文件名,这不成问题,因为我们在创建文件时将 lpSecurityAttributes 标志设置为 NULL,因此您将拥有执行此操作所需的访问权限。对于 lpSecuirytAttributes 的任何其他参数或预先存在的文件,在此函数成功执行之前可能需要进行一些额外的工作。此函数“GetNamedSecurityInfo()”实际上可以通过组合第三个参数的值和按位 OR 运算符来返回对象的任意数量的属性(假设它们存在),但今天我们只关注此文件“New.txt”的 DACL。作为第一个参数,我们传递文件名。然后第二,我们告诉它我们的对象是 SE_FILE_OBJECT 类型。第三个是 SECURITY_INFORMATION 标志(DWORD),指示我们正在查找的内容,在这种情况下,我们想要我们第一个参数命名的对象的 DACL。接下来的两个参数 ppsidGroup 和 ppsidOwner 可以是指针,它们包含指向对象所有者和/或主要组的 SID 的指针(同样,假设它们存在),但今天我们不关注这些,所以我们将它们设置为 NULL。现在,到第六个参数,我们来到了 ppDacl,这是我们传递以接收指向对象当前 discretionary access control list 的指针的参数。此处至关重要的是要注意此函数此参数的定义是指向指针的指针,因此您不能传递 ACL 的实际实例或对 ACL 的引用。此参数必须是指向 ACL 的指针,并通过引用传递,使用“&”运算符。下一个变量 ppSacl 可以是指向正在查询的对象的 SACL 的指针,但此 ACL 可能不存在,并且不是本文的重点,因此我们将其传递为 NULL。最后是指向将保存我们命名的对象的 DACL 的指针。为此,我们使用对变量“pSecurityDescriptor”的引用,该变量在第 27 行定义为指向 SECURITY_DESCRIPTOR 数据类型的指针。此参数将接收此函数请求的实际安全描述符或安全描述符的组合。

“GetNamedSecurityInfo()”返回的安全描述符不是您能理解的格式,因此为了真正从中收集有意义的信息,我们在第 100 行使用“ConvertSecurityDescriptorToStringSecurityDescriptor()”函数处理字符串。此函数首先需要一个指向我们安全信息结构的指针,即由“GetNamedSecurityInfo()”函数填充的“pSecurityDescriptor”。接下来,您需要指定您传入的安全描述符的修订号,在本文撰写时,仅支持 SDDL_REVISION_1。第三个参数与传递给“GetNamedSecurityInfo()”的标志组合相同。第四个参数是指向实际将保存安全描述符信息的纯文本字符串的指针。我们使用在第 41 行定义的 DACLDescriptorAsString 变量作为指向新 LPSTR 的 LPSTR 指针。这可能看起来令人困惑,但 LPSTR 是指向 CHAR 数据类型的指针,因此 DACLDescriptorAsString 实际上是指向指向 CHAR 类型的指针。第五个也是最后一个参数是安全描述符字符串的长度,您可以将其设为 NULL,但我选择将其存储在 SecDescStringLengthNeeded 中,没有特别的原因。现在要输出此字符串,我们只需使用 std::cout 并将 DACLDescriptorAsString 解引用一次,就得到了一个指向 CHAR 数组的指针,该数组包含我们要显示的数据。

在这里,我使用了我的“pause()”函数来使程序暂停,您可以随意使用调试器,但我不想排除那些不知道如何使用它的用户。程序暂停是为了让您可以将存储在 DACLDescriptorAsString 中的字符串与 Windows 资源管理器中此文件属性的“安全”选项卡下显示的内容进行比较。要继续处理,只需按 Enter 键即可,但让我们花点时间看看控制台窗口中显示的字符串,因为您会注意到这并非完全是纯文本。安全描述符字符串有自己的格式,在代码的第 105 行的 URL 中有详细描述。我的字符串以“D:“开头,这表示该字符串是 DACL。接下来的部分是第一个 ACE 字符串(如果您想查找您字符串中条目的特定含义,可以在第 106 行找到链接),它以“(A;”开头,这告诉我这个描述符的目的是定义一个 ACCESS_ALLOWED_ACE_TYPE,它允许访问与其关联的对象。我接下来看到“ID;FA;”,这些是标志,告诉我是 INHERITED_ACE 类型,具有 FILE_ALL_ACCESS 权限。object_gui 和 inherit_object_gui 的条目是“;;”,因为这是一个文本文件,所以它没有 GUI。最后一个条目是一个很长的字符串,实际上是我的 Windows 帐户的 SID,后跟一个关闭的大括号“)”,表示该访问控制条目的结束。接下来的两个 ACE 字符串与第一个几乎相同,只是每个字符串的最后一个条目指示了字符串所属的帐户。如果存在我这里的这两个,那么您会注意到它们要短得多,每个只包含两个字符:“BA”和“SY”。如果我们转到第 107 行的 URL,我们会看到 SID 字符串格式文档,其中告诉我们这些是 SDDL_BUILTIN_ADMINISTRATORS 和 SDDL_LOCAL_SYSTEM 帐户的常量值。

此时,您可以按 Enter 键继续程序的运行。如果一切顺利,您的程序将在第 114 行继续,在那里我们调用“SetEntriesInAcl()”来将我们之前定义的 EXPLICIT_ACCESS 结构用于文件访问控制列表 (ACL) 的实际条目。对于第一个参数,我使用了与定义我们的 EXPLICIT_ACCESS 数组大小相同的 const 表达式,因为这两个值没有理由不同。第二个参数需要一个指向我们想要用于定义新 ACL 的 EXPLICIT_ACCESS 结构的指针列表。由于我们的 EXPLICI_ACCESS 条目存储在数组中,我们只需传递其名称。第三个条目是指向我们旧 DACL 的指针,这个是可选的,但既然我们有了,我们就传递它。如果我们不包含旧 DACL,则将使用我们提供的 EXPLICIT_ACCESS 结构和从父文件夹继承的任何权限创建全新的 DACL。要看到这一点,您需要注释掉第 80 行的“DeleteFile()”命令,将第 114 行的‘pOldDacl’变量替换为‘NULL’,并在运行程序之前通过属性->安全选项卡在 Windows 资源管理器中为文件添加另一个用户或访问权限。您将看到您通过 Windows 资源管理器手动添加的条目将被删除。最后,“SetEntriesInAcl()”的最后一个参数是指针,该指针接收此函数创建的新 ACL 的指针。然后我们使用“IsValidAcl()”函数在第 123 行检查我们新 ACL 的一致性。

最后我们来到第 129 行,这是“SetNamedSecurityInfo()”,这个函数几乎是我们开始时使用的“GetNamedSecurityInfo()”函数的反向操作。它根据您在第三个参数中传递的标志以及您之后传递的结构来设置对象的安全信息。在这里,您可以看到我们只告诉它设置 DACL_SECURITY_INFORMATION,并且我们只传递了刚刚创建的 pNewDacl。此函数设置信息而不返回它,因此没有等同于 ppSecurityDescriptor 的条目。

在那之后,您的文件现在已设置了您提供的新安全描述符信息。我们获取当前的 DACL(现在是新的),然后像以前一样将其转换为字符串,以便您可以将旧条目与新条目逐一进行比较。

附件:[main.cpp]