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

Windows 访问控制模型 第 4 部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (27投票s)

2005年6月27日

43分钟阅读

viewsIcon

236529

downloadIcon

6809

访问控制系列的最后一篇文章介绍了一个访问控制编辑器及其相关的 ISecurityInformation 接口。

Filepermsbox. A program to view the security descriptor of a file

图 40:Filepermsbox,一个用于目录和文件的 ACL 编辑器。

关于系列

这个分为 4 部分的系列将讨论 Windows 访问控制模型及其在 Windows NT 和 2000 中的实现。

第 4 部分是访问控制系列的最后一部分。本来这只会是我关于这个主题发布的一篇文章,但是你需要一些介绍性主题作为前提。

在第 4 部分,我将向你展示如何在自己的应用程序中实现 ACL 编辑器,这样你就可以保护任何东西。我将从描述 ACL 编辑器及其功能(借助一些截图)开始。我将描述如何为自己的对象利用 ACL 编辑器。然后,真正的挑战将开始:ISecurityInformation 接口将被解释和实现。ISecurityInformation 的每个方法都将被解释,并提供示例实现。将专门用一节来讨论 ISecurityInformation::SetSecurity 方法(因为它非常复杂)。完成之后,我们将简要讨论你可以实现的其他接口(ISecurityInformation2ISecurityTypeInfoIEffectivePermission)。

ACL 编辑器仅适用于 Windows 2000 或更高版本。

Windows NT3 和 4 包含一个较旧的 ACL 编辑器。要显示旧的 ACL 编辑器,你必须调用 MSDN 上未记录的函数(你需要访问 Sysinternals [^] 网站来获取)。如果你使用的是 .NET,Keith Brown 有一个 可重用类库 [^],它允许你使用 ACL 编辑器(相关文章中也包含一些关于 ACL 编辑器的很酷的信息)。

不幸的是,我不会解释如何将 ACL 编辑器用作良好的安全描述符,也不会帮助你恢复对那个被锁定的文件夹的访问权限。如果你需要解锁文件的访问权限,请访问 KB308421 [^]。

示例程序 Filepermsbox 是 Windows XP 家庭版的属性表扩展(尽管它在 Windows 2000 以上的任何操作系统上都可以工作)。它会重新创建完整的安全选项卡,即使启用了简单的文件和文件夹共享。这意味着你的家庭用户不再需要重新启动到安全模式来更改文件安全描述符。然而,Filepermsbox 也可能对第三方 Shell 和安全实用程序有用。你可以通过两种方式访问安全选项卡。第一种方式是使用前端应用程序(图 40)。只需选择你的文件,ACL 编辑器就会出现,以便你可以编辑它。如果你注册了 DLL(通过安装程序或 regsvr32),你可以通过 Shell 扩展访问 Filepermsbox。选择一个文件,打开其属性,然后选择 Filepermsbox 选项卡。

提供了源代码、二进制文件和安装程序。

目录

目录是针对整个系列的。

  1. 第 1 部分 - 背景和核心概念。访问控制结构
    1. 安全标识符(SID)
    2. 安全描述符(SD)
    3. 访问控制列表(ACL)
    4. 选择一个好的自由访问控制列表
    5. Windows 2000 继承模型
    6. 令牌
    7. 关于安全描述符定义语言的说明
    8. 即将推出
  2. 第 2 部分 - 基本访问控制编程
    1. 选择你的语言
    2. SID 的乐趣
    3. 你是哪个组的成员?
    4. 启用令牌权限
    5. 解析安全描述符
    6. 遍历访问控制列表
    7. 我有访问权限吗?
    8. 创建访问控制列表
    9. 创建和编辑安全对象
    10. 使你的类安全
    11. 玩具程序下载
  3. 第 3 部分 - 使用 .NET v2.0 进行访问控制编程
    1. .NET 安全性的历史。
    2. 在 .NET 中读取 SID。
    3. Whoami .NET。
    4. .NET 中的权限处理。
    5. 在 .NET 中以非特权用户身份运行。
    6. 在 .NET 中获取和编辑安全描述符,并将其应用于对象。
    7. .NET 中的访问检查。
    8. NetAccessControl 程序和 AccessToken 类库。
  4. 第 4 部分 - Windows 2000 风格的访问控制编辑器
    1. ACL 编辑器的功能(ACLUI)
    2. 入门
    3. 实现 ISecurityInformation 接口
    4. ISecurityInformation::SetSecurity
    5. 可选接口
    6. 介绍接口
    7. Filepermsbox - 一个显示 NTFS 文件或文件夹安全描述符的程序
    8. 历史

28. ACL 编辑器的功能(ACLUI)

如果你运行了 第 3 部分中的示例程序,你可能会遇到 ReadSD 程序,它可以读取和编辑注册表安全描述符。除了不显眼的用户界面,你可能还会注意到我的 ACL 编辑器存在许多问题。

  • 访问掩码显示为未翻译的十六进制数字。理想情况下,访问掩码应以文本形式显示(例如,“Read”、“Write”或“All Access”)。
  • 自动继承的 ACE 单独显示。因此,大多数安全描述符将显示为空,从而使用户感到困惑。我们需要找到一种方法来同时显示显式 ACE 和继承的 ACE。
  • DataGridView 直到为时已晚才对用户名或访问掩码进行任何验证。
  • Inheritance 标志编辑器显示为一个模态对话框。在设置标志时,你无法编辑任何其他内容。
  • 如果你不允许编辑注册表安全(当你作为受限用户运行时很常见),ReadSD 可能会失败。
  • 大多数选项对缺乏经验的用户来说都太令人困惑了。这增加了用户出错的可能性。
  • 最后但同样重要的是,ReadSD 仅适用于注册表项。

对话框 1:通用 ACL 编辑器

Windows 2000 ACL 编辑器最初是作为 Windows NT4.0 的可再发行组件出现的。它提供了一个通用的用户界面,可以读取和编辑安全描述符及其部分,包括控制标志、DACL、SACL 和所有者。它被称为 ACL 编辑器(也可能称为安全选项卡、权限列表或 ACLUI)。ACL 编辑器这个术语有些用词不当——该界面可以编辑对象的 SACL 和所有者,以及其 DACL。更重要的是,ACL 编辑器是对象无关的。相同的 ACL 编辑器可用于更改注册表项、COM 接口、DCOM 类、文件、目录、服务或进程对象的安全性。

让我们看看这个界面。选择一个文件,然后打开其属性。在随后的属性表中,选择“安全”选项卡,现在你就可以开始查看 ACL 编辑器了。

如果你使用的是 Windows XP,默认情况下“安全”选项卡将不可用。在这种情况下,你可以尝试在“文件夹选项”中禁用“简单文件共享”,或者如果无法做到,可以下载我的 Filepermsbox 示例程序。

An annotated ACL editor with its developer features

图 41:ACL 编辑器的主要属性表。

通用 ACL 编辑器是一个属性表,由一个列表视图(用户名)和一个复选框列表(通用访问权限)组成。第一个列表负责显示 DACL 应用于哪些用户/组/计算机(它们被统称为主体,但为了简单起见,我只称它们为用户。等等……我之前不是称它们为受托人吗?)。底部的复选框显示所选用户的通用访问权限。请注意通用属性表的一些功能:

  1. 框的标题显示对象的名称。ACL 编辑器会询问你对象的标题。
  2. SID 列表是从 DACL 获取的,ACL 编辑器可能会要求你将这些 SID 翻译成用户名。
  3. 有一个“高级”按钮,可以打开另外三个对话框。再次,ACL 编辑器会询问你是否要启用此按钮。
  4. 如果你想添加用户,可以单击“添加...”按钮。这将显示图 42 中的对话框。
  5. 你可以将 ACL 编辑器设置为只读。处理 ACL 编辑的控件将显示为灰色。
  6. ACL 编辑器显示适用于该对象的相关访问权限。它会向你询问访问权限列表。
  7. 继承的 ACE 和其他允许/拒绝 ACE 合并为一个用户权限。ACL 编辑器将拒绝 DACL 和允许 DACL 分组在一起,询问你通用访问权限,然后重新映射权限以便合并。简而言之,如果有两个 DACL 应用于同一用户,它们将被追加。

IdsObjectPicker Dialog Box

图 42:选择服务器/用户

当你按下“添加...”时,你会看到另一个对话框(图 42)。这个窗口是 IDsObjectPicker 对话框,它允许你选择用户、组或计算机。对于这个对话框,ACL 编辑器会要求你提供域名称,以便它可以查找用户列表。除非你正在编辑远程对象,否则这通常是默认的域名称。再次,当 ACL 编辑器填充对话框底部的详细信息视图时,它会要求你将 SID 翻译成用户名。

如果正在编辑的 DACL 的条目过于复杂,无法表示为通用权限,ACL 编辑器将把 DACE 命名为“Special”。在这种情况下,你需要进入高级 ACL 编辑器。

高级 ACL 编辑器

The advanced permissions tab of the ACL editor (with annotations)

图 43:高级 ACL 编辑器。

