使用公共语言运行库强制代码访问权

类别:.NET开发 点击:0 评论:0 推荐:

摘要 基于组件的软件容易受到攻击。大量没有被严密控制的 DLL 是造成这个问题的主要因素。Microsoft .NET Framework 的公共语言运行库中的代码访问安全性用于解决这个常见的安全漏洞。在该模型中,CLR 充当了程序集的交通警察,对它们来自哪里以及应当对它们实施什么安全限制进行跟踪。.NET Framework 解决安全问题的另一个方式是提供有内置安全性的预先存在的类。这些类是那些在执行危险操作(例如,读取和写入文件、显示对话框等)时在 .NET 中被调用的类。当然,如果组件调用非托管代码,它就可以跳过代码访问安全性措施。本文讨论这些措施和其他安全问题。
*
本页内容
概述 概述
请求权限 请求权限
引诱攻击 引诱攻击
暗含的权限 暗含的权限
保护您自己 保护您自己
断言您自己的权限 断言您自己的权限
断言到哪里? 断言到哪里?
声明性属性 声明性属性
对代码访问安全性的攻击 对代码访问安全性的攻击
安全策略 安全策略
证据 证据
评估安全策略 评估安全策略
微调权限集 微调权限集
查看和编辑安全策略 查看和编辑安全策略
小结 小结

COM 组件可能是非常有用的。它们也可能十分危险。在 Windows_ 上构建应用程序的常用途径越来越多地依赖于购买第三方 COM 组件(甚至是带有传统的基于 C 接口的 DLL),并将它们聚集到一个进程内。当然,小心使用这种模块化途径可以提高重用性、减少耦合性,并给软件开发过程带来了其他长期探求的好处,但它还经常会导致明显的安全漏洞。

在 2000 年 7 月刊的 MSDN_Magazine 中,针对 Microsoft Internet 信息服务 (IIS) 安全性我发表了分为两个部分的系列文章,在第二部分中,我讨论了一个最常见和最可怕的对软件程序的攻击:缓冲区溢出利用。潜伏在 DLL 某处的一个愚蠢但非常常见的程序错误,就可以使一个坚定的攻击者不仅能够破坏主机进程,而且能够欺骗它的安全上下文。从可以广泛获取的第三方 DLL 粘合进程甚至会使问题更糟糕,因为攻击者有很多时间来发现每个正在使用的 DLL 中可以被利用的各种安全缺口。在对您的应用程序实施攻击之前,他有大量时间在安全的位置准备他的攻击。

Authenticode_ 据认为可以帮助解决该问题,虽然它要比什么也不做更好,但问题是它采取惩罚性方式,而不是预防性方式。在攻击者用电子邮件将色情 Web 站点的链接列表发送给您的所有密友和亲戚并用邮件显示您的返回地址之后,知道攻击者的身份根本就不能对您有所安慰。在典型用户准备并愿意安装据称可以增强其在线体验的任何 DLL 的情况下,即使作者对这些 DLL 进行了合法签名,非程序员又如何确定实际上是哪个 DLL 造成了这些破坏呢?在攻击期间安装 DLL 的硬盘驱动器遭到数据删除或损坏怎么办?证据发生了什么事情?如何在法庭上证明攻击者的罪行?

显然,您不仅需要有人为此负责,而且需要对代码(尤其是您所依赖的、像 ActiveX_ 组件这样的移动代码)进行访问控制。当管理员运行从组件构造的进程时,他应当得到某种担保,即他的安全上下文不会被恶意组件破坏。利用对托管代码进行验证来限制缓冲区溢出问题是解决问题的第一步。代码访问安全性(虽然不是万能钥匙)是重要的第二步,这是本文的重点讨论内容。

请注意,本文基于公共语言运行库 (CLR) 的技术预览版,而 CLR 在最后发布之前可能会有更改。

概述

首先,我将概述代码访问安全性的基本工作方式。请将这一节作为我随后将要深入探讨的详细讨论的路线图。对于那些熟悉 Java 2 中的安全模型的人来说,您将发现 CLR 使用了类似的模型。

通常,如果需要执行安全敏感的操作(例如,读取或写入文件、更改环境变量、访问剪贴板、显示对话框等),将使用 CLR 提供的预先打包的类来执行这些操作。这些类在编写时加入了安全要求,这样的安全要求会告诉系统被请求的操作的类型,从而让系统有机会批准或拒绝请求。如果系统拒绝请求,它将通过产生类型为 SecurityException 的异常来表示拒绝。系统如何决定是否批准或拒绝每个请求?它这样做的方式是,逐个机器并且逐个用户地查找可以自定义的安全策略。

