译:Windows 访问控制模型(第一部分)

作者:oshah

摘要:对基于 ACL 的安全性以及 Windows 访问控制模型的简介。

Screenshot of the ACL Editor

图 1: ACL 编辑器

介绍

在这一系列的文章中,我将讨论 Windows 访问控制模型以及其在 Windows NT 和 2000 上的实现。我计划将此系列分为四部分。开始的时候我认为只需要一篇就行了(我只想写关于 Windows 2000 风格的 ACL 编辑器),但由于需要读者对 Windows 访问控制了解相当多的内容,因此我不得不完成针对这一主题的一个完整的系列。

第一篇是对 Windows NT 上基于 ACL 的许可(从编程的观点)以及访问控制模型的实现的一个介绍。如果你从未用 Windows 安全性进行过编程,或者是刚刚开始,那么本文正适合于你。尽管在本文里使用 C 语言来呈现相关的结构,但是即使你不使用 C 语言编程,你也应该读一下本文。它包含了与所有程序员相关的概念。

本系列不是有关安全管理的初级读本(已经有大量的这方面的文章网站教程书籍了),而是试图从编程的角度讨论 Windows NT 的安全性模型。我将假定你知道如何设置文件或注册表键的许可权限,而且对 ACL 编辑器相当精通。我也将假定你知道什么是用户、什么是组。

访问控制在 16 位 Windows 或者 Windows 95/98/ME 上不可用。

目录

整个系列的目录如下。

  1. 第一部分 – 背景知识和核心概念。访问控制相关结构
    1. 安全标识符(SID)
    2. 安全描述符(SD)
    3. 访问控制列表(ACL)
    4. 选择一个好的自由访问控制列表
    5. Windows 2000 的继承模型
    6. 令牌
    7. 对安全描述符定义语言的一些解释
    8. 小结
  2. 第二部分 – 基本的访问控制编程
    1. 选择语言
    2. 玩玩 SID
    3. 你是哪一组?
    4. 启用令牌特权
    5. 剖析安全描述符
    6. 遍历访问控制列表
    7. 创建访问控制列表
    8. 我可以访问吗?
    9. 创建并编辑安全的对象
    10. 保护自己的类
    11. 玩具程序下载
  3. 第三部分 – 用 .NET v2.0 进行访问控制编程
    1. .NET 安全性的历史
    2. 在 .NET 中读取一个 SID
    3. .NET 版本的 Whoami
    4. 在 .NET 中进行特权处理
    5. 在 .NET 中以非特权用户运行
    6. 在 .NET 中获取并编辑安全描述符,并将之应用到对象
    7. 在 .NET 中进行访问检查
    8. NetAccessControl 程序以及 AccessToken 类库
  4. 第四部分 – Windows 2000 风格的访问控制编辑器
    1. ACL 编辑器的特性(ACLUI)
    2. 开始
    3. 实现 ISecurityInformation 接口
    4. ISecurityInformation::SetSecurity
    5. 可选接口
    6. 呈现接口
    7. Filepermsbox – 一个显示 NTFS 文件或者文件夹的安全描述符的程序
    8. 历史

背景

Windows NT 的原始目标之一就是提供一个可以为操作系统实现安全性的层。Windows NT 为其对象实现安全性的途径就是使用访问控制模型。甚至基于角色的安全性和 .NET 类都不能够取代这一基于 ACL 的安全模型。ACL 和 NTFS 体系里的对象联系过于紧密,而且注册表也要过时了,尽管如此,.NET v2.0 对 ACL 模型也终将妥协并提供支持。

一开始,我将会描述在访问控制编程过程中可能遇到的不同类型的结构。本文的初衷是面向所有的程序员,这里面没有代码,只有结构和编程概念。

1. 安全标识符(SID)