如果已启用,你可以按“高级”按钮来获得 ACL 编辑器的全部功能。高级 ACL 编辑器是一个巨大的对话框,如图 43 所示。它包含四个选项卡,分别编辑 DACL、SACL、所有者和(从 WinXP 开始)有效权限。你看到的第一个对话框是高级权限对话框,它允许你完全编辑 DACL。其中心用户界面是一个详细视图,显示 DACL 的所有条目。这一次,继承的 ACE 和显式 ACE 分开显示。

对于每个 ACE,都有 ACE 类型(允许/拒绝)、用户名、权限名称、传播标志和(从 WinXP 开始)继承源的条目。除了 ACE 类型之外,详细视图中的所有条目都是对象特定的(例如,“列表文件夹内容”可能对目录对象有意义,但对互斥体则没有意义)。为了完成封装,ACL 编辑器会要求你提供名称。当高级权限框初始化时,你将被要求翻译以下内容:

  • SID 到用户名。
  • 通用权限到特定权限。
  • 访问权限到文本。
  • 传播标志到文本。
  • 继承的 ACE 到继承源(ACE 的来源)。

如果你愿意,你还可以有一个“默认”按钮,它将 DACL 重置为默认值。当用户按下“默认”时,ACL 编辑器会要求你提供默认安全描述符。这假设你知道默认安全描述符是什么(在添加默认安全描述符之前,请记住你定义的默认安全描述符可能不是每个人都这么定义)。

如果你的对象支持自动继承,你可以显示另外两个复选框。“继承自父项”复选框切换 DACL 的自动继承。当你禁用自动继承时,会询问你是否要将继承的 ACE 显式化,或者删除它们(如果你阅读了 第 3 部分,这相当于询问 ObjectSecurity.SetAccessRuleProtectionpreserveInheritance 成员)。

“替换权限条目”复选框被 Keith Brown [^] 亲切地称为“大锤按钮”。之所以这么称呼,是因为它会深入到每个子对象并将其所有安全描述符重置为简单的默认值(例如,它可以取消隐藏你的“秘密文档”文件夹)。

Annotated version of the ACE Dialog box

图 44:ACE 对话框允许你编辑 ACL 中的单个访问控制条目。

当用户按下“添加...”或双击列表中的条目时,会出现一个新对话框,类似于图 44(我称之为 ACE 对话框)。ACE 对话框允许用户编辑该 ACE 的属性。他们可以编辑/添加 ACCESS_MASK 的权限,控制访问权限的传播,以及(对于域风格的对象)对象属性。

在渲染窗口时,ACL 编辑器会要求你提供以下信息:

  • 每个访问权限的文本形式。(提醒:访问权限组合起来形成访问掩码。)
  • AceFlags 到文本的翻译。(例如,“CI”表示“将权限传播到子注册表项”)。
  • ObjectType 中 GUID 的文本形式(仅限域 ACE)。
  • 将 SID 翻译成用户名(已在其他地方实现)。

请注意,你可以在同一个 ACE 中指定允许和拒绝的访问权限。实际上,ACL 编辑器会将允许和拒绝的 ACE 分别拆分成不同的条目。当你按“确定”退出此框时,你将看到生成的 ACL。

如果你的对象不支持这些功能中的某一项(例如,文件没有子项,因此传播不适用于它们),你可以要求 ACL 编辑器删除某些功能。

当尝试编辑自动继承的条目时,ACE 对话框会变得很有趣。从 第 3 部分,你将知道你无法编辑自动继承的 ACE。为了弥补这一点,ACE 对话框会将自动继承的访问掩码显示为灰色,告知用户他们无法编辑它们。相反,用户必须单击“拒绝”才能删除自动继承的条目。由于这种奇特的 UI 设计,ACL 编辑器让用户用拒绝条目覆盖了自动继承 ACE!

ACL 编辑器的其他功能。

当用户按下“应用”以提交安全描述符时,会在后台进行一系列检查。其中一项检查是访问控制条目的排序。如果 ACL 编辑器确定 ACL 未正确排序,它将对 ACL 进行排序(并告知用户它所做的操作)。与文档可能说的相反,可以覆盖此排序。

如果你启用了“SeSecurityPrivilege”,你可能需要考虑显示“审核”选项卡。这使用户可以编辑 SACL,这与编辑 DACL 非常相似。ACL 编辑器将处理 SACL 和 DACL 之间的差异;因此,对你来说,审核页面是 DACL 页面的克隆。第三个选项卡允许用户更改安全描述符的所有者。由于 Windows 2000 ACL 编辑器中的一个 bug,用户实际上只能获得文件(或其组)的所有权。他们不能放弃所有权,也不能将所有权交给他人(此 bug 在 Windows Server 2003 中得到修复,任何用户都可以选择)。除非你在 Windows 2000 上,否则有一个称为“有效权限”的最后一个选项卡。此选项卡充当 GetEffectiveRightsFromAcl() API 的前端,并允许用户查看他们实际拥有的权限(在包含组权限、拒绝条目和自动继承 ACE 之后)。

为了显示 ACL 编辑器,你需要实现一个名为 ISecurityInformation 的接口。如果你想实现该接口……你将不得不学习一大批新的 AdvAPI 库函数。

  • MapGenericMask()
  • BuildTrusteeWithSid()
  • GetInheritanceSource()
  • FreeInheritedFromArray()
  • GetEffectiveRightsFromAcl()
  • SetNamedSecurityInfo()
  • GetNamedSecurityInfo()
  • TreeResetNamedSecurityInfo()

29. 入门

尽管有示例(尤其是在 Platform SDK 中)可以帮助你入门,并且有两篇 MSDN Magazine 文章 [^] 提到了该编辑器,但 ACLUI 存在一些非常奇怪的 bug,未在其他任何地方提及。当你查看 ISecurityInformation 接口时,它可能看起来像一个 COM 对象,但实际上它不是 COM 对象(图 45)。

class CSecurityInformation: ISecurityInformation, IUnknown
{
public:
  /* IUnknown methods */
  STDMETHOD(QueryInterface)(REFIID, LPVOID *);
  STDMETHOD_(ULONG, AddRef)(void);
  STDMETHOD_(ULONG, Release)(void);

  /* ISecurityInformation methods */
  STDMETHOD(GetObjectInformation)(PSI_OBJECT_INFO pObjectInfo);
  STDMETHOD(GetSecurity)(SECURITY_INFORMATION si, \
    PSECURITY_DESCRIPTOR *ppSD, BOOL fDefault);
  STDMETHOD(SetSecurity)(SECURITY_INFORMATION si, \
    PSECURITY_DESCRIPTOR pSD);
  STDMETHOD(GetAccessRights)(const GUID* pguidObjectType, \
    DWORD dwFlags, PSI_ACCESS *ppAccess, ULONG *pcAccesses, \
    ULONG *piDefaultAccess);
  STDMETHOD(MapGeneric)(const GUID *pguidObjectType, \
    UCHAR *pAceFlags, ACCESS_MASK *pmask);
  STDMETHOD(GetInheritTypes)(PSI_INHERIT_TYPE *ppInheritTypes, \
    ULONG *pcInheritTypes);
  STDMETHOD(PropertySheetPageCallback)(HWND hwnd, UINT uMsg, \
    SI_PAGE_TYPE uPage);

  /* Your custom methods, constructor, destructor */
private:
  ...
};

图 45:实现 ISecurityInformation 接口的示例蓝图。

这是一个相当令人生畏的函数列表需要实现,对吧?(其复杂性可与 DirectX 接口媲美。)更糟糕的是,没有 Typelib 来帮助你实现这个接口(因此 ATL 和 .NET 都帮不了你)。你必须以 低级方式 [^] 实现这些“COM”函数,或者获取一个 [^] 来为你完成。

构造函数/析构函数

如果你仔细观察,你会发现 ISecurityInformation 的方法中没有一个接受文件名。请记住,ACL 编辑器不仅仅适用于文件。它适用于注册表项、内核对象、服务或任何你喜欢的对象。原因如下:对象的名称和类型由你管理,而不是由 ACL 编辑器。所以对于上面的类,我建议你添加一个成员变量来表示你的对象(或对象,如下文所述)。

这是一个启用你可能需要的任何权限的好地方。你可能需要的一些权限包括“SeSecurityPrivilege”、“SeBackupPrivilege”、“SeRestorePrivilege”、“SeTakeOwnershipPrivilege”和“SeChangeNotifyPrivilege”。

如果你维护用户界面,我也建议你有一个成员变量来表示 ACL 编辑器的所有者窗口。

IUnknown

由于此对象实现了 IUnknown,你必须维护一个引用计数(从 1 开始)。对于 QueryInterface,实现的接口是 IUnknownISecurityInformation(如果你愿意,还可以是 ISecurityInformation2ISecurityObjectTypeInfoIEffectivePermission)。由于 ACLUI 中的一个未记录 bug,你的对象必须支持单线程单元模型。

30. 实现 ISecurityInformation 接口

现在你已经实现了构造函数、析构函数和 IUnknown,是时候在你的应用程序中添加实际功能了。

1. ISecurityInformation::GetObjectInformation() 和 SI_OBJECT_INFO