CLR 中的安全策略在概念级别确实很简单。安全策略在加载时向程序集提出问题。现在,有两个常见问题,它们可能以稍微不同的方式被询问,随后将介绍它们。该程序集来自哪里?谁是该程序集的作者?

安全策略这种方式可以将这些问题的不同答案映射到特定权限集。例如,您可以指定“允许来自 https://www.foobar.com/baz 的代码从 c:\quux 及其下面读取文件,并且不允许它显示对话框”。在这种情况下,提出的问题是“该程序集来自什么 URL?”,这只是第一个问题的另一种问法。从宏观上看,这种方式总结了 CLR 代码访问安全性,但如果您略微深究下去,它就会变得更加有趣。现在就让我们来做这件事。

请求权限

请想像您正在编写下面这个类,该类允许执行简单的文件读写操作(省略了实现):

public class MyFileAccessor {
  public MyFileAccessor(String  path,
                        bool    readOnly) {}
  public void Close() {}
  public String ReadString() {}
  public void WriteString(String stringToWrite) {}
}

下面是这个类的典型使用方式:

class Test {
  public static void Main() {
    MyFileAccessor fa = new
      MyFileAccessor("c:\foo.txt", false);
    fa.WriteString("Hello ");
    fa.WriteString("world");
    fa.Close();  // flush the file
  }
}

给定该使用模型后,很明显,执行安全检查的最简单位置是在构造函数中。构造函数的参数明确地告诉您要访问哪个文件以及是否以读取或读/写模式访问它。如果将该信息提供给基础安全策略,并且由此导致产生了 SecurityException,您只需允许该异常传播回调用方。在这种情况下,由于构造函数永远不会完成,您的类的实例将拒绝调用方,因此调用方无法调用任何其他非静态的成员函数。这种方式极大地简化了对类执行的安全检查,从程序员的角度来看这是好事,从安全角度来看也是好事。在代码中编写的访问检查逻辑越少,发生错误的机会就会越少。

这种方式的缺点可以在该情形中看到。如果客户端在构造函数中的初始请求之后构造了一个您的类的实例,然后该客户端与另一个客户端(有可能在另一个程序集中)共享该引用,那么新的客户端不会受到以构造函数为中心的代码访问请求的影响。这与目前 Windows 2000 中内核处理工作的方式相似。在这里,您再次看到性能和安全之间的紧张关系。

其实在实际的开发过程中,您不会编写像 MyFileAccessor 这样的类来访问文件。相反,您会使用系统提供的类(例如,System.IO.FileStream),这些类已被设计为自动执行适当的安全检查。通常,这些检查是在构造函数中完成的,对此,我以前已讨论过涉及到的所有的性能和安全结论。与很多 Microsoft_ .NET 体系结构一样,出于可扩展性的目的,FileStream 类所调用的安全检查还可以直接被您自己的组件使用。图 1 显示了如何向 MyFileAccessor 的构造函数添加相同的安全检查(如果您无法依靠系统提供的类代表您执行安全检查)。

图 1 中的代码按两个步骤执行安全检查。它首先创建一个表示所讨论的权限的对象,在这里,权限是文件访问权。然后它请求该权限。这导致系统查看调用方的权限,如果调用方没有该特定权限,则 Demand 引发 SecurityException。实际上,Demand 执行了比这略微复杂的检查,我们随后讨论它。

图 2 总结了运行时当前公开的各种代码访问权限。应当知道,还存在一组代码标识权限,这些权限直接测试我在概述部分提到的各种问题的答案。但是,为了避免将安全策略决策硬编码到组件中,常规用途的代码并不经常使用代码标识权限。随后我将更详细地论述安全策略。

此时,值得注意的是,即使代码成功通过了 .NET 安全检查,底层操作系统还将执行它自己的访问检查。(例如,Windows 2000 和 Windows NT_ 通过访问控制列表来限制对 NTFS 分区中的文件的访问。)因此,即使 .NET 安全机制将不受限制的本地文件系统访问权授予程序集,并且来自程序集的组件正寄宿在以 Alice 的身份运行的进程中,这些组件将只能打开某些文件,即那些 Alice 在正常情况下基于底层操作系统的安全策略可以打开的文件。

