简介:
托管代码的.NET内置安全是不可或缺的一部分,几乎所有的API调用都涉及到一些安全API。这和老式的非托管代码的工作方式有着本质的区别,况且,确保适当的地方和适当的时机的安全机制对任何应用程序都是至关重要的。
对于并非是.NET安全专家(的人来说),这里有相当多的要点要记住。
首先,通常来说,安全的概念是并非所有的编译出来的都会被执行,不同的限制适应不同的情况,例如,传统的Windows NT的安全取决于当前用户是否某些执行的基础之上的。
.NET安全(这是第二点)的决断建立在代码标识,而不是用户(这也是它命名为CAS——Code Access Security的原因)。这也意味着,为了检查某段代码是允许执行的,它需要查看如下点:代码从何处来的;有没有任何标识作者的特征(例如:授权证书);代码的散列值等等。但是这并不意味着Windows安全被重载覆盖,它依然起作用,.NET Security只是防止攻击的另一个层面。
在现实生活中,几乎有着无限多的安全配置会影响到托管代码的执行,显然是不可能涵盖所有的,不过,一些简单安全场景倒是可以完全包含的。
典型问题:
“我构建了一个需要更新注册表的应用程序,从C:/运行它正常工作,但是从一个网络共享位置启动它就是失败。客户正在等待(回复)我该怎么做呢?”
这可能是最常见的问题了,不幸的的是对于作者来说这是预期的结果。任何来自网络(共享也被认为是网络)的代码,相对于本地代码,只获得很少的信任,例如,默认情况下是不允许访问注册表的,这是情有可原的——你为什么会让某个素未蒙面的人在你本地机器上写一些敏感信息呢?
另外一点,可能你也知道,一些Dll只允许带有已知关键字的数字签名的调用者(调用),举例来说,无论如何,除非提供证据证明你是来自Microsoft,否则将不能调用它们。由此可见,这些是很难测试的,但是至少你需要记住它们。
如何测试:
这里我假设你的专注目标不是安全方面,而是其他的,如此我们要牵出一些基本的东西来溜溜。
首先要介绍的概念是信任等级(Trust Level)。一般情况下,信任等级可以应用于程序或者函数,并且一系列允许的权限集,例如,有些代码允许对给定文件进行读写操作,无限制的网络操作,读取系统环境变量,但是不允许写注册表。
理解调用每一个API都在相应的信任等级有相应的要求这一点是很重要的,例如,如果你试图通过FileStream类去打开一个文件,首当其冲的是在做任何事情之前检查API的调用者(有权限),否则就会失败。
不同信任等级的需求量是非常多的,当然,简而言之,我们可以分成三大类:
1. 完全信任(FullTrust):在这个信任里,CAS允许做任何事情。如果某个API是需求是完全信任,那么它很有可能可以做某些相当危险的事情,一个真实的例子是Process类,它在完全信任才可以创建实例。默认的,从本地启动的一切都将获得完全信任,所以,如果你仅仅从C:/运行你的程序,是不能真实的测试它的安全的。
2. 部分信任(Partial trust):这个真的有很多细微构成,范围涉及从显示UI的能力到通过反射调用私有方法或者文件读写。通常实现这些功能的函数要求调用者有某些相应的能力(但是没有必要完全信任,它不是“所有或者没有”之类),.NET类库中相当多的函数都有不同的部分信任需求,一般来说,来自互联网(Internet)和局域网(Intranet)的应用程序都是运行在不同程度的部分信任等级下的。
3. 没有特殊需求(仅仅执行 (Execution only)):任何人都可以调用,因为它不代表任何安全风险,例如Math.Sqrt()这样的API。
通过上面的分类,即使不是安全专家,通过下面三步,应该能够很容易对基础级别的安全做相关测试:
1. 根据API的设计列出实际的信任等级,使用场景和测试程序。在大多数情况下,找到以上三点就足够了,当然,如果能够对需求有更深的认识(譬如文件读写),那是再好不过了。
2. 在需要的或者根高的信任等级运行测试程序,如果不能运行,那么表示你有安全bug(译注:原文是Security overenforcement bug,不知道是个什么东东,如果你知道,欢迎指正)
3. 在需要的信任等级以下运行程序,如果能够运行,这是一个安全漏洞(Security hole.)。
下一个问题是:我该怎么来改变信任等级呢?
工具:
有几个简单的办法去操控安全性以达到测试目的。其实,这些仅仅是一些小技巧,而不是系统的做法,但是对测试来说还是有些帮助的:
1. [非常简单]:假设你的机器名为Box1,你的IP地址是111.111.111.111,而你的应用程序名为App.exe,放在C:/目录下, 并且你的.NET安全策略和IE安全设置都是默认状态。这种情况下,下面的命令行:
//Box1/C$/App.exe
将有效的使得App.exe运行在本地局域网域(LocalIntranet zone),并降低信任等级。这将让你知道如果某人从共享位置运行程序结果会怎么样——非常简单而且便宜的测试。
另外,如下命令行:
//111.111.111.111/C$/App.exe
将从局域网域(Internet zone)启动你的应用程序,这将更加减弱信任等级,在这种情况下,你得到的执行环境和网络浏览区(Internet Explorer)相当接近(虽然不完全相同),再一次得到一个相当好的基础测试方法。
2. 稍微复杂一点,但是有更好的灵活性:使用caspol.exe或者.NET框架配置工具(.NET Framework Configuration tool)改变安全策略。要想达到100%(掌握),你需要对安全有较深的了解,但是一些砂箱场景(sandboxing scenarios)还是很容易实现的。
例如,下面的步骤将分配任何权限给任意一个从C:/Temp目录下启动的应用程序:
2.1. 启动.NET框架配置工具(可以通过运行mmc.exe,然后加载适当的管理单元(snap-in),或者通过管理工具(Administrative tools)下的快捷方式;
2.2. 展开“控制台根节点(Console Root)->.NET框架配置(.NET Framework Configuration)->我的电脑(My Computer)”节点。
2.3. 展开“运行时安全策略(Runtime Security Policy) ->机器(Machine) ->代码组(Code Groups)”节点。
2.4. 选中“所有代码”(All_Code)节点,鼠标右击,选择“新建...”(New...)菜单。
2.5. 在接下来弹出的向导填上某个名字,点击“下一步”(Next).
2.6. 在下拉列表菜单中选择使用“URL”作为成员条件。
2.7. 在“URL”框里键入
2.8. 无论是利用现有的集合还是创建一个新的都是可以的,但是我鼓励你创建一个新的集合,因为这样可以让你在任意种你感兴趣的安全配置下测试你的应用程序,同时还可以学习权限集和.NET之间的联系。通过向导创建新的集合是一个很容易的、自我描述的过程。
2.9. 当完成创建组之后,你会在“所有代码”下面发现这个组,鼠标右击并选择“属性”(Properties),选中“该策略仅拥有和该组相关联的权限集的权限”(译注:偶没看到中文版里面到底是什么,这里原文是:"This policy level will only have the permissions from the permission set associated with this group")。
file://C:/TMP/*, ,点击“下一步”。至此,如果你从C:/TMP启动应用程序,它就可以得到你分配的这些权限——并且仅仅能得到这些。
当你结束测试时,不要忘记恢复策略到初始值。你可以通过.NET框架配置工具或者运行脚本“caspol.exe -pp off -all -reset”。
不过,如果测试的不是一个整个程序,而是一个Dll或者一个函数,是否有容易的办法调整信任等级呢?答案是肯定的。
3. 程序集等级权限需求是更好的选择(译注:偶从意义角度翻译的,原文是:Assembly level requests are our friends)
这些(程序集等级权限需求)使用类似下面的语句去构造,并放在程序集的开始:
[assembly: {Some permission | PermissionSet}Attribute(SecurityAction.Request{Minimum|Optional|Refuse}, ...)]
对于测试,最感兴趣的是RequestRefuse,它告诉策略:“该程序将从不授予某个命名权限”之类类似信息,下面这一行就是一个例子:
[assembly: RegistryPermissionAttribute(SecurityAction.RequestRefuse, Unrestricted = true)]
这里意味着程序集将从不授予注册表读写权限,即使运行环境允许授予也不会授予。
一个有趣的点是FullTrust,因为对于任意的,任何权限的子集都不是完全信任,所以任和要求完全信任的测试都一定会启动失败——很好的测试用例!
另一有用的技术是把RequestMinimum合并到RequestOptional里,如果你的程序集有这样的需求(譬如说对A集合做ReqMin,对B集合做ReqOptional),那意味这:
a) 如果环境授予的权限小于A,程序集将不能启动;
b) 将绝对不会授予超过A和B的并集的权限(从本地启动,最终授予的权限就等于A和B的合集)。
所以举例来说,如下这几行将确保你的程序集运行在运行时的最小权限集——程序运行需要的最小权限:
[assembly: SecurityPermissionAttribute(SecurityAction.RequestMinimum, Execution = true)]
[assembly: PermissionSetAttribute(SecurityAction.RequestOptional, Unrestricted = false)]
以上技术是相当强大的,并且涵盖了实际生活的大多数测试场景;不过,要想使用好,需要熟悉程序集需求语法和权限使用的语法,幸运的是,这些都不在是问题,因为在MSDN上提供了这些点的信息,至少提供一些常用用例。
4. Stack walk修饰。
其实,这是一些比较高级的改变类或者方法级别的信任等级的方法。这些对测试有用的修饰是Deny()和PermitOnly(),它们是一些建立在权限和权限集类基础上的方法。下面的例子演示如何确保在Foo方法内,一切(代码)都可以执行,并且所有从Foo里调用的方法都可以获得正确的权限去弹出文件保存或者文件打开对话框:
// This is the method we control and use to call into Foo:
public void Bar()
{
PermissionSet pSet = new PermissionSet(PermissionState.None);
FileDialogPermission FP = new FileDialogPermission(PermissionState.Unrestricted);
SecurityPermission SP = new SecurityPermission(SecurityPermissionFlag.Execution);
pSet.AddPermission(FP);
pSet.AddPermission(SP);
pSet.PermitOnly();
Foo();
}
// Method that we test
public void Foo()
{
//...
}
不过,有几个问题点必须牢记于心,例如:
a) 在某些情况下,修饰符效果可能会被其他修饰符影响;
b) 有几种类型的安全检查(比如LinkDemand)不受修饰符影响;
所以通过使用它来获取某种程度的安全知识是实际有用的。关于这方面的知识可以阅读MSDN上的代码访问安全(Code Access Security)、权限集(PermissionSet)、安全行为(SecurityAction)等相关资料。