发布日期: 4/1/2004 | 更新日期: 4/1/2004

摘要:本文概述了 Microsoft .NET 框架安全系统的基本特性,包括对于动态下载和执行模式以及远程执行模式,限制代码在严格约束的、管理员定义的安全上下文中运行的能力。(21页打印页)

.NET 框架安全性概述

Demien Watkins 博士,项目 42Sebastian Lange,Microsoft Corporation

2002 年 1 月

本页内容
.NET 框架安全性概述 简介
.NET 框架安全性概述 执行概述
.NET 框架安全性概述 验证
.NET 框架安全性概述 代码访问安全性
.NET 框架安全性概述 权限
.NET 框架安全性概述 证据
.NET 框架安全性概述 安全策略
.NET 框架安全性概述 堆栈审核
.NET 框架安全性概述 声明性方式和命令性方式
.NET 框架安全性概述 忠告
.NET 框架安全性概述 基于角色的安全性
.NET 框架安全性概述 小结
.NET 框架安全性概述 致谢
.NET 框架安全性概述 附录 A:MsCorCfg.msc

简介

传统的程序开发模式中,管理员通常将软件安装到本地磁盘的一个固定位置;当从这种模式转向支持动态下载和执行、甚至支持远程执行的环境时,安全性是要考虑的一个最重要的因素。为了支持这种模式,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 通常会在执行前被实时编译为本机代码。(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 自然既是类型安全的又是有效的,当然不是无效的。

除了类型安全检查之外,运行库中的 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 框架附带了一些类,在安全系统中,这些类是以证据的标准形式使用的:

区域:与 Internet 中使用的区域具有相同的概念。

URL一个标识特定资源的特定 URL 文件位置,如 http://www.microsoft.com/test

散列:使用像 SHA1 这样的散列算法生成的程序集散列值。

强名称:程序集的强名称签名。强名称代表着一种版本化、加密的加强方式,用于引用和标识特定签名方的一个(或全部)程序集。有关详细信息,请参阅 .NET Framework SDK

站点:代码来自的站点。URL 比站点的概念更具体;例如 www.microsoft.com 就是一个站点。

应用程序目录:要从中加载代码的目录。

发行者证书:程序集的 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.得到的权限集与程序集声明的运行所需的权限集或拒绝权限进行比较,然后相应地修改权限的授予。

.NET 框架安全性概述

1. 一个程序集的授予权限集的计算

图 1 显示了大概的计算过程。运行库的宿主提供有关程序集的证据,该证据作为程序集收到权限集的计算的一个输入。可管理的安全策略(企业策略、计算机策略和用户策略)是决定程序集的权限集计算的第二个输入,本文前面称之为安全性设置。然后安全策略代码(包含在 SecurityManager 类中)遍历各个策略级别设置提供的程序集证据,然后生成一个权限集,它代表了该程序集可访问受保护资源的权限集合。

每个策略级别是如何管理的呢?策略级别表示一个独立、可配置的安全策略单元 – 每个级别都将程序集证据映射到一个权限集。每个策略级别都具有类似的结构。每个策略级别都由三个部分组成,这三个部分组合在一起用来表示策略级别的配置状态:

代码组树

命名权限列表

策略程序集列表

现在我们来详细阐述所有策略级别的这些组成部分。

代码组

每个策略级别的核心是代码组树。它表示策略级别的配置状态。代码组实质是一个条件表达式和一个权限集。如果程序集满足该条件表达式,那么就会被授予该权限集。每个策略级别的代码组集是按树的形式组织的。一旦条件表达式的计算结果为真,就会授予该权限集,然后继续在那个分支中遍历。只要不满足条件,就不授予权限集,也就不再进一步检查那个分支。例如,有一个代码组树,它是按照下面的这种情况运行的。

.NET 框架安全性概述

2. 策略级别的代码组树

这里我们只讨论用于实现默认安全策略的代码组类型的代码组语义。也可以包括自定义编写的、语义完全不同于这里所描述语义的代码组。再提一下,安全系统是完全可以扩展的,因此它为引入新的策略计算语义提供了无限的可能性。

假设有一个程序集,它具有下列证据:它来自 www.monash.edu.au,因为它来自 Monash University 的 m-Commerce Center,所以它的强名称为 mCommerce。

代码组树遍历则会按照下列方式进行:

根节点有一个任何代码都满足的条件 "all code"。因此将 "all code" 权限授予了我们的这个程序集,这个权限集称为 ”Nothing”,它不允许代码具有任何权限。下一个检查的代码组是要求从 My Computer 加载代码的代码组。因为这个条件不满足,所以未授予权限集,因此也不会检查这个条件的任何下级节点。然后我们返回上一个成功授权的代码组(本例中是 all code),然后继续检查它的下级节点。下一个代码组是 Zone:Internet。因为我们的代码是从 Internet 上下载的,所以满足这个条件,从而授予了权限集(可能是 Internet 权限集),然后您就可以继续检查这个分支中的下一个下级代码组了。下一个代码组有一个 Url: 条件:指出代码来自 www.microsoft.com。由于代码是来自 www.monash.edu.au 的,所以不满足这个条件。此时我们返回到 Zone:Internet 代码组,查找它下面的其他节点。我们为 URL:www.monash.edu.au 查找节点。由于满足了这个条件,我们得到了 MonashPSet 权限集。接下来我们为 Strong Name:m-Commerce center 查找节点。由于满足这个条件,我们得到了 m-Commerce 权限集。因为这个级别下面没有代码组,所以我们返回到匹配条件并且具有下级代码组的上一个代码组,然后继续。

最终,满足的条件和从这个策略级别中授予的权限集包括:

条件:All code,权限集:Nothing

条件:Zone:Internet,权限集:Internet

条件:URL:www.monash.edu.au,权限集:MonashPSet

条件:Strong Name:m-Commerce,权限集:m-CommercePSet

在一个策略级别找到的适用于某个特定程序集的所有权限集通常会进行联合,以生成该策略级别授予的总权限集。

检查一个策略级别的代码组树是非常简单的。附录 A 描述了一个 Microsoft 管理控制台单元,该单元提供了一个用于查看和修改代码组的层次结构(以及策略级别的其他所有可配置组成部分,请参阅下面的内容)的可视界面。

命名的权限集

一个策略级别包含一个命令权限集列表。每个权限集代表着一个信任声明,用于访问各种受保护资源。命名的权限集是代码组按其名称进行引用的一些权限集。如果满足了代码组的条件,那么就授予被引用的命名权限集(请参阅上面的示例)。下面是一些预定义的命名权限集示例:

FullTrust允许不受限制地访问系统资源。

SkipVerification可以跳过验证的程序集。

Execution允许代码执行。

Nothing不授予权限。不授予执行的权限可有效停止代码的运行。

Internet 适合来自 Internet 的代码的权限集。代码将不能收到对于文件系统或注册表的访问权限,但可以执行一些有限的用户界面操作,并且可以使用称为独立存储的安全文件系统。

为了查看策略级别的权限集,只要在附录 A 提及的 GUI 工具中打开策略级别节点,然后打开权限集文件夹即可。

下面是一个很小的示例程序,它列出了所有策略级别上所有已知的命名权限集:

下面的程序显示了所有策略级别上的命名权限集列表。该应用程序是一个从本地磁盘上运行的 C# 程序,因此它会从默认的策略设置中收到一个相当强大的权限集。

using System;
using System.Collections;
using System.Security;
using System.Security.Policy;
namespace SecurityResolver
{
class Sample
{
static void Main(string[] args)
{
IEnumerator i = SecurityManager.PolicyHierarchy();
while(i.MoveNext())
{
PolicyLevel p = (PolicyLevel) i.Current;
Console.WriteLine(p.Label);
IEnumerator np = p.NamedPermissionSets.GetEnumerator();
while (np.MoveNext()) 
{
NamedPermissionSet pset = (NamedPermissionSet)np.Current;
Console.WriteLine("\tPermission Set: \n\t\t Name: {0}
\n\t\t Description {1}", 
pset.Name, pset.Description);
}
}
}
}
}

该程序的输出。为简洁和明确起见,已对该输出进行了编辑。

Enterprise
Permission Set:
Name: FullTrust
Description: Allows full access to all resources
Permission Set:
Name: LocalIntranet
Description: Default rights given to applications 
on your local intranet
...
Machine
Permission Set:
Name: Nothing
Description: Denies all resources, including the right to execute
...
User
...
Name: SkipVerification
Description: Grants right to bypass the verification
Permission Set:
Name: Execution
Description: Permits execution
...

策略程序集

在安全计算期间,可能需要加载其他的程序集以在策略计算过程中使用。例如,一个程序集可以包含代码组发出权限集的用户定义权限类部分。当然也需要计算这个包含自定义权限的程序集。如果这个自定义权限的程序集被授予包含它自己实现的自定义权限的权限集,那么就会引发循环依赖项。为了避免这种情况,每个策略级别都包含了一个它在策略计算中所需的受信任程序集列表。这个必需程序集的列表自然地被称为了“策略程序集”列表,它包含了在这个策略级别实现安全策略所需的所有程序集的过渡闭包。包含在该列表中的所有程序集的策略计算是短路的,以避免发生循环依赖项。该列表可以使用附录 A 提及的GUI 管理工具来修改。

这就完成了每个策略级别上的可配置组成部分的检查:代码组树、命名权限集列表和策略程序集列表。现在该来看一下从安全策略状态派生的授予权限集是如何在由每个策略级别的配置实例化时,与安全强制的基础结构进行连接的。换言之,迄今为止,我们只讨论了程序集是如何收到授予的权限集的。如果没有一个基础结构要求程序集具有某一级别的权限,那么安全系统将不能发挥任何作用。事实上,使安全强制成为可能的技术是安全堆栈审核。

堆栈审核

堆栈审核是安全系统的重要组成部分。堆栈审核以如下方式进行操作。每次调用方法时,都会在堆栈中放入一个新的激活记录。这个记录包含了要传递给该方法的参数(如果有参数的话)、此函数结束时要返回的地址以及任何局部变量。当程序执行时,堆栈就会随着函数的调用而增长和收缩。在执行中的某些阶段,该线程可能需要访问系统资源,如文件系统。在允许这种对受保护资源的访问之前,可能需要进行一次堆栈审核来验证调用链中的所有方法是否具有访问系统资源的权限。在这一阶段会进行堆栈审核,并且通常会检查每个激活记录,以查看这些调用方是否确实具有所需的权限。与完全堆栈审核相比,CAS 系统还允许开发人员使用链接时间检查来批注资源,这种链接时间检查只检查直接调用方。

修改堆栈审核

在执行期间的任何阶段,在某个函数访问特定资源之前,它都可能需要检查其调用方的权限。在这一阶段,该函数可以要求对一个特定的权限或权限集进行安全检查。这就会触发堆栈审核,其结果是:如果所有的调用方都具有授予的权限,那么该函数就继续执行;如果调用方不具有所需的一个(或多个)权限,就会引发异常。下图说明了这个过程。

.NET 框架安全性概述

3. 堆栈审核示例

函数可以选择修改堆栈审核,有一些机制可完成这种修改。首先,一个函数可能需要保证调用它的多个函数。在这种情形下,它可以断言一个特定的权限。如果发生了查找断言的权限的堆栈审核,那么在检查这个函数的激活记录寻找该权限时,如果该函数具有它断言的权限,则该检查将成功,堆栈审核也会终止。断言本身是一个受保护操作,因为它将向断言访问受保护资源权限的函数的所有调用方开放对该受保护资源的访问权限。因此,在运行库中,安全系统会检查包含自我断言的函数的程序集是否具有它试图断言的权限。

修改堆栈审核的另一种办法是支持函数拒绝权限。当一个函数知道它不应该访问某个资源并拒绝权限时,就可能发生这种情形。PermitOnly 提供了类似 deny 的功能,因为它会导致堆栈审核失败。但 deny 指定的是会导致堆栈审核失败的权限集,而 PermitOnly 指定的则是继续堆栈审核所需的权限集。

当使用 Deny 堆栈修饰符时,您应该非常谨慎。如果早期的堆栈帧是一个断言,则会忽略 Deny 修饰符。另外,拒绝基于路径的权限是相当困难的,这是因为经常有各种不同的路径字符串实际是指向相同位置的。拒绝一个特定路径表达式仍会开放其他的路径。

还有最后一个需要知道的要点。在任何时刻,一个堆栈帧只能有一个 Deny,一个 PermitOnly 和一个 Assert 处于有效状态。例如,如果开发人员需要断言许多权限,他们就应该创建一个 PermissionSet 来表示该集合,并只进行一个单独的断言。有一些方法可用来删除一个堆栈审核修饰符的当前 PermissionSet 设置,以便注册其他的权限集。这样方法的一个示例是 System.Security.codeAccessPermission.RevertPermitOnly

下面的示例说明了前面介绍的各种堆栈修改技术:

using System;
using System.Security;
using System.Security.Permissions;
namespace PermissionDemand
{
class EntryPoint
{
static void Main(string[] args)
{
String f = @"c:\System Volume Information";
FileIOPermission p = 
new FileIOPermission(
FileIOPermissionAccess.Write, f);
p.Demand();
p.Deny();
p.Demand();
CheckDeny(p);
p.Assert();
CheckDeny(p);
}
static void CheckDeny(FileIOPermission p)
{
try
{
p.Demand();
}
catch(SecurityException)
{
Console.WriteLine("Demand failed");
}
}
}
}

前面的程序产生了下面的输出,这些输出起先看起来很不直观:

Demand failed
Demand failed

上面的代码示例中,尽管第一个 Demand 访问的是一个受限制系统目录,但它还是成功了。记住,运行库安全系统是在基础操作系统设置上面的。因此,可以让运行库安全策略对某些目录授予这样的访问权限,即托管代码实际试图访问这些目录时,它将会因为这样做引发操作系统访问冲突。Deny 后面的下一个 Demand 也成功了。当执行 Demand 时,没有检查请求函数的激活记录,而只检查它的调用方。因此,尽管函数已经拒绝访问,但也不会被 Demand 检测到。调用 CheckDeny 和后面的 Demand 确实失败了。现在检查前面方法中的 Deny,因为它位于调用方的堆栈帧中。接下来我们返回到 Main 并进行一个 Assert。这里已经断言的一个权限在这个堆栈帧中也被拒绝了。我们进入 CheckDeny ?±,Demand 再次引发一个异常,但这是为什么呢?从本质上讲,Deny 重写了 Assert;这是因为 Deny 权限集总是在 Assert 权限集之前进行检查。

概括起来,使托管资源可以引发托管安全堆栈审核的功能是保护资源的运行库安全系统方法。授予的权限集是程序集从每个策略级别上运行的授权计算收到的,该权限集会与资源请求的权限进行对照检查。如果后者形成了前者的一个子集,那就可以访问受保护资源。除非已像上面描述的那样对堆栈进行了修改,否则就会对调用链中托管资源的所有调用方执行这种子集检查。因此安全堆栈审核综合了运行库安全系统的这两个方面:1} 据和权限之间的可配置映射;2) 通过强制所有的调用方拥有一定级别的权限来保护资源。