在钻研授权(authorization)的概念之前,我们先讨论一下安全标识符(SID)。我们人类喜欢拿用户名来指代一个用户(例 如”Administrator”),而计算机需要用二进制数据来指代用户。它使用被称为 SID 的哈希值来辨识用户。当一个用户被创建时,计算机会为该用户创建一个 SID,从那时起,计算机将使用用户的 SID 而不是用户名来指代用户。要看你自己的 SID 列表,可以打开注册便编辑器并展开 HKEY_USERS 树(可以看到当前登录的用户们的 SID 列表)(译者注:原文如此。但译者自己的看法是可以看到本机所有用户的 SID)。可以使用 ConvertSidToStringSid() 把原始的 SID 结构转换为文本形式。文本的 SID 通常以 S-1-5-21- 开头,由至少三个用短线分开的值组成,这些值代表了修订版本、职权(authority)、子职权(sub authority)、ID 以及主组(primary groups)。

SID 也可以表示一个用户组、一台计算机、一个域乃至森林。Windows 维护着一个被称为周知 SID 的硬编码 SID 列表,它们代表了诸如 LocalSystem 或者NetworkService 这样的帐户。可以在 WELL_KNOWN_SID_TYPE 枚举中查看到这些 SID 的清单。

要把一个 SID 转换为用户名,需要调用 LookupAccountSid(),而 LookupAccountName() 则可以把用户名转换为 SID。不过,大多数的安全性函数既接受 SID 也接受用户名(实际上是接受一个 TRUSTEE 结构,该结构是一个用户名和 SID 的联合体)。

2. 安全描述符(SD)

Windows NT 使用称为安全描述符的结构来保证对象的安全( SECURITY_DESCRIPTOR)。安全描 述符是 Windows 创建对象时所基于的结构的一个主要部分。这意味着 Windows NT 可识别的每一个对象(可以是文件对象、注册表键、网络共享、互斥量、信号灯、进程、线程、令牌、硬件、服务、驱动…)都可以保证其安全。安全描述符结 构既存在于内核模式也存在于用户模式,而且在两个模式中是一致的。虽然这允许了内核模式与用户模式在安全性方面的代码重用,但同时也意味着 SECURITY_DESCRIPTOR 从内核模式里继承了一些糟糕的怪异特性。

如果打开 Winnt.h 并滚动到 SECURITY_DESCRIPTOR 结构,你就可以看到安全描述符的构成(图 2)。 SECURITY_DESCRIPTOR 实际上有可能是两个结构之一,一个是绝对安全描述符( SECURITY_DESCRIPTOR),另一个是自相关安全描述符(图 3 SECURITY_DESCRIPTOR_RELATIVE)。

图 2: 绝对安全描述符

我们可以看到,去掉两个版本控制成员,一个安全描述符由五个成员组成:

  • 自由访问控制列表( Dacl):这儿保存着对象的许可(允许谁访问对象,而拒绝谁)。
  • 系统访问控制列表( Sacl):指定要对对象执行的审核的类型。如果发生了审核事件,会被存储到审核事件的日志中。
  • Owner:指定对象的所有者(一个 SID)。对象的所有者总是可以更改安全描述符,而不管其他人对访问的锁定。
  • Group:指定对象的主组(一个 SID)。Windows 通常忽略此参数(这是为了 POSIX 兼容性,但它现在已经退化了)。
  • Control:表现为一个 16 位整数的一组标志。可以为零或以下标志:
    • SE_DACL_PRESENTDacl 成员有效。
    • SE_SACL_PRESENTSacl 成员有效。
    • SE_DACL_AUTO_INHERITED:DACL 从其包含的父那里自动继承并得到其中各项。
    • SE_SACL_AUTO_INHERITED:和 SE_DACL_AUTO_INHERITED 一样,只不过应用于 SACL。
    • SE_DACL_PROTECTED:如果父的 ACL 与这里定义的任何 ACL 冲突,覆盖父的 ACL。用于覆盖继承
    • SE_SACL_PROTECTED:和 SE_DACL_PROTECTED 一样,只不过用于 SACL。
    • SE_DACL_DEFAULTEDDacl 成员等同于此类对象的缺省 DACL。
    • SE_SACL_DEFAULTEDSacl 成员等同于此类对象的缺省 SACL。
    • SE_GROUP_DEFAULTEDGroup 成员等同于此类对象的缺省组。
    • SE_OWNER_DEFAULTEDOwner 为缺省的。
    • SE_SELF_RELATIVE:表示此安全描述符是自相关的。

