.NET安全系列之一:代码访问安全(CAS)
从这篇文章起,我将用一个系列总结一下.NET平台中安全相关问题,本系列将分7篇文章,如下:
-
.NET安全系列之一:代码访问安全(CAS)
这一部分重点介绍CAS,CAS技术允许我们通过验证源代码的方式衡量一个程序集的受信任程度,并确保其没有被篡改。 -
.NET安全系列之二:独立存储区相关知识
这一部分介绍了独立存储的相关概念及使用。 -
.NET安全系列之三:Windows与.NET中用户及角色与资源访问控制
这一部分讨论如何衡量一个用户的受信任程度,用户的概念在Windows及其平台上许多技术中都有实现。资源访问控制技术基于用户与角色的思想实现所以被放到一起讨论。 -
.NET安全技术之四:.NET中密码学的应用及证书技术
这一部分介绍了.NET中支持的一些加密算法,包括对称加密算法,非对称加密算法,散列算法,以及一些证书,签名相关的技术。 -
.NET安全技术之五:数据保护API
这一部分介绍了Windows 2000系统开始支持的DPAPI(数据包括API)技术。通过DPAPI,可以在用户、进程、会话或机器级别上加密应用程序的数据从而确保其机密性。 -
.NET安全系列之六:强名程序集
这一部分介绍了对.NET程序集进行签名的方法。 -
.NET安全技术之七:.NET通信方面的安全问题
这一部分讨论了两台计算机之间建立安全链接的常用技术,及部分ASP.NET中的安全话题。
这是本系列的第一篇,详细介绍了CAS。
首先介绍几个与.NET安全相关的概念并引出CAS的概念。
移动代码
移动代码通常指通过网络发布的应用程序代码。由于网络的不可信性,所以有必要限制授予下载代码(移动代码)的权限集。
CLR这样的虚拟机让.NET平台以一种优雅的方式解决了这个问题。如,遇到代码会破坏文件的情况,CLR能截获并阻止这个恶意操作,这个机制称为CAS – 代码访问安全。
CAS纵览
.NET中定义了大约30个权限,可以被用来准予或者拒绝程序集代码的运行。每个权限定义了对重要资源如注册表、文件或文件夹等进行访问的控制规则,对程序集授予信任意味着授予它一些必要的权限以使它能正常运行。
在CLR中,CAS机制用在以下两种情况中:
-
在程序集装载期间,CLR授予它一些权限。
-
当代码请求执行某个重要操作时,CLR必须验证包含这个代码的程序集具备适当的权限。
针对上述第一种情况,.NET中对程序的信任需要获取,仅当程序集能提供一系列证据,CLR才认为它是可信任的。这些证据与程序的来源和其中的数据有关。
根据程序集的证据给程序授予权限的步骤是可配置的。下图说明了整个过程:
由上图可以看出即使一个应用程序提供了证据,它也有可能不被授予权限。这全由安全策略中的配置决定。安全策略中的信息类似:如果程序集里的信息是由xxx发布的,则赋予它如下权限集。
当程序集获取一个权限集之后,程序集可以运行。而且可以在运行期间在这个被许可的权限集范围内修改这个权限集和安全管理行为。
CAS发挥作用的第二个场合是运行程序集代码时进行的检查权限。
.NET Framework的代码请求CLR验证调用方代码是否具备合适的权限。CLR把调用方代码看作当前线程栈上的一组方法。CLR验证包含这一组方法的所有程序集都具备多要求的权限,这称作栈审核。栈的遍历防止可信度低的程序集利用可信度高的程序集(例如微软开发的)进行某些恶意操作。
做个总结,代码访问安全性的主要目的是保护资源(例如本地磁盘,注册表或网络等)免受有害代码的破坏。代码组(Code Group)与权限(Permissions)是代码访问安全中两个最重要的概念。代码组用于把具有相似特征的代码集合到一组。把程序集放到代码组所使用信息称为"证据"。权限是允许每一个代码组执行的动作。这些概念将在下文详细介绍。CAS的作用过程:CLR中负责载入和运行托管代码的VES(Virtual Execution System)加载程序集并把程序集与一个或多个代码组相匹配。每一个代码组都被赋予了一个或多个权限,这样指定了各代码组的程序集可以执行什么动作。程序集可以在相应的权限下运行。
证据和权限
证据(evidence)是从程序集里提出来的一块信息,可以为程序集的验证提供证据。
.NET Framework类库中提供了8种用于程序集验证的证据,当我们需要验证程序集时可以由其中提取这些证据的一种,分别介绍如下:
-
System.Policy.ApplicationDirectory类实例表示的证据证明程序集存储在某个文件夹里。
-
System.Policy.Gac类实例表示的证据证明程序集是存储在GAC中的。
-
System.Policy.Site类实例表示的证据证明程序集是从某个网站(如www.cnblogs.com)获取或下载的。
-
System.Policy.Url类实例表示的证据证明程序集获取自某个URL处(如www.cnblogs.com/a.dll)。
-
System.Policy.Zone类实例表示的证据证明程序集是由某个区域获取的:
.NET提供了以下5个区域:
Internet
已添加到IE受限站点列表的Internet网站。
已添加到IE可信站点列表的Internet网站。
本地内部网
本地系统存储(我的电脑)
以上每个区域对应着System.Security.SecurityZone枚举的一个值。这些区域在Internet Explorer中配置并应用到整个系统。
-
System.Policy.Publisher类实例表示的证据表示发布者使用验证码技术签署程序集,根据这个证书生成的证据
-
System.Policy.StrongName类实例表示的证据表示程序集的强名(如果有)生成的证据。强名的区域设置不被该特征考虑。
-
System.Policy.Hash类实例表示的证据表示根据程序集的散列值生成的证据。
可以给应用程序集添加自己的证据(必须在签署程序集之前添加),这样可以完全配置安全机制。
System.Security.Policy.Evidence类的实例表示一个数据集合,其中包含两个证据集合,一个用于存储.NET Framework所提供的证据,另一个提供专有证据。用assembly.Evidence可获取程序集的证据。
证据提供方
程序集的证据由CLR在程序集载入AppDomain之前提供。有两种情况:
-
由AppDomain运行库宿主在将第一个程序集载入AppDomain之前提供
包含AppDomain运行库宿主的程序集必须获得名为ControlEvidence的元权限。(这对微软开发的运行库不是问题)
-
由类装载器在包含请求的程序集被载入AppDomain之前提供。
类装载器是CLR的一部分,被授予完全信任并且拥有元权限。
综上,任何情况下,证据获取机制都需要ControlEvidence元权限。
权限
权限允许代码执行一组操作,.Net中有4种权限,分别介绍如下:
1. 标准权限
下面列出的权限定义了所有能被程序集利用的资源。
-
System.Security.Permissions.EnvironmentPermission
读写环境变量的能力
-
System.Security.Permissions.FileDialogPermission
访问用户在Open对话框中选择文件的能力。通常用于没有赋予FileIOPermission权限,不能对文件进行有限的访问时。
-
System.Security.Permissions.FileIOPermission
处理文件的能力(其中包括读文件、写文件、添加文件的内容,创建、更改和访问文件夹)
-
System.Security.Permissions.IsolatedStoragePermission
访问独立存储的能力,独立存储与各用户相关,并具有代码身份的一些特征。
-
System.Security.Permissions.IsolatedStorageFilePermission
指定允许使用私有虚拟文件系统
-
System.Security.Permissions.ReflectionPermission
使用System.Reflection在运行期间查找类型信息的能力
-
System.Security.Permissions.RegistryPermission
读、写、创建和删除注册表项和值的能力
-
System.Security.Permissions.UIPermission
访问用户界面的能力
-
System.Security.Permissions.DataProtectionPermission
允许在代码中为实现数据保护添加安全行为
-
System.Security.Permissions.KeyContainerPermission
控制访问密匙容器的能力
-
System.Security.Permissions.StorePermission
控制访问包含X509证书的存储区域的能力
-
System.Security.Permissions.SecurityPermission
执行、断言权限、调用非托管代码、忽略验证和其他权限的能力
-
System.Configuration.UsersettingsPermission
-
System.Security.Permissions.ResourcePermissionBase
控制使用代码访问安全权限的能力
-
System.Diagnostics.EventLogPermission
读写事件日志的能力
-
System.Diagnostics.PerformanceCounterPermission
利用性能计数器的能力
-
System.DirectoryServices.DirectoryServicesPermission
通过System.DirectoryServices类访问Active Directory的能力
-
System.ServiceProcess.ServiceControllerPermission
控制Windows服务的能力
-
System.Net.DnsPermission
使用TCP/IP域名系统(DNS)的能力
-
System.Net.SocketPermission
在网络传输地址上创建或接受TCP/IP连接的能力
-
System.Net.WebPermission
连接Web或接受Web连接的能力
-
System.Net.NetworkInformation.NetworkInformationPermission
-
System.Net.Mail.SmtpPermission
-
System.Web.AspNetHostingPermission
-
System.Messaging.MessageQueuePermission
通过Microsoft Message Queue使用消息队列的能力
-
System.Drawing.Printing.PrintingPermission
打印的能力
-
System.Data.Common.DBDataPermission
-
System.Data.OleDB. OleDbPermission
使用OLE DB提供程序的能力
-
System.Data.SqlClient.SqlClientPermission
使用SQL Server的.NET数据提供程序访问SQL Server数据库的能力
-
System.Data.Odbc.OdbcPermission
-
System.Data.OracleClient.OraclePermission
-
System.Data.SqlClient.SqlNotificationPermission
-
System.Transactions.DistributedTransactionPermission
这些权限派生自System.Security.CodeAccessPermission类。这个基类中的方法允许在代码中确保拥有权限、请求权限来拒绝授权访问,但它们不能用于获取安全策略无法授予的权限。对于以上这些权限,还可以进行更细粒别的操作。如FileIOPermission权限,不仅仅是文件的访问权限,还可以指定文件访问的具体级别。在使用这些权限时,应该把这部分代码放入try/catch块中,在程序运行受到限制时,应该给出用户可选的替换解决方案。而不是让程序无声的崩溃掉或不给提示的停止执行。
2. 标识权限(也称身份权限)
CLR为(几乎所有)程序集提供的证据。其不同之处在于它们的授予不依赖于安全策略,而是仅依赖于程序集提供的证据。所以只有CLR能把身份权限赋予代码,所以身份权限不能包括在权限集合中。例如,一段代码是来自某个发布者,管理员把与另一个发布者相关的身份权限赋予这段代码是毫无意义的。
标识权限有如下几种:
-
System.Security.Permissions.PublisherIdentityPermission
-
System.Security.Permissions.SiteIdentityPermission
-
System.Security.Permissions.StrongNameIdentityPermission
-
System.Security.Permissions.URLIdentityPermission
-
System.Security.Permissions.GacIdentityPermission
-
System.Security.Permissions.ZoneIdentityPermission
这些类均派生自CodeAccessPermission,所以它们可以像其它权限那样用在代码里。
3. 安全权限
安全权限(又称元权限)是授予安全管理程序本身的一类权限。以下几点需要特别注意:
可以通过System.Security.Permissions.SecurityPermission在代码中操作元权限。
-
元权限允许执行非托管的代码(SecurityPermissionFlag.UnmanagedCode)
-
元权限允许从程序中提取证据(SecurityPermissionFlag.ControlEvidence)
-
元权限允许执行没有经过验证的代码(SecurityPermissionFlag.SkipVerfication)
4. 自定义权限
为资源访问定义自己的权限,可以从通过CodeAccessPermission的派生自定义的类,这些类可以实现与.NET代码访问安全系统相同的功能,包括堆栈和策略管理。
需要创建自己的代码访问权限的原因:
-
保护.NET Framework没有保护的资源。
-
提供比现有权限更精细的管理。
权限集合
.NET中定义了几个权限集合,可以向代码组应用一个权限集合,已命名的权限集合有:
-
FullTrust: 没有权限的限制
-
SkipVerification: 不进行验证
-
Execution: 运行的能力,但是不能访问受保护的资源
-
Nothing: 没有权限,代码不能执行
-
LocalIntranet: 本地内部网的默认策略,它是权限全集的子集。
-
Internet: 未知来源的代码的默认策略,这是限制最严格的策略。(没有IO,访问事件日志等能力)
-
Everything: 这个集合中的所有权限,其中不包括忽略代码验证的权限。
这其中只有Everything权限集合是可以修改的,其它的都不可以修改。
通过应用安全策略根据证据授予权限
安全策略级别
.NET提供了4种安全策略级别
安全策略 |
配置者 |
应用于 |
企业 |
管理员 |
位于企业计算机上的程序集里的托管代码 |
计算机 |
管理员 |
存储于计算机上的程序集里的托管代码 |
用户 |
管理员或相关用户 |
使用特定用户权限运行的进程里的托管代码 |
应用程序域 |
应用程序域的宿主 |
位于应用程序里的托管代码 |
授予程序集的权限集是授予每个策略的权限集的交集,安全策略是由层次的。一般来说,大多数安全规则集中于"计算机"策略(事实上,默认情况下其它策略授予所有权限,就像只有"计算机"安全策略这一个)。
安全策略深入分析
安全策略有以下部分组成:
-
存储在树结构中的代码组
-
一组权限集
-
一组被安全策略授予完全信任的程序集(策略程序集或完全信任程序集)
一个代码组将一个证据和一个安全策略的权限集合对应起来。代码组有一个称之为成员条件的入口需求(entry requirement),如果要把程序集划归某个代码组,该程序集就必须符合那个代码组的成员条件。每一个代码组有且只有一个条件。官方文档解释:"如果程序集的一个证据和代码组的证据相同,那么这个证据集隶属于这个代码组。"所以如果创建了自定义证据,那么还需要创建自定义代码来使用它们。代码组可以使用的成员条件即证据参见上文。
代码组是分层安排的,根部是All Code成员条件的代码组。如下图:
程序集的每一个代码组都将扩展程序集的权限,代码树中下面的代码组具有的权限比上面的代码组高。Every指忽略代码验证的权限即元权限。
调整代码访问安全策略的工具
.NET中提供了两种调整代码访问安全策略的工具,一个是基于命令行的工具 – Caspol.exe,另一个是基于mmc管理单元的工具。
命令行工具的优势在于可以创建脚本,更改安全策略,并把策略应用到多台计算机上。所以在接下来的部分将重点介绍Caspol工具:
基本用法
caspol.exe -? 查看帮助。
caspol.exe > C:/a.txt 输出到指定文件
这些用法与Windows中其他运行于cmd的工具相同。
下面把重点放在与调整CAS策略相关的用法
参数:-listdescription 缩写 -ld
作用:以层次结构列出机器上的代码组及其描述。
其输出如下:
参数:-listgroups
作用:查看机器中的代码访问组。
执行结果:
在以上两条命令的执行结果中,
"安全为ON"
"执行检查是ON" 意味着所有的程序在运行之前必须赋予执行权限。
"策略更改提示为 ON"表示在更改安全性策略时,是否可以看到"Are you sure"的警告信息。
参数 -caspol.exe –execution on|off
作用:可以关闭执行检查,这样即使程序集没有权限也可运行,但在执行过程中违背安全策略就会产生异常。
参数 –resolvegroup assembly.dll
作用:查看程序集所属的代码组
用此命令查看mscorlib.dll(.NET核心类库)的代码组可以看到如下提示:
这三个级别中默认情况下真正起作用的级别是Machine,但是程序集获得的权限是三个级别的交集,所以如果在Enterprise级中限制某一程序级的权限,则该程序集不会获得该权限。在一个等级内,由代码组获得的权限是程序集所属的代码组的权限的交集。即代码组越深入,权限越高。
参数 -resolveperm assembly.dll
作用:此命令可以查看程序集被赋予的代码访问权限与身份权限。可以看到定义这些权限的类,及定义这些类的程序集,版本与加密标记。
参数 -user –listgroups与caspol.exe -enterprise -listgroups
作用:这两条命令查看user/enterprise级别的代码组,当管理员登录时,默认是-machine,即caspol.exe -machine -listgroups与caspol.exe -listgroups作用相同。当普通用户登录时caspol.exe -listgroups采用-user参数。
由于三个级别对一个程序集赋予的权限是交集,所以当给一个程序集赋予一个权限时,要保证三个等级中给此程序集的权限要大于等于程序集所需权限。
应用安全策略算法
如果一个程序集所在的程序集列表拥有完全信任的安全策略,那么将授予这个程序集FullTrust权限(见后文)。
否则,算法将根据下列规则遍历代码组:
-
算法验证程序集是否为每个根代码组的成员。
-
仅当程序集是父代码组的成员,算法才考虑子代码组
-
安全策略授予程序集的权限集是该程序集所属的所有代码组的权限的并集。
-
每个代码组可以用某种方式来标记以完成其所属的安全策略。在这种情况下,属于这组的程序集将不会评估接下来的策略级别。
-
每个代码组可以标记为排除的。在这种情况下,属于这个代码组的程序集将不会获得与该组关联的权限。(如果一个程序集属于同一安全策略的两个排除代码组,那么将不会授予其权限。)
默认安全策略配置
默认情况下,"计算机"安全策略的配置如下图:
如图,.Net区域定义了程序集的来源,每个程序集都提供与它来源区域相关的证据。默认情况下,"计算机"安全策略将完全信任授予公钥属于微软或ECMA的私钥所签署的程序集。
FullTrust权限
FullTrust权限基本上允许跳过所有的CAS验证,持有该权限的程序集能够访问所有的标准权限。默认情况下,签名程序集的代码只能从拥有FullTrust权限的程序集中调用。这条规则的原因是只有签名程序集可以被放入GAC中并且可能被恶意代码所利用(未签名代码由于位置,功能等无法预测而很难被移动代码利用)。可以通过System.Security.AllowPartiallyTrustedCallersAttributes关闭这一限制。
安全策略的管理
安全配置文件
安全策略(Enterprise、Machine、User三个级别)把代码组、权限和权限集连接起来。.NET中的安全配置信息保存在XML配置文件中,这个文件受Windows安全策略保护(只有Administrator、Power User和SYSTEM这些组中的用户能修改Machine级别的安全策略)
保存安全策略文件的位置如下:
- Enterprise策略配置:<windir> \Microsoft.NET\Framework\<version>\CONFIG\enterprise.config
- Machine策略配置:<windir> \Microsoft.NET\Framework\<version>\CONFIG\security.config
- User策略配置:%USERPROFILE%\Application Data\Microsoft\CLR Security Config\<version>
尽量使用caspol.exe或MMC管理单元中的工具来编辑安全策略,特殊情况下可以手动编辑这些XML文件。
caspol.exe配置策略文件实例:
以下操作将Intranet区域的代码组的权限集由LocalIntranet改为FullTrust,使Intranet区域程序可以不受限制的运行。
首先获得LocalIntranet代码组的数字标签,可以使用下列命令:
caspol.exe -listgroups
结果中可看到:" 1.2. (联合) 区域 - Intranet: LocalIntranet"即LocalIntranet被列为1.2。现在使用下面的命令应用完全信任级别:
caspol.exe -chggroup 1.2 FullTrust
出现一个安全提示:
您正在执行的操作将改变安全策略。
确实要执行此操作吗? (yes/no)
确认后执行成功。
参数 -security off/on
作用:关闭/启用.NET安全性(注意,建议总是开启.NET安全性检查)
参数 -reset
作用:把安全策略重置为默认状态
参数 -addgroup
创建代码组的方法,比如,要实现信任www.cnblogs.com此站点的程序集,就可以为其创建一个代码组。首先使用上述讲过的命令获取Zone:Internet的标记 - 为1.3。
命令如下:
caspol.exe -addgroup 1.3 -site www.cnblogs.com FullTrust
用-listgourps命令查看会有以下提示:
" 1.3.2. (联合) 站点 - www.cnblogs.com: FullTrust"
表示新建代码组成功。
参数 -remgroup
作用:移除代码组,如下命令删除刚创建的代码组:
caspol.exe -remgroup 1.3.2
注意:系统中的All Code代码组无法删除,其下的代码组可以删除。
参数 -addset
作用:创建权限集,例如:
caspol.exe -addpset MyCustomPermissionSet permissionset.xml
该命令创建名为MyCustomPermissionSet的权限集,并由permissionset. xml指定权限集中权限。(可以裁减权限集Everything的文件获得自己想要的权限集)
参数 -listpset
作用:该命令可以查看所有XML格式的权限集文件。
参数 -chgpset
作用:该命令将XML中的PermissionSet应用到现有的权限集上,给已有的权限集指定新定义。如下面的例子:
caspol.exe -chgpset permissionset.xml MyCustomPS
从源代码中进行权限检查
方式一:从代码中以专用手工验证的方式验证权限
这种方法主要用到CodeAccessPermission类中的相关方法和PermissionSet类的示例表示的权限集合。可以使用CodeAccessPermission类及其派生类的Demand()、Deny()/RevertDeny()、PermitOnly()/RevertPermitOnly()及Assert()/Resert()方法在运行时操作授予代码的权限。PermissionSet类也具有同样的四组方法,可以在权限集上进行同样的操作。下面依次介绍这4组方法。
Demand()方法
Demand()方法验证当前代码是否拥有特定的权限集合所表示的权限。当所验证的方法不具有全部所需的权限会引发SecurityException异常。
下述示例代码确保方法有读取文件的权限:
1 using System.Security; 2 3 using System.Security.Permissions; 4 5 class Program { 6 7 static void Main() 8 9 { 10 11 string sFile = @"C:\data.txt"; 12 13 CodeAccessPermission cap = new FileIOPermission(FileIOPermissionAccess.Read, sFile); 14 15 try 16 17 { 18 19 cap.Demand(); 20 21 // Read the "C:\data.txt" file. 22 23 } 24 25 catch ( SecurityException ){ 26 27 // The code is not allowed to read "C:\data.txt". 28 29 } 30 31 } 32 33 }
显示请求的目的是预测可能发生的拒绝访问,来提前修改程序的行为。
Tips: 显示请求中可以实现一些策略,来根据上下文决定要测试的权限的集合,比如根据当前用户的角色。
Deny()与RevertDeny()方法
CodeAccessPermission类和PermissionSet类的Deny()方法指定代码不需要的权限,这个功能用于建立应用程序所用权限的全局观念,从一开始就强制限制一些操作。当临时需要这些权限时可以使用RevertDeny()来停止Deny()操作。
下面的代码示例了这两种函数的使用:
1 using System.Security; 2 3 using System.Security.Permissions; 4 5 class Program { 6 7 static void Main() 8 9 { 10 11 PermissionSet ps = new PermissionSet( PermissionState.None ); 12 13 ps.AddPermission( new FileIOPermission( 14 15 FileIOPermissionAccess.AllAccess , @"C:\WINDOWS" ) ); 16 17 ps.AddPermission( new RegistryPermission( 18 19 RegistryPermissionAccess.AllAccess , string.Empty ) ); 20 21 ps.Deny(); 22 23 // 此处是一个受保护的环境。在此处可以放心的进行操作,不用担心此处调用的代码会对系统文件及注册表造成影响。往往在此处调用的是第三方代码库,因为我们不能保证第三方类库中代码的安全。 24 25 CodeAccessPermission.RevertDeny(); 26 27 } 28 29 }
注意,Deny()是在Permission类的实例对象上调用的,而RevertDeny()是静态调用的,它可以恢复当前栈上的所有拒绝请求,所以如果多次调用了Deny(),则只需要调用一次RevertDeny()。
下面是一个完整例子
1 using System; 2 3 using System.IO; 4 5 using System.Security; 6 7 using System.Security.Permissions; 8 9 class Program 10 11 { 12 13 static void Main() 14 15 { 16 17 CodeAccessPermission permission = 18 19 new FileIOPermission(FileIOPermissionAccess.AllAccess, @"C:\"); 20 21 permission.Deny(); 22 23 //不被信任的代码 24 25 UntrustworthyClass.Method(); 26 27 CodeAccessPermission.RevertDeny(); 28 29 } 30 31 } 32 33 class UntrustworthyClass 34 35 { 36 37 public static void Method() 38 39 { 40 41 try 42 43 { 44 45 StreamReader din = File.OpenText(@"C:\textfile.txt"); 46 47 } 48 49 catch 50 51 { 52 53 Console.WriteLine("Failed to open file"); 54 55 } 56 57 } 58 59 }
执行这段代码,catch将会捕获异常异常,并显示"Failed to open file"的错误提示。
PermitOnly()与RevertPermitOnly()方法
类似于上面一组可以临时修改当前方法许可的权限集合的方法。PermitOnly()指定许可的权限,RevertPermitOnly()放弃这种许可。
Assert()方法与RevertAssert()方法
Assert()方法用于指定调用者不需要检查的权限。Assert()方法暂停该权限从调用处开始的调用栈遍历。这样调用Assert()的方法中调用Assert()之后的代码就拥有了这个不受检查的权限。所以拥有Assert()方法的类可能导致此类被调用方借用进行恶意攻击。
实例说明:
Assert()应用的场合也是有很多,如在.NET Framework中FileStream类需要通过P/Invoke完成读写文件的操作,由于平台调用这需要FileStream类所有方法拥有SecurityPermission(UnmanagedCode)的元权限。但是赋给FileStream这样的元权限是不可接受的,解决方法就是通过在FileStream类的方法中使用Assert()停止对FileStream类中方法元权限的检查。(停止对于SecurityPermission(UnmanagedCode)元权限检查更常见的做法是在方法或其所在的类上标记System.Security.Suppress UnmanagedCodeSecurityAttribute,这个attribute会通知CLR停止对此类/方法进行SecurityPermission(UnmanagedCode)元权限的检查。)
代码中包含了一个Audit类,这个类执行一个接受字符串作为参数的Save()方法,并把数据保存到C:\audit.txt中。Audit类中的方法断言它需要的权限,以巴审计行添加到文件中。(此测试代码中,Main()函数显示拒绝了Audit方法需要的文件访问权限。)
using System; using System.IO; using System.Security; using System.Security.Permissions; class Program { static void Main() { CodeAccessPermission permission = new FileIOPermission(FileIOPermissionAccess.Append, @"C:\audit.txt"); permission.Deny(); //即使此处显示拒绝写入权限,但由于断言的存在,内容仍可以被写入文件。 Audit.Save("some data to audit"); CodeAccessPermission.RevertDeny(); } } class Audit { public static void Save(string value) { try { FileIOPermission permission = new FileIOPermission(FileIOPermissionAccess.Append, @"C:\audit.txt"); permission.Assert(); FileStream stream = new FileStream(@"C:\audit.txt", FileMode.Append, FileAccess.Write); // 写入内容到文件的代码…… //恢复断言 CodeAccessPermission.RevertAssert(); Console.WriteLine("Data written to audit file"); } catch { Console.WriteLine("Failed to write data to audit file"); } } }
上述Main()函数中调用Audit类中方法不会触发异常,即使Audit的方法在执行时没有访问本地磁盘的权限。同之前的Revertxxx()代码,RevertAssert类也会恢复所有的断言。
注意:使用断言一定要注意安全问题,我们显示把权限赋给一个方法,而且这种权限不被Deny()这样的机制阻止,所以我们的代码很可能被没有这些权限的代码所利用,从而威胁代码的安全。
使用Assert()方法几个需要注意的地方
-
Assert()方法在同一方法内不能调用多次。要想调用多次,在两次调用之间需要调用RevertAssert()静态方法。
-
如果要停止同一方法中的多个权限的调用栈的遍历检查,须调用PermissionSet实例上的Assert()方法。
-
为了安全性考虑,到需要的地方再调用Assert方法,且尽早调用RevertAssert()恢复权限检查。将对Assert()的调用放入一个try/finally块中是一个很好的做法(finally中放入RevertAssert())。
-
调用Assert()的方法必须具有SecurityPermission(Assertion)元权限,同时,该方法必须有遍历调用栈的相关权限。如果没有此种权限,调用Assert()的方法没有任何效果,也不会引发异常。
CodeAccessPermission类与PermissionSet类其它方法说明
-
FromXml()方法与ToXml()方法
这对方法提供使用xml文档建立和保存复杂的权限集的功能。
-
实现System.Security.IPermission接口的方法
这些方法主要有:
Union():求两组权限的交集
Intersect():求两组权限的并集
IssubsetOf():计算两组权限之间的包含关系
示例:比如,允许访问"C:\MyDir"文件夹的权限包含了访问"C:\MyDir\MySubDir"文件夹的权限。
using System.Security; using System.Security.Permissions; class Program { static void Main() { string dir1 = @"C:\MyDir"; string dir2 = @"C:\MyDir\MySubDir"; CodeAccessPermission cap1 = new FileIOPermission( FileIOPermissionAccess.AllAccess, dir1); CodeAccessPermission cap2 = new FileIOPermission( FileIOPermissionAccess.AllAccess, dir2); System.Diagnostics.Debug.Assert( cap2.IsSubsetOf(cap1) ); } }
方式二:使用Attribute进行声明式权限验证。
此种方法假设标记某Attribute的方法内所有代码一定具有某个特定的权限。每个CodeAccessPermission的派生类都有一个标准的Attribute与之对应。例如,对应RegistryPermission类,RegistryPermissionAttribute类表示注册表相关的访问权限。
下面的两段代码对比了使用Assert()方法停止注册表读取权限的方法与使用Attribute的做法
Assert()方法:
using System.Security; using System.Security.Permissions; class Program { static void Main() { CodeAccessPermission cap = new RegistryPermission( RegistryPermissionAccess.NoAccess , string.Empty ); cap.Assert(); // 读取注册表 RegistryPermission.RevertAssert(); } }
RegistryPermissionAttribute:
using System.Security.Permissions; class Program{ [RegistryPermission(SecurityAction.Assert)] static void Main(){ // 此处拥有读取注册表的权限 } }
System.Security.Permissions.SecurityAction枚举值有与上节中Demand、Deny、PermitOnly与Assert这四个方法对应的值,可以用于指定希望的操作。
另外SecurityAction的枚举值表示的操作中还有CodeAccessPermission与PermissionSet类中所不具备的功能。如下:
SecurityAction值 |
描述 |
Inheritance-Demand |
当程序集加载时,该值可以给attribute声明所在的类的派生类型强加指定的权限 |
LinkDemand |
强制JIT编译器验证授予方法的一个或多个权限,而不验证它所调用方法的权限。该动作比Demand更为严格,但性能代价较低,因为它仅在JIT编译时进行验证 |
LinkDemandChoice |
要求当前调用者有一个特定的权限 |
SecurityAction中还有3个用于程序集级别的值。它们可以被用来在程序集加载时通知CLR必须对其所有的权限执行的操作。如下表:
SecurityAction值 |
描述 |
RequestMinimum |
指定一个或多个权限,不具有这些权限的程序集就不能加载 |
RequestOptional |
指定一个或多个权限,这些权限是正确执行程序集所需的。然而如果没有授予这些权限,程序集仍然会加载。我们称这些权限为可选权限 |
RequestRefuse |
指定一个或多个权限,在程序集加载时这些权限必须没有授予给程序集 |
使用示例,有个访问数据库的程序集需要SqlClientPermission权限。它可能需要RegistryPermission权限以便访问特殊参数(如果应用程序通过了默认参数就不用去访问),不需要WebPermission和UIPermission这样的权限。
using System.Security.Permissions; using System.Data.SqlClient; [assembly: SqlClientPermission( SecurityAction.RequestMinimum )] [assembly: RegistryPermission( SecurityAction.RequestOptional )] [assembly: UIPermission( SecurityAction.RequestRefuse )] [assembly: System.Net.WebPermission( SecurityAction.RequestRefuse )] class Program { public static void Main() { } }
可以使用Attribute来请求内置的程序集,由于Everything权限集可以在运行过程中被安全策略修改,所以不要请求这个权限集。下面代码示例了请求内置的权限集:
[assembly: PermissionSetAttribute(SecurityAction.RequestMinimum, Name="FullTrust")]
这行代码表示程序集至少请求内置权限集FullTrust。
这种在程序集开始时请求权限有以下几点作用:
- 如果程序集需要某些权限才能运行,则只有在程序集开始执行时而不是在执行期间声明需要权限才有意义,这样可以确保用户不会在程序开始工作后遇到障碍。
- 只赋予所需的权限,不赋予其他任何权限。没有明确的请求权限,程序集就会得到比执行所需更多的权限。这会增加程序集被其他代码用于恶意目的的可能性。
- 如果只请求权限的最小集合,则可以增加程序集运行的可能性,因为不能预测最终用户实际使用的安全策略。
在Visual Studio中,选择项目属性的安全选项卡,可以检查程序集的权限,如下图:
.NET2.0中新增的permcalc.exe工具可以完成与vs相同的计算权限的功能。
permcalc.exe -show -stacks -cleancache Assembly.dll
此命令可以创建一个必须权限的XML文件。-show参数表示命令结束后立即打开此文件。
-stacks参数指示把堆栈信息添加到XML文件中,可以查看权限请求来自何处。
通常,考虑应用程序的权限需求,必须由如下两种情况选择一种:
- 在开始执行时请求所需的所有权限,如果没有赋予那些权限,则退出执行。
- 在开始执行时不请求权限,但是执行过程中一直准备着处理安全异常。
命令式与声明式的对比
Attribute声明式权限验证的缺点体现在:
-
权限请求或断言失败时,无法在异常引发的地方就地捕获。
-
传给权限的参数必须在编译时已知,通常情况下,你无法设置动态的安全逻辑(如执行过程中获取当前用户角色)。
使用Attribute也有以下优点:
-
有可能会通过程序集元权限或permview.exe工具访问这些attribute及其参数。
-
有可能在程序集级别使用其中的一些attribute。
-
可以通过反射访问设置信息。
其它问题
由于安全异常导致应用程序失败时的选择
-
放松策略权限。
-
移动程序集,将信任网络中的程序集移动到本地系统可以使其获得更高的权限,这也体现了.NET安全中一个有缺陷的地方,即本地硬盘中的程序集很容易获得FullTrust权限。
-
把强名应用到程序集上,再创建一个信任强名的代码组。
CAS中关于独立存储的部分放在下一篇中详细介绍
参考:
- C#和.NET2.0实战
- C#2005&.NET3.0高级编程(第5版)