实际上有两种不同的方式可用于以编程的方式表达堆栈审核请求和堆栈修改操作 — 声明性安全和命令性安全。

声明性方式和命令性方式

.NET 框架允许开发人员以两种方式表达安全约束。以 声明性方式 表达安全约束意味着使用自定义属性语法。这些批注保留在类型的元数据中,在编译时会有效地溶入到程序集中。下面是声明性方式安全的一个示例:

[PrincipalPermissionAttribute(SecurityAction.Demand,
Name=@"culex\damien")]

以命令性方式表达安全要求意味着在运行时创建 Permission 对象的实例并调用它们的方法。下面是本文前面示例中声明性方式安全的一个示例:

FileIOPermission p = new FileIOPermission(
FileIOPermissionAccess.Write, f);
p.Demand();

为什么选择一种方式而不选择另一种方式的原因有几个。首先,可以使用声明性方式来表达所有的安全操作;但对于命令性方式,则不行。不过,声明性方式要求在编译时表达所有安全约束,而且只允许在完全方法、类或程序集范围内存在批注。命令性方式更加灵活,因为它允许在运行时表达约束,如对于方法中可能执行路径的子集。将命令性安全要求保留在元数据中的一个副作用是,有一些工具可提取这些元数据信息并根据这些信息提供功能。例如,某个工具可以显示程序集上声明性安全属性集的列表。而对于命令性安全,这种情况是不可能发生的。开发人员需要了解并熟悉这两种方式。

