.NET旧漏洞分析 #5:安全透明编译表达式(CVE-2013-0073)
很久没有写过关于我发现的旧.NET漏洞的博客文章了。我在研究一些.NET代码时,发现在CAS沙盒中序列化委托时会出现问题,系统抛出了SerializationException异常,提示信息如下:
无法序列化指向非托管函数指针、动态方法或委托创建者程序集外部方法的委托。
我不记得这个限制是一直存在还是新加的。于是我在Twitter上向我信任的朋友@blowdart求助,他很快把我推给了Levi。但关键点是,委托序列化的行为在某次更改中发生了变化,这是为了引入安全委托(Secure Delegates)而进行的更广泛更改的一部分。
这时我才意识到,.NET框架拥有这个功能几乎(大部分)是我的错,于是我挖出了导致这一现状的其中一个漏洞。让我们快速了解一下安全委托试图防止的问题,然后再看看最初的漏洞。
正如我在讨论.NET PAC漏洞时提到的,.NET代码访问安全(CAS)允许.NET“沙盒”限制不受信任的代码只能拥有特定的权限集。当权限请求(Demand)发生时,CLR会遍历调用堆栈,并检查每个堆栈帧的程序集授权集(Grant Set)。如果堆栈上有任何代码没有所需的权限授权,堆栈遍历就会停止,并生成一个SecurityException,从而阻止函数继续执行。下图展示了一些不受信任的代码试图打开文件,但由于堆栈遍历看到不受信任的代码而停止,因此被FileIOPermission的请求所阻止。
这与委托有什么关系?如果攻击者能够找到一些在断言权限(Asserted Permissions)下调用委托的代码,就会出现问题。例如,在上图中,堆栈底部有一个Assert,但堆栈遍历在遇到不受信任的调用者帧时会提前失败。
然而,只要我们有一个委托调用,并且委托调用的函数是受信任的,我们就可以将其放入调用链中,并成功执行特权操作。
这种技术的问题在于找到一个受信任的函数,我们可以将其包装在一个委托中,并附加到某些东西上,比如Windows Forms事件处理程序,其原型可能是:
|
|
而它会调用File.OpenRead函数,其原型是:
|
|
这是一件相当棘手的事情。如果你了解C#,你会知道Lambda函数,我们能否使用类似下面的代码?
|
|
不幸的是,不行。C#编译器会处理这个lambda,在你的程序集中生成一个自动类,其中包含该函数原型。因此,调整参数的调用将通过一个不受信任的函数进行,堆栈遍历会失败。在CIL中,它看起来类似于以下内容:
但还有另一种方法。看看你能否发现这里的区别。
|
|
我们仍然在使用lambda,难道没有什么变化吗?让我们看看CIL。
这太疯狂了。发生了什么?关键在于Expression的使用。当C#编译器看到这种类型时,它决定不在你的程序集中创建委托,而是创建一种称为表达式树(expression tree)的东西。然后,该树被编译成最终的委托。对于我报告的漏洞来说,重要的是这个委托是受信任的,因为它是使用AssemblyBuilder功能构建的,该功能从调用程序集获取权限授权集。由于调用程序集是框架代码,它获得了完全信任。它不被信任去断言权限(这是一个安全透明函数),但也不会阻止堆栈遍历。这使我们能够实现任何任意的委托适配器,将一个委托调用点转换为调用任何其他API,只要你能在断言的权限集下完成这一点。
我能够在WinForms中找到许多在断言权限下调用事件处理程序的地方,并可以利用它们。最初的修复是修复这些调用点,但真正的修复后来才出现,即前述的安全委托。
Silverlight一直都有安全委托,它会在创建委托时捕获堆栈上当前的CAS权限集,并在需要时向委托添加一个蹦床(trampoline),以便在调用中插入一个不受信任的堆栈帧。似乎这后来被添加到了.NET中。序列化被阻止的原因是因为当委托被序列化时,这个蹦床会丢失,因此存在被用于利用某些东西逃离沙盒的风险。当然,CAS已经死了。
最终结果如下图所示:
总之,这些是从安全角度从未完全考虑过的设计决策。它们并非.NET、Java或其他在“沙盒化”上下文中运行任意代码的东西(包括V8或JSCore等JavaScript引擎)所独有。