引诱攻击

回忆您为了确定程序集应当被授予什么权限而向程序集提出的两个基本问题:“该程序集来自哪里?”和“谁是该程序集的作者?”这些问题的答案指定了程序集将被授予的基本权限集。


3 MyFileAccessor

请设想 MyFileAccessor 在 FileStream 对象方面的实现,如图 3 所示。代码大体可能是这样的:

using System.IO;
public class MyFileAccessor {
  private FileStream m_fs;
  public MyFileAccessor(String  path,
                        bool    readOnly) {
    
    m_fs = new FileStream(path, FileMode.Open,
      readOnly ? FileAccess.Read :
                 FileAccess.ReadWrite);
  }
  // ...
}

假设您已经以这种方式实现了 MyFileAccessor,并将它交给您的朋友 Alice,而 Alice 已经将它安装在她的本地硬盘驱动器上。那么,MyFileAccessor 程序集现在有什么样的代码访问权限呢?如果我查看我自己的本地安全策略,我会看到它将一个名为 FullTrust 的权限集授予本地组件,FullTrust 包括对文件系统的不受限制的访问权(您应当记住,底层操作系统可能进一步限制该权限)。

图 4 显示安装在 Alice 的本地硬盘驱动器上的 MyFileAccessor 可能被各种类型的组件使用。有可能一个被信任的本地组件使用 MyFileAccessor 来访问文件。但是,如果 Alice 碰巧将她的浏览器指向某个无赖 Web 站点,那么从该站点下载的 .NET 组件就可以使用 MyFileAccessor 来执行恶意操作。这里是一个引诱攻击的示例:MyFileAccessor 可以被引诱执行恶意操作(比如,打开 Alice 不愿意公开的私有文档)。


4 引诱攻击示例

CLR 并不会强制每个中间组件执行它自己的访问检查来避免发生这种不正常的操作,CLR 只是确认调用链中的每个调用方都有 FileStream 所请求的权限。在图 4 中,当称为 NotepadEx 的本地组件使用 MyFileAccessor 打开文件时,它将被授予不受限制的访问权,这是因为整个调用链的起点是安装在本地的程序集。但是,当 RogueComponent 试图使用 MyFileAccessor 打开文件时,在 FileStream 调用 Demand 时 CLR 将执行堆栈审核,然后会发现调用链中的一个调用方没有必需的权限,因此 Demand 将引发 SecurityException。

暗含的权限

在进一步讨论之前,有必要注意到某些权限在被授予后还暗含了其他权限。例如,如果您被授予对目录 c:\temp 的所有访问权,您也被隐式地授予了对其子级目录、孙级目录等等的所有访问权。可以通过所有代码访问权限对象上都有的 IsSubsetOf 方法来发现这种情况。例如,如果运行图 5 中的代码,将获得如下输出:

p2 is subset of p1

暗含权限的存在使管理成为一件相当容易的事情,但请记住,在请求权限时要尽可能具体。CLR 将自动比较您的请求,以确定它是否是被授予的权限的子集。

保护您自己

程序集在加载时会被授予一组基本权限。CLR 及其宿主将通过向安全策略提出有关程序集的问题并给出答案(然后,安全策略将答案转换为权限)来发现这些权限。但是,正因为程序集被基于策略授予一组基本权限,所以并不意味着所有这些权限都将在运行时可用来满足需要。

如果程序集被安装在本地,它将有可能拥有大量的、甚至是完全不受限制的权限,至少在代码访问安全性涉及的领域是这样。请设想,如果这样一个高度被信任的程序集将调用用户提供的脚本。取决于脚本应执行的操作,调用脚本的程序集可能想在进行调用之前限制有效权限(请参阅图 6)。

该代码会在当前堆栈帧中设置额外的限制。这意味着,如果 Calculate 试图访问环境变量、偷窥剪贴板内容、破坏代码中所指定的两个敏感目录下面的文件系统内任何地方的文件,或者如果 Calculate 内部使用的任何组件试图执行任何这样的操作,那么,当 CLR 进行堆栈审核以检查访问权时,它会注意到堆栈帧显式地拒绝了这些权限,因此它会拒绝该请求。注意,在这种情况下,我已经将几个权限组合到单个 PermissionSet 中并拒绝了整个权限集。这是因为,每个堆栈帧最多只能有一个用于拒绝的权限集,并且对 Deny 函数的调用替换了当前堆栈帧的旧权限集。这意味着以下代码不会执行您可能期望它执行的操作:

FileIOPermission p1 = new FileIOPermission(
  FileIOPermissionAccess.AllAccess,
  "c:\\sensitiveStuff");
FileIOPermission p2 = new FileIOPermission(
  FileIOPermissionAccess.AllAccess,
  "c:\\moreSensitiveStuff");
p1.Deny();  // p1 is denied
p2.Deny();  // now p2 is denied (not p1)

在该代码中,第二次 Deny 调用实际上会覆盖第一次 Deny 调用,因此,在这里只有 p2 将被拒绝。使用权限集允许同时拒绝多个权限。调用 CodeAccessPermission 类的静态 RevertDeny 函数将清空当前堆栈帧的拒绝权限集。

如果发现您自己要拒绝很多单个权限,您可能会发现采取另一个方式更为容易,就是说,不使用 Deny 和 RevertDeny,而使用 PermitOnly 和 RevertPermitOnly。当您确切地知道希望允许哪些权限时,这种方式效果会很好。

断言您自己的权限

堆栈审核机制用于保护可以按很多不同方式使用的常规用途的类(比如 FileStream 和 MyFileAccessor)。例如,正确使用 FileStream 可以将错误记录到用户硬盘驱动器上定义完善的目录内的定义完善的日志文件中。恶意使用 FileStream 将会危及本地安全策略文件的内容。堆栈审核机制被设计为确保 FileStream 在 FileStream 对象实例化之前不管要遍历多少个中间程序集,所有这些程序集都必须满足对文件系统访问权的请求。这就避免了与前面描述的情况类似的引诱攻击。

虽然堆栈审核机制提供了所有这些好处,但有时它只会给您带来妨碍。图 7 显示了一个称为 ErrorLogger 的类,该类提供了一个我在更早时提到的定义完善的错误日志记录服务。假如 ErrorLogger 类被安装在 Alice 的本地硬盘驱动器上,并且该类的程序集被 Alice 的机器上的代码访问安全策略授予对文件系统的完全访问权(到编写本文时为止,在默认安全策略中,本地组件将被授予对文件系统的不受限制的访问权)。但是,如果该类被设计为向其他程序集提供服务,而其他程序集中的一部分却未被授予写入本地文件系统的权限,那么情况又会如何呢?

很明显,如果要被任意组件使用(这将允许访问客户端所指定的任何文件),那么 ErrorLogger 要比 MyFileAccessor 安全得多。ErrorLogger 是一个简单的类,只能用来将字符串追加到一个定义完善的文件的末尾。但是,因为堆栈审核机制不知道这一点,因此,当 FileStream 构造函数请求它的调用方的权限时,除非调用链中每个调用方都请求了 FileIOPermission,否则该请求将失败。如果这是一个妨碍,可以通过让 ErrorLogger 断言它自己的权限以写入日志文件从而将它清除。图 8 显示了新的实现。

ErrorLogger 类的新版本(作为本地组件进行安装)还将被授予对文件系统的完全访问权。在这种情况下,它将在使用 FileStream 实际打开文件之前断言文件 IO 权限。注意,只能断言您的程序集实际上已经被授予的权限。

每个堆栈帧都有可能拥有所断言的权限集,当堆栈审核对该堆栈帧进行审核时,它将认为所断言的权限已经被满足。堆栈审核甚至不会继续,除非存在所断言的权限集不满足的其他被请求的权限。注意,我省略了调用 RevertAssert 的麻烦。在这种情况下 RevertAssert 是不必要的,因为断言可以安全地在原地保留,直到对 Log 的调用返回,在该返回点上,堆栈帧(包括所断言的权限集)将被销毁。这还适用于 Deny 和 PermitOnly。

断言到哪里?

显然,断言权限的能力可能被滥用。例如,安装在本地的组件如果被授予完全信任,它就可以只是断言所有权限就能执行任意操作,不管它的客户端是谁。这显然是很可怕的想法,但您怎么知道组件一旦安装在您的机器上它就不会这样做呢?由于断言是这样一项可能被滥用的强大功能,因此它的使用还受到权限类 SecurityPermission 的控制。这个类实际上代表了用于管理如何使用与安全相关的类和策略的几个不同权限。可以将大多数这样的权限当作元权限。