当调用此方法时,你需要填充一个 SI_OBJECT_INFO 结构(它会影响 ACL 编辑器的行为)并将其返回给调用者。SI_OBJECT_INFO 结构大部分是自解释的(带有一些可选成员),但有一个成员值得讨论:

  • dwFlags: 一组标志,影响 ACL 编辑器的行为。例如,如果你不希望用户按“高级”按钮显示高级 ACL 编辑器,可以在此处禁用它。如果你不希望用户编辑安全描述符,请设置 SI_READONLY 标志。以下是一些更重要的标志:
    • SI_ADVANCED:指定将显示“高级”按钮,以便用户可以进入高级 UI。
    • SI_CONTAINER:表示你的对象是目录(或表现得像目录)。如果对象是目录,你应该设置此标志,以便 ACL 编辑器可以将其应用于对象。你可能还想启用继承的东西。
    • SI_EDIT_AUDITS:显示“审核”选项卡(以便用户可以编辑 SACL)。要查看/编辑 SACL,你必须拥有“SeSecurityPrivilege”。
    • SI_EDIT_PERMS:显示“高级”权限选项卡(以便用户可以完全编辑 DACL)。
    • SI_EDIT_OWNER:显示所有者选项卡(以便用户可以更改或获取所有权)。
    • SI_EDIT_PROPERTIES:显示属性选项卡(以便用户可以编辑与对象关联的属性)。
    • SI_EDIT_EFFECTIVE (未记录,适用于 Windows XP 及更高版本):显示“有效权限”选项卡。
    • SI_READONLY:不允许用户更改安全描述符。
    • SI_OWNER_READONLY:不允许用户更改所有者(要更改所有权,用户必须是所有者,或拥有“SeTakeOwnershipPrivilege”)。
    • SI_MAY_WRITE (未记录):“不确定用户是否可以写入权限”。
    • SI_NO_ADDITIONAL_PERMISSION (未记录):?????
    • SI_NO_ACL_PROTECT:如果你没有实现继承,请选择此项。这将隐藏自动继承复选框。
    • SI_OWNER_RECURSE:显示“大锤”按钮,该按钮允许用户将所有子项的安全描述符重置为默认值。除非你已在对象中实现了递归遍历,否则不要显示此项。
    • SI_RESET_DACL_TREE:类似于 SI_OWNER_RECURSE,但用于高级权限选项卡。
    • SI_RESET_SACL_TREE:类似于 SI_RESET_DACL_TREE,但用于“审核”选项卡。
    • SI_SERVER_IS_DC:如果已确定计算机位于域环境中,请设置此项。
    • SI_EDIT_ALLSI_EDIT_AUDITS | SI_EDIT_PERMS | SI_EDIT_OWNER 的简写。
    • SI_NO_TREE_APPLY:隐藏“仅将这些权限应用于此容器内的对象和/或容器”复选框。如果你的对象不支持继承,请设置此标志。
    • SI_PAGE_TITLE:表示你希望在 ACL 编辑器中显示自定义标题。在 pszPageTitle 成员中指定备用标题。
    • SI_RESET:显示“默认”按钮,该按钮会将安全描述符重置为默认值。如果你不知道默认安全描述符是什么,请不要启用此标志。
    • SI_RESET_SACLSI_RESET_DACLSI_RESET_OWNER:仅显示某些选项卡的“默认”按钮。
    • SI_OBJECT_GUID:(特定于域)如果你的对象支持多重继承,你应该启用此标志。

我关于选择合适的 dwFlags 的建议:首先禁用你的对象不支持的功能(内核对象不支持自动继承,因此你可以删除大部分容器相关的东西)。然后禁用你被禁止执行的功能(如果你没有“SeSecurityPrivilege”,则不要显示审核选项卡)。然后禁用你觉得实现起来太复杂的功能。例如,如果你不想实现复杂的大锤按钮(更多内容请参见第 33 节),可以禁用 SI_RESET_DACL_TREE。Filepermsbox 示例程序有两个预设的 dwFlags(一个用于文件,一个用于文件夹),然后根据它允许的操作以编程方式禁用标志。

警告:传递给 ACLUI 的字符串和句柄必须在编辑器生命周期内保持有效,并且只有在析构函数中才能有机会释放任何资源。如果你使用动态分配的内存而没有某种写时复制优化,就开始注意你的应用程序内存泄漏吧。

STDMETHODIMP CSecurityInformation::GetObjectInformation
  (PSI_OBJECT_INFO pObjectInfo)
{
  pObjectInfo->dwFlags = SI_ADVANCED | SI_EDIT_ALL | SI_EDIT_EFFECTIVE;
  pObjectInfo->hInstance = GetModuleHandle(NULL);
  pObjectInfo->pszServerName = NULL;
  pObjectInfo->pszObjectName = this->FileName;
  return S_OK;
}

图 46:<SMALL>GetObjectInformation</SMALL> 的示例实现。

2. ISecurityInformation::GetAccessRights() 和 SI_ACCESS[ ]

当我们游览 ACL 编辑器时,我描述了 ACLUI 如何每次在填充访问权限时向你的类请求一个字符串。这就是 ACLUI 使用的函数。ACLUI 不会为每个访问权限调用 GetAccessRights(),而是调用此函数一次。你的做法是提供一个哈希表(实现为 SI_ACCESS 数组,C 风格),ACL 编辑器将使用此哈希表将访问掩码映射到显示字符串及其值。

访问权限是对象特定的(例如,进程对象将具有“Create Process”访问权限,而不是“Read File”权限),但作为你应该提供的内容的指南,你可以查看 Filepermsbox 的 SI_ACCESS 结构,如图 47 所示。SI_ACCESS 结构包含以下成员:

  • pguid:如果你正在保护域风格的对象,请在此处指定对象 GUID。否则,将其设置为 GUID_NULL
  • mask:分配给操作的值(例如,对于文件读取,这将是 FILE_GENERIC_READ)。
  • pszName:操作的显示名称(例如,“Read”)。
  • dwFlags:权限的显示位置(在主表、高级表、显示文件夹或文件等)。
    • SI_ACCESS_GENERAL:条目将显示在基本安全页面上(用于通用访问权限,如“Modify”、“Read & Execute”)。
    • SI_ACCESS_SPECIFIC:条目将显示在高级安全页面上(用于高级访问权限,如“Append Data”、“Read Extended Attributes”)。
    • SI_ACCESS_PROPERTIES:条目将显示在属性页面上(用于属性更改权限)。
    • 0(零):如果 ACL 中遇到此条目,它将被标记为高级安全页面上的内容(例如,“Read, Write and Execute”)。
    • SI_ACCESS_CONTAINER:仅当对象是容器(表现得像目录)时显示该条目。
    • CONTAINER_INHERIT_ACE:ACL 中设置了 CI 标志。
    • OBJECT_INHERIT_ACE:ACL 中设置了 OI 标志。
    • INHERIT_ONLY_ACE:ACL 中设置了 IO 标志。
const SI_ACCESS g_siObjAccesses[] =
{
  {&GUID_NULL, FILE_ALL_ACCESS, L"Full Control", SI_ACCESS_GENERAL |
    SI_ACCESS_SPECIFIC | CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE |
    DELETE, L"Modify", SI_ACCESS_GENERAL | CONTAINER_INHERIT_ACE |
    OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_EXECUTE, L"Execute", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ | FILE_GENERIC_EXECUTE, L"List Folder Contents",
    SI_ACCESS_CONTAINER | CONTAINER_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_READ, L"Read", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},
  {&GUID_NULL, FILE_GENERIC_WRITE, L"Write", SI_ACCESS_GENERAL |
    CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE},

  {&GUID_NULL, FILE_EXECUTE, L"Traverse Folder/Execute File",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_DATA, L"List Folder/Read Data",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_ATTRIBUTES, L"Read Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_READ_EA, L"Read Extended Attributes", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_DATA, L"Create Files/Write Data", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_APPEND_DATA, L"Create Folders/Append Data",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_ATTRIBUTES, L"Write Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_WRITE_EA, L"Write Extended Attributes",
    SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_DELETE_CHILD, L"Delete Children", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, DELETE, L"Delete", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, READ_CONTROL, L"Read Permissions", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, WRITE_DAC, L"Set Permissions", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, WRITE_OWNER, L"Take Ownership", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, SYNCHRONIZE, L"Synchronize", SI_ACCESS_SPECIFIC},
  {&GUID_NULL, FILE_GENERIC_EXECUTE, L"Traverse/Execute", 0},
  {&GUID_NULL, FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE,
    L"Write and Execute", 0},
  {&GUID_NULL, FILE_GENERIC_EXECUTE | FILE_GENERIC_WRITE |
    FILE_GENERIC_READ, L"Read Write and Execute", 0},

  { &GUID_NULL, 0, L"None", 0 }
};

STDMETHODIMP CSecurityInformation::GetAccessRights
  (const GUID*, DWORD, PSI_ACCESS *ppAccesses,
  ULONG *pcAccesses, ULONG *piDefaultAccess)
{
  *ppAccesses = const_cast<SI_ACCESS *>(g_siObjAccesses);
  *pcAccesses = sizeof(g_siObjAccesses) /
    sizeof(g_siObjAccesses[0]);
  *piDefaultAccess = 2;
  return S_OK;
}

