原文:http://phrack.org/papers/escaping_the_java_sandbox.html
在上一篇文章中,我们为读者详细介绍了糊涂的代理人漏洞方面的知识,在本文中,我们将继续为读者介绍实例未初始化漏洞。
----[ 4.2 - 实例未初始化漏洞
------[ 4.2.1 - 背景知识
Java对象的初始化过程中,非常关键的一个步骤就是调用相应类型的构造函数。在构造函数中,不仅含有初始化变量所需的代码,同时,也可能含有执行安全检查的代码。因此,为了保证平台的安全性和稳定性,必须在完成对象的初始化以及允许其他代码调用该类型的方法之前强制调用构造函数,这一点非常重要。
构造函数调用的强制执行是由字节码验证器负责的,它会在加载过程中对所有的类进行相应的检查,以确保其合法性。
除此之外,字节码验证器还负责(例如)检查跳转是否落在有效指令上,而不是落在指令的中间,并检查控制流是否以return指令结尾。此外,它还检查指令的操作对象是否为有效类型,这是用来防御类型混淆攻击的。关于这类攻击的介绍,请参考第3.1.1节。
过去,为了检查类型的有效性,JVM需要通过分析数据流来计算固定点(fix point)。该分析过程可能对同一路径检查多次。由于这种检查方式非常耗时,会拖慢类的加载过程,因此,后来人们开发了一种新型方法,能够在线性时间内完成类型检查,其中,每个路径仅被检查一次。为此,可以为字节码添加称为堆栈映射帧的元信息。简而言之,堆栈映射帧用来描述每个分支目标的可能类型。通常情况下,堆栈映射帧被存储在一种称为堆栈映射表[25]的结构中。
如果安全分析人员能够创建一个实例,但不为其执行<init>(*)
调用(即不执行对象的构造函数或超类的构造函数)的话,就会出现实例未初始化漏洞。实际上,该漏洞直接违反了虚拟机的相关规范[21]。它对JVM安全性的影响是,借助于实例未初始化漏洞,安全分析人员能够实例化他原本无权访问的对象,进而访问他原本无权访问的属性和方法。这样的话,就可能会导致沙箱逃逸。
------[ 4.2.2 - 示例: CVE-2017-3289
通过阅读该CVE的描述,会发现“该漏洞的成功攻击可能导致Java SE、Java SE Embedded被完全接管”[22]。
就像CVE-2017-3272那样,这意味着能够利用该漏洞实现Java沙箱的逃逸。
据Redhat的bugzilla称,“在OpenJDK的Hotspot组件中发现了一个不安全的类构造漏洞,它与异常堆栈帧的错误处理方式有关。不受信任的Java应用程序或applet能够利用这个漏洞绕过Java沙箱的限制”[23]。我们可以从中推断出两条有用的信息:(1)该漏洞出现在C/C++代码中(Hotspot是Java VM的名称),以及(2)该漏洞与非法类构造和异常堆栈帧有关。并且,通过第2条信息,我们可以进一步推断出,该漏洞可能位于检查字节码的合法性的相关C/C++代码中。此外,该页面还提供了该漏洞的OpenJDK补丁的链接。
OpenJDK的更新补丁,即“8167104: Additional class construction refinements”可以修复该漏洞,该补丁可在线获取,具体见参考文献[24]。该程序对5个C ++文件进行了更新,它们分别是:“classfile/verifier.cpp”,负责检查类文件的结构和合法性的类;“classfile/stackMapTable.{cpp, hpp}”,处理堆栈映射表的文件;以及“classfile/stackMapFrame.{cpp, hpp}”,描绘堆栈映射帧的文件。
借助于diff命令,我们发现,函数StackMapFrame::has_flag_match_exception()已经被删除,并且更新了一个我们将称为C1的条件,即删除了对has_flag_match_exception()函数的调用。此外,方法match_stackmap()和is_assignable_to()现在只剩下一个参数了,因为“bool handler”已被删除。当该验证程序正在检查异常处理程序时,唯一的参数,即“handler”将被设为“true”。现在,条件C1已经变成下面的样子:
---------------------------------------------------------------------------
....
- bool match_flags = (_flags | target->flags()) == target->flags();
- if (match_flags || is_exception_handler &&
has_flag_match_exception(target)) {
+ if ((_flags | target->flags()) == target->flags()) {
return true;
}
....
---------------------------------------------------------------------------
这个条件在函数is_assignable_to()中,用于检查作为参数传递给该函数的当前堆栈映射帧,是否可赋值给目标堆栈映射帧。在打补丁之前,返回“true”的条件是match_flags || is_exception_handler && has_flag_match_exception(target)
。也就是说,要满足当前堆栈映射帧和目标堆栈映射帧的标志相同或者当前指令位于异常处理程序中,并且函数“has_flag_match_exception”返回“true”。注意,只有一种叫做“UNINITIALIZED_THIS”(又名FLAG_THIS_UNINIT)的标志。如果该标志的值为true,则表示“this”引用的对象还没有进行初始化操作,即尚未调用其构造函数。
在打完补丁之后,条件变为“match_flags”。这意味着,在易受攻击的版本中,可能存在一种方法能够构造出这样的字节码,能够使得:“match_flags”为“false”(即“this”在当前帧中具有未初始化的标志,但在目标帧中则没有该标志)、“is_exception_handler”为“true”(当前指令位于异常处理程序中)以及“has_flag_match_exception(target)”返回“true”。然而,这个函数什么情况下会返回“true”呢?
函数has_flag_match_exception()的代码如下所示。
---------------------------------------------------------------------------
1: ....
2: bool StackMapFrame::has_flag_match_exception(
3: const StackMapFrame* target) const {
4:
5: assert(max_locals() == target->max_locals() &&
6: stack_size() == target->stack_size(),
7: "StackMap sizes must match");
8:
9: VerificationType top = VerificationType::top_type();
10: VerificationType this_type = verifier()->current_type();
11:
12: if (!flag_this_uninit() || target->flags() != 0) {
13: return false;
14: }
15:
16: for (int i = 0; i < target->locals_size(); ++i) {
17: if (locals()[i] == this_type && target->locals()[i] != top) {
18: return false;
19: }
20: }
21:
22: for (int i = 0; i < target->stack_size(); ++i) {
23: if (stack()[i] == this_type && target->stack()[i] != top) {
24: return false;
25: }
26: }
27:
28: return true;
29: }
30: ....
---------------------------------------------------------------------------
为了让这个函数返回“true”,必须满足以下所有条件:(1)当前帧和目标帧的最大局部变量个数与堆栈的最大长度必须相同(第5-7行);(2)当前帧必须将“UNINIT”标志设为“true”(第12-14行);(3)目标帧中没有使用未初始化的对象(第16-26行)。
下面是满足以上述三个条件的字节码:
---------------------------------------------------------------------------
<init>()
0: new // class java/lang/Throwable
1: dup
2: invokespecial // Method java/lang/Throwable."<init>":()V
3: athrow
4: new // class java/lang/RuntimeException
5: dup
6: invokespecial // Method java/lang/RuntimeException."<init>":()V
7: athrow
8: return
Exception table:
from to target type
0 4 8 Class java/lang/Throwable
StackMapTable: number_of_entries = 2
frame at instruction 3
local = [UNINITIALIZED_THIS]
stack = [ class java/lang/Throwable ]
frame at instruction 8
locals = [TOP]
stack = [ class java/lang/Throwable ]
---------------------------------------------------------------------------
我们可以将局部变量的最大数目和堆栈的最大尺寸都设置为2,以满足第1个条件。此外,第3行代码处,当前帧会将“UNINITIALIZED_THIS”设置为true,以满足第2个条件。最后,未初始化的局部变量不会用于“athrow”指令的目标操作数(第8行),因为局部变量的第一个元素被初始化为“TOP”,这样第3个条件也能得到满足。
请注意,这些代码位于try/catch语句块中,以便通过函数is_assignable_to()将“is_exception_handler”设置为“true”。
此外,还需要注意的是,该字节码都位于构造函数(字节码形式的<init>()
)中。要想将标志“UNINITIALIZED_THIS”设置为true,必须如此。
我们现在已经知道,安全分析人员能够构造出返回其自身尚未被初始化的对象的字节码了。乍一看,可能很难看出这种对象是如何供安全分析人员使用的。但是,通过仔细观察就会发现,所需的类可以实现为一个系统类的子类,可以在不调用超类的构造函数super.<init>()
的情况下完成初始化。这个类可用于实例化因构造函数是私有的或包含权限检查而无法由不受信任的代码实例化的那些公共系统类。下一步是寻找含有安全分析人员“感兴趣的”功能的类。这样做的目的是,将所有功能组合在一起,以便能够在沙箱环境中执行任意代码,从而绕过沙箱。然而,寻找有用的类本身就是一项非常复杂的任务。
具体而言,我们面临着以下挑战。
挑战1:到哪里寻找助手代码
JRE提供了许多包含JCL(Java类库)类的jar文件。这些类作为_trusted_类进行加载,并且可以在构造漏洞利用代码时使用。当前,有越来越多的类被标记为“restricted”,这意味着_untrusted_代码将无法直接实例化它们——对于安全分析人员来说,这是非常不幸的;但是对于Java用户来说,这又是非常幸运的。在1.6.0_01版本中,访问权限为restricted的包的数量只有1个,到1.8.0_121版本发布时,这种类型的包的数量已经变为47个。这意味着安全分析人员在构建漏洞利用代码时无法直接使用的代码的百分比,从1.6.0_01版本升级到1.8.0_121版本的过程中,已经从20%提升到54%了。
挑战2:字段可能未初始化
如果没有适当的权限,通常无法实例化新的类加载器。在构造函数中接受检查的_ClassLoader_类的权限,看起来似乎是一个不错的目标。
借助于CVE-2017-3289漏洞,我们确实可以在没有相应权限的情况下实例化新的类加载器,因为构造函数代码——以及权限检查代码——不会被执行。但是,由于绕过了构造函数,因此,这时会使用默认值来初始化各个字段(例如,对于整数来说,将被初始化为0;对于引用来说,将被初始化为null)。所以,这可能导致某些问题:我们感兴趣的方法通常是允许为定义的新类赋予全部权限的那些方法,但是在这种情况下,这些方法都无法正常运行,因为代码将尝试解除对未正确初始化的字段的引用。在手动检查之后,我们发现似乎很难绕过字段的解引用,因为所有路径都是通过该指令来解除对非初始化字段的引用的。这样看来,利用_ClassLoader_似乎是一个死胡同。当利用CVE-2017-3289中的漏洞时,非初始化字段是一个主要挑战:除了要求目标类的访问权限是public、非final和非restricted之外,其感兴趣的方法也不应该执行撤销对未初始化的字段的引用的方法。
对于Java version 1.8.0 update 112来说,我们还没有找到有用的助手代码。为了阐明CVE-2017-3289漏洞的形成机制,我们将展示用于利用编号为0422和0431的漏洞的助手代码。这两个漏洞依赖于MBeanInstantiator,该类定义了可以加载任意类的方法,即findClass()。类_MBeanInstantiator_只提供了私有构造函数,因此无法直接进行实例化。
最初,这些漏洞都是通过_JmxMBeanServer_来创建_MBeanInstantiator_的实例。这里,我们将证明,安全分析人员可以直接子类化MBeanInstantiator,并利用编号为3289的漏洞来获取它的实例。
用于实例化_MBeanInstantiator_的原始助手代码依赖于JmxMBeanServer,具体如下所示:
---------------------------------------------------------------------------
1: JmxMBeanServerBuilder serverBuilder = new JmxMBeanServerBuilder();
2: JmxMBeanServer server =
3: (JmxMBeanServer) serverBuilder.newMBeanServer("", null, null);
4: MBeanInstantiator instantiator = server.getMBeanInstantiator();
---------------------------------------------------------------------------
实例化_MBeanInstantiator_的代码利用了CVE-2017-3289漏洞:
---------------------------------------------------------------------------
1: public class PoCMBeanInstantiator extends java.lang.Object {
2: public PoCMBeanInstantiator(ModifiableClassLoaderRepository clr) {
3: throw new RuntimeException();
4: }
5:
6: public static Object get() {
7: return new PoCMBeanInstantiator(null);
8: }
9: }
---------------------------------------------------------------------------
请注意,由于_MBeanInstantiator_没有任何公共构造函数,_PoCMBeanInstantiator_必须在源代码中扩展一个虚拟类,在我们的示例中为java.lang.Object。我们将通过ASM [28]字节码操作库,把_PoCMBeanInstantiator_的超类改为MBeanInstantiator。此外,我们还将使用ASM来修改构造函数的字节码,以绕过对super.<init>(*)
的调用。
自Java 1.7.0 update 13版本以来,Oracle已将_com.sun.jmx._添加为受限程序包。类_MBeanInstantiator_就位于这个程序包中,因此,我们无法在更高版本的Java中继续使用该助手代码。
出乎我们意料之外的是,这个漏洞影响了40多个不同的公开发行版本。Java 7的所有版本,包括从update 0到update 80,都含有这个漏洞。从update 5到update 112的所有Java 8版本也会受到该漏洞的影响。不过,Java 6版本并没有受到该漏洞的影响。
通过检查Java 6 update 43发行版的字节码验证器与Java 7 update 0发行版的源代码,我们发现主要的区别对应于上面提供的补丁的逆操作。
这意味着堆栈帧可分配给构造函数中异常处理程序内的目标堆栈帧的条件已被削弱。diff中的注释表明,这个新代码是应7020118号请求[26]而添加的。该请求要求更新字节码验证程序的代码,以使NetBeans的分析器(profiler)能够生成可以覆盖构造函数的全部代码的处理程序。
这个漏洞已经通过收紧约束条件得到了修复,只有满足了这个加强版的约束条件,当前堆栈帧(位于try/catch代码块中的构造函数中)才可以分配给目标堆栈帧。这样就能有效地防止字节码从构造函数返回未初始化的“this”对象了。
据我们所知,Java至少有三个已经公开的_uninitialized instance_漏洞。其中,第1个漏洞是本文介绍的CVE-2017-3289。第2个漏洞于2002年被发现,具体见参考文献[29]。同时,该文献的作者还利用了字节码验证器中的漏洞,该漏洞的作用是让Java平台无法调用超类的构造函数。但是,利用这些漏洞时,无法开发出能够实现沙箱的完全逃逸的利用代码。但是,它们能够可以用来访问网络并将文件读写入磁盘。第3个漏洞是普林斯顿的一个研究小组于1996年发现的,具体见参考文献[30]。同样,这个安全问题也是位于字节码验证器中。它允许构造函数捕获调用super()时抛出的异常,并返回部分初始化的对象。请注意,利用该漏洞发动攻击时,ClassLoader类没有任何实例变量。因此,利用该漏洞来实例化类加载器的时候,能够获得一个完全初始化的类加载器,可以在其上调用任何方法。
------[ 4.2.3 -讨论
这个漏洞的根本原因是对C/C++编写的字节码验证代码的修改,而原来验证代码的作用是,保证安全分析人员构造出的Java字节码无法绕过对子类构造函数中的super()的调用。但是,该漏洞直接违反了虚拟机的相关规范[21]。
但是,如果没有合适的_helper_代码,这个漏洞将毫无用处。不过,Oracle已经开发了一款静态分析工具,专门用于查找危险的gadget,并将其列入黑名单[31]。这使得安全分析人员在开发用于绕过沙箱的漏洞利用程序的时候,难度更大了。实际上,我们只发现了能够与旧版JVM配套使用的gadget。由于它们已被列入最新版本的黑名单,因此,这种攻击方法已经失效了。
然而,即使可以使用静态分析工具进行防御,但是仍然面临两个问题:(1)可能会引发许多假正例,这使得识别真正危险的gadget变得更加困难,并且(2)可能导致许多假负例,因为它无法模拟语言的所有特性,比如反射和JNI,因此,这种防御方式还不够健全。
小结
在本文中,我们为读者详细介绍了实例未初始化漏洞。在下一篇文章中,我们将继续为读者介绍更多精彩内容,敬请期待。