此结构的后四项都是指针,指向 ACL 等所在的缓冲区。这些指针可以指向内存中的任意合法地址,而且并不需要在一个连续块中。由于安全描述符不是连续的,所以将安全描述符写到磁盘或者令之跨越进程将会是一件相当麻烦的事情。微软通过引入一个称为自相关安全描述符的结构来解决这一问题(图 3 SECURITY_DESCRIPTOR_RELATIVE)。

图 3:自相关安全描述符的结构

自相关安全描述符比起绝对安全描述符来更加复杂。(相当复杂,甚至不能用 C++ 来表示它)。虽然前三个字段和绝对安全描述符一样,但其后就变得大不相同。在 Control 成员之后是四个表示数据偏移的 DWORD。安全描述符相关的数据全部在这四个 DWORD 之后。我在结构中插入了填充字节以显示数据可以出现在缓冲区中的任意位置,简单地改变 Offset 成员即可移动缓冲区。四个成员也不需要以任何特殊的顺序出现。使用特定的偏移,可以使 Owner 成员出现在 Group 之后。付出令人生畏的复杂性的代价之后,自相关安全描述符占据了一个连续内存快,使它适合于在应用边界间以及存储介质间传递。

现在你可以说出绝对安全描述符和自相关安全描述符之间的区别了?区别在于,对于一个绝对安全描述符,在 Control 成员之后是四个指针,分别依次指向 Group / Owner / SACL / DACL,而且这四个指针指向内存中用以存放数据的不同位置。而对于一个自相关安全描述符,在 Control 成员之后是四个 DWORD,以表示到 Group / Owner / SACL / DACL 的偏移。举例来说,如果 group 成员的值为 0xf,则就是在告诉 Windows,”你可以在我之后的 0xf 字节处找到组 SID”。由于这两个结构具有不同的大小,所以 64 位的 Windows 会使得这件事情更加令人迷惑!

更糟糕的是,微软在文档中并不区分绝对安全描述符和自相关安全描述符。微软还保留了对安全描述符作内部更改的权利,也就是说你不能认为将来此结构和现在是一样的。如果要操作安全描述符,你应该使用授权 API。使用 API 可以隐藏这些结构带来的复杂性。因此,要区别绝对安全描述符和自相关安全描述符应该调用 GetSecurityDescriptorControl(),然后再测试 SE_SELF_RELATIVE 标志。

知道哪个 API 需要绝对安全描述符而哪个需要自相关安全描述符很重要。在此系列的第二部分[^],你将自己构建一个安全描述符。

3. 访问控制列表(ACL)

在 Windows NT 里,无论什么时候,当你要对一个对象执行操作(比如说读)时,要执行的操作会被编码为一个 32 位的整数(称为 ACCESS_MASK)。 ACCESS_MASK 对于你试图创建的对象是特定的,如果你要读文件,那么 ACCESS_MASK 就应该是 FILE_GENERIC_READ。当你用请求 ACCESS_MASK 打开对象时,Windows 会取得你在线程令牌里的用户名并开始读取从安全描述符中取得的自由访问控制列表(Discretionary Aaccess Control List,DACL)。

DACL 可以被想象为一张表,其中有用户的 SIDACCESS_MASK 以及访问类型。不过不要试图把它写成一个结构数组。如果你要分析 ACL 的话,可以使用低级 ACL 函数 GetAce()GetAclInformation() 以及其他的辅助函数。在 NT4 中,微软提供了 ACL 的一个示意,看起来就是由 SIDACCESS_MASK 和类型组成的一个表(即 EXPLICIT_ACCESS 结构)。