图 47:<SMALL>GetAccessRights</SMALL> 的示例实现,适用于文件和文件夹。

如果你正确设置了 dwFlags 参数,你就可以只使用一个 SI_ACCESS 数组发送给 ACL 编辑器。但是,如果你需要不同的列表,具体取决于显示的是高级页面还是基本页面,GetAccessRights() 会告诉你正在初始化哪个页面。不要忘记在返回给调用者之前设置条目数和默认访问权限。如果 ACL 编辑器遇到一个无法映射到这些条目之一的访问权限,它将显示对象具有“Special permissions”。SI_ACCESS 数组中的一个错误条目可能意味着拥有所有“Special permissions”与拥有正确的权限名称之间的区别。

SI_ACCESS 数组是你需要提供给 ACLUI 的 C 风格哈希表之一。

3. ISecurityInformation::MapGeneric() 和 GENERIC_MAPPING

除了对象抽象之外,这是 ACLUI 与我的 ReadSD 程序 [^] 区分开来的一个地方。此方法充当 MapGenericMask() 的桥梁。ACL 编辑器向你提供了一个 ACCESS_MASK,并期望你使用 MapGenericMask() 将通用权限重新映射到对象特定权限。GENERIC_MAPPING 结构(另一个哈希表)是对象特定的,也可能在类的其他地方使用,因此 Microsoft 建议你将 GENERIC_MAPPING 作为全局结构(不用担心,你可以将其保持为常量)。如果你仔细查看 ReadSD [^],你可能已经知道如何将通用权限映射到对象特定权限,但如果还没有,这里有两个任务需要执行:

  1. 填充一个 GENERIC_MAPPING 结构,它会将系统通用访问权限转换为特定权限。
  2. 调用 MapGenericMask(),以便从 ACCESS_MASK 中清除通用权限。

如果你的对象支持 GUID,你可以根据需要根据提供的 GUID 来构建不同的 GENERIC_MAPPING。对于文件/文件夹,GENERIC_MAPPING 相对简单。

GENERIC_MAPPING g_ObjMap =
{
  FILE_GENERIC_READ,
  FILE_GENERIC_WRITE,
  FILE_GENERIC_EXECUTE,
  FILE_ALL_ACCESS
};

STDMETHODIMP CSecurityInformation::MapGeneric
  (const GUID*, UCHAR *, ACCESS_MASK *pmask)
{
  MapGenericMask(pmask, &g_ObjMap);
  return S_OK;
}

图 48:将 MapGeneric() 连接到 MapGenericMask()

4. GetInheritTypes() 和 SI_INHERIT_TYPE[ ]

此方法负责将晦涩的 CONTAINER_INHERIT_ACE 等转换为更有意义的条目(例如,“This folder, and child folders”)。这还包括组合名称,如 CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE(应该翻译为“This folder, child folders and files”)。该过程涉及填充 SI_INHERIT_TYPE 数组(ACL 编辑器确实使用了大量的 C 风格哈希表)。将 GetInheritTypes() 视为传播标志的 GetAccessRights()

const SI_INHERIT_TYPE g_InheritTypes[] =
{
  /* Change these strings as necessary */
  &GUID_NULL, 0, L"This Object",
  &GUID_NULL, CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE,
    L"This object, inherited objects and containers",
  &GUID_NULL, CONTAINER_INHERIT_ACE,
    L"This object and containers",
  &GUID_NULL, OBJECT_INHERIT_ACE,
    L"This object and inherited objects",
  &GUID_NULL, INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE |
    OBJECT_INHERIT_ACE, L"Inherited containers/objects",
  &GUID_NULL, INHERIT_ONLY_ACE | CONTAINER_INHERIT_ACE,
    L"Inherited Containers",
  &GUID_NULL, INHERIT_ONLY_ACE | OBJECT_INHERIT_ACE,
    L"Inherited Objects"
};


STDMETHODIMP CSecurityInformation::GetInheritTypes
  (PSI_INHERIT_TYPE *ppInheritTypes, ULONG *pcInheritTypes)
{
  *ppInheritTypes = const_cast<SI_INHERIT_TYPE *>(g_InheritTypes);
  *pcInheritTypes = sizeof(g_InheritTypes) /
    sizeof(g_InheritTypes[0]);
  return S_OK;
}

图 49:使用 GetInheritTypes()AceFlags 转换为文本.

5. ISecurityInformation::PropertySheetPageCallback()

此方法会在显示或销毁新页面时通知你。如果你正在对 ACL 编辑器进行子类化,这可能很有用,因为它会向你发送新的窗口句柄。我倾向于使用 GetLastActivePopup() 来获取对话框窗口,但你可能需要或不需要它们。如果你不需要窗口句柄,可以简单地返回 S_OK

STDMETHODIMP CSexurityInformation::PropertySheetPageCallback
  (HWND /* hwnd */, UINT /* uMsg */, SI_PAGE_TYPE /* uPage */)
{
  return S_OK;
}

图 50:PropertySheetPageCallback() 的实现。

6. ISecurityInformation::GetSecurity()

我故意将 Get / SetSecurity() 方法留到最后,因为这是真正的乐趣开始的地方。当调用 GetSecurity() 方法时,你需要用请求的 SECURITY_INFORMATION 填充一个 SECURITY_DESCRIPTOR 变量。

乍一看,此方法似乎是一个对 Get*ObjectSecurity() API 的诱人过滤器(毕竟有 SECURITY_INFORMATIONSECURITY_DESCRIPTOR,我们还有对象名称)。如果你阅读了 第 2 部分,你将知道你不应使用这些 API。提醒:这些 API不支持继承。那么我们应该使用什么呢?

你应该使用 GetSecurityInfo()GetNamedSecurityInfo()。由于 ISecurityInformation::GetSecurity() 调用是为 Get*ObjectSecurity() 设计的,因此需要一些翻译才能使用 GetNamedSecurityInfo()。Get 变体是两者中最容易翻译的。

如果用户按了“默认”按钮,ACLUI 将调用 GetSecurity() 并将 fDefault 设置为 true。然后你应该将 ppSecurityDescriptor 设置为默认的 SECURITY_DESCRIPTOR(Filepermsbox 调用其辅助函数 GetDefaultSecurity() 来执行此任务)。选择默认安全描述符时,请确保默认值是安全的。我在 第 1 部分中给出了一些关于选择默认安全描述符的指南。

Windows 安装程序从文件“%windir%\security\templates\setup security.inf”获取默认安全描述符。

当你设计默认安全描述符时,请注意,你定义的默认安全描述符可能与最终用户定义的默认安全描述符不一致。如果你不确定默认安全描述符应该是什么,请禁用 ACLUI 的“默认”按钮功能(Windows 会禁用它)。

还有一点,在你将安全描述符返回给调用者之前,它必须指向一个 LocalAlloc() 分配的内存块。这是因为 ACL 编辑器稍后会 LocalFree() 它。GetNamedSecurityInfo() 调用 LocalAlloc() 来自动分配安全描述符,所以你可以直接将返回的安全描述符传递给 ACL 编辑器。不幸的是,如果你使用 .NET 等技术,那就麻烦了,因为这意味着你必须在范围外封送一个本机对象。

STDMETHODIMP CSecurityInformation::GetSecurity
  (SECURITY_INFORMATION si, PSECURITY_DESCRIPTOR *ppSD,
  BOOL fDefault)
{
    *ppSD = NULL ;
    if (fDefault)
    {
        return E_NOTIMPL ;
    }

    SetLastError(ERROR_SUCCESS) ;
    GetNamedSecurityInfo(this->FileName,
      SE_FILE_OBJECT, si, NULL, NULL, NULL, NULL, ppSD) ;

    return S_OK;
}

图 51:GetSecurity() 的简单实现(GetDefaultSecurity() 未实现)。

ISecurityInformation::SetSecurity()

一旦用户更改了安全描述符,按了“应用”并响应了警告,就会调用此方法。这是最难实现的方法。有些问题可能是自找的(Filepermsbox 将文件名存储在 STL 列表中,需要将每个名称转换为一个可写的 C 风格字符串),有些是由于 ACLUI 是对象无关的,但大多数是因为 ACLUI 在此方法之前几乎没有做任何工作。

你可以通过查看 SECURITY_INFORMATION 参数来找出用户修改了哪些部分。所有更改都将放在修改后的安全描述符中。你的任务是将更改应用到对象。恢复你的对象名称并开始启用你需要的任何权限。然后深呼吸,祝你好运。

7. 使用 SetSecurityInfo() 设置安全。

SetSecurity()文档 [^] 说:“应用程序必须将新的安全描述符部分合并到对象的现有安全描述符中”。那些替换安全描述符部分而不是合并它们的旧安全函数已经不适用。

