.NET旧漏洞分析 #5:安全透明编译表达式(CVE-2013-0073)
很久没写关于我发现的旧.NET漏洞的博客了。我在研究一些.NET代码时发现了一个问题:在CAS沙箱中序列化委托时,会抛出SerializationException
异常,提示信息如下:
无法序列化指向非托管函数指针、动态方法或委托创建者程序集外部方法的委托。
我不记得这个限制是一直存在还是新加的。我在Twitter上向我信任的朋友@blowdart求助,他很快把我推给了Levi。但关键点是,委托序列化的行为曾在某个时间点被更改,作为添加安全委托(Secure Delegates)的更广泛变更的一部分。
这时我才意识到,.NET框架拥有这个功能几乎肯定(大部分)是我的错,于是我挖出了导致这一现状的其中一个漏洞。让我们快速回顾一下安全委托试图防止的问题,然后再看原始漏洞。
正如我在讨论.NET PAC漏洞时提到的,**.NET代码访问安全(CAS)**允许.NET“沙箱”限制不受信任的代码只能使用特定权限集。当请求权限要求时,CLR会遍历调用堆栈并检查每个堆栈帧的程序集授权集。如果堆栈上有任何代码没有所需的权限授权,堆栈遍历就会停止,并生成SecurityException
,从而阻止函数继续执行。我在下图中展示了这一点:一些不受信任的代码试图打开文件,但由于堆栈遍历看到不受信任的代码而停止,因此被FileIOPermission
需求所阻止。
这与委托有什么关系?问题在于,如果攻击者能找到一些代码在断言权限下调用委托,就可能绕过安全机制。例如,在上图中,堆栈底部有一个Assert
,但堆栈遍历在遇到不受信任的调用者帧时提前失败。
然而,只要我们有一个委托调用,并且委托调用的函数是受信任的,我们就可以将其放入调用链中,并成功执行特权操作。
这种技术的难点在于找到一个受信任的函数,我们可以将其包装在委托中,并附加到诸如Windows Forms事件处理程序上,其原型可能为:
1
|
void Callback(object obj, EventArgs e)
|
并调用File.OpenRead
函数,其原型为:
1
|
FileStream OpenRead(string path)
|
这很难实现。如果你熟悉C#,你会知道Lambda函数,我们能否使用类似这样的代码?
1
|
EventHandler f = (o,e) => File.OpenRead(@"C:\SomePath")
|
不幸的是,不行。C#编译器会处理lambda,在你的程序集中生成一个自动类,其中包含该函数原型。因此,调整参数的调用将通过一个不受信任的函数,堆栈遍历会失败。在CIL中,它看起来像这样:
1
2
3
|
ldsfld class Program/'<>c' Program/'<>c'::'<>9'
ldftn instance void Program/'<>c'::'<Main>b__0_0'(object, class [mscorlib]System.EventArgs)
newobj instance void [mscorlib]System.EventHandler::.ctor(object, native int)
|
但还有另一种方法。看看这里能否发现差异:
1
2
|
Expression<EventHandler> lambda = (o,e) => File.OpenRead(@"C:\SomePath")
EventHandle f = lambda.Compile()
|
我们仍然在使用lambda,难道什么都没有改变吗?让我们看看CIL:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
stloc.0
ldtoken [mscorlib]System.Object
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
ldstr "o"
call class [System.Core]System.Linq.Expressions.ParameterExpression [System.Core]System.Linq.Expressions.Expression::Parameter(class [mscorlib]System.Type, string)
stloc.2
ldtoken [mscorlib]System.EventArgs
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
ldstr "e"
call class [System.Core]System.Linq.Expressions.ParameterExpression [System.Core]System.Linq.Expressions.Expression::Parameter(class [mscorlib]System.Type, string)
stloc.3
ldnull
ldtoken method class [mscorlib]System.IO.FileStream [mscorlib]System.IO.File::OpenRead(string)
call class [mscorlib]System.Reflection.MethodBase [mscorlib]System.Reflection.MethodBase::GetMethodFromHandle(valuetype [mscorlib]System.RuntimeMethodHandle)
castclass [mscorlib]System.Reflection.MethodInfo
ldc.i4.1
newarr [System.Core]System.Linq.Expressions.Expression
dup
ldc.i4.0
ldstr "C:\\SomePath"
ldtoken [mscorlib]System.String
call class [mscorlib]System.Type [mscorlib]System.Type::GetTypeFromHandle(valuetype [mscorlib]System.RuntimeTypeHandle)
call class [System.Core]System.Linq.Expressions.ConstantExpression [System.Core]System.Linq.Expressions.Expression::Constant(object, class [mscorlib]System.Type)
stelem.ref
call class [System.Core]System.Linq.Expressions.MethodCallExpression [System.Core]System.Linq.Expressions.Expression::Call(class [System.Core]System.Linq.Expressions.Expression, class [mscorlib]System.Reflection.MethodInfo, class [System.Core]System.Linq.Expressions.Expression[])
ldc.i4.2
newarr [System.Core]System.Linq.Expressions.ParameterExpression
dup
ldc.i4.0
ldloc.2
stelem.ref
dup
ldc.i4.1
ldloc.3
stelem.ref
call class [System.Core]System.Linq.Expressions.Expression`1<!!0> [System.Core]System.Linq.Expressions.Expression::Lambda<class [mscorlib]System.EventHandler>(class [System.Core]System.Linq.Expressions.Expression, class [System.Core]System.Linq.Expressions.ParameterExpression[])
stloc.1
ldloc.1
callvirt instance !0 class [System.Core]System.Linq.Expressions.Expression`1<class [mscorlib]System.EventHandler>::Compile()
|
这太疯狂了。发生了什么?关键在于Expression
的使用。当C#编译器看到这种类型时,它决定不在你的程序集中创建委托,而是创建一种称为表达式树的东西。然后,该树被编译成最终的委托。对于我报告的漏洞来说,重要的是这个委托是受信任的,因为它是使用AssemblyBuilder
功能构建的,该功能从调用程序集获取权限授权集。由于调用程序集是框架代码,它获得了完全信任。它不被信任来断言权限(一个安全透明函数),但也不会阻止堆栈遍历。这使我们能够实现任何任意的委托适配器,将一个委托调用点转换为调用任何其他API,只要你能在断言的权限集下这样做。
我能够在WinForms中找到许多在断言权限时调用事件处理程序的地方,并加以利用。最初的修复是修复这些调用点,但真正的修复后来才出现,即前述的安全委托。
Silverlight一直有安全委托,它会在创建委托时捕获堆栈上的当前CAS权限集,并在需要时向委托添加一个蹦床(trampoline),以将不受信任的堆栈帧插入调用中。似乎这后来被添加到.NET中。序列化被阻止的原因是因为当委托被序列化时,这个蹦床会丢失,因此存在被用于利用某些东西逃离沙箱的风险。当然,CAS已经死了。
最终结果如下图所示:
总之,这些是从安全角度从未完全范围化的设计决策。它们并非.NET或Java独有,也不是任何其他在“沙箱”上下文中运行任意代码的东西(包括JavaScript引擎如V8或JSCore)所独有的。