请考虑图 8 显示的 ErrorLogger2 类。通过断言它自己的权限从而将数据写入单个可分辨文件,它是否已经破坏了系统的安全策略?有可能发生什么种类的攻击?恶意组件可以注入假的错误消息来迷惑用户。它还可以发送非常大的字符串来尝试填满用户的硬盘驱动器。因此,即使在断言自己的权限时 ErrorLogger2 似乎比更为通用的类(比如 MyFileAccessor)更安全,但是由于断言本身的存在,仍然有可能发生攻击。

只是因为类似这样的问题就应当避免使用断言吗?像大多数有关安全的问题一样,并没有一个明确的回答。它肯定会使安全模型变得很复杂,所以一个好的经验做法是,由您的同事组成评判小组对该功能的任何使用进行评判。还要注意,您的断言可能被安全策略拒绝,因为很多管理员希望对除最受信任的本地组件以外的所有组件禁止使用断言。如果在您的整个代码中都依赖于断言,那么,这样的拒绝可以使您的应用程序非正常停止运行。您可能发现这样做对您有所帮助:特别是在您捕获由调用 Assert 所生成的 SecurityException,以及试图在断言被禁止且存在完全的堆栈审核的情况下完成您的工作时。

一个必须使用断言的场合是当跨越从托管代码到非托管代码的边界时。以系统定义的 FileStream 类作为示例。显然,为了实际打开、关闭、读取和写入文件,该类需要调用以非托管代码实现的底层 OS。在进行这些调用时,互操作层将请求 SecurityPermission,具体来说是请求 UnmanagedCode 权限。如果该请求将沿堆栈传播,那么,除非代码还被授予调用非托管代码的权限,否则将禁止所有代码打开文件。

FileStream 类实际上将把这个非常通用的请求转换为粒度更细的请求,具体来说是转换为对 FileIOPermission 的请求。它将通过在它的构造函数中请求 FileIOPermission 来实现这一点。如果该请求成功,则 FileStream 对象在实际调用操作系统之前可以顺利地断言 UnmanagedCode 权限。FileStream 所进行的非托管调用不是对非托管代码的随机调用,而是为实现具体目标(该目标是由构造函数中前面的请求所指定的)而打开具体文件的调用。承载 FileStream 和其他被信任的组件的 mscorlib 程序集被认为可以被信任以执行这些策略转换,因而被授予 Assertion 权限。在以 Assertion 权限信任任何其他程序集之前,您应当对该程序集将有助于强制执行、而不会破坏您的安全策略这一点有高度的信任。

声明性属性

如果计划在您的组件中使用 Deny、PermitOnly 或 Assert,应当知道不仅可以通过编程而且可以通过声明来完成每个这样的操作。例如,图 9 显示了错误记录器的第三个实现,该实现使用了声明性属性(在这里是 FileIOPermissionAttribute)。应当记住,在 C# 中声明属性时,为了简便起见可以省略 Attribute 后缀。

采用这种方式有两个好处。第一,更容易键入。第二,这也是更重要的,声明性属性成为组件的元数据的组成部分,并且很容易通过反射被发现。这就允许工具扫描程序集并发现诸如它是否利用了断言这样的情况,也许还能列出断言了各种权限的方法和类。工具还可以发现与安全策略的潜在冲突;记住,断言通常是被禁止的,尤其是如果组件没有安装在本地硬盘驱动器上。

该方式的主要缺点是,如果断言请求被拒绝,则方法不可能捕获异常。这个特殊的缺点只涉及到断言权限。如果您只是使用声明性属性来限制权限,您就永远不会有这个问题。

SecurityAction 枚举与声明性权限属性一起使用,它包括的几个选项可以用来对您的代码能够使用的权限进行微调,并且可以用来在加载时或运行时请求您的客户端的权限。图 10 来自 .NET Framework SDK 文档,它列出了这些选项,并对它们进行了分类。例如,比较以下两个属性声明:

[SecurityPermission(SecurityAction.Demand,
                    UnmanagedCode = true)] 
[SecurityPermission(SecurityAction.LinkDemand,
                    UnmanagedCode = true)]