如果你阅读了 第 2 部分,你将知道使用 SetSecurityInfo()(或 SetNamedSecurityInfo())是写入安全描述符的首选方法。为了使用 SetSecurityInfo(),你需要将 SECURITY_DESCRIPTOR 分解成它的各个部分。获取 DACL、SACL、组和所有者并不太困难(特别是借助 GetSecurityDescriptorDacl() 和朋友)。

STDMETHODIMP CSecurityInformation::SetSecurityForObject
  (TCHAR *ObjectName, SECURITY_INFORMATION psi, PSECURITY_DESCRIPTOR pSD)
{
  ...
  DWORD dwErr = 0;
  BOOL AclPresent = FALSE, AclDefaulted = FALSE;

  if(psi & DACL_SECURITY_INFORMATION)
  {/* Get the new DACL. */
    dwErr = GetSecurityDescriptorDacl(pSD, &AclPresent,
      &NewDAcl, &AclDefaulted);
  }
  if(psi & SACL_SECURITY_INFORMATION)
  {/* Get the new SACL. */
    dwErr = GetSecurityDescriptorSacl(pSD, &AclPresent,
      &NewSAcl, &AclDefaulted);
  }
  if(psi & GROUP_SECURITY_INFORMATION)
  {/* Get the New Group SID */
    dwErr = GetSecurityDescriptorGroup(pSD, &SidGroup, &AclDefaulted);
  }
  if(psi & OWNER_SECURITY_INFORMATION)
  {/* Get the New Owner SID */
    dwErr = GetSecurityDescriptorOwner(pSD, &SidOwner, &AclDefaulted);
  }

  /* You now have the owner, group, DACL, and SACL. */
  ...
}

图 52:提取安全描述符的各个部分,以便你可以调用 SetSecurityInfo()

更难的部分是翻译控制位。在 第 2 部分(设置安全描述符时)中,我提供了一个映射,可以将控制位转换为适当的安全信息标志。此映射将帮助你检测用户是否勾选了自动继承框,并将任何继承设置转换为适当的 SECURITY_INFORMATION 标志。

  ...
  /* Continued from Figure 52 */

  SECURITY_DESCRIPTOR_CONTROL pSDControl = {0};
  DWORD SDRevision = 0;
  dwErr = GetSecurityDescriptorControl(pSD, &pSDControl, &SDRevision);
  if(psi & DACL_SECURITY_INFORMATION)
  {
    if(pSDControl & SE_DACL_PROTECTED)
    {/** The DACL must not inherit. Switch psi to
     *   PROTECTED_DACL_INFORMATION
     **/
      psi |= PROTECTED_DACL_SECURITY_INFORMATION;
      dwErr = SetSecurityDescriptorControl(ppSD, SE_DACL_PROTECTED,
        SE_DACL_PROTECTED);
    }
    if(pSDControl & SE_DACL_AUTO_INHERIT_REQ)
    {/* We must toggle the DACL's inheritance. */
      psi |= UNPROTECTED_DACL_SECURITY_INFORMATION;
      dwErr = SetSecurityDescriptorControl(ppSD,
        SE_DACL_AUTO_INHERIT_REQ, SE_DACL_AUTO_INHERIT_REQ);
    }
  }
  if(psi & SACL_SECURITY_INFORMATION)
  {
    if(pSDControl & SE_SACL_PROTECTED)
    {/* ...And SACL */
      psi |= PROTECTED_SACL_SECURITY_INFORMATION;
      dwErr = SetSecurityDescriptorControl(ppSD, SE_SACL_PROTECTED,
        SE_SACL_PROTECTED);
    }
    if(pSDControl & SE_SACL_AUTO_INHERIT_REQ)
    {/* ...And SACL. */
      psi |= UNPROTECTED_SACL_SECURITY_INFORMATION;
      dwErr = SetSecurityDescriptorControl(ppSD,
        SE_SACL_AUTO_INHERIT_REQ, SE_SACL_AUTO_INHERIT_REQ);
    }
  }
  ...
  /* To Figure 54. */

图 53:重新映射控制位到 SECURITY_INFORMATION 标志。

如果你的对象是容器,那么你可能需要添加对“大锤”复选框的支持。

8. 处理“大锤”复选框。

SDK 中没有记录,但为了检测用户是否选择了“大锤”复选框之一,你需要测试 psi 参数是否包含以下标志之一:SI_OWNER_RECURSESI_RESET_DACL_TREESI_RESET_SACL_TREE。如果存在任何这些标志,你将需要递归地将安全描述符应用于所有子对象。你可以保证这段代码会涉及到集合。

由于 ACLUI 对你的对象一无所知,它不知道如何遍历它们,所以你需要自己遍历目录树。遍历目录树应该不难。它只是 FindFirstFile()FindNextFile()FindClose()(或者如果你的对象不是文件夹,则使用等效函数)。然后使用获取的文件列表,逐个设置安全描述符。

这种技术似乎可行,但存在一个缺陷。假设你当前被拒绝访问该文件夹。在这种情况下,FindFirstFile() 将失败,你将无法遍历树。我们如何遍历当前无法访问的目录?不要忽视这个问题,重置安全是你的应用程序的一个主要用例(用户刚刚被锁定在某个文件夹之外,现在需要你来挽救)。

这里的答案是作弊!你无法遍历文件系统,因为访问检查机制阻止我们这样做。我们需要做的就是完全禁用访问检查机制!如果你拥有“SeBackupPrivilege”,你可以禁用访问检查机制,以 FILE_TRAVERSE 权限打开任何文件,并成功遍历所有子对象!这正是 Windows 在 Windows 2000 中所做的。不幸的是,只有当你属于 BackupOperators/Administrators 组时,才可以使用这个“关闭”开关。我们如何在不是管理员的情况下遍历文件夹?

如果你在 Windows XP 或更高版本上,可以使用最近文档化的 TreeResetNamedSecurityInfo() API。TreeResetNamedSecurityInfo() 的行为与普通 SetSecurityInfo() 类似,但也可以遍历文件系统文件夹或注册表项并为所有子项设置安全。它通过模拟一个高度特权帐户,并使用特权帐户来遍历树。可选地,你可以提供一个回调函数,该函数可用于接收每个文件的通知。如果你需要取消树遍历操作,应将 PROG_INVOKE_SETTING 成员设置为 ProgressCancelOperation。Filepermsbox 使用回调来提供进度对话框。

此方法具有优点,即它是受支持的 API,因此与未来 Windows 版本兼容性更好。然而,TreeResetNamedSecurityInfo() 是一个较新的 API,在 Windows 2000 中不可用,并且有其自身尚未发现的 bug。

VOID WINAPI TreeCallBackFunc(wchar_t *pObjectName, DWORD Status,
  PROG_INVOKE_SETTING *pInvokeSetting, void *PtrToThis, BOOL SecuritySet)
{

  CSecurityInformation *ThisClass =
    reinterpret_cast<CSecurityInformation *>(PtrToThis);

  if(SecuritySet != TRUE)
  {/* Something occurred. Inform the User. */
    INT_PTR Choice = DisplayErrorDialog(pObjectName, ThisClass, Status);
    /* Choice will be one of: IDCANCEL, IDRETRY, IDIGNORE, IDC_BUTTON1 */

    switch(Choice)
    {
      default:
      case IDCANCEL:
        *pInvokeSetting = ProgressCancelOperation;
        break;
      case IDRETRY:
        *pInvokeSetting = ProgressRetryOperation;
        return;
      case IDIGNORE:
        *pInvokeSetting = ProgressInvokeEveryObject;
        break;
      case IDC_BUTTON1:
        *pInvokeSetting = ProgressInvokeNever;
        break;
     }
  }
  else *pInvokeSetting = ProgressInvokeEveryObject;
}


{
  /* Continued from Figure 53 */
  ...
  if((psi & SI_OWNER_RECURSE || psi & SI_RESET_DACL_TREE ||
    psi & SI_RESET_SACL_TREE) && this->IsContainer(ObjectName))
  {/* Call the XP provided function. */
    psi = psi &~ ( SI_OWNER_RECURSE | SI_RESET_DACL_TREE |
      SI_RESET_SACL_TREE);
    dwErr = this->pfnTreeResetNamedSecurityInfo(ObjectName,
      this->SeObjectType, psi, SidOwner, SidGroup, NewDAcl,
      NewSAcl, this->KeepExplicit, TreeCallBackFunc,
      ProgressInvokeEveryObject, reinterpret_cast<void *>(this));
    SetLastError(dwErr);
  }
  else
  {
    psi = psi &~ ( SI_OWNER_RECURSE | SI_RESET_DACL_TREE | SI_RESET_SACL_TREE);
    SetLastError(ERROR_SUCCESS);
    dwErr = SetNamedSecurityInfo(ObjectName, this->SeObjectType, psi, SidOwner,
      SidGroup, NewDAcl, NewSAcl);
    SetLastError(dwErr);
  }

  ...
  /* Cleanup, update shell and inform user */
}

图 54:使用 TreeResetNamedSecurityInfo() 设置安全.

如果你打算支持 Windows 2000 客户端,你需要模拟 TreeResetNamedSecurityInfo() API。这就引出了最后一个选项:手动遍历目录,并使用调整过的遍历算法。

