|
Security Briefs...小心完全信任的代码
原著:Keith Brown
翻译:红裤子
原文出处:Security
Briefs:Beware of Fully Trusted Code
大多数的托管应用程序都以完全信任环境下运行。然而,根据我自己的经验,在给那些经验丰富的开发人员讲授 .NET 安全问题的时候,我发现他们大多数
人都没有理解完全信任代码的真正含义。我收集了很多例子,在这些例子中,完全信任的代码可以绕过通用语言运行时(CLR)的安全特性。
每个例子都以一个提问开始,而每个问题的答案似乎都显而易见。
CLR 是否会在运行以前检查所有的代码?
很多CLR的安全特性是基于类型安全的。以一个简单的包含私有成员的类为例。CLR 会在运行时限制对私有成员的访问。但是,这
只有在使用合法手段进行访问时才成立。考虑如下非托管 C++ 类定义: class DiskQuota {
private:
long MinBytes;
long MaxBytes;
};
我之所以要在这里选择非托管 C++,是因为它将示范当类型系统被破坏时,下面这段程序是如何被利用进行类型诈骗的:
void EvilCode(DiskQuota* pdq) {
// use pointer arithmetic to index
// into the object wherever we like!
((long*)pdq)[1] = MAX_LONG;
}
通过使用指针和不安全的强制类型转换,攻击者可以访问甚至修改某个类的私有成员。在非托管代码中,运行时没有提供针对这方面的保护
。然而,CLR 被设计成能在 JIT 编译期间(在此特殊情况下的不安全指针运算)探测到这种滥用类型的行为并产生一个异常。缓冲区溢出是另外一个
违反类型系统的例子,它会导致令人厌恶的安全漏洞。CLR 通过保证所有代码的类型安全来封堵这些漏洞,是这样吗?
是的,除非你搬起石头砸自己的脚,盲目信任这些程序集。任何程序集都可以用一个标志来标记自身,技术上称作许可请求,它告诉 CLR 跳过对
代码的安全检查。下面是一个 C# 例子:
// evilcode.cs
using System.Security.Permissions;
[assembly: SecurityPermission(
SecurityAction.RequestMinimum,
Flags=SecurityPermissionFlag.
SkipVerification)]
// your evil code goes here
你甚至可以不用知会 CLR 而在程序集中使用这个许可请求。如果用 /unsafe 选项编译 C#
代码,编译器会悄悄添加这个许可请求。如果用托管 C++ 代码生成一个程序集,默认情况下也会这样,虽然在下一个 C# 版本中,这种状况将会
被新的编译选项改变,代号为“Whidbey”。当 CLR 用 SkipVerification 请求成功加载某个程序集时,它会按照在
JIT 编译期间所发生的那样跳过类型安全检查。Framework Class Library(FCL)使用这个特性,即便是
MSCORLIB.DLL 这种核心的 .NET Framework 的程序集都在使用这种许可请求。有句老话说得好,既然有所得,总要付出代价
。
你必须做出决定,哪个程序集可以忽略许可请求,哪些不能。首选项是通过 .NET 得安全策略来实现。任何以 FullTrust
方式运行的程序集,在默认情况下包含 .NET Framework 自己以及安装在本地硬盘上的程序集都允许跳过检查的请求。从网络上下载的
程序集在默认情况下其跳过安全检查的请求将被拒绝。
不知道你怎么想,但我知道了这些后,肯定会尽量使我的每个程序集以部分信任方式运行。
私有方法真的就私有吗?
在我的上一个例子中,演示了完全信任代码如何跳过代码检查并用指针运算读写任何类的私有数据成员变量。其实你没必要非得用指针,使用反射(reflection)可以完成同样的事情(参见
Figure
1)。
当另一个类试图调用,甚至是通过反射调用私有方法时,CLR 的确会以抛出异常的方式来保护它们。
真是这样吗?再说一遍,针对这种搞笑的事情,唯一能保护你的类的方法是通过 .NET 的安全策略。在这种情况下,重要的许可是
ReflectionPermission。完全信任代码要具备这个许可,必须要反射私有方法。
想想看,这意味着什么?你发布了一个程序集,其中包含只能在内部使用的敏感的私有方法。那么用什么来阻止客户端直接调用这些私有方法,
多半是传递可能导致你的方法出现故障和危险行为的畸形输入?。这种情况下毫无办法,因为你没有控制客户端的安全策略。但是等等,也许你听说过
一种叫强名称 LinkDemand (strong name LinkDemand)的特性,Framework Class
Library(FCL)使用它,这样看来还不错。看看下面的程序:
public class NuclearReactor {
[StrongNameIdentityPermission(
SecurityAction.LinkDemand,
PublicKey="002400000...")]
private void Meltdown() {
// calling assembly must have specified public key!
}
}
现在前面使用反射的诀窍行不通了,没有人能调用 Meltdown 方法,除非调用程序集具备专门的公共密钥。链接需要(Link
demand)命令 CLR 检查在 JIT 编译时链接到该方法的程序集,以保证它能满足链接需要。通过使用 StrongNameIdentityPermission,我要求调用者必须具备一个特殊的公共密钥。
不幸的是,完全信任代码也能以不止一种方法避开这个许可需求。最简单的方法是用下面的命令行选项完全关闭代码存取安全(CAS)。
caspol -s off
幸好,你必须是管理员才能运行这条命令(即使你是管理员,也决不要这么做!)。实际上,这么一条命令很难找到存在的理由,Whidbey 团队正在考虑
将它从下一个版本中删除掉。然而,如果应用程序具备高级别信度,即使非管理员也能编写如下代码来做同样的事情:
using System.Security;
class EvilCodeWithFullTrust {
static void Main() {
SecurityManager.SecurityEnabled = false;
// now call Meltdown via reflection!
}
}
这些用于关闭 CAS
的机制效果相同,虽然前者将它在整个机器上关闭,而后者只在单进程中有效——尽管不能保证它会稳定地工作。更多细节请参见相关文档。
结果是所有的需求都成功,即便是先前所示的链接需求(link
demand)。第二个例子也示范了另一个非常危险的代码存取许可,即一个称为 ControlPolicy 的 SecurityPermission。任何获取了该许可的代码都可以仅用一行代码来关闭进程的代码存取安全。完全信任代码具备所有许可,包括这一个。
但是,你也许不想落到这个地步,实际上你也不会如此。只要用需要的公共密钥延迟签署(delay-sign)程序集并关闭此密钥的强名称检查。具体详细参见我在
MSDN 上发表的有关强名称的文章:Security Briefs: Strong Names and Security in the
.NET Framework。以我对 CLR 的研究,微软在类似 MSCORLIB.DLL 的核心框架库中使用强名称
LinkDemand,但它决不会阻止我调用其中的私有方法。
我希望我已经清晰地做了表达:给某人一个程序集,并限制他调用某些方法或存取某些变量是不可能的。
当然,除非你控制了运行你的代码的这台机器,这种情况下,你可以强制他们在部分信任环境下运行。在完全信任环境下,许多安全保证都将消失。
IStackWalk.Deny 有屏障作用吗?
很少有人知道通过使用 deny 修饰符可以限制调用堆栈许可:
using System.Security;
using System.Security.Permissions;
class WellMeaningCode {
public void CallPlugIn(EvilCode plugin) {
// put a CAS modifier on the stack that denies all file system access
new FileIOPermission(
PermissionState.Unrestricted).Deny();
plugin.DoWork();
CodeAccessPermission.RevertDeny();
}
}
其思路是即便 WellMeaningCode 可能为完全信任,也不能信任插件或某些第三方扩展。所以,在对插件
进行调用前,要临时降低许可。但是,如果安全策略给予插件完全信任的话,方能阻止天真的程序员所创建的可怕插件代码。下面是针对这种情况的实现代码: class EvilCodeWithFullTrust {
void DoWork() {
new PermissionSet(
PermissionState.Unrestricted).Assert();
// happily access the file system
// regardless of the caller''s deny!
}
}
完全信任代码非常难以信任的!Assert 是令一个堆栈修饰符,它能有效地取消 Deny 限制。但是什么样地代码被允许 Assert
呢?任何被准许 SecurityPermission 的代码调用 Assertion。当然,它包含所有完全信任代码。所以如果程序集 B 已被准许
FullTrust,那么在程序集 A 中尝试使用 Deny 来约束程序集 B 是毫无意义的。为了让 Deny 起作用,以部分信任来运行。
幕间休息:AppDomain 安全策略
如果你熟悉 .NET Framework Configuration Tool,你大概已经注意到有三个安全策略等级:企业级、机器级和用户级。实际上,还有
第四种级别安全策略,它对我上面所提到的类似插件这种动态沙箱代码十分有用。由于篇幅所限,我不能很深入地讨论这个
主题,如果你以前从未涉及此领域,只能在此点到为止。
第四种级别与 AppDomain 有关,在默认情况下,它外加对由企业级、机器级
和用户级发布的许可集进行限制。但是,如果你想对代码段进行沙箱处理,比如可能是安装在本地机器上的某个插件,它可能通过创建第二个 AppDomain
并调用 AppDomain.SetAppDomainPolicy 来实现。策略本身可以通过 SecurityManager.LoadPolicyLevelFromFile
从文件加载。如果你觉得可能使用这个技术,有关这方面目前我所知道的最好的参考资料是 Brian LaMacchia,Sebastian Lange
以及微软的几个家伙所写的一本书:《.NET Framework Security》(Addison-Wesley, 2002)。特别是第十章。
AppDomain 安全策略有屏障作用吗?
如果说整个 AppDomain 概念是 ASP.NET 不可缺少的,我对此并不感到惊奇。ASP.NET
使用它来在进程中隔离应用程序。如果你不熟悉 AppDomain,它类似于进程,只不过是一个轻轻量级的进程。当某个进程与 CLR
交互时,总是具备一个默认的 AppDomain,其中所有的托管类型都被加载。而通过创建多个 AppDomain
,一个进程可以和几个不同的应用程序交互,根本不用担心其中一个 AppDomain 中的类型会破坏该进程中任何别的 AppDomain 的类型。 AppDomain
间共享对象引用需要利用远程机制(remoting),并且它不会自动发生。如果你我载单独的 AppDomain
中,并且我想接触你的对象,你传递了一个引用给我,从理论上讲,未经许可,不得擅用。
实际上,在某个 AppDomain 中以完全信任方式运行的程序集确实不会被 AppDomain 边界约束。
完全信任程序集不必是类型安全的。最糟糕的是,如果我愿意,我可以仅凭一个指针,就可以搜刮该进程的整个虚拟地址空间,查找感兴趣的数据。
另外一个需要注意的事情是完全信任程序集被允许通过将 SecurityPermission 置为 UnmanagedCode,便可调用本地代码。这使得侵入他人的 AppDomain
易如反掌,假设你我单独运行在某个 ASP.NET 工作者进程中。如果我想将自己的代码注入到你的 AppDomain,如果我被完全信任,这简直太容易了。我只要引用你的 AppDomain,并且我可以使用象 AppDomain.DoCallBack
这样简单的东西便可将攻击代码发送到你的域(domain)中。Figure 2 中的代码示范了一个人为的例子,在这个例子中,我通过骗取受害者的 AppDomain
使攻击者获得一个引用。
为了玩这种游戏,一个 AppDomain 如何盗取另一个 AppDomain 的引用呢?通过调用非托管代码是可以做到的。CLR
提供了与非托管环境交互的 API,用 C++ 编写一个 DLL,然后从托管代码中调用它,你便可以获得该进程中所有 AppDomain 的引用。
所以,当所有代码都被信任时,AppDomain 将没有安全性可言。你需要部分信任环境,以给予AppDomain 真正的保护屏障,
部分信任需要验证托管代码并限制对本地代码的存取。默认情况下,ASP.NET
服务器应用程序以完全信任模式运行,因为所有代码都被安装在该服务器上。实际上,1.1 版本之前的 .NET Framework
如果没有完全信任环境,它甚至都不可能运行 ASP.NET 应用程序。现在,我们很容易强制服务器上的 Web 应用以部分信任方式运行,方法是在 machine.config
文件中这样设置:
<configuration>
<system.web>
<trust level=''Medium''/>
</system.web>
</configuration>
坦白地讲,我怀疑你会发现很多 ISPs 这么做,因为很多开发者不知道,也不情愿编写被部分信任的 Web
应用。尽管我希望这种情况得到改变。就现在的状况,如果你决定在共享环境中部署你的 ASP.NET 应用,那么应该问许多有关如何隔离你的 ASP.NET
应用的问题。你应该首选 Windows Server®
2003,而不是 Windows Server 2000,因为后者只支持单工作者进程的 ASP.NET 应用。确认 ISP 给你自己拥有的具备专用
Windows 用户帐号的私有工作者进程。有些运行 Windows 2000 的 ISP 会用虚拟目录将一个 Web
应用与另一个隔离开来。一定要问清楚,以便了解自己的处境。当然,最好是在自己的专用 Web 服务器上部署自己的应用程序。
完全信任代码有任何限制吗?
现在,经过上面的讨论,你可能对完全信任代码印象深刻,它们无所不能。但是记住了,CLR 仍然运行在操作系统最上层,它应该有自己的安全约束。当我说
到完全信任代码,我其实是在说这个代码可以做用户运行它时被允许做的任何事情。举个例子:鲍勃运行了一个安装在它本地机器上的托管应用程序,目前只涉及到
CLR,默认情况下,该应用以完全信任方式运行。但是如果鲍勃登录的 Windows 防止他存取某个特定的文件,托管应用会有相同的约束。CLR
中的信任是按比例增减其上限,所谓完全信任,即达到用户运行运行该应用程序特权级别。象 ASP.NET
这样的服务器应用程序,这个上限是根据所选的服务器进程的安全来考虑的,这也是我选择 Windows Server 2003
的原因,它允许我在单独的进程中运行每一个 ASP.NET 应用,并且每个都具备我想要的任何特权级别。
总结
本文目的是论证 CLR 的许多安全特性只能在部分信任环境被强制。虽然完全信任的概念对某些人似乎显而易见,我回顾了大量设计,这些设计假设
CLR 的安全完全不会用到完全信任的情况。如果你将 CLR 的内建安全与 Windows
的内建安全做个比较,完全信任方式运行类似以SYSTEM身份运行。完全信任代码可以获得 CLR
所有内建的安全特性。这就是为什么它被称为完全信任的原因——它必须被信任去做正确的事情。SYSTEM 可以获得 Windows
中的任何安全约束,这就是为什么以 SYSTEM 身份运行的代码必须被信任的原因。
你面临的挑战是学习编写在部分信任环境中运行的代码。Ivan Medvedev 在
Writing managed code
for semi-trusted environment 中有一些好的忠告。如果我在 PDC 上所看到的是暗示的话,那么对于下一个
Windows 版本,代号为“Longhorn”,届时即便本地安装的代码都不能以完全信任方式运行。牢记最少特权原则,设计和编码是都要了然于心!
对 Keith 有任何问题和建议,请用
briefs@microsoft.com 联系。
|