如果这两个声明中的第一个被应用于某个方法,那么在运行时当每次调用该方法时会发生正常的堆栈审核。另一方面,如果使用了第二个声明,则在每次引用受保护的方法时只会发生一次检查。这会发生在实时 (JIT) 编译时。而且,第二个声明只请求与其相关的代码的权限;并不会对 LinkDemand 执行完整的堆栈审核。在本文随后讨论安全策略时,我将回过来讨论该列表中的一些其他属性。

对代码访问安全性的攻击

由于我有更多时间来实验代码访问安全性基础结构,所以我希望看到暴露出来的其他需要注意的攻击。现在,您显然容易遭受的两个攻击是滥用 Assertion 和 UnmanagedCode 安全权限。我已经讨论过断言的危险,但调用非托管代码是另一个棘手的问题。

如果允许程序集调用非托管代码,那么它就可以跳过几乎所有代码访问安全性。例如,如果程序集没有被授予对本地文件系统的权限,而只被允许调用非托管代码,那么,它只是可以直接调用 Win32_ 文件系统 API 来执行它的恶意操作。我在更早时已经提到,这些调用将受到任何有效的操作系统安全检查的检查,但通常这并不可靠,尤其是攻击者的代码最终会被加载到有特权的环境(例如,管理员的浏览器或运行在 SYSTEM 登录会话中的 daemon 进程)。

从管理员的登录会话,您可以很容易设想使用 Win32 文件 API 的攻击者会轻易改写本地机器的安全策略(当前存储在可由管理员写入的 XML 文件中)。或者就这一点而论,攻击者可以使用相同的 Win32 文件 API 替换 CLR 执行引擎本身。如果攻击者使用管理特权执行非托管代码,您就满盘皆输了。显然,通过小心管理安全策略可以阻挠这些攻击,随后我将讨论这一点。