这是深度优先递归优于广度优先递归的少数情况之一。当你当前被拒绝访问对象时,你必须先写入安全描述符,然后读取目录内容。如果你是授予自己访问权限,这就可以工作,但如果你拒绝自己访问呢?也就是说,你当前可以访问目录,但你将要写入一个将拒绝你自己访问的安全描述符。一旦你写入了主文件夹,你就无法再读取目录了。在这种情况下,你需要先遍历目录,然后设置安全描述符。这需要两种不同的算法。

Filepermsbox 通过应用这两种算法来解决这个难题。首先获取文件列表,递归进入所有这些子项,然后将安全描述符应用于根文件夹,然后获取文件列表,然后重复递归。

如果你遇到这个问题,我建议你找到一种绕过或关闭访问检查机制的方法(例如,启用权限,或联系系统服务)。尝试设计一个处理访问检查的算法是相当繁琐的。

如果你正在写入 DACL 或 SACL,还有一项任务。虽然根文件夹应该获得用户创建的安全描述符,但文件夹的子项应该获得一个不同的安全描述符:“D:ARAIS:ARAI”。也就是说,所有子项都应该获得一个空的、自动继承的安全描述符。此安全描述符将擦除所有显式 ACE,只留下来自根文件夹的自动继承 ACL。对于 Filepermsbox,这恰好与默认安全描述符(来自 ISecurityInformation::GetSecurity())相同。如果你使用了 TreeResetNamedSecurityInfo(),你可以利用 KeepExplicit 参数来为你完成此操作。

DWORD CSecurityInformation::TreeResetWin2k(TCHAR *m_ObjectName,
  PSECURITY_DESCRIPTOR ppSD, SECURITY_INFORMATION psi)
{

  /* The default security descriptor needs
     to be applied to all children of this object. */
  PSECURITY_DESCRIPTOR pSD2 = NULL;
  if(psi & DACL_SECURITY_INFORMATION || psi & SACL_SECURITY_INFORMATION)
    dwErr = this->GetSecurity(psi, &pSD2, TRUE);

  if(pSD2 == NULL)
  {/* If default security cannot be used,
      use a copy of the current security descriptor. */
    LPTSTR lpszSD = NULL;
    ConvertSecurityDescriptorToStringSecurityDescriptor(ppSD, 
        SDDL_REVISION_1, psi, &lpszSD, NULL);

    ConvertStringSecurityDescriptorToSecurityDescriptor(lpszSD, 
       SDDL_REVISION_1, &pSD2, NULL);
    LocalFree(lpszSD); lpszSD = NULL;
  }

  /* Apply ppSD to the root object, and pSD2 to all children. */

  ...
  LocalFree(pSD2); pSD2 = NULL;
}

图 55:为子项创建安全描述符。

9. 处理多个对象。

有人曾经在新闻组中问过这个问题,所以我添加了这一节。ACL 编辑器可以处理多个对象吗?乍一看,当然不能。ACL 编辑器只能显示一个安全描述符,这意味着你只能显示一个对象。ACL 编辑器如何能编辑多个对象?

如果对象具有相等的安全描述符,它可以显示。如果两个对象具有相等安全描述符,则只需显示其中一个,然后你可以将其应用于两者。但是,什么构成了相等安全描述符?没有名为 IsEqualSecurityDescriptor() 的 API 可以帮助我们,所以我们需要自己比较安全描述符。这不仅对 ACL 编辑器有用,还可以用于安全审计应用程序(例如,AccessEnum [^])。

两个安全描述符可以被认为是相等的,如果 AccessCheck() 对它们两者的行为方式相同。组、所有者、版本和持久控制位(SE_DACL_AUTO_INHERIT_REQ 是一个临时标志,不计入)对于两个安全描述符都必须相等。对于 DACL 和 SACL,它们不仅要有相同的条目,你还必须考虑 ACE 的正确排序。

  1. 如果组、所有者、持久控制标志或版本不同,则不相等。
  2. 如果 DACL 具有不同的条目(即,一个授予你自己的访问权限,而另一个拒绝访问),那么它们不可能相等(AccessCheck() 的行为将不同)。
  3. 如果允许 ACE 在拒绝 ACE 之前,ACL 的顺序错乱将改变 AccessCheck() 的行为。
  4. 然而,如果两个允许 ACE 的顺序错误,DACL 的正确排序仍然会得到维持,并且 AccessCheck() 对于两者来说都是相同的。通常,任何保持 DACL 排序的更改都不会改变安全描述符。
  5. 2、3 和 4 应适用于 SACL 和 DACL。

很明显,你需要将安全描述符分解成它的各个部分来进行相等性检查。将安全描述符分解成各个部分已在 第 2 部分中解释过。对于 DACL/SACL,你可以进行字节比较(Windows 就是这样做的),但这会让你将情况 4 视为不相等的安全描述符。Filepermsbox 更进一步——它取出第一个 DACL 中的一个 ACE,并在第二个 DACL 中搜索此值。这种方法成功通过了情况 4,但如果 DACL 顺序错误(情况 3),则 Filepermsbox 会错误地通过 DACL。你需要执行的最终测试是检查 ACE 的正确排序。

因此,总而言之,两个安全描述符是相等的,如果:

  1. 组和所有者 SID 相同。
  2. 版本和持久控制标志相同。
  3. DACL 和 SACL 包含相同数量的条目。
  4. DACL 和 SACL 包含相同的条目。
  5. DACL 和 SACL 按规范顺序排列。

如果你可以利用 .NET/ATL/SDDL/正则表达式,请随时使用它们来帮助你。

如果你发现安全描述符不相等,你可以询问用户是否要更改安全描述符以使其相等(Windows 就是这样做的)。一旦你使安全描述符相等,你就可以显示第一个安全描述符。用户将只编辑第一个安全描述符,当用户按“应用”时,将调用你的 SetSecurity() 方法。你现在需要做的就是在循环中将这个安全描述符应用到所有选定的对象(如果你实现了“大锤”功能,这应该不难)。

一切就绪后,就可以根据需要调用 SetPrivateObjectSecurityEx()SetSecurityInfo()SetNamedSecurityInfo()TreeResetNamedSecurityInfo()。然后清理权限和资源,并将任何错误返回给 ACLUI。

STDMETHODIMP CSecurityInformation::SetSecurity
  (SECURITY_INFORMATION psi, PSECURITY_DESCRIPTOR pSD)
{
  ...
  /* Initialise UI, enable privileges, etc. */

  std::list< const std::basic_string<TCHAR> > DirectoryCollection;
  /* ... Fill in DirectoryCollection... */


  for(std::list <const std::basic_string<TCHAR> >::const_iterator
    m_ObjectName = DirectoryCollection.begin();
    m_ObjectName != DirectoryCollection.end(); m_ObjectName++)
  {/* Use our helper class to convert the wstring
      into a writable TCHAR array */
    sized_array<TCHAR> FinalName(*m_ObjectName);

    dwErr = this->SetSecurityForObject(FinalName.get(), psi, pSD);
    /* Call Figure {49 50 51} for this filename */

    if(dwErr != ERROR_SUCCESS) break;
  }

  /* Cleanup, handle any errors, etc. */
  ...

  return HRESULT_FROM_WIN32(dwErr);
}

图 56:使用 SetSecurityInfo() 等写入安全描述符。

32. ISecurityInformation2、ISecurityObjectTypeInfo 和 IEffectivePermission

在实现了 ISecurityInformation(特别是 SetSecurity())后,你可能想休息一下,然后继续下一个项目。然而,还有三个其他接口可以实现。如果你选择不实现这三个接口,你将失去一些非常有用的功能。如果你实现这些接口,请不要忘记通过更新 IUnknown::QueryInterface()ISecurityInformation::GetObjectInformation() 来通知 ACLUI。

10. ISecurityObjectTypeInfo::GetInheritSource() 和 INHERITED_FROM[ ]

只有当你的类实现 ISecurityObjectTypeInfo 时才需要此方法。此方法仅在操作系统为 Windows XP 及更高版本时可用。此方法使 ACL 编辑器能够显示继承的 ACE 来自何处。它将在高级 DACL 页面上显示结果。为了实现此接口,你需要使用 GetInheritanceSource(),它会填充(隆重推出)……一个哈希表!

调用 GetInheritanceSource() 后,最好对返回的 INHERITED_FROM 数组调用 FreeInheritedFromArray() 来释放内存。

ACL 编辑器不会为你做这件事。你必须实现代码来将此数组复制到 LocalAlloc() 分配的一个数组中,并返回 LocalAlloc() 分配的数组。操作系统会使用 LocalFree() 释放该块(尽管未记录)。最后一个问题是如何释放你复制的字符串内容。Filepermsbox 重用了 第 13 节中使用的技术,以及 C FAQ [^] 中提到的方法,即分配一个大的内存块并使用一些创造性的指针算术来模拟二维数组。

这是微软在实现此 API 时做出的一项相当值得商榷的设计决定,我们不得不 resort 到这种不干净的 hack。

