《深入Java虚拟机》学习笔记三:安全性(上)

第三章 安全性
1、为什么需要安全性
      因为Java是为网络而生,而网络提供了一条攻入连入网络计算机的潜在途径,因为安全性非常重要。
2、基本沙箱
      组成Java沙箱的基本组件如下:
            类装载器结构
            Class文件检查器
            内置于Java虚拟机(及语言)的安全性
            安全管理器及JavaAPI。 
      Java的沙箱安全模型,最重要的优点之一就是这些组件中的类装载器和安全管理器是可以由用户定制的。
3、类装载器体系结构
      在Java沙箱中,类装载器体系结构式第一道防线,毕竟是由类装载器将代码装入java虚拟机中的。它起的作用如下:
      1) 它防止恶意代码区干涉善意的代码。
      2) 它守护了被信任的类库边界。 
      3) 它将代码归入某类(称为保护域),该类确定了代码可以进行哪些操作。
      类装载器体系结构可以防止恶意代码区干涉善意代码,是通过为不同的类装载器装入的类提供不同的命名空间来实现的。命名空间是由一系列唯一的名称组成,每一个被装载的类有一个名字,这个命名空间是由Java虚拟机为每一个类装载器维护的。命名空间有助于安全的实现,因为你可以有效地在装入了不同的命名空间的类之间设置一个防护罩。在Java虚拟机中,在同一个命名空间内的类可以直接进行交互,而不同的命名空间中的类甚至不能察觉彼此的存在,除非显示的提供了允许它们进行交互的机制。
      类装载器体系结构守护了被信任的类库边界,是通过分别使用不同的类装载器装载可靠的包河不可靠的包来实现的。虽然通过赋给成员受保护(或包访问)的访问限制,可以在同一个包中的类型间授予彼此访问的特殊权限,但是这种特殊的权限只能授给在同一个包中的运行时成员,而且它们必须是由同一个类装载器封装的。
      用户自定义类装载器经常依赖其他类装载器,至少依赖于虚拟机启动时创建的启动类装载器来帮助它实现一些类装载请求。 在版本1.2之前,非启动类装载器必须显式地求助于其他类装载器,类装载器可以请求另一个用户自定义的类装载器来装载一个类,这个请求是通过对被请求的用户自定义类装载器调用loadClass()来实现的。除此以外,类装载器也可以通过调用findSystemClass()来请求启动类装载器来装载类,这是ClassLoader中的一个静态方法。 
      从版本1.2之后,类装载器请求另一个类装载器来装载类型的过程被形式化,称为双亲委派模式除启动类装载器之外的每一个类装载器,都一个“双亲”类装载器,在某一个特定的类装载器试图以常用的方式装载类型以前,它会先默认的将这个任务委派给他的双亲,请求他的双亲来装载这个类型。然后这个双亲依次请求他自己的双亲来装载这个类型,这个委派的过程一直向上继续,直到达到启动类装载器,通常启动类装载器是委派链中的最后一个雷装载器,如果一个雷装载器的双亲类装载器有能力来装载这个类型,则这个类型装载器返回这个类型。否则,这个类装载器试图自己来装载这个类型。
      在版本1.2之前的大多数虚拟机实现中,内置类装载器(原始类装载器)负责在本地装载可用的class文件。这些class文件通常包括哪些要运行的Java应用程序的class文件,以及这个应用程序所需的任何类库,这些类库中包含JavaAPI的基本class文件。从版本1.2以后,装载本地可以class文件的工作被分配到多个类装载器中,刚才称为原始类装载器的内置类装载器被重新命名为启动类装载器,表示它现在只负责装载哪些核心JavaAPI class文件,因为核心JavaAPI的class文件是用于启动Java虚拟机的class文件,所以启动类装载器也因此得名。
      从版本1.2之后,由用户自定义类装载器来负责其他class文件的封装。当Java虚拟机开始运行时,在应用程序启动之前,它至少创建一个用户自定义类装载器,也肯能创建多个。所有这些类装载器被链接在一个双亲-孩子的关系链中,在这条链的顶端是启动类装载器,在这条链的末端是一个被称为“系统类装载器”的类装载器。系统类装载器是指由Java应用程序创建的,新的用户自定义类装载器的默认委派双亲,这个默认的委派双亲通常是一个用户自定义类装载器(被称为用户自定义的类装载器,和启动类装载器相对而言,实际上有Java虚拟机实现提供)。
      如何使用类装载器来保护可信任类库?类装载器体系结构是通过剔除装作被信任的不可靠类,来保护可信任类库的边界的。在有双亲委派模式的情况下,启动类装载器可以抢在标准扩展类装载器之前图装载类,而标准扩展类装载器可以抢在路径类装载器之前去装载那个类,类路径类装载器又可以抢在网络类装载器之前去装载它。这样,在使用双亲-孩子委派链的方法中,启动类装载器会在最可信的类库—核心JavaAPI中首先检查每个被装载的类型,然后才依次到标准扩展,类路劲上的本地文件中检查。
      运行时包是在Java虚拟机第二版的规范中第一次出现的,它指由同一个类装载器装载的。属于同一个包的,多个类型的集合。在允许两个类型之间对包内可见的成员进行访问前,虚拟机不但要确定这两个类型属于同一个包,还必须确认它们属于同一个运行时包—它们必须是由同一个类装载器装载的。之所以提出运行时包的概念,动机之一是使用不同的类装载器装载不同的类。
      类装载器可以用另一种方法来保护被信任的类库的边界,,它只需要通过简单地拒绝装载特定的禁止类型就可以了。
