摘要:本文概述了 Microsoft .NET 框架安全系统的基本特性,包括针对动态下载和执行模式以及远程执行模式,限制代码在严格约束的、管理员定义的安全上下文中运行的能力。
简介
在传统的程序开发模式中,管理员通常将软件安装到本地磁盘的一个固定位置;当从这种模式转向支持动态下载和执行、甚至支持远程执行的环境时,安全性是要考虑的一个最重要的因素。为了支持这种模式,Microsoft .NET 框架提供了一个强健的安全系统,该系统能够限制代码在严格约束的、管理员定义的安全上下文中运行。本文研究了 .NET 框架中的一些基本安全性特性。
许多安全模型将安全性与用户和它们的组(或角色)关联起来。这就意味着,用户和代表这些用户运行的所有代码或者被允许对重要资源执行操作,或者不被允许执行操作。在大多数操作系统中,安全性都是按照这种模型构建的。.NET 框架提供了一种开发人员定义的安全模型,称为基于角色的安全性,它也是按照这种类似的结构运行的。基于角色的安全性的主要抽象是 Principals 和 Identity。此外,.NET 框架也在代码上提供了安全性,这种安全性称为代码访问安全性(也称为基于证据的安全性)。使用代码访问安全性,某个用户可以获得信任以访问某个资源,但是如果用户执行的代码不受信任,那么访问资源将会被拒绝。与基于特定用户的安全性对比,基于代码的安全性是允许安全性得以在可移动代码 (mobile code) 上体现的一个基本工具。可移动代码可以由任意数量的用户下载和执行,而这些用户在开发时都是不了解的。代码访问安全性主要集中于一些核心的抽象,它们是:证据、策略和权限。基于角色的安全性和代码访问安全性的安全抽象是用 .NET 框架类库中的类型来表示的,而且是用户可扩展的。这里有两点需要注意:以一致和连贯的方式向一些编程语言公开安全模型,以及保护和公开 .NET 框架类库中代表资源(使用这些资源可能会破坏安全性)的类型。
.NET 框架安全系统是在传统操作系统安全性的上层运行的。这种方式在操作系统安全性上又增加了一层更具表现力和可扩展的安全性。这两层安全性相互补充。(操作系统安全系统也可以将一些责任委托给托管代码的公共语言运行库安全系统,因为该运行库安全系统比传统的操作系统安全性粒度更细,可配置性也更强。)
本文提供了 .NET 框架安全性方面的概述,具体讲述基于角色的安全性、验证、代码访问安全性和堆栈审核方面的内容,并使用一些小型编程示例揭示一些概念。本文没有论及其他的运行库安全工具,如加密和独立存储。
顺便提一下,本文通常描述的是上面这些安全特性的默认行为。然而,.NET 框架安全系统具有极强的可配置性和可扩展性。这就是该系统的一个主要优点,但遗憾的是,在这篇概念性描述的文章中不能详细地对这一点进行讨论。
执行概述
运行库既执行托管代码,又执行非托管代码。托管代码是在运行库的控制下执行的,因此可以访问运行库提供的服务,如内存管理、实时 (JIT) 编译,以及本文涉及的最重要部分 — 安全性服务,如安全策略系统和验证。非托管代码是经过编译、要运行在特定硬件平台上的代码,它不能直接利用运行库。然而,当语言编译器生成托管代码时,该编译器的输出是以 Microsoft 中间语言 (MSIL) 表示的。MSIL 通常被描述为一种用于抽象、基于堆栈的计算机的面向对象的伪汇编语言。之所以说 MSIL 是面向对象的,是因为它有一些支持面向对象概念的指令,如对象分配 (newobj) 和虚函数调用 (callvirt)。之所以说是抽象的计算机,是因为 MSIL 不依赖于任何特定的平台。也就是说,它对于在其上运行的硬件不会作出任何假设。它之所以是基于堆栈的,是因为从本质上看,MSIL 是通过向堆栈压入 (push) 值和从中弹出 (pop) 值以及调用方法来执行的。MSIL 通常会在执行前被 JIT 编译为本机代码。(MSIL 也可以在运行该代码前编译为本机代码。这有助于缩短程序集的启动时间,但是 MSIL 代码通常是在方法级别进行 JIT 编译的。)
验证
在运行库会进行两种形式的验证:MSIL 验证和程序集元数据验证。运行库中的所有类型都指定了它们将要实现的协定,这些信息将作为元数据与 MSIL 一起保留在托管 PE/COEFF 文件中。例如,如果一个类型指定它从另一个类或接口进行继承,这就表明它将要实现一些方法,这就是一个协定。协定也可以与可见性联系起来。例如,可以将类型声明为从它们的程序集公开(导出)或其他的内容。因为类型安全只能根据它们的协定访问类型,所以就此而言,它是代码的一个属性。可以验证 MSIL 以证明它是类型安全的。验证是 .NET 框架安全系统中的一个基本构造块,目前只在托管代码上执行验证。因为非托管代码不可由运行库进行验证,所以由运行库执行的非托管代码必须是完全受信任的。
要理解 MSIL 验证,关键在于理解如何对 MSIL 进行分类。MSIL 分为下列类型:无效的 MSIL、有效的 MSIL、类型安全的 MSIL 以及可验证的 MSIL。
注 应该指出的是,下面的定义比标准定义提供了更多信息。有关更加准确的定义版本,请参阅其他文档,如 ECMA standard:
-
无效 MSIL 是 JIT 编译器无法为它生成本机表示的 MSIL。例如,不能将包含无效操作码的 MSIL 翻译成本机代码。另一个示例是跳转指令,该指令的目标是操作数的地址,而不是操作码。
-
有效 MSIL 可以被认为是满足 MSIL 语法的所有 MSIL,因此它可以用本机代码表示。这种分类包括的 MSIL 可以使用非类型安全形式的指针算法获取对类型成员的访问。
-
类型安全的 MSIL 只通过它们向公众公开的协定与类型进行交互。试图从一个类型访问另一个类型的私有成员的 MSIL 就不是类型安全的。
-
可验证的 MSIL 是可以通过一个验证算法来证明是类型安全的 MSIL。验证算法比较保守,因此某些类型安全的 MSIL 可能不会通过验证。当然,可验证的 MSIL 既是类型安全的又是有效的,当然不是无效的。
除了类型安全检查之外,运行库中的 MSIL 验证算法还会检查堆栈上溢/下溢的发生、异常处理工具的正确使用以及对象初始化。
对于从磁盘加载的代码,验证过程是 JIT 编译器的一部分,它会在 JIT 编译器中间歇性地进行。验证和 JIT 编译不是作为两个独立的进程来执行的。如果验证期间在程序集中找到了一连串不可验证的 MSIL,那么安全系统就会检查该程序是否足够受信任,可以跳过验证。例如,如果一个程序集是在安全模型的默认设置下从本地硬盘上加载的,那可能就是这样的情况。如果程序集受信任,可以跳过验证,则会将 MSIL 翻译成本机代码。如果程序集的受信任程度不够,不能跳过验证,则会用一个存根来代替有问题的 MSIL,如果使用了该执行路径,该存根就会引发一个异常。一个常见的问题是:“为什么不在验证程序集之前检查它是否需要验证呢?”因为验证通常是作为 JIT 编译的一部分执行的,所以它通常比检查是否允许程序集跳过验证更快。(决定跳过验证比这里描述的过程更加智能。例如,可以缓存前面一些验证尝试的结果,以提供快速的查找方案。)
除了 MSIL 验证之外,还要验证程序集元数据。事实上,类型安全依赖于这些元数据检查,因为它假定 MSIL 验证期间使用的元数据标记是正确的。在全局程序集缓存 (GAC) 或下载缓存中加载程序集时,会验证程序集元数据;如果没有将它插入到 GAC 中,则从磁盘中读取它时也会验证程序集元数据。(GAC 是一些程序使用的程序集的中央存储。下载缓存保存了从其他位置(如 Internet)下载的程序集。)元数据验证包括检查元数据标记和消除缓冲区溢出,前者用于检查它们是否会正确索引到它们访问的表中,以及到字符串表的索引是否并不指向长度大于应该保存它们的缓冲区大小的字符串。通过 MSIL 验证和元数据验证来消除非类型安全的类型安全代码是运行库安全性的第一部分。
代码访问安全性
从本质上讲,代码访问安全性根据程序集证据向程序集分配权限。在决定代码应对哪些资源具有访问权限时,代码访问安全性会使用从中取得可执行代码的位置和有关代码标识的其他信息作为一个主要因素。有关程序集标识的信息称为证据。一旦将程序集加载到运行库用于执行时,宿主环境就会向程序集附加一些证据。运行库中的代码访问安全系统负责将这些证据映射到一个权限集,该权限集将决定此代码对一些资源(比如注册表或文件系统)具有何种访问权限。这种映射是以可管理的安全策略为基础的。
对于托管代码的大多数应用程序方案,所设计的默认代码访问安全性策略是安全而且是足够的。它严格地限制了来自不完全受信任或不受信任环境(如 Internet 或本地 Intranet)的何种代码在本地计算机上执行时能够运行。因此代码访问安全性默认策略模型代表一种通往安全的可行途径。默认情况下,资源是安全的;管理员需要采取显式的操作才能使系统安全性降低。
我们为什么还需要另一种安全方案呢?与用户标识不同,代码访问安全性是以代码标识为中心的。这使得代码可以在一个用户上下文中、以无限数量的信任级别运行。例如,即使运行其中的操作系统用户上下文允许完全访问所有系统资源,来自 Internet 的代码也只能在限定的安全边界中运行。
现在让我们来看一下代码访问安全系统的主要输入和输出:证据和权限。
权限
权限代表可执行受保护操作的授权。这些操作通常包括对特定资源的访问。通常,这些操作包括访问资源(如文件)、注册表、网络、用户界面或执行环境等。不涉及实际资源的权限的一个示例是跳过验证功能。
注 System.Security.Permissions.SecurityPermission 类包含一个标志,该标志决定是否允许权限实例的接收者跳过验证。SecurityPermission 类包含了其他类似的权限标志,它们涵盖了核心的运行库技术,如果未正确使用这些技术(如控制为在特定应用程序域中运行的程序集提供的证据的能力),就可能公开安全漏洞。核心运行库技术是由请求调用方来保护的,以使得必需的 SecurityPermission 类设置合适的权限标志。
权限的基本抽象是 IPermission 接口,它要求特定的权限类型实现一组标准的权限操作,如返回与具有相同权限类型的其他权限实例的联合或子集。
可以将权限整理到一个权限集中,该权限集代表对各种资源的访问权限的一种声明。System.Security.PermissionSet 类代表权限的集合。这个类的方法包括 Intersect 和 Union。这些方法采用另一个 PermissionSet 作为参数,并提供了一个 PermissionSet,该 PermissionSet 要么是这两个集合中所有权限的联合,要么是这两个集合中所有权限的交集。(运行库中的权限集合是以一个简单的、未排序的集合表示的。)有了这些工具,安全系统就可以使用权限集,而不必理解每种权限类型的语义了。这使得开发人员可以扩展权限的层次结构,而无需修改安全引挚的功能。
注 每种权限类型都必须派生自 IPermission 接口,该接口要求任何权限类型实现标准的权限操作,如联合、交集、子集和请求方法。这些权限类型不用实现特定于它们所包含的权限状态类型的语义。例如,与一个包含文件名的权限相交所产生的结果,就不同于与一个包含简单布尔值状态的权限相交所产生的结果。当权限集 A 与权限集 B 相交时,如果 A 和 B 包含相同权限类型 X 的不同实例,那么权限集类 A 就会调用 X 实例上的交集方法,而不必知道有关 X 语义的任何内容。
根据在程序集加载时提供给安全系统的证据,安全系统将授予一个权限集,该权限集代表访问各种受保护资源的权限。相反,资源是由权限请求来保护的,该请求会触发一个安全检查,以查看是否将一个特定的权限授予了该资源的所有调用方;如果请求失败,就会引发一个异常。(有一个称为链接请求的特定安全检查,它只检查直接调用方,但通常会检查调用方的整个调用堆栈。)
证据
无论何时向运行库中加载程序集时,宿主环境都会向安全系统提供该程序集的证据。证据构成了向代码访问安全性策略系统输入的内容,这些输入决定了程序集会收到哪些权限。
.NET 框架附带了一些类,在安全系统中,这些类被用作标准形式的证据:
-
Zone :与 Internet Explorer 中使用的区域具有相同的概念。
-
URL :一个标识特定资源的特定 URL 文件位置,如 http://www.microsoft.com/test。
-
Hash :使用像 SHA1 这样的散列算法生成的程序集散列值。
-
Strong Name :程序集的强名称签名。强名称代表一种版本化、加密的加强方式,用于引用和标识特定签名方的一个(或全部)程序集。有关详细信息,请参阅 .NET Framework SDK。
-
Site :代码来自的站点。URL 比站点的概念更具体;例如 www.microsoft.com 就是一个站点。
-
Application Directory :要从中加载代码的目录。
-
Publisher certificate :程序集的 Authenticode 数字签名。
注 从理论上说,任何托管代码都可以组成证据。上面只是一些在 .NET 框架中具有相应成员条件的类型,因此可以将它们集成到安全策略中,而不必编写自定义安全对象。有关安全策略和代码组的详细信息,请参阅下面的内容。
下面的程序是证据的一个简单示例,其中证据是在加载程序集时被传递到运行库安全系统的。在本例中,mscorlib 是加载的程序集,它是包含了许多运行库类型(如 Object 和 String)的程序集。 using System;
using System.Collections;
using System.Reflection;
using System.Security.Policy;
namespace AssemblyEvidence
{
class Class1
{
static void Main(string[] args)
{
Type t = Type.GetType("System.String");
Assembly a = Assembly.GetAssembly(t);
Evidence e = a.Evidence;
IEnumerator i = e.GetEnumerator();
while(i.MoveNext())
Console.WriteLine(i.Current);
}
}
}
程序的输出显示了对于此程序集向安全系统传递了哪些证据。为简洁起见,已对下面的输出进行了编辑。安全系统采用这个证据,然后根据管理员设置的安全策略为该程序集生成了一个权限集。 <System.Security.Policy.Zone version="1">
<Zone>MyComputer</Zone>
</System.Security.Policy.Zone>
<System.Security.Policy.Url version="1">
<Url>
file:///C:/winnt/microsoft.net/framework/v1.0.2728/mscorlib.dll
</Url>
</System.Security.Policy.Url>
<StrongName version="1"
Key="00000000000000000400000000000000"
Name="mscorlib"
Version="1.0.2411.0"/>
<System.Security.Policy.Hash version="1">
<RawData>4D5A90000300000004000000FFFF0000B8000000000000...
0000000000000000000000000000000000000000000000000000
</RawData>
</System.Security.Policy.Hash>
安全策略
可管理的安全策略决定了宿主环境提供给程序集的证据和授予该程序集的权限集之间的映射。System.Security.SecurityManager 类实现了这种映射功能。因此,您可以将代码访问安全性策略系统看作一个带有两个输入变量(证据和可管理的安全策略)的函数,并可以将程序集特定的权限集看作输出值。本节重点讲述可管理的安全策略系统。
有一些可配置的策略级别可由安全管理器识别,它们是:
-
企业策略级别
-
计算机策略级别
-
用户策略级别
-
应用程序域策略级别
企业策略级别、计算机策略级别和用户策略级别可由安全策略管理员进行配置。应用程序域策略级别可以通过宿主以编程的方式来配置。
当安全管理器需要决定安全策略授予程序集的权限集时,它是从企业策略级别开始的。将程序集证据提供给这个策略级别将会从该策略级别授予权限集。通常,安全管理器会以相同的方式继续收集企业策略级别以下策略级别的权限集。然后这些权限集会相交,以生成该程序集的策略系统权限集。所有策略级别都必须首先允许一个特定的权限,然后才能使其进入为该程序集授予的权限集中。例如,如果在程序集的计算期间,企业策略级别没有授予一个特定的权限,那么不管其他级别指定了什么权限,也不会授予该权限。
注 有一些特殊的情况,某个策略级别中(如企业策略级别)可能包含了一条指令,指定不计算该级别之下的任何策略级别,如计算机策略级别和用户策略级别。在这种情况下,计算机策略级别和用户策略级别都不会生成一个权限集,并且在授予程序集权限集的计算中不会考虑这两个级别。
程序集的开发人员可以影响程序集运行库进行的权限计算。尽管程序集不能简单地取得运行所需的权限,但它可以声明一个最低限度需要的权限集或拒绝某些权限。安全管理器可以确保仅当需要的一个(或多个)权限是策略级别结构授予的权限集的一部分时,程序集才会运行。相反,安全管理器还可以确保程序集不会收到它拒绝获取的任何权限。程序集的开发人员可以使用安全自定义属性将最低限度需要的权限、拒绝的权限或可选的权限放入程序集中。有关详细信息,请参阅下面的声明性方式和命令性方式部分或 .NET Framework SDK。
决定授予程序集一个实际权限集的过程包括三个步骤:
-
各个策略级别计算程序集的证据,然后生成特定于策略级别的授予权限集。
-
为每个 [1] [2] [3] 下一页 |