忠告

因为按照默认策略,从本地硬盘上执行的代码将会明显比从其他任何位置执行的代码取得更多的信任,所以下载代码、将代码储存到磁盘然后执行它与从远程位置执行代码具有完全不同的语义。对于以前的系统,这种情况不是很明显,例如浏览器选择的是下载代码而不是远程执行它。这里假定在代码下载后,执行前还会对它进行检查,比如病毒扫描。使用了代码访问安全性,情况就完全不同了。如果使用默认的安全策略,则将代码作为远程代码执行会明显增加安全性。然而这时可能会增加系统或用户的负担,因为他们需要知道托管代码和非托管代码的区别。最后是有关运行库安全系统的另一方面。对于旧式安全系统的用户来说,他们应该更熟悉这个系统,因为它是以用户标识为基础的:基于角色的安全性。

基于角色的安全性

迄今为止介绍的代码访问安全系统,基本上是以代码标识为中心的,而不是以用户或角色为中心的。然而仍需要根据用户标识来表达安全设置。因此运行库安全系统也附带了基于角色的安全性特性。

基于角色的安全性利用了用户和角色的概念,这与目前许多操作系统中的安全实现相类似。基于角色的安全性中的两个核心抽象是 Identity 和 Principal。 Identity 是代码执行时所代表的用户。请谨记,它们可能是应用程序或开发人员定义的逻辑用户,而不必是操作系统可以看到的用户。 Principal 代表了用户和用户所属角色的抽象。代表用户标识的类实现 Identity 接口。在 .Net 框架中,提供这个接口的默认实现的一般类是 GenericIdentity。代表 Principal 的类实现 IPrincipal 接口。在 .Net 框架中,提供这个接口的默认实现的一般类是 GenericPrincipal。

