Java语言是最为流行的面向对象编程语言之一, Java运行时环境(JRE)拥有着非常大的用户群,其安全问题十分重要。近年来,由JRE漏洞引发的JVM逃逸攻击事件不断增多,对个人计算机安全造成了极大的威胁。研究JRE安全机制、JRE漏洞及其挖掘、JVM逃逸攻防技术逐渐成为软件安全领域的热门研究方向。
针对Java层API与原生层API, JRE安全机制分别包括JRE沙箱与JVM 类型安全机制。本文针对JRE沙箱组件及其工作原理进行剖析,总结其脆弱点;分析调研JVM安全机制,提出其脆弱点在于Java原生层漏洞,为JRE漏洞挖掘工作提供理论基础。
对于JRE漏洞,本文进行漏洞分类研究,提取Java API设计缺陷、Java原生层漏洞两种JRE漏洞类型的典型漏洞进行分析,总结漏洞特征,为漏洞挖掘工作建立漏洞模型。
根据JRE漏洞分析中建立的漏洞模型,本文采用源代码审计的方法开展Java API设计缺陷类型的漏洞挖掘工作,发现了数个Oracle JRE、OpenJDK和Apple JRE 的 Java API 设计缺陷问题。在 Java原生层漏洞挖掘工作中,出于Java原生层漏洞的特殊性,本文基于程序分析领域的符号执行技术提出一种寄存器符号化监控方法,选取开源符号执行平台S2E作为漏洞挖掘工具,并且基于其实现了针对JRE原生层漏洞挖掘的辅助插件 SymJava 和 SymRegMonitor,基于 OpenJDK 和 Oracle JRE逆向代码进行源代码白盒审计并构建了用于进行漏洞挖掘的 Java 测试用例,最后对36个调用Java原生层API的Java测试用例进行实际测试发现了共计6 个 JRE原生层安全隐患,其中2 个可被攻击者恶意利用,并给出漏洞分析和 PoC。
针对 JVM 逃逸攻防问题,本文分别从攻击和防御角度,提出 JVM逃逸攻击的5 个关键元素,针对每个元素进行攻防技术研究,并通过绕过杀毒软件静态检测的实验证明了本文提出的 JVM 逃逸攻击技术。最后,本文从多角度给出JVM逃逸攻击的防御策略。
目录
2 Java 安全机制研究
2.1 JRE 沙箱组件研究
2.1.1 类装载器
2.1.2 安全管理器
2.1.3 权限提升代码块
2.1.4 Java 反射机制
2.2 JVM 安全机制研究
2.3 本章小结
2 Java 安全机制研究
为了更好地进行JRE漏洞分析与挖掘工作,更好地理解JVM逃逸攻击的流程,探究Java的安全机制就显得至关重要。以Oracle JRE为例, Java API分为Java层和原生层,其中Java层由Java语言本身编写,原生层主要由C语言编写,是一些与操作系统接合较为紧密的底层API封装,在Windows系统中编译为动态链接库("*.dll”文件),作为JRE的重要组成部分而存在。Java层中存在着可以自定义安全规则的沙箱(Sandbox),在分析JRE漏洞和进行漏洞挖掘工作之前,探究沙箱组件以及原理显得极为重要。原生层由于跟操作系统接合紧密,且由C语言编写,所以会存在如缓冲区溢出、整数溢出等二进制层面的漏洞。针对JRE原生层,本章重点研究原生层API调用机制, JVM的安全特性,以及原生层潜在安全问题等等。
2.1 JRE 沙箱组件研究
Java应用程序大多为本地应用,即部署于本地操作系统上,由本地JRE加载并执行的Java应用程序。本地应用程序拥有JRE的最高权限,可以对本地操作系统的大部分资源进行控制和操作,如执行shell/cmd 命令等。所下图示是一段在Windows下执行cmd命令的Java本地应用程序,其执行效果是打开Windows系统自带的计算器程序calc.exe。
import java.io.IOException;
public class Example1 {
public static void main(String[] args) {
try {
Runtime.getRuntime().exec("calc.exe");
} catch (IOException e) {
e.printStackTrace();
}
}
}
核心代码原封不动地迁移到 Applet 程序开发中,如下所示。使用 html 页加载该 Java Applet,发现 Windows 计算器并没有被打开,也就意味着 cmd 命令执行失败,此时打开JRE的控制台,可以看到带有“access denied”字样的异常信息,提示权限检查未通过。
import java.applet.Applet;
import java.io.IOException;
public class Example2 extends Applet{
public void init() {
try {
Runtime.getRuntime().exec("calc.exe");
}catch (IOException e) {
e.printStackTrace();
}
}
}
由图2-1的方框标示部分可知, JRE限制了本地文件的“可执行” (execute)权限,这是与执行本地Java应用完全不同的一种安全策略。在JRE中,由众多安全组件组成的系统称为JRE沙箱(JRE Sandbox),负责限制类似Applet这样来自互联网的不可信Java程序在执行中的权限。
自互联网的Java Applet下载到本地JRE中加载执行时,会判断该Applet是否可信(如是否具备可信的数字签名,等等),若可信,则该Applet可以执行任意代码,接触到操作系统中的所有可获取的资源;若不可信,则只能在沙箱中以较低的权限执行,且不能任意获取操作系统的本地资源。
JRE沙箱限制非可信Applet获取的权限有很多,与安全相关性较大的主要有以下几种:
1.本地文件操作。本地文件读取、写入,可执行文件的执行权限的限制。
2. 网络连接操作。建立自定义socket通信,发起自定义的http请求,等等。
3.系统属性读取。禁止使用读取系统属性的API,如System.getProperty.
4.加载动态链接库。禁止使用加载外部动态链接库的API,如System.loadLibrary
JRE沙箱的存在一定程度上提高了JRE的安全性,但由于JRE漏洞的存在,攻击者可以利用JRE漏洞突破沙箱,在目标计算机上执行恶意代码。因此无论攻击者还是防御者,都必须探究JRE沙箱的安全机制及其安全相关组件。下文分小节介绍本文对JRE沙箱各安全相关组件的研究。
2.1.1 类装载器
首先探究JVM 加载运行 Java 程序的过程。Java 不是一门像 Python 那样的纯解释型语言, JVM可以加载执行的是一段字节码,也就是后缀名为class的文件。class文件是一组以字节为基础单位的二进制流,其内容主要包括:Magic Number(文件开头4字节,必须为OxCAFEBABE),版本号,常量池,访问标志,字段表,方法表,属性表等等。各个数据项严格按照顺序紧凑地排列在class文件之中,中间没有添加任何分割符,使得整个 class 文件中存储内容几乎全部都是程序运行的必要数据。
这样一个class文件,从被加载到JVM中开始直到卸载,其生命周期要经过七个阶段:加载、验证、准备、解析、初始化、使用、卸载。那么顾名思义,从安全角度来考虑, class文件的加载和校验环节是比较重要的。一般来说,只要顺利通过了这两个环节,一个 class 直到它运行之前都不会有安全检查了。
类的加载是由Classloader来完成的,以Hotspot虚拟机为例(最常见的一种Java 虚拟机), Classloader可划分为两大类:第一类是 Bootstrap Classloader,是由C++语言实现的;第二类则独立于Java虚拟机之外,且全部继承自抽象类java.lang.Classloader,也就是说,它们全部都是由Java语言实现的。
在类加载的过程中还涉及到一个命名空间的问题。以Oracle官方的JRE为例,除第三方扩展包以外,所有的Java API都存在于<JAVA_HOME>Vlibirt.jar这个压缩包里。
以Java语言中常见的java.lang.System类来举例,如果开发者自己创建一个名叫"System"的类,如表2-3所示:
注意这个类是属于自定义的 test 包中,类中同样有一个 out 对象,一个静态的println方法,可以注意到,在自定义的printn方法中实际上调用的还是方法java.lang.System.out.println。再创建一个类去调用test.System类,如表2-4所示。
编译、运行,控制台输出:
helloworld!
My System Class:helloworld!
可见即使类名相同,命名空间(也就是包名)的不同也就保障了不会引起Java类混淆。但若是开发者自己定义一个java.lang.System类如何呢?把上面定义的test.System类改为java.lang.System,同样运行Example3,发现控制台输出了异常信息, 自己定义的java.lang.System类不会得到调用。这种手段可以在很大程度上防止Java虚拟机加载恶意的混淆类,但值得一提的是,任何一个类一旦由Bootstrap Classloader加载,那么它将拥有Java API级别的权限,完全被Java虚拟机所信任。从攻击者的角度讲,寻找可以调用Bootstrap Classloader的脆弱点是一个很好的利用思路。
Class文件的验证过程同样也是保障Java虚拟机安全的一个重要环节。验证环节主要有四个步骤,分别是:文件格式验证、类型数据语义检查、字节码验证和符号引用验证,其顺序如图6所示。
2.1.2 安全管理器
安全管理器即JRE中的SecurityManager,通俗来讲,其功能是当调用Java API进行一些“不安全”操作(如本地文件读写,网络通信,系统属性读取等等)时,SecurityManager会被调用,来执行一些相关的权限检查。如果执行“不安全”操作的这次调用拥有相应的权限,那么它将会实际执行这个操作;如果没有执行这些操作的权限, Java主进程会抛出异常,终止该行为。
public static void setSecurityManager(final SecurityManager s) {
setSecurityManager0(s);
}
private static synchronized void setSecurityManager0(final SecurityManager s) {
SecurityManager sm = getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission ("setSecurityManager"));
}
if ((s != null) && (s.getClass().getClassLoader() != null)) {
……
}
security = s;
}
2.1.3 权限提升代码块
在Java 6与Java 7当中,权限检查的工作交给了AccessController类,每当触发权限检查时, AccessController会对方法调用栈上的所有方法进行检查。所谓方法调用栈,可以参考本章的图2-1,控制台打印的异常信息就恰好反馈了异常方法的部分调用栈。以表2-2中的Applet类Example2为例,调用栈的顶端是AccessController.checkPermission方法,该方法会自顶向下地检查整个调用栈上的类和方法是否都是受信任的,一旦发现有一个不受信任的方法调用,JVM就会抛出异常,程序终止执行。
AccessController.doPrivileged(new PrivilegedAction<SomeClass>() {
public SomeClass run() {
try {
…… // Some dangerous function
} catch (SecurityException e) {
assert false;
} catch (NoSuchFieldException e) {
assert false;
}
return null;
}//run
});
2.1.4 Java 反射机制
Java反射机制(Reflection)是Java程序开发语言的重要特性,它为Java提供了在运行过程中动态获取及调用一个类及其方法和变量的功能,从编程的表现形式来讲,可以理解为通过间接的方式去调用一些API。一般来说,要调用一个类的某个方法,先要使用new关键字创建一个对象出来,此时JVM完全知晓这个对象是什么类的对象;而通过反射机制,在真正执行这个方法前,可以不用声明这个对象, JVM会自动匹配这个方法的全限定名,并定位到它所在的类。以上文中提到的System.setSecurityManager方法,是一个静态方法,直接调用代码是:
System.setSecurityManager(null);
由表2-8 可知,java.lang.Class 类中的反射 API 可以获取到一个类的所有关键元素:类对象本身、所有定义的方法(无论公有私有)、构造函数、所有定义的公有私有域(也就是该类的全局变量)。这些 API 还只能获取到这些元素,另外,java.lang.reflect包中对应有Field、Constructor、Method类,提供了一些可以操作这些元素的 API,分别如表2-9、2-10 和2-11 所示。
关于反射机制的使用,这里举一个简单例子,假设实现如表2-12所示的类,其构造函数和eprint方法只实现一些打印指定字符串的功能,使用new关键字调用eprint方法的代码如表2-13所示。使用表2-8, 2-9, 2-10, 2-11中的反射API,调用代码示例如表2-14所示。经过编译运行测试可知,这两种调用方式,其执行效果是完全一样的。
方法句柄 MethodHandle 可以用于获取域、方法或构造函数,要得到方法句柄MethodHandle类,可以通过MethodHandles.Lookup类中提供的API来获得,如表2-15 所示。
再拿表2-12中的Example4做例子,使用Java 7提供的新反射API,调用形式如表2-16所示。
2.1.5 包访问限制
包访问限制(Package Access)是 JRE 沙箱安全机制的重要组成策略。它的实现原理也非常简单,通过阅读OpenJDK源代码可知,几乎所有敏感API,在调用前都会加一个checkPackageAccess方法,其功能是检查当前类是否具有访问、调用某些包的权限。
2.2 JVM 安全机制研究
除了2.1.1小节中提到的四趟class文件校验机制以外, JVM在执行class文件中的字节码时还内置了一些其他的安全机制。这些安全机制有力的维护了Java程序的健壮性,是 Java 语言类型安全特性的基础。比较主要的安全特性有以下几点:
1. 类型安全的引用转换;
2. 结构化的内存访问;
3. 自动化的垃圾回收;
4,数组边界检查;
5,空引用检查。
2.3 本章小结
本章主要总结了 Java安全机制的两大模块:JRE 沙箱和 JVM 安全。在第一小节,本文介绍了JRE沙箱的主要安全相关的组件或策略:类装载器、安全管理器、权限提升代码块、反射机制以及包访问限制策略。在第二小节,本文简要介绍了JVM的类型安全等机制,还有Java原生层API及其潜在安全问题。通过型对Java安全机制的研究,可以更好地理解JRE 漏洞成因,同时,该研究工作也是 JRE漏洞挖掘工作的基础研究。