4、class文件检查器
      和类装载器一起,class文件检验器保证装载的class文件内容有正确的内部结构,并且这些class文件相互协调一致。如果class文件检验器在class发现了问题,将抛出异样。
      Class文件检查器实现的安全目标之一就是程序的健壮性。Java虚拟机的Class文件检验器在字节码执行之前,必须完成大部分检查工作。他只在执行前而不是在执行中对字节码进行一次分析(并检验它的完整性),每一次遇到一个跳转指令时都进行检验。作为字节码确认工作的一部分,虚拟机将确认所有的跳转指令会到达另一条合法的指令,而且这条指令是在这个方法的字节流中的。
     Class文件检验器要进行四趟独立的扫描来完成他的操作:
     1)第一趟扫描,对每一段将被作为类型导入的字节序列,class文件检查器都会确认它是否符合Java class文件的基本结构。 在这次扫描中,检验器将进行许多检查:以魔数0XCAFEBABE开头?确定class文件中声明的主版本号和次版本号,这个版本号必须在这个Java虚拟机实现的可支持范围之内。确定这个class文件没有被删节,尾部也没有附带其他字节。
第一趟扫描的主要目的就是保证这个字节序列正确的定义了一个新的类型,它必须遵从Java的class文件固有格式,这样才能被编译成方法中的内部数据结构。第二、第三和第四趟扫描不是在符合class文件格式的二进制数据上进行的,而是在方法区中的,由实现决定的数据结构上进行的。 
     2)第二趟:类型数据的语义检查。在第二趟扫描中,class文件检查器进行的检查不需要查看字节码,也不需要查看和装载任何其他类型:在这趟扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。检验器对每个组成部分进行检查的目的之一是,为了确认每个方法描述都是符合特定语法的、格式正确的字符串。另外,class文件检验器检查这个类本身是否符合特定的条件,它们是由Java编程语言规定的。 
     3) 第三趟:字节码验证。在class文件检验器成功地进行了两趟检查后,它将注意力放在字节码上,这一趟扫描被称为“字节码验证“。在这趟扫描中,Java虚拟机对字节流进行数据流分析,这些字节流代表的是类的方法。
     字节码流代表了Java的方法,它是由被称为操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。操作数用于在Java虚拟机执行操作码指令时提供的额外的数据。执行字节码时,依次执行每个操作码,这就在Java虚拟机内构成了执行的线程。每一个线程被授予自己的Java栈,这个栈是由不同的栈帧构成的,每一个方法调用将获得一个自己的栈帧,栈帧其实就是一个内存片段,其中存储着局部变量和计算的中间结果。在栈帧中用于存储方法的中间结果的部分被称为该方法的操作数栈。操作码和它的(可选的)操作数可能指存储在操作数栈中的数据,或存储在方法栈帧中的局部变量中的数据。 这样,在执行一个操作码时,除了可以使用紧随其后的操作数,虚拟机还可以使用操作数栈中的数据,或局部变量中的数据,或者两者都用。
      字节码检查器要进行大量的检查,以确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型。它必须保证局部变量在赋予合适的值以前都不能被访问,而且类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确的数值和类型的参数。字节码还必须保证每个操作码都是合法的,即每一个操作码都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节码检验器所做的大量检查工作中的一个小部分,在整个检验过程中,它就能保证这个字节码流可以Java虚拟机安全的执行。
      字节码检验器并不试图检测出所有的安全问题。因为会遭遇“停机问题”,停机问题是计算机科学领域的一个著名论题:即不肯能写出一个程序,用它来判断作为其输入而读入的某个程序在执行时是否停机。
      字节码检验器处理停机问题的方法是,不去试图精确地让每个安全的程序都通过检查,虽然不能写出一个程序来判定任何给定程序是否会停机,但是可以写出一个简单的程序,让它只是识别出某些一定会停机的程序。Java字节码检验器就是这么做的,这个检验器检查确认读入的每一个字节码集合是否符合一个特定的规则集合。如果一个字节码集合能够遵从所有这些规则,那么检验器就知道它可以被虚拟机安全地执行。这样,通过识别一些安全的字节码流,但不是全部,检验器就绕过了停机问题。
      在第一、第二、第三趟扫描中,class文件检验器可以保证导入的class文件构成合理,内在一致,符合Java编程语言的限制条件,并且包含的字节码可以被Java虚拟机安全地执行。
      4) 第四趟:符号引用的验证。在动态链接的过程中,如果包含在一个class文件中的符号引用被解析时,class文件检验器将进行第四趟检查。在这趟检查中,Java虚拟机将追踪那些引用,从被验证的class文件到被引用的class文件,以确保这个引用是正确的。因为第四趟扫描必须检查被检测的class文件以外的其他类,所以这次扫描可能需要装载新的类。大多数Java虚拟机的实现采用延迟装载类的策略,直到类真正地被程序使用时才装载。即使一个实现确实需要预先装载了这些类,这是为了加快装载过程的速度,那它还是会表现为延迟装载。
       如果Java虚拟机在预先装载中发现它不能找到某个特定的被引用类,它并不在当时抛出NoClassDefFoundError错误,而是直到(或者除非)这个被引用的类首次被运行程序使用时才抛出。这样,如果Java虚拟机进行预先链接,第四趟扫描可以紧随第三趟扫描发生。但是如果Java虚拟机在某个符号引用第一次被使用时才进行解析,那么第四趟扫描将在第三趟以后很久,当字节码被执行时才进行。
      Class文件检验器的第四趟扫描仅仅是动态链接过程的一部分,当一个class文件被装载时,它包含了对其他类的符号引用以及它们的字段和方法。一个符号引用是一个字符串,它给出了名字,并且可能还包含了其他关于这个被引用项的信息,这些信息必须足以惟一地标识一个类,字段或方法。这样,对于其他类的符号引用必须给出这个类的全名,对于其他类的字段符号引用,必须给出类名、字段名以及字段描述符。对于其他类中的方法的引用必须给出类名、方法名以及方法的描述符。
       动态链接是一个将符号引用解析为直接引用的过程。当Java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用,那么虚拟机必须解析这个符号引用,在解析时,虚拟机执行两个基本任务:一个是查找被引用的类,另一个是将符号引用替换为直接引用。 
      5) 二进制兼容
     因为Java程序是动态连接的,所以class文件检查器在第四次扫描中,必须检查相互引用的类之间是否兼容。

 

    原文作者:java虚拟机
    原文地址: https://blog.csdn.net/iteye_19663/article/details/81803032
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