图 4:ACL 和 ACE 的概貌

如果 DACL 是一个表的话,则其中的每一行就是一个访问控制项(Access Control Entry)。当要执行操作时,Windows 就遍历此 ACE 列表来寻找指向 就是线程令牌)的那一项。Windows 按照在 ACL 中的出现次序遍历 ACE。乍一看这与你的直觉、帮助文档甚至 MCP 的书中所述相悖(”我认为 Windows 首先遍历所有的拒绝 ACE,然后才是允许 ACE。而现在,你竟然告诉我这是错的?”)。实际上,我们都是对的。当 Windows 设置 DACL 时,它将访问控制项排序[^],拒绝 ACE 会优先于允许 ACE,从而访问控制模型也符合你的先前所学。

如果对低级函数进行调用,你就可以绕过排序而使 ACE 以你喜欢的任意顺序出现。Cygwin 工具使用这一技术来创建不排序的 DACL。不过这些 DACL 不能遵从访问控制规则。你也可以使用这些不正常的 ACL 来创建文件。

如果在 ACL 中没有找到你的令牌),则打开对象的函数就会失败(访问被拒绝)。如果被找到了,Windows 会通过 ACE 来查看到底允许做什么。如果 ACE 允许打开此对象,则被允许访问。如果这中间有什么事情失败了,则会被拒绝访问。这是安全性最优方法的一个例子:”如果有什么错了,那么就安全地失败”。

现在,你已经被允许或者拒绝访问了,Windows 开始检查另一个 ACL – 系统访问控制列表(System Access Control List)。SACL 和 DACL 的不同在于 DACL 包含允许/拒绝项,而 SACL 包含审核项。SACL 告诉 Windows 哪些动作要向安全事件日志中记录(审核成功/失败)。除此之外,你可以将 SACL 和 DACL 同等对待。如果 Windows 找到一个 SACL (译者注:更准确地说应该是 SACL 中的某一个 ACE)说要审核此访问,则此次的访问企图会被写到事件日志里。

如果想知道一个 ACL 是否允许访问某个对象,可以对那个对象使用 AccessCheck() API。

4. 创建一个好的自由访问控制列表

在大多数情况下,Windows 已经为你确立了一个良好的自由访问控制列表。如果你遇到一个需要安全描述符或者 SECURITY_ATTRIBUTES 结构的 API,可以简单地为此参数传递 NULL。将 SECURITY_ATTRIBUTES 或者 SECURITY_DESCRIPTOR 传递为 NULL 会通知系统使用缺省的安全描述符。而它也许恰好就是你所需要的。

如果需要更高级的安全性,则要你要确保创建了一个拥有填充过的 DACL 的安全描述符。如果你初始化了一个安全描述符,却忘记了为之构建一个关联的 DACL,你会得到一个 NULL DACL。Windows 将此 NULL DACL 对待为其具有以下 ACE:

“所有人:完全控制”(”Everyone: Full control”)

所以,Windows 会允许使用任何操作访问该对象。这是微软打破自己的最优方法的一个地方。当遇到了非预期的东西时,Windows 应该安全地失败,而在这种情况下它却不是。利用 NULL DACL,包括恶意软件在内的任何人都可以对对象做任何事情,包括设置一个具有威胁性的 rootkit 式 DACL 在内。出于这一原因,所以,不要创建具有 NULL DACL 的安全描述符

设置一个 NULL DACL 并不等同于使用一个 NULL 安全描述符。将 NULL 作为安全描述符传递时,Windows 将使用一个缺省的安全描述符来替换它,而此安全描述符是安全的,允许对对象进行恰当的访问。设置一个 NULL DACL 意味着传递一个合法的安全描述符,只不过它的 Dacl 成员为 NULL