在运行库,每个线程都有且只有一个当前的 Principal 对象与之相关联。当然代码可以根据安全要求来访问和更改这个对象。每个 Principal 都有且只有一个 Identity 对象。从逻辑上讲,对象的运行库结构与下面的内容相类似:

.NET 框架安全性概述

4. 基于角色安全性结构

下面的程序说明了开发人员可以如何使用这些一般类。本例中,开发人员正在提供安全模型。例如,名称 “Damien” 和角色 “Lecturer“、“Examiner“ 与操作系统可以支持的任何用户和角色都无关。

using System;
using System.Threading;
using System.Security;
using System.Security.Principal;
namespace RoleBasedSecurity
{
class Sample
{
static void Main(string[] args)
{
String [] roles = {"Lecturer", "Examiner"};
GenericIdentity i = new GenericIdentity("Damien");
GenericPrincipal g = new GenericPrincipal(i, 
roles);
Thread.CurrentPrincipal = g;
if(Thread.CurrentPrincipal.Identity.Name == 
"Damien")
Console.WriteLine("Hello Damien");
if(Thread.CurrentPrincipal.IsInRole("Examiner"))
Console.WriteLine("Hello Examiner");
if(Thread.CurrentPrincipal.IsInRole("Employee"))
Console.WriteLine("Hello Employee");
}
}
}