另一个明显的攻击涉及更改位置所造成的权限提升。从 Internet URL 使用程序集时,通常程序集会有大大少于本地安装时所拥有的权限。攻击者的一个首要目标将是尝试使受害人相信应当在其本地硬盘驱动器上安装程序集的副本,从而立即使它的权限得以提升。由于很多用户不用多想就愿意安装来自 Internet 站点的 ActiveX 控件,因此这将是很复杂的问题。有关潜在攻击和防御措施的讨论,请参阅我的 Web 站点(请参阅 http://www.develop.com/kbrown)。

安全策略

在本文中,我已经暗示过存在用于分配代码访问权限的安全策略。该策略可能变得非常复杂,但一旦掌握了基本知识就很容易理解它的原理。首先,一定要注意到权限是基于每个程序集进行分配的。我已经将发现这些权限的过程划分成三个基本步骤:

收集证据

向安全策略出示证据并发现被分配的权限集

基于程序集要求对权限集进行微调

证据

当我第一次开始用 CLR 进行实验时,我认为“证据”是一个陌生的术语。它听起来更像是由一组律师而不是计算机科学家所设计的安全基础结构。但在花了一些时间熟悉 CLR 之后,我发现这个名称确实很合适。在法庭上,证据用于提供可以帮助回答陪审团所提问题的信息:“谋杀武器是什么?”,或“谁签署了这份合同?”

在 CLR 的情况中,证据是对安全策略所提出的问题的答案所组成的集合。基于这些答案,安全策略就可以将权限自动授予代码。下面是到编写本文为止策略提出的问题:

该程序集是从什么站点获得的?

该程序集是从什么 URL 获得的?

该程序集是从什么区域获得的?

该程序集的强名称是什么?

谁签署了该程序集?

前三个问题只是查询程序集来源位置的不同方式,而剩余两个问题则重点关注程序集的作者。

在法庭上,证据是由一方提交的,但可以被反对方辩驳,通常陪审团帮助决定证据是否正确成立。在 CLR 的情况中,有两个实体可能收集证据:CLR 本身和应用程序域的宿主。由于这是自动系统,因此没有陪审团;无论是谁提交了需要由策略评估的证据,都必须得到没有提交假证据的信任。这就是需要特殊安全权限 ControlEvidence 的原因。CLR 本身自然地得到提供证据的信任,因为您必须已经信任它,才能执行安全策略。因此,ControlEvidence 安全权限将应用于宿主。到编写本文时为止,默认情况下提供了三个宿主:Microsoft Internet Explorer、ASP .NET 和外壳宿主(用于从外壳启动 CLR 应用程序)。

为了使这一点更具体,请考虑在 System.AppDomain 类中发现的这个函数:

public int ExecuteAssembly(
   string   fileName,
   Evidence assemblySecurity,
);

尽管浏览器可能已经将程序集下载到本地文件系统上的缓存中,但它应当通过第二个参数提供程序集实际来源地的证据。

评估安全策略

一旦宿主和 CLR 已经收集到所有证据,就会将证据作为一组对象(这些对象被封装在类型为 Evidence 的一个集合对象中)提交给安全策略。该集合中的每个对象的类型都指示了它所代表的证据的类型,并且证据有相应的类来代表我在上面列出的每个问题:

Site
Url
ApplicationDirectory
Zone
StrongName
Publisher

安全策略是从三个不同级别组成的,每个级别都是序列化对象的集合。这些对象中的每一个都称为代码组,并且代表了向程序集提出的问题以及对如果证据证实该问题则应当导致的权限集的引用。所提的问题从技术上称为成员条件,并且权限集在被命名时要便于管理员可以重用它们。图 11 显示了成员条件和相应的命名权限集。


11 具有成员条件的代码组

我始终觉得术语“代码组”有点使人犯糊涂,但由于我尚未提出更好的术语,通常我只是将代码组看作构成安全策略级别的图形中的节点。图 12 显示了一组代码组(或节点)是如何形成具有单个根的层次结构的。记住,策略级别中的每个节点都代表成员条件和对权限集的引用,所以,通过取得收集到的证据并将它与层次结构中的节点进行匹配,CLR 最后可以得到一个代表由该级别的策略所授予的权限的权限集的联合。由于根节点实际上只是遍历的起点,它会匹配所有代码并且默认情况下引用名为 Nothing(可以猜想,它不包含任何权限)的权限集。


12 安全策略级别图形

实际的图形遍历受两个规则控制。首先,如果父节点不匹配,则不会测试它的子节点是否匹配。这将允许图形代表类似 AND 和 OR 逻辑运算符的某些内容。第二,每个节点还可能拥有可控制遍历的属性。在这里应用的属性是 Exclusive,并且如果具有 Exclusive属性的节点被匹配,将只使用该特定节点的权限集。自然地,策略级别中有两个匹配节点拥有该属性将没有任何意义,这种情况被看作是错误。需要由系统管理员确保不会发生这种情况,但如果出现这种情况,系统将引发 PolicyException 并且不会加载程序集。


13 遍历策略级别

图 13 显示了从 https://q.com/downloads/foobar.dll(由 ACME Corporation 签署)下载的程序集示例。注意图形中的四个节点是如何匹配的,并且在遍历期间只有一个发布商节点被匹配。该图形的左半部分阐释了来自 ACME Corporation 的代码的两个逻辑 AND 关系。它说“由 ACME Corporation 发布并且是从 Internet 下载的代码获得权限集 bar 和 baz,同时由 ACME Corporation 发布并且安装在本地的代码获得权限集 foo 和 gimp”。

此时,您可能想知道为什么我一直在谈论策略级别。原因是,实际上有三个可能的策略级别,每个级别包含一个节点图形,如图 12 所示。有机器策略级别、用户策略级别和应用程序域策略级别,并且将按该顺序对它们进行评估。所得到的权限集是在遍历这三个策略级别中的每一个中的图形期间所发现的权限集的交集。


14 三个策略级别

应用程序域策略级别在技术上是可选的,它是由主机动态提供的。该功能的最明显的示例是 Web 浏览器,它可能想让限制性更强的策略选项应用于它的应用程序域。图 14 显示了我如何看待策略级别。还可以使用节点上的另一个属性来中止策略级别的遍历:LevelFinal。如果在匹配节点上发现该属性,则不会进一步遍历策略级别。例如,这将允许域管理员在机器策略级别编写语句,而单个用户无法通过编辑用户级别的策略来更改机器策略级别。

微调权限集

一旦 CLR 从三个策略级别收集了一组权限,最后的步骤将允许程序集本身独立操作。请回忆一下,代码可以在运行时通过拒绝或断言权限,以编程或声明方式微调可用的权限集。好,通过小心使用 SecurityAction 枚举的下面这三个元素,程序集可以微调由策略授予它的权限(这三个元素还显示在图 10 中):

SecurityAction.RequestMinimum
SecurityAction.RequestOptional
SecurityAction.RequestRefuse

这些元素的名称很清楚地说明了它们的作用。如果策略没有授予程序集所请求的最低权限集,则不会运行程序集。谨慎使用此特定功能使您能够对有关环境的情况作出某些假设,并且可使编程更容易一点。但是,如果用得太多,该功能有可能带来不良后果。例如,使用 RequestMinimum 来请求您的程序集可能需要的所有权限时,将使它在更多的环境中无法加载,而这是不必要的。这还可能导致管理员将他的安全策略放松到稍微超过必要的水平,以便允许您的组件运行。

RequestRefuse 似乎(至少在这些早期阶段)是自由使用的有用工具。这就允许您只是拒绝可能已被策略授予的、您自己的权限。应当拒绝您知道您的程序集不需要的权限集。安全地使用它肯定是无害的。

最后,RequestOptional 允许您指定没有也可以但如果有也可以使用的可选权限。如果您的程序集公开了那些需要少量额外权限的可选功能,将会是有用的。

假如有了从策略派生的权限集,加上程序集的最低、可选和被拒绝的权限集,下面是 CLR 文档中描述的公式,该公式用于确定程序集被授予的权限:

G = M + (O?) - R

G = 被授予的权限,M = 最低请求,O = 可选请求,P = 策略派生的权限,R = 被拒绝的权限。

查看和编辑安全策略

如果想要探究安全策略的具体情况,请使用 CASPOL.EXE,这是一个代码访问安全策略工具。下面是我喜欢的几个命令行,它们可以作为您的入门示例:

caspol -a -listgroups
caspol -a -resolvegroup c:\inetpub\wwwroot\bar.dll
caspol -a -resolveperm  c:\inetpub\wwwroot\bar.dll

第一个示例列出了机器和用户策略级别的代码组。如果仔细查看,您将看出节点的层次结构,它们中的每一个都有成员条件(后面是权限集名称)。第二示例请求特定程序集的匹配代码组列表,而第三个示例则实际解析程序集的权限。

例如,请看当您通过 HTTP 引用相同程序集时事情是如何变化的:

caspol -a -resolvegroup http://localhost/foo.dll

虽然 CASPOL.EXE 可以用来编辑安全策略,但是,除非我正在做某些非常简单的事情,否则我更喜欢只是提出 EMACS 并手动编辑策略文件(因为它是 XML 文档)。如果决定您自己尝试该操作,请将原始文件备份。到编写本文时为止,您可以在 %SYSTEMROOT%\ComPlus\v2000.14.1812\security.cfg 中找到机器策略文件。您的版本号可能与我的不一致,请作相应更改。用户安全策略被存储在相同路径下面的用户配置文件目录中。到编写本文时为止,默认用户策略将 FullTrust 授予所有代码,这实际上意味着安全策略完全受机器策略控制。

小结

代码访问安全性在与 CLR 中的代码验证相结合时比在以前的平台中所采用的放任的方法前进了一大步,在以前的平台中,与只是构建可能更加安全的大型独立应用程序相比,悬挂式 DLL 被认为非常流行。

代码访问安全性承认今天的应用程序是从组件生成的这一事实。在作出预防性而不是惩罚性的安全策略决策的过程中它还会考虑这些组件的来源,并且将广泛地提高很多新兴种类的移动代码应用程序的安全性。

代码访问安全性绝不是万能的。它引入了一整套复杂问题,其中每个问题都是对管理的挑战。如果没有经过培训的、愿意花时间了解该功能的管理员,在发生很多新攻击时它可能只是一块障眼布。最好参考 Java 安全性的斑驳的历史(Java 安全性处理移动代码的历史已经有几年时间,并取得了不同程度的成功),并用历史的眼光来评估这种新的体系结构。请访问我的 Web 站点,在那里我收集了最新的 .NET 安全新闻和示例代码,并列举了移动代码安全的现有著作。最后,请积极与我分享您对 CLR 安全体系结构的意见和看法。

相关文章,请参阅:

Code Access Security

.NET Framework Developer Center

Keith Brown 在 DevelopMentor 工作,他的任务是针对程序员的安全意识进行研究、写作、教学和推广工作。Keith 编写了 Programming Windows Security (Addison-Wesley, 2000) 一书。他还参与编写了Effective COM,现在正在编写一本有关 .NET 安全性的书。
                                                                                                                                                           ( Keith Brown )

本文地址:http://com.8s8s.com/it/it43594.htm