出于同样的理由,你也许不希望设置一个没有包含任何访问控制项的 DACL。对于这样的 DACL,Windows 在遍历访问控制项时,第一次尝试就会失败,因此,它会拒绝任何操作。其结果就是一个完全不可访问的对象,这样的对象不会比一个不存在的对象好到哪儿去。如 果你还想要访问一个对象,那你就必须有一个填充过的 DACL。

那么我们怎样才能构造一个自由访问控制列表呢?

考虑一下你想要谁能访问而又不想让谁访问,你的 DACL 是基于黑名单(全部都是拒绝 ACE)呢,还是基于白名单(全部都是允许 ACE)。如果你正要为安全描述符创建一个 ACL,最好还是使用白名单的方式。对象要持续多长时间?究竟是一个长时间运行的对象(就像长时间运行的进程中的应用程序互斥量那样),还是一个短时间运 行对象(比如一个同步事件对象),或者是一个永久对象(例如一个文件)?

确定一个好的安全描述符取决于你计划对对象做些什么事情。如果你要保证一个内核对象的安全,你可能只需要同步、读以及读取其上的安全性。如果你要保证一个文件的安全,你可能会需要所有可用的访问模式(一整天)。

一般来说,对于永久对象至少需要有两个账户能访问它。第一个是 LocalSystem 账户,对一个永久对象来说,限制此账户的访问会导致很严重的问题;另一个则是对象的创建者或者当前的所有者。还需要考虑赋予管理员对此对象的完全控制。其 他需要考虑的技术是仅允许用户的读访问和执行访问。当要删除文件时,他们必须自己添加 DELETE 权限,然后再删除。

对于短时间运行的对象来说,应该对对象的负责人赋予最少的权限。例如,如果你只希望从对象中读取并对之进行同步,那么就创建一个只允许读访问和同步访问的 DACL,在这种情况下你甚至可以对 LocalSystem 账户进行限制。

如果你的对象支持继承,除非你的文件在安全性方面有特殊要求,那么对它来讲不需要特殊的 DACL,你可以只是对它应用其父的 DACL,你可以通过在安全描述符的 Control 成员中设定自动继承标志来做到这一点。缺省的安全描述符会代你使用继承标志。

最后,如果仍然有疑问,去问管理员!

5. Windows 2000 的继承模型

从 Windows 2000 开始,ACL 可以被设置为从其父那里继承。这就是说,父的 ACL 会被应用到子的身上,例如”\Program Files\Common Files\Microsoft Shared”的访问许可将会和”\Program Files\Common Files“是一样的。ACE 中的一个特定标志会表明它是继承过来的。

正如在前一节中提到的,Windows 遍历 ACE 列表直到末尾或者是找到了匹配的 ACE。如果开启了继承,当 Windows 到达列表的末尾时,它会开始遍历父文件夹的 ACL 来寻找匹配的 ACE。因此,对象会自动将其父的 ACL 应用到自身。这种情况仅当在 ACL 被设置为从其父对象那儿继承才会发生。

如果文件夹也是继承的,Windows 将继续向上遍历。这一过程会持续到 Windows 遇到了一个禁用了继承的文件夹或者是驱动器的根。结果是得到一个由当前对象及其父对象合并而成的一个组合 ACL。来自于父的 ACE 称为 继承 ACE,来自文件自身的 ACE 称为 保护 ACE。

为了支持 ACL 的继承,你必须把你的类设计为具有父子关系,就像文件/文件夹或者注册表键/值那样。父被看作是像文件夹那样的容器,而子被看作是像文件那样的对象。