STDMETHODIMP CSecurityInformation::GetInheritSource
  (SECURITY_INFORMATION psi, PACL pAcl, PINHERITED_FROM *ppInheritArray)
{
  DWORD dwErr = 0;
  size_t i = 0, dwSize = 0;
  PINHERITED_FROM InheritTmp = reinterpret_cast<PINHERITED_FROM>
    (LocalAlloc(LPTR, (1 + pAcl->AceCount) * sizeof(INHERITED_FROM)));

  BOOL Container = (this->m_dwSIFlags & SI_CONTAINER) ? TRUE : FALSE;

  dwErr = GetInheritanceSource(this->FileName, SE_FILE_OBJECT, psi,
    Container, NULL, 0, pAcl, NULL, &g_ObjMap, InheritTmp);

  /* Find the required size of our 2 dimensional array */
  for(i = 0 ; i < pAcl->AceCount ; i++)
  {
    const std::basic_string<TCHAR> &GenName =
      InheritTmp[i].AncestorName;
    dwSize += GenName.size() + 1;
  }

  /* Allocate the data all in one. */
  PINHERITED_FROM InheritResult =
    reinterpret_cast<PINHERITED_FROM>(LocalAlloc(LPTR, (1 + pAcl->AceCount) *
      sizeof(INHERITED_FROM) + dwSize * sizeof(TCHAR)));

  /** It may help if we can get a pointer to the data segment as well as the
  *   array header.
  **/
  TCHAR *DataPtr = reinterpret_cast<TCHAR *>(reinterpret_cast<BYTE *>
    (InheritResult) + pAcl->AceCount * sizeof(INHERITED_FROM));

  for(i = 0 ; i < pAcl->AceCount ; i++)
  {
    const std::basic_string<TCHAR>
      &GenName = InheritTmp[i].AncestorName;
    GenName.copy(DataPtr, GenName.size(), 0);
    /* copy over the strings */

    /* Make the headers point to the newly created data */
    InheritResult[i].GenerationGap = InheritTmp[i].GenerationGap;
    InheritResult[i].AncestorName = DataPtr;

    /* and move the pointer forward */
    DataPtr += GenName.size() + 1;
  }

  /* Cleanup */
  FreeInheritedFromArray(InheritTmp, pAcl->AceCount, NULL);
  LocalFree(InheritTmp); InheritTmp = NULL;

  *ppInheritArray = InheritResult;
  return HRESULT_FROM_WIN32(dwErr);
}

图 57:查找自动继承 ACE 的来源。

11. IEffectivePermission::GetEffectivePermission()

只有当你的类实现 IEffectivePermission 时才需要此方法。此方法仅在操作系统为 Windows XP 及更高版本时可用。ACL 编辑器使用此函数来实现其“有效权限”功能(它为 GetEffectiveRightsFromAcl() API 提供用户界面)。要显示“有效权限”选项卡,你需要在 SI_OBJECT_INFO 结构中指定一个未记录的标志(SI_EDIT_EFFECTIVE)。要在代码中实现它,你需要完成四个任务:

  1. 从提供的安全描述符中提取 ACL。
  2. 分配一个 ACCESS_MASK 数组。

  3. 从提供的 SID 构建一个 TRUSTEE。如果你需要将 SID 翻译成名称,请确保使用提供的 pszServerName 参数进行 SID 查找。
  4. 使用你在第 2 步中分配的缓冲区和你在第 3 步中构建的 TRUSTEE 调用 GetEffectiveRightsFromAcl()。然后将输出参数设置为输出结果。

你应该 LocalAlloc() 分配 ACCESS_MASK 数组的原因是,ACL 编辑器在完成缓冲区后会调用 LocalFree()。如果你的对象支持 GUID,你将需要额外的工作。你必须用对象的属性树(以及 GUID)填充一个 OBJECT_TYPE_LIST[] 数组。

STDMETHODIMP CSecurityInformtion::GetEffectivePermission
  (const GUID *, PSID pUserSid, LPCWSTR /*pszServerName*/,
  PSECURITY_DESCRIPTOR pSD, POBJECT_TYPE_LIST* ppObjectTypeList,
  ULONG* pcObjectTypeListLength, PACCESS_MASK* ppGrantedAccessList,
  ULONG* pcGrantedAccessListLength)
{
  DWORD dwErr = 0 ;
  BOOL AclPresent = FALSE, AclDefaulted = FALSE ;
  PACCESS_MASK AccessRights = NULL ;
  PACL Dacl = NULL ;

  *ppObjectTypeList = const_cast<OBJECT_TYPE_LIST *>(g_DefaultOTL) ;
  *pcObjectTypeListLength = 1 ;

  GetSecurityDescriptorDacl(pSD, &AclPresent, &Dacl, &AclDefaulted) ;
  TRUSTEE Trustee = {0} ;
  BuildTrusteeWithSid(&Trustee, pUserSid) ;

  AccessRights = reinterpret_cast<PACCESS_MASK>
    (LocalAlloc(LPTR, sizeof(PACCESS_MASK) + sizeof(ACCESS_MASK))) ;

  dwErr = GetEffectiveRightsFromAcl(Dacl, &Trustee, AccessRights) ;

  *ppGrantedAccessList = AccessRights ;
  *pcGrantedAccessListLength = 1 ;
  return S_OK ;
}

图 58:实现未记录的有效权限选项卡。

12. ISecurityInformation2::LookupSids() 和 SID_INFO_LIST

有时,ACLUI 无法将 SID 从二进制形式翻译成用户名。它就是无法传递正确的参数给 LookupAccountSid(),或者可能需要 LsaLookupSids()。在这些情况下,你可能能够翻译 SID。如果你重写了 ISecurityInformation2::LookupSids(),你就可以重写 SID 查找过程,从而允许你翻译无法翻译的 SID。

LookupSids() 传递给你一个 SID 数组。你被要求翻译尽可能多的 SID,并将结果主体/显示名称放入 SID_INFO 结构数组(SID_INFO_LIST)中。要将数据返回给 ACLUI,你被要求将结构包装在 IDataObject 中。这意味着要实现另一个 COM 接口(只需要实现 IDataObject::GetData())。由于 SID_INFO_LIST 的生命周期至少要和你的 CDataObject 一样长,所以你应该让你的 CDataObject 管理 SID_INFO_LIST。Filepermsbox 没有实现此接口(J. Brown [^] 应该比我更能帮助你)。但是,如果你创建了一个 CDataObject,你的 LookupSids 实现可以看起来像图 53。

STDMETHODIMP CSecurityInformation::LookupSids(ULONG /* cSids */,
  PSID * /* rgpSids */, LPDATAOBJECT * /* ppdo */)
{/* ISecurityInformation2. Let ACLUI handle this work. */
  SID_INFO_LIST *sidList = new SID_INFO_LIST[(sizeof(ULONG) +
    cSids * sizeof(SID_INFO)) / sizeof(SID_INFO_LIST) + 1];
  memset(sidList, 0, sizeof(ULONG) + cSids * sizeof(SID_INFO));
  for(ULONG i = 0; i < cSids; i++)
  {
    SID_NAME_USE peUse = SidTypeUnknown;
    DWORD cchName = 0, cchReferencedDomainName = 0;
    LookupAccountSid(NULL, rgpSids[i], NULL,
      &cchName, NULL, &cchReferencedDomainName, &peUse);
    TCHAR *UserName = new TCHAR[cchName];
    memset(UserName, 0, sizeof(TCHAR) * (cchName));
    TCHAR *DomainName = new TCHAR[cchReferencedDomainName];
    memset(DomainName, 0, sizeof(TCHAR) * (cchReferencedDomainName));

    LookupAccountSid(NULL, rgpSids[i], UserName, &cchName,
      DomainName, &cchReferencedDomainName, &peUse);
    sidList->aSidInfo[i].pSid = rgpSids[i];
    sidList->aSidInfo[i].pwzClass = _T("User");
    sidList->aSidInfo[i].pwzCommonName = UserName;

    std::basic_string<TCHAR> UPNName = UserName;
    UPNName.append(_T("@"));
    UPNName.append(DomainName);

    TCHAR *lpszUPN = new TCHAR [UPNName.size() + 1];
    UPNName.copy(lpszUPN, UPNName.size(), 0);
    lpszUPN[UPNName.size()] = _T('\0');
    sidList->aSidInfo[i].pwzUPN = lpszUPN;
    sidList->cItems++;
    /* Domainname is not referenced in the final array, so delete it here. */
    delete [] DomainName; DomainName = NULL;
  }

  CDataObject *outObj = new CDataObject(sidList);
  *ppdo = dynamic_cast<IDataObject *>(outObj);
  /** In the destructor for outObj, you are responsible for deleting sidList
  *   and its contents.
  **/
  return S_OK;
}

图 59:将 SID 翻译成用户名。

13. ISecurityInformation2::IsDaclCanonical()

前面,我告诉你 ACL 编辑器会排序 ACL 以遵守正确的排序规则。如果你尝试给它一个无序的 ACL,ACLUI 会将这些条目排序为正确的顺序。如果你实现了此方法,你可以覆盖 ACL 排序。该方法很简单:你得到一个指向 ACL 的指针,如果 ACL 顺序正确,则返回 TRUE,否则返回 FALSE。至于如何检查 ACL 是否已排序,这留给读者作为练习。