程序产生了如下的输出:

Hello Damien
Hello Examiner

如果开发人员希望的话,也可以使用 Microsoft (R) Windows (R) 安全模型。在这种情形中,用户和角色会与宿主计算机中的用户和角色紧密相连,因此可能需要在宿主系统上创建这些帐户。下面的示例使用的是本地计算机上的用户帐户。本例中也使用了一些 syntactic sugar(语法糖块),.NET 框架中的 PrincipalPermissionAttribute 类有效地封装了一些方法的调用(如 IsInRole),以使得开发人员可以使用简化的语法。

namespace RoleBased
{
class Sample
{
[PrincipalPermissionAttribute(SecurityAction.Demand, 
Name=@"culex\damien")]
public static void UserDemandDamien()
{
Console.WriteLine("Hello Damien!");
}
[PrincipalPermissionAttribute(SecurityAction.Demand, 
Name=@"culex\dean")]
public static void UserDemandDean()
{
Console.WriteLine("Hello Dean!");
}
static void Main(string[] args)
{
AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.WindowsPrincipal);
try
{
UserDemandDamien();
UserDemandDean();
}
catch(Exception)
{
Console.WriteLine("Exception thrown");
}
}
}
}

PrincipalPermissionAtribute 保证了每次调用 UserDemandDamienUserDemandDean 方法时都会进行运行库检查。当然,程序可能由 Dean、Damien 也可能由其他人执行,因此对这两个方法调用的安全检查即使未全部失败,也至少有一个会失败。Main 的第一行将用户策略设置成了 Windows(执行本例的操作系统)的用户策略。当用户 ”culex\damien” 执行程序时将产生如下输出:

