分类: C/C++
2008-03-17 17:31:38
大多数的托管应用程序都以完全信任环境下运行。然而,根据我自己的经验,在给那些经验丰富的开发人员讲授 .NET 安全问题的时候,我发现他们大多数 人都没有理解完全信任代码的真正含义。我收集了很多例子,在这些例子中,完全信任的代码可以绕过通用语言运行时(CLR)的安全特性。 每个例子都以一个提问开始,而每个问题的答案似乎都显而易见。 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 通过保证所有代码的类型安全来封堵这些漏洞,是这样吗? // 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 的程序集都在使用这种许可请求。有句老话说得好,既然有所得,总要付出代价 。 public class NuclearReactor { [StrongNameIdentityPermission( SecurityAction.LinkDemand, PublicKey="002400000...")] private void Meltdown() { // calling assembly must have specified public key! } } 现在前面使用反射的诀窍行不通了,没有人能调用 Meltdown 方法,除非调用程序集具备专门的公共密钥。链接需要(Link demand)命令 CLR 检查在 JIT 编译时链接到该方法的程序集,以保证它能满足链接需要。通过使用 StrongNameIdentityPermission,我要求调用者必须具备一个特殊的公共密钥。 caspol -s off 幸好,你必须是管理员才能运行这条命令(即使你是管理员,也决不要这么做!)。实际上,这么一条命令很难找到存在的理由,Whidbey 团队正在考虑 将它从下一个版本中删除掉。然而,如果应用程序具备高级别信度,即使非管理员也能编写如下代码来做同样的事情: using System.Security; class EvilCodeWithFullTrust { static void Main() { SecurityManager.SecurityEnabled = false; // now call Meltdown via reflection! } } 这些用于关闭 CAS 的机制效果相同,虽然前者将它在整个机器上关闭,而后者只在单进程中有效——尽管不能保证它会稳定地工作。更多细节请参见相关文档。 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 概念是 ASP.NET 不可缺少的,我对此并不感到惊奇。ASP.NET 使用它来在进程中隔离应用程序。如果你不熟悉 AppDomain,它类似于进程,只不过是一个轻轻量级的进程。当某个进程与 CLR 交互时,总是具备一个默认的 AppDomain,其中所有的托管类型都被加载。而通过创建多个 AppDomain ,一个进程可以和几个不同的应用程序交互,根本不用担心其中一个 AppDomain 中的类型会破坏该进程中任何别的 AppDomain 的类型。 AppDomain 间共享对象引用需要利用远程机制(remoting),并且它不会自动发生。如果你我载单独的 AppDomain 中,并且我想接触你的对象,你传递了一个引用给我,从理论上讲,未经许可,不得擅用。 坦白地讲,我怀疑你会发现很多 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 身份运行的代码必须被信任的原因。 |
作者简介 |
本文出自 的 期刊,可通过当地 报摊获得,或者最好是 |