Filepermsbox 要求 ACL 的排序始终正确,因此它始终返回 FALSE(它没有实现此方法)。

33. 介绍接口

如果你已经读到本系列文章的这个部分,你就已经成功实现了一个 ISecurityInformation 对象,可以调用了。现在唯一剩下的就是打开 ACL 编辑器。你可以调用 CreateSecurityPage()(如果你有一个属性表对话框)或 EditSecurity()(如果你没有)。以上任何一个函数都会显示 ACL 编辑器。

DoSecurityBox
  (HWND TheirhWnd,
   SE_OBJECT_TYPE seObjType,
   std::list<const std::basic_string<TCHAR> > &FileNames)
{
  CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
  {/* SecPage needs to be destructed before CoUninitialize. */
    std::auto_ptr<CSecurityInformation> SecPage
      (new CSecurityInformation(FileNames, seObjType, TheirhWnd));
    EditSecurity(TheirhWnd, SecPage.get());
  }
  CoUninitialize();
  return TRUE;
}

图 60:显示 ACL 编辑器。

就这样!Filepermsbox 添加了以下用户界面元素……

14. 添加进度对话框。

ISecurityInformation::SetSecurity() 可能需要很长时间才能完成。(它确实花了很长时间才写好!)因此,你可能需要考虑添加一个进度对话框。由于失败的可能性很高(你通常不允许写入安全描述符),你应该在出现任何失败时通知用户,这时进度对话框就派上用场了。我选择了 Michael Dunn 的进度对话框(此处)。它位于一个单独的工作线程上,在创建 ACL 编辑器之前创建(我无法在 ISecurityInformation::SetSecurity() 中创建线程,因为该线程当时正在模拟)。为了支持“取消”按钮,我使用了 TreeResetNamedSecurityInfo() 中的回调。

15. 属性表扩展

Filepermsbox 可选择地提供一个与所有文件和文件夹关联的 Shell 扩展。Shell 扩展基于 SDK 示例(并得到 “编写 Shell 扩展的完整傻瓜指南” 的一些帮助)。这是少数几个可以处理多个文件和多个文件夹的 Shell 扩展之一。当你选择多个文件和文件夹时,Filepermsbox 会检查每个文件的安全描述符。如果安全描述符不可用(例如,FAT32 驱动器),则不显示属性表。

所有安全描述符必须相等,否则用户会被提示重置安全描述符。显然,如果文件来自不同的路径(例如,一个文件来自“C:\Windows”,另一个文件来自“C:\Program Files\Common Files”),那么安全描述符肯定会不同。但在这种情况下,你知道它们不同的原因。任何重置安全描述符的尝试都不会使它们相等——只会使情况变得更糟。Windows 就存在这个问题。Filepermsbox 则没有。(Filepermsbox 解析路径以查看它们来自哪个目录。在这种情况下,它不会显示选项卡。)

在显示 ACL 编辑器之前,属性表会显示安全描述符的 SDDL 格式。

Filepermsbox`s SDDL Viewer

图 61:Filepermsbox 属性表对话框的视图。

16. Filepermsbox API。

目前,Filepermsbox API 完全是死板的。只有一个 API 可用,而且只有在使用 Visual C++ 2003 时才可用(这是由于 FileNames 参数)

/** Displays the ACL editor for the filenames specified, so
*   the user can edit their security descriptor.
*
*   All parameters are in parameters
*
*   HWND TheirhWnd - The parent window
*   SE_OBJECT_TYPE seObjType - The type of object specified.
*     Currently, only SE_FILE_OBJECT is supported.
*   const list<const wstring> &FileNames - The list of
*     filenames the ACL editor will display. Only the first
*     security descriptor will be passed to the ACL editor.
*   BOOL AddWritability - Unless set to TRUE, the ACL editor
*     will be read-only, meaning the user can't make any
*     changes.
*
*   Returns TRUE for success and FALSE for failure. A error
*   message may be displayed to the user if a unhandled
*   exception occurs. You might get more information from
*   GetLastError().
**/
USRDLL_IMPORT BOOL __stdcall DoSecurityBox(HWND TheirhWnd,
   SE_OBJECT_TYPE seObjType,
   const std::list<const std::basic_string<TCHAR> > &FileNames,
   BOOL AddWritability);

图 62:从 Filepermsbox.dll 导出的主 API。

我使用 C++ / CLI 创建了一个更易于使用的 .NET 包装器(你可以通过 OLE 自动化调用它),它不要求 STL 列表,而是要求一个 System.String[]。但是,由于你需要 Visual Studio 2005 才能使用它,所以目前还无法提供。当 Visual Studio 2005 正式发布时,我将在 VS2005 中重新编译解决方案,然后你就可以使用我的 API 了。

34. 最后的想法。

基于 ACL 的安全是在 Windows NT(早在 1989 年)的最初几个月就设想出来的。它仍然是不同类型授权中最复杂的一种。基于 ACL 的安全围绕一个函数:AccessCheck()。所有结构、ACL、权限和安全描述符都只是为了定制 AccessCheck() 的行为。定制此 API 的主要结构是 SECURITY_DESCRIPTOR 结构。

原始授权 API 被开发人员(包括微软内部的)普遍认为不可用。创建能够维持功能和安全性之间正确平衡的安全描述符过于困难。因此,开发人员通常完全回避安全描述符,或者创建次标准的安全描述符。然而,这些选择不当的安全描述符导致了 IT 行业有史以来最糟糕的一些安全漏洞(以及与之相伴的最具破坏性的病毒)。微软(和其他公司)意识到使用基于 ACL 的安全性的难度,并为基于 ACL 的函数创建了新的接口(例如,C++、ATL,以及最近的 .NET)。

尽管复杂,但借助授权 API,安全描述符可以应用于任何你喜欢的对象,但最常应用于文件、内核句柄和注册表项。授权 API 依赖于身份验证 API 正常工作,并从身份验证 API 借用许多结构,如 SID、访问令牌和模拟级别。

在本系列中,你被展示了如何使用旧方法和三种可用的安全库来编程 ACL。在此基础上,你被展示了如何在你的应用程序中实现 ACL 编辑器,以便允许用户编辑安全描述符。

我希望你喜欢我的 Windows 访问控制模型系列。该系列旨在涵盖足够多的主题,以便你可以编程 ACL 编辑器,因此它在安全主题方面相当单薄。我们没有谈论基于角色的安全(一种更简单的安全模型,根据你所属的组来保护),或基于代码的安全(另一种更简单的模型,根据你正在采取的行动来保护)。这些较新的安全模型最好通过 .NET 框架来利用(尽管它们也可用于本机应用程序)。

基于私有安全描述符的 Active Directory 安全在本系列中被巧妙地忽略了。然而,由于私有安全描述符可能非常复杂,特别是对管理员而言,微软为它们提供了一个备用的脚本 API。Resource Kit / Technical Reference 是研究 Active Directory 安全的好参考。

即使你没有从本系列中受益,我也肯定从撰写它中受益。感谢本系列,我能够将 Filepermsbox 更新到 v1.10,并进行了以下修复:

  • 权限代码已重写。这修复了权限泄漏 bug。权限现在只在非常短的时间内启用,并且不再是进程范围的。
  • 进度对话框代码已重写,以提高响应速度、准确性和稳定性。
  • “大锤复选框”算法已重写。Win2K 应该会稳定得多。
  • 更新了代码以使用 VS2005 进行编译。
  • 启动了一个开发人员 API(但你需要等待 .NET v2.0 发布)。
  • 代码不再因无效文件名而崩溃。
  • 远程文件的 SID 现在被正确翻译。
  • Win9x 运行时不再崩溃。
  • 开始将应用程序通用化(因此它不仅适用于文件)。

参考书目/延伸阅读

我从以下书籍中获取了本系列的大部分材料:

互联网上也有许多不错的文章;这里有一些入门资源

如果您想要测试程序

35. 历史

  • 10/9/2004
    • 开始开发 QueryServiceConfig,一个服务安全编辑器。
  • 12/12/2004
    • QueryServiceConfig 被修改为处理文件而非服务。因此,Filepermsbox 应运而生。
  • 24/12/2004
    • Filepermsbox 和 QueryServiceConfig 发布。
  • 12/2004 - 3/2005
    • Filepermsbox 更新至 v1.09。QueryServiceConfig 更新至 v1.03。
  • 24/3/2005
    • 开始为 CodeProject 记录我的经验。
  • 26/3/2005
    • 将文章分成四部分。
  • 6/4/2005
    • 第一部分上传。
  • 12/4/2005
    • 将第二部分中的几节移回第一部分。同时修复了水平滚动条。
  • 23/4/2005
    • 第二部分上传。包含更新的目录。
  • 14/5/2005
    • 第三部分上传。目录再次更新。
  • 21/6/2005
    • 整个系列现已全部上传。所有文章都已更新以反映此次更改。
  • 07/09/2005
    • Filepermsbox 更新至 v1.11。
© . All rights reserved.