Hello Damien!
Exception thrown

小结

安全性是 .NET 框架的一个基本和内置的因素。本文旨在对安全系统进行一下概述。可以从本文总结出来的一些主要概念有:

安全系统是可扩展的,因为在 .NET 框架中,很多概念都是以类型表示的,因此开发人员可根据他们自己的需要进行扩展和修改。

安全系统提供了不同类型的安全模型,具体地说,有基于角色的安全模型和基于证据的安全模型。这些不同的模型满足了不同的需要,并且是相互补充的。

代码访问安全性是以代码的标识为中心的,因此即使代码运行的操作系统用户上下文授予了它对计算机的管理权限,也只允许在不完全受信任的安全上下文中执行该代码。

本文没有论及像加密这些方面的安全系统问题,也没对任何方面进行非常详细的论述。请参阅白皮书,特别是有关这些主题的内容,以了解本文没有讨论的一些细节。

致谢

在编写本文的过程中,Brian Pratt、Loren Kohnfelder 和 Matt Lyons 提供了大力的帮助和支持,谨在此表示感谢。

附录 A:MsCorCfg.msc

有一个 Microsoft 管理控制台单元使我们能够可视化地操纵代码访问安全性策略。下图显示了这个管理单元的界面,其中突出显示了本文论及的一些概念。

.NET 框架安全性概述

5. Microsoft 管理控制台单元界面

您可以在 Control Panel 中单击 Administrative Tools,然后单击 Microsoft .NET Framework Configuration 快捷键,来访问该工具。

相关文章: