本节书摘来自异步社区《深入解析Android 虚拟机》一书中的第2章,第2.3节,作者:钟世礼著,更多章节内容可以访问云栖社区“异步社区”公众号查看
2.3 JVM的安全性
除了平台无关性以外,Java还必须解决的另一个技术难题就是安全。因为网络运行多台计算机共享数据和分布式处理,所以它提供了一条侵入计算机系统的潜在途径,使得其他人可能窃取信息、改变或破坏信息、盗取计算资源等。因此,将计算机联入网络产生了很多安全问题。为了解决由网络引起的安全问题,Java体系结构采用了一个扩展的内置安全模型,这个模型随着Java平台的主要版本而不断发展。在本节的内容中,将简要讲解JVM安全性的基本知识,为读者步入本书后面知识的学习打下基础。
2.3.1 JVM的安全模型
Java安全模型侧主要用于保护终端用户免受从网络下载的、来自不可靠来源的、恶意程序的侵犯。为了达到这个目的,Java提供了一个用户可配置的“沙箱”,在沙箱中可以放置不可靠的Java程序。沙箱对不可靠程序的活动进行了限制,程序可以在沙箱的安全边界内做任何事,但是不能进行任何跨越这些边界的举动。例如,原来在版本1.0中的沙箱对很多不可靠Javaapplet的活动做了限制,主要包括:
对本地硬盘的读写操作;
进行任何网络连接,但不能连接到提供者applet的源主机;
创建新的进程;
装载新的动态连接库。
由于下载的代码不可能进行这些特定的操作,这使得Java安全模型可以保护终端用户避免受到有漏洞的代码的威胁。在沙箱内有严格的限制,其安全模型甚至规定了对不可靠代码能做什么、不能做什么,所以用户可以比较安全地运行不可靠代码。但是对于1.0系统的程序员和用户来说,这个最初的沙箱限制太过严格,善意的代码常常无法进行有效的工作。所以在后来的1.1版本中,对最初的沙箱模型进行了改进,引入了基于代码签名和认证的信任模式。签名和认证使得接收端系统可以确认一系列class文件已经由某一实体进行了数字签名(有效,可被信赖),并且在经过签名处理以后,class文件没有改动。这使得终端用户和系统管理员减少了对某些代码在沙箱中的限制,但这些代码必须已由可信任团体进行数字签名。
虽然1.1版本的安全API包含了对认证的支持,但是其实只是提供了完全信任和完全不信任策略。Java 1.2提供的API可以帮助建立细粒度的安全策略,这种策略是建立在数字签名代码的认证基础上的。Java安全模型的发展经历了1.0版本的基本沙箱,然后是1.1版本的代码签名和认证,最后是1.2版以后的细粒度访问控制。
2.3.2 沙箱模型的4种组件
在计算机系统中,个人电脑中运行一个软件的前提是必须信任它。普通用户只能通过小心地使用来自可信任来源的软件来达到安全性,并且定期扫描,检查病毒来确保安全性。一旦某个软件有权使用我们的系统,那么它将拥有对这台电脑的完全控制权。如果这个软件是恶意的,那么它就可以为所欲为。所以在传统的安全模式中,必须想办法防止恶意代码有权使用你的计算机。
沙箱安全模型使得工作变得容易,即使某个软件来自我们不能完全信任的地方,通过沙箱模型可以使我们接受来自任何来源的代码,而不是要求用户避免将来自不信任站点的代码下载到机器上。当运行来自不可靠来源的代码时,沙箱会限制它进行任何可能破坏系统的动作指令。并且在整个过程中,无需指出哪些代码可以信任,哪些代码不可以信任,也不必扫描查找病毒。沙箱本身限制了下载的任何病毒或其他恶意的,有漏洞的代码,使得它们不能对计算机进行破坏。
如果你还有疑问,在确信它能保护你之前,用户必需确认沙箱没有任何漏洞。为了保证沙箱没有漏洞,Java安全模型对其体系结构的各方面都进行了考虑。如果在Java体系结构中有任何没有考虑到安全的区域,恶意的程序员很可能会利用这些区域来绕开沙箱。因此,为了对沙箱有一个了解,必须先看一下Java体系结构的几个不同部分,并且理解它们是怎样一起工作的。
下面列出了组成Java沙箱的基本组件。
类加载体系结构。
class文件检验器。
内置于Java虚拟机(及语言)的安全特性。
安全管理器及Java API。
Java的上述安全模型的前3个部分——类加载体系结构、class文件检验器、Java虚拟机(及语言)的安全特性一起达到一个共同的目的:保持JVM的实例和它正在运行的应用程序的内部完整性,使得它们不被下载的恶意代码或有漏洞的代码侵犯。相反,这个安全模型的第四个组成部分是安全管理器,它主要用于保护虚拟机的外部资源不被虚拟机内运行的恶意或有漏洞的代码侵犯。这个安全管理器是一个单独的对象,在运行的Java虚拟机中,它在对于外部资源的访问控制起中枢作用。
2.3.3 分析Java的策略机制
沙箱安全模型的最大优点之一是可以是用户自定义的,通过从Java1.1版本就已经引入的代码签名和认证技术,使正在运行的应用程序可以对代码区分不同的信任度。通过自定义沙箱,被信任的代码可以比不可靠的代码获得更多的访问系统资源的权限。这就防止了不可靠代码访问系统,但是却允许被信任的代码访问系统并进行工作。Java安全体系结构的真正好处在于,它可以对代码授予不同层次的信任度来部分地访问系统。
Microsoft提供了ActiveX控件认证技术,它和Java的认证技术相类似,但是ActiveX控件并不在沙箱中运行。这样使用了ActiveX,一系列移动代码要么是被完全信任的,要么是完全不被信任的。如果一个ActiveX控件不被信任,则它将被拒绝执行。虽然这对于没有认证来说是一个很大的提高,但是如果一些恶意的或是有漏洞的代码得到了认证,这段危险的代码将拥有对系统的完全访问权。Java的安全体系结构的优点之一就是,代码可以被授予只对它需要的资源进行访问的有限权限。即使一些恶意的或者有漏洞的代码得到了认证,它也很少有机会进行破坏。例如,一段恶意的或者有漏洞的代码可能只能删除一个固定目录下的为它设置的文件,而不是在本地硬盘上的所有文件。
从1.2版本开始的安全体系结构的主要目标是建立(以签名代码为基础的)细粒度的访问控制策略,这样不但过程更为简单而且更少出错。为了将不同的系统访问权限授予不同的代码单元,Java的访问控制机制必须能确认应该给每个代码段授予什么样的权限。为了使这个过程变得容易,载入1.2版本或其他虚拟机的每一个代码段(每个class文件)将和一个代码来源关联。代码来源主要说明了代码从哪里来,如果它被某个人签名担保的话,是从谁那里来。在1.2版本以后的安全模型中,权限(系统访问权限)是授给代码来源的。因此如果代码段请求访问一个特定的系统资源,只有当这个访问权限是和那段代码的代码来源相关联时,Java虚拟机才会把对那个资源的访问权限授予这段代码。
在1.2版本的安全体系结构中,对应于整个Java应用程序的一个访问控制策略是由抽象类java.security.Policy的一个子类的单个实例所表示的。在任何时候,每一个应用程序实际上都只有一个Policy对象。获得许可的代码可以用一个新的Policy对象替换当前的Policy对象,这是通过调用Policy.setPolicy()并把一个新的Policy对象的引用传递给它来实现的。类装载器利用这个Policy对象来帮助它们决定,在把一段代码导入虚拟机时应该给它们什么样的权限。
安全策略是一个从描述运行代码的属性集合到这段代码所拥有的权限的映射。在1.2版本的安全体系结构中,描述运行代码的属性被总称为代码来源。一个代码来源是由一个java.security.CodeSource对象表示的,这个对象中包含了一个java.net.URL,它表示代码库和代表了签名者的零个或多个证书对象的数组。证书对象是抽象类java.security.Certificate的子类的一个实例,一个Certificate对象抽象表示了从一个人到一个公钥的绑定,以及另一个为这个绑定作担保的人(以前提过的证书机构)。CodeSource对象包含了一个Certificate对象的数组,因为同一段代码可以被多个团体签名(担保)。这个签名通常是从Jar文件中获得的。
从1.2版本开始,所有和具体安全管理器有关的工具和访问控制体系结构都只能对证书起作用,而不能对公钥起作用。如果附近没有证书机构,可以用私钥对公钥签名,生成一个自签名的证书。当使用keytool程序生成密钥时,总是会产生一个自签名的证书。例如在上一节的签名例子中,keytool不仅产生了“公钥/私钥”对,而且还为别名friend和stranger产生了自签名的证书。
权限是用抽象类java.security.Permission的一个子类的实例表示的。一个Permission对象有3个属性,分别是类型、名字和可选的操作。权限的类型是由Permisstion类的名字指定的,例如java.io.FilePermission、java.net.SocketPermission和java.awt.AWTPermission。权限的名字是封装在Permission对象内的。例如某个FilePermission的名字可能是“/my/finances.dat”,某个SocketPermission的名字可能是“applets.artima.com:2000”,某个AWTPermission的名字可能是“showWindowWithoutBannerWarning”。Permission对象的第3个属性是它的动作。并不是所有的权限都有动作。例如,FilePermission的动作是“read, write”,SocketPermission的动作是“accept, connect”。如果一个FilePermission的名字为“/my/finances.dat”,并且有动作“read, write”,那么它就表示对文件“/my/finance.dat”可以进行读写操作。名字和动作都是由字符串来表示的。
Java API有一个很大的权限层次结构,在里面表示了所有可能潜在危险的操作。可以根据自己的目的创建自己的Permission类来表示自定义的权限,例如可以创建一个Permission类来表示对属性数据库的特定记录的访问权限。定义自定义的Permission类也是一种扩展版本1.2的安全机制类满足自己需要的方法。如果创建了自己的Permission类,可以像使用Java API中的Permission类一样来使用它们。
在Policy对象中,每一个CodeSource是和一个或多个Permission对象相关联的。和一个CodeSource相关联的Permission对象被封装在java.security.PermissionCollection的一个子类实例中。类装载器可以调用Policy.getPolicy()来获得一个当前有效的Policy对象的引用。然后它们可以调用Policy对象的getPermission()方法,传入一个CodeSource,从而得到和那个CodeSource对应的Permission对象的PermissionCollection。然后类装载器可以使用这个从Policy对象中得到的PermissionCollection来帮助判断应该给导入的代码授予什么权限。