为了实现真正高级的 ACL 控制,Windows 2000 还支持仅将 ACE 应用于文件夹而不是文件(或者反过来)。控制继承的 ACE 标志同时也指出了要使用的继承类型。有四个标志:

  1. OBJECT_INHERIT_ACEOI):表示此 ACE 可以被子对象继承(例如文件)。
  2. CONTAINER_INHERIT_ACECI):表示此 ACE 可以被子容器继承(例如子文件夹)。
  3. INHERIT_ONLY_ACEIO):表示此 ACE 不作用于对象自己,而是作用于子(例如仅子文件/子文件夹)。
  4. NO_PROPAGATE_INHERIT_ACENP):不将此 ACE 应用于孙,仅作用于直接的子(例如应用于不在子文件夹里的文件)。

这些标志可以并且经常组合使用。要使 ACE 作用于每个子对象,包括在子文件夹中的,可以指定 (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)

关于 ACE 继承更完整的描述,参看 Keith Brown 的书籍

6. 访问令牌

要描述的最后一个结构是访问令牌(Access token)。我并不计划深入钻研这一结构,否则的话有可能会跑题,因为它不仅关系到访问控制,还牵扯着特权、策略、组成员资格以及跨登录会话的 IPC。访问令牌是线程对象和进程对象的主要组成部分。在安全性上下文中,访问令牌标识着你是谁。Windows NT 为每个进程维护着用户信息,从而使得进程和线程可以以另外一个用户的身份运行。如果你想知道任务管理器是如何知道一个进程是以哪个用户身份运行的,其答案 就是访问令牌。访问令牌是允许服务、以其他用户身份运行和快速用户切换等功能存在的关键。

访问令牌可以是进程令牌、线程主令牌或者线程模拟令牌。除非你的线程在模拟他人身份,否则就不需要有令牌,而是使用进程的令牌。一旦你让线程进行身份模拟,就会有一个模拟令牌关联到你的线程上。通过调用 DuplicateTokenEx(),你可以把模拟令牌转换为一个用于 CreateProcessAsUser() 的主令牌。

当你登录到 Windows 后,进程在运行时会把它们的令牌设置为你的用户名。如果你打开令牌并查看其内部,你就可以找到以其身份运行的用户的名字,完全不需要调用 GetUserName()

Screenshot of the Graphical Whoami App In Process Explorer

图 5:访问令牌图解

通过 GetTokenInformation() 可以从令牌中的到很多有用的信息。比方说:

  • TokenUser:进程以哪个用户身份运行
  • TokenGroupsandPrivileges:用户属于哪些组(也就是 TokenGroupsTokentRestrictedSidsTokenPrivilegesTokenUser
  • TokenGroups:用户所属组的 完整 列表(会比”net.exe user <user>“、WMI、用户管理控制台和”control userpasswords2“得到更多信息)
  • TokenPrivileges特权列表
  • TokenDefaultDaclTokenOwnerTokenDefaultGroup:缺省安全描述符定义
  • TokenType:是否是模拟令牌
  • TokenSource:访问令牌的来源
  • TokenRestrictedSids:受限 SID 列表

我们出于访问控制的目的需要从访问令牌中获取的大部分信息都在 TokenGroupsandPrivileges 结构中。有了它,你就是把 GetUserName()NetUserGroups()CheckTokenMembership()LookupPrivilegeValue() API 集于一身了。令牌显示出,作为 administrators 组的成员也将使你成为以下组的成员:None 组、Authenticated Users 组、Users 组、Power Users 组和 LOCAL 组。这非常有用,因为现在你有了一个所属组的列表,而且还可以使用这一列表来搜索一个 ACL。要看一个 ACL 是否授予了你访问:

  1. 打开你的线程令牌
  2. 取得组列表
  3. 获取对象的 SECURITY_DESCRIPTOR
  4. 从安全描述符中得到 ACL
  5. 对 ACL 中的每个 ACE,都找到其相关的 SID
  6. 在第二步中你所创建的祖列表里查找这个 SID
  7. 如果找到了,看他是拒绝 ACE 还是允许 ACE
  8. 若是一个允许 ACE,将其 ACCESS_MASK 与你所希望的 ACCESS_MASK 进行比较
  9. 如果所有的访问方式都允许,那你被就允许访问
  10. 如果有错误发生,那你就被拒绝访问

在以上列出的步骤里加上错误和参数的检查以及审核,那你就创建了自己的 AccessCheck() 函数。

在前面已经接触过缺省安全描述符了。如果你遇到一个需要 SECURITY_ATTRIBUTES 参数的 API,通常可以将之传递为 NULL,也即意味着”缺省的安全描述符”。通过调用 GetTokenInformation(TokenDefault*),就可以找出缺省安全描述符究竟是什么。

特权(privilege)是指一系列的策略,在允许你执行危及系统的操作(比如关闭系统或是安装驱动程序)之前,他们必须是启用了的。这些特权可以通过组策略(用户权限分配)来配置,并罗列到你的线程令牌中。特权在缺省状态下是被禁用了的(一个例外是 SeChangeNotifyPrivilege),因此你在使用之前首先要启用它。Platform SDK 提供了一个示例函数 SetPrivilege() 可以禁用或者启用你所需要的特权。我强烈建议你把这个函数添加到你的安全辅助函数库里,因为你会一再使用该函数,尤其是在进行访问控制的时候。启用一个特权对你而言只不过是两行代码的事:

图 6:使用 Platform SDK 的示例函数启用或者禁用特权

我不计划深入到沙盒(sandboxing)(译者注:沙盒式在进行 Internet 开发时保证安全性的两个基本方法之一,另一个是使用数字签名。)或者模拟中去,因为他们都偏离了访问控制这一主题。如果你希望得到关于模拟和特权的更多内容,可以去看 WindowsSecurity 这篇文章,或者是 Paul Cooke 的书籍

7. 对安全描述符定义语言的一些解释

安全描述符定义语言是使用文本来描绘安全描述符的一个尝试,使用的文本既可以让管理员明白也可以让计算机理解。在帮助文档中有 SDDL 格式的完整参考,而且,窃以为,那是唯一像样的参考。

简单地说,SDDL 串包含以下部分:一个组,一个所有者,DACL 和 SACL。这些部分使用带有一个冒号的单字母标记分开, O: 用以区分出所有者, G: 区分组, D: 区分 DACL, S: 区分 SACL。紧随组以及分隔符的是表示用户和组的 SID。在 D:S: 分隔符之后是一串用括号括起来的独立的项,其中每一对括号都表示一个 ACE。

每个括起来的 ACE 由分号分隔的六部分组成,分别表示 ACE 类型、继承标志、 ACCESS_MASK、GUID、继承 GUID,还有用户 SID。把列表中括起来的 ACE 加起来就得到了完整的 ACL。在 ACE 括号之前可以放其他一些安全描述符的控制标志。下面是我们即将剖析的一个示例 SDDL,清晰起见,我已经把不同的阶段分开了:

首先,使用分隔符把这个串分开:

O: G: S: D:
AO DA (A;;RP WP CC DC LC SW RC WD WO GA;;;S-<span style="font-family: Courier New;">1-0-0) (A;;GA;;;SY)</span>

图 7a:SDDL 串被分为多个单独的串

其次,把 SID 串和 ACE 串展开:

所有者 SACL DACL
Account Operators Domain Administrators (A;;ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP | ADS_RIGHT_DS_CREATE_CHILD | ADS_RIGHT_DS_DELETE_CHILD | ADS_RIGHT_DS_LIST | ADS_RIGHT_DS_SELF | READ_CONTROL | WRITE_DAC | WRITE_OWNER | GENERIC_ALL;;; null SID )(A;;GENERIC_ALL;;; LocalSystem )

图 7b: 展开了 SID 串和 ACE 串的 SDDL 串

最后,把常量串展开:

所有者 SACL DACL
Account Operators Domain Administrators Allow (null SID): 0x01e000bb
Allow LocalSystem: 0x100000

图 7c: SDDL 串中指定的安全描述符

这就是 SDDL 串的详细含义。想要更多 SDDL 串的示例,可以看看 %windir%\security\templates\*.inf 这些文件。这是 Windows 安装程序用来为 Windows 应用缺省的安全描述符所使用的东西。看看你能不能把这些串表示的安全描述符搞明白。至于答案嘛,可以对安全描述符字符串调用 ConvertStringSecurityDescriptorToSecurityDescriptor() 函数。

很抱歉对于 SDDL 串只有少的可怜的解释。就像我说过的,SDDL 的最佳参考在 SDK 文档里,现在我仍然持相同看法。

8. 小结

在这一部分里,你学习了 Windows NT 如何使用安全描述符来保证对象的安全。知道了 Windows 使用 SID 来识别用户、组以及计算机。你还了解到安全描述符最初来自于内核模式,因此它是一个复杂的怪物。还给你看了组成安全描述符的五个部分以及它们在内存中是怎 么存储的。然后又看到了两种类型的访问控制列表。向你揭示了 ACL 的组织结构,Windows 如何读一个 ACL,以及在访问控制模型里继承是如何实现的。学到的最后一个结构是访问令牌,告诉你从令牌里可以读出什么信息,最后以开放一个许可结束。

在第二部分里,你就要写一些代码对安全描述符进行读写了。还要通过从主令牌里获取信息来打造一个 WhoAmI 程序的复制品。在你期待下一篇文章到来之时,我希望你能选择一下你想要用以编程的技术。我将使用四种方法针对安全性进行编程:

  1. 低级方法。如果你需要对 NT3.5 编程而又没有 ATL,那除此之外你就再也没有别的选择了。这一方法要直接调用低级的 ACL 函数,而那些函数实在是太恐怖了!除了极其难于编写以外,这种方法又复杂又容易出错(Windows NT 有如此多的安全问题[^]实在不足为奇!)。除非你要支持后向兼容(兼容到何时呢?),否则只要可能,我就强烈建议你避开这一方法。如果你想事先了解一下,可以查看 SDK 文档里的 SetEntriesInAcl() [^]。
  2. Windows 2000 的方法。由于意识到了方法 1 是多么的难于应用,微软又造出了另外的一组 API 来用于创建安全描述符,即安全描述符定义语言(SDDL)。最初你会觉得 SDDL 可能不比低级的许可方式好多少,当然,仅是对复杂度而言,而非兼容性。但是,其文本形式使得 SDDL 对于程序员 管理员来说极其易读。如果你仔细看上一些 SDDL 及其相应的 ACL,你很快就能掌握它。Windows 2000 还引入了辅助 API 以自动完成一些重复工作,比如说用文本形式打印一个 SID。如果你计划先了解一下,可以查看 SDK 文档里的 ConvertStringSecurityDescriptorToSecurityDescriptor() [^]。
  3. ATL 方法。作为可信赖主动计算(Trustworthy Computing Initiative)的一部分,微软对 ATL 作了许多更新,将 ATL 中的安全性相关类推到了极致。如果你有 Visual C++ .NET,而又不介意使用 ATL C++ 编程,你会希望使用这一方法。想提前了解的话,可以从 Visual C++ 的帮助里查看 CSecurityDesc() [^ ]类。
  4. .NET 方法。从 .NET Framework 2.0 版本开始,你可以选择在托管环境中使用完全面向对象的方法来进行安全性编程。注意,在本系列的第二部分里我不会涵盖此内容。在 .NET 里的安全性编程将会有单独属于自己的一篇,也就是第三篇。如果你有 Visual Studio .NET 2005,你应该强烈考虑选择这一方法。可以通过 System.Security.AccessControl [^] 事先了解一下。

历史

历史和参考书目在第四部分[^]。

下一部分…[^]

 

原文地址:http://www.codeproject.com/win32/accessctrl1.asp

发表评论

电子邮件地址不会被公开。 必填项已用*标注