java安全——类加载器+字节码校验+安全管理器与访问权限

【0】README

0.1)本文文字描述转自 core java volume 2,旨在学习 java安全 的相关知识;


【1】类加载器

1)java 技术提供了以下3种确保安全的机制(mechanism):

  • m1)语言设计特性: (对数组边界的检查, 无不受检查的类型转换);
  • m2)访问控制机制: 用于控制代码能够执行的操作;(比如文件访问,网络访问等);
  • m3)代码签名: 利用该特性, 代码的作者就能够用标准的加密算法来认证java 代码;

2)类加载器功能: 它可以在将类加载到虚拟机中的时候检查类的完整性;

【1.1】类加载器

1)java 编译器会为虚拟机转换源指令。 虚拟机代码存储在以 .class 为扩展名的类文件中, 每个类文件都包含某个类或者接口的定义和代码实现。 这些类文件必须有一个程序进行解释,该程序能够将虚拟机的指令集翻译成目标机器的机器语言;
Attention)虚拟机只加载程序执行时所需要的类文件。
2)看个荔枝, 假设程序从 MyProgram.class 开始运行, 下面是虚拟机运行的steps:

  • step1)虚拟机有一个用于加载类文件的机制, 它使用该机制来加载MyProgram 类文件中的内容;
  • step2)如果MyProgram 类拥有类型为另一个类型的域,或者是拥有超类, 那么这些类文件也会被加载。(加载某个类所依赖的所有类的过程称为类的解析)(干货——类的解析定义)
  • step3)虚拟机执行 MyProgram 中的main方法;
  • step4)如果main 方法或者 main 调用的方法要用到更多的类,那么接下来就会加载这些类;

3)类加载机制并非只使用单个的类加载器,每个java程序至少拥有3个类加载器:

  • 3.1)引导类加载器: 负责加载系统类(从JAR文件 rt.jar 中进行加载), 它是虚拟机不可分割的一部分,而且通常是用C 语言来实现的;引导类加载器没有对应的 ClassLoader 对象,例如,该方法: String.class.getClassLoader 将返回 null;
  • 3.2)扩展类加载器: 它用于从 jre/lib/ext 目录加载 标准的扩展。 可以将 JAR 文件放入该目录,这样即使没有任何类路径,扩展类加载器也可以找到其中的各个类。
  • 3.3)系统类加载器(也称为应用类加载器): 系统类加载器用于加载应用类。 它在由 CLASSPATH 环境变量或者 -classpath 命令行选择设置的类路径中的目录里或者是 JAR/ZIP 文件里查找这些类;
    Warning) 如果将 JAR 文件放入 jre/lib/ext 目录中, 并且在它的类中有一个类需要调用 系统类或者扩展类, 那么就会遇到麻烦。 扩展类加载器并不使用类路径。在使用扩展目录来解决类文件的冲突之前, 要牢记这种情况;
    Attention) 除了所有已经提到的位置, 还可以从 jre/lib/endorsed 目录中加载类。这种机制只能用于将某个标准的java 类库替换为 更新的版本;

【1.1.1】类加载器的层次结构

1)类加载器有一种父子关系: 除了引导类加载器外,每个类加载器都有一个父类加载器。根据规定,类加载器会为它 的父类加载器提供一个机会, 以便加载任何给定的类,并且只有在其父类加载器加载失败时,它才会加载该给定类;
2)看个荔枝: 当要求系统类加载器加载一个系统类(如, java.util.ArrayList) 时,它首先要求扩展类加载器进行加载, 该扩展类加载器则首先要求 引导类加载器进行加载。引导类加载器查找并加载 rt.jar 中的这个类, 而无需其他类加载器做更多 的搜索;
3)某些程序具有插件架构,如何加载? 如果插件被打包为 JAR 文件, 那就可以直接用 URLClassLoader 类的实例去加载这些类: (干货——引入URLClassLoader)

URL url = new URL("file://path/to/plugin.jar");
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Class c1 = loader.loadClass("mypackage.MyClass");
  • 3.1)因为在URLClassLoader 构造器中没有指定父类加载器, 因此 loader 的父亲就是 系统类加载器。 下图展示了这种层次结构:
    java安全——类加载器+字节码校验+安全管理器与访问权限_第1张图片

  • 3.2) 大多数时候,你不必操心类加载的层次结构。 通常,类是由于其他的类需要它而被加载的, 这个过程是透明的;

4)上下文类加载器: 每个线程都有一个对类加载器的引用,称为上下文类加载器。 (干货——上下文类加载器)

  • 4.1)主线程的上下文类加载器是系统类加载器;
  • 4.2)当创建新线程时, 它的上下文件类加载器会被设置成为创建该线程的上下文类加载器;因此,如果你不做任何特殊的操作, 那么所有线程就都将他们的上下文类加载器设置为系统类加载器;
  • 4.3)但,我们可以通过下面语句将其设置为任何类加载器:
Thread t = Thread.currentThread();
t.setContextClassLoader(loader);
  • 4.4)然后助手方法可以获取这个上下文类加载器:
Thread t = Thread.currentThread();
ClassLoader loader = t.getContextClassLoader();
Class c1 = loader.loadClass(className);

【1.1.3】编写你自己的类加载器

1)如果要编写自己的类加载器:只需要继承 ClassLoader 类,然后覆盖findClass(String className)方法;
2) ClassLoader 超类的 loadClass 方法用于将类的加载操作委托给其父类加载器去进行, 只有当该类尚未加载并且父类加载器也无法加载该类时,才调用 findClass 方法;
3)如果要实现 findClass 方法, 必须做到如下几点(key):

  • k1)为来自本地文件系统或者其他来源的类加载其字节码;
  • k2)调用 ClassLoader 超类的 defineClass 方法, 向虚拟机提供字节码;

【2】字节码校验

1)当类加载器将新加载的java 平台类的字节码传递给虚拟机时, 这些字节码首先要接受校验器的校验。
2)除了系统类外, 所有的类都要被校验。下面是校验器执行的一些检查(check):

  • c1)变量要在使用之前进行初始化;
  • c2)方法调用与对象引用类型间要匹配;
  • c3)访问私有数据和方法的规则没有被违反;
  • c4)对本地变量的访问都落在运行时堆栈内;
  • c5)运行时堆栈没有溢出;

3)in a word, 校验器的作用: 校验器总是在防范被故意篡改的类文件, 而不仅仅只是检查编译器产生的类文件;
4)看个荔枝(如何建立一个不良的类文件, 字节码校验实验):

  • step1) 编译文件;
    这里写图片描述

  • step2) javap 查看编译器是如何编译 fun 方法的:
    java安全——类加载器+字节码校验+安全管理器与访问权限_第2张图片

  • step3) 用16进制编辑器(vim)打开该.class 文件, 并将指令3的 istore_1 改为 istore_0,也就是说,局部变量0(==m) 被初始化了两次, 而局部变量1(==n)根本没有被初始化。

    0x10 ,bipush, 将单字节的常量值(-128~127)推送至栈顶
    0x3b, istore_0, 将栈顶int型数值存入第一个本地变量
    0x3c, istore_1, 将栈顶int型数值存入第二个本地变量

  • 所以 fun 方法的字节码为:10 64 3b 10 63 3c …..(0x64=100,0x63=99)
    java安全——类加载器+字节码校验+安全管理器与访问权限_第3张图片
    java安全——类加载器+字节码校验+安全管理器与访问权限_第4张图片
    java安全——类加载器+字节码校验+安全管理器与访问权限_第5张图片
    java安全——类加载器+字节码校验+安全管理器与访问权限_第6张图片


【3】安全管理器与访问权限

1) java平台的第二种安全机制:一旦某个类被加载到虚拟机中, 并由检验器检查之后,java 平台的第二种安全机制就会启动, 这个机制就是 安全管理器;
2)安全管理器: 是一个负责控制具体操作是否允许执行的类。安全管理器负责检查的操作包括以下内容(Content): (干货——安全管理器定义)

  • C1)创建一个新的类加载器;
  • C2)退出虚拟机;
  • C3)使用反射访问另一个类的成员;
  • C4)访问本地文件;
  • C5)打开 socket 连接;
  • C6)启动打印作业;
  • C7)访问系统剪贴板;
  • C8)访问 AWT 时间队列;
  • C9) 打开一个顶层窗口;

Attention) 在运行 java 应用程序时, 默认的设置是不安装 安全管理器的, 这样所有的操作都是允许的;

【3.1】java 平台安全性

1)JDK 1.0 具有一个非常简单的安全模型: 即本地类拥有所有权限,而 远程类只能在沙盒里运行;
2)java se 1.2 开始,有了更灵活的安全机制:他的安全策略建立了代码来源 和访问权限集之间的映射关系。 
java安全——类加载器+字节码校验+安全管理器与访问权限_第7张图片

  • 2.1)代码来源:由一个代码位置和一个证书集指定的。代码位置指定了代码来源, 而证书的目的是 由某一方来保障代码没有被篡改过;
  • 2.2)权限:是指由安全管理器负责检查的任何属性。
  • 2.3)权限荔枝( 下面这个类允许在 /temp 目录下读取和写入任何文件):
    FilePermission p = new FilePermission(“/temp/*”, “read, write”);

3)权限类的层次结构:
java安全——类加载器+字节码校验+安全管理器与访问权限_第8张图片

4)每个类都有一个保护域:

  • 4.1)他是一个用于封装类的代码来源和权限集合的对象。 当 SecurityManager 类需要检查某个权限时, 他要查看当前位于调用堆栈上的所有方法的类,然后他要获得所有类的保护域, 并且询问每个保护域, 其权限集合是否允许执行当前正在被检查的操作。如果所有的域都同意, 那么检查通过,否则, 就抛出一个 SecurityException 异常;

【3.2】安全策略文件

1)策略管理器: 要读取相应的策略文件,这些文件包含了将代码来源映射为权限的指令。 (干货——策略管理器)
2)看个荔枝(一个典型的策略文件):

grant codeBase "http://www.hosrtmann.com/classes"
{
    permission java.io.FilePerssion "/temp", "read, write";
}
  • 对上述代码的分析(Analysis): 该文件给所有下载自 http://www.hosrtmann.com/classes 的代码授予在 /temp 目录下读取和写入文件的权限。

3) 可以将策略文件 安装在 标准位置上。 默认情况下, 有两个位置可以安装策略文件(position):

  • p1) java 平台主目录的 java.policy 文件;
  • p2) 用户主目录的 .java.policy 文件(注意文件名前面的小圆点);

Attention)

    • 可以在 java.security 配置文件中 修改这些文件的位置, 默认位置为:

    policy.url.1=file: java.home/lib/security/java.policypolicy.url.2=file: {java.home}/.java.policy

  • A2) 系统管理员可以修改 java.security 文件, 并可以指定 驻留在另外一台server 上且用户无法修改的 策略URL;

  • 4)相比于修改上述的标准文件, 我们更愿意为每个应用程序配置显式的策略文件, 这样将权限写入一个独立的文件中即可;

    • 4.1)要应用这个策略文件, 可以有两个选择selects:

      • s1) 一种是在 应用程序的main 方法内部设置系统属性:
        System.setProperty(“java.security.policy”, “MyApp.policy”);
      • s2)可以像下面这样启动 jvm;
        java -Djava.security.policy=MyApp.policy MyApp
    • 4.2)在上述例子中, MyApp.policy 文件被添加到了 其他有效的策略中。 如果在命令行中添加了第二个等号, 如: java -Djava.security.policy==MyApp.policy MyApp, 那么应用程序就只使用 指定的策略文件, 而标准策略文件将被忽略;

    5)default case 下, java 应用程序是不安装安全管理器的。 因此, 在安装安全管理器之前, 看不到策略文件的作用。

    • 5.1) 安装安全管理器: System.setSecurityManager(new SecurityManager()); 添加到 main 方法中;
    • 5.2) 也可以添加命令行选项 : java -Djava.security.manager -Djava.security.policy=MyApp.policy MyApp

    6)如何描述策略文件的权限:

    • 6.1)一个策略文件包含 一系列 grant 项。 每一项都具有以下形式:
    grant codesource
    {
        permission1;
        permission2;
    };
    • 6.2)代码来源:包含一个代码基-code base(如果某一项适用于所有来源的代码,则代码基可以省略) 和 值得信赖的用户特征与证书签名者的名字;
    • 6.3)代码基可以设定为: codeBase “url” ;
    • 6.4)如果URL 以“/”结束,那么它是一个目录。否则,他将被视为一个 JAR文件的名字。如:
    grant codeBase "www.horstmann.com/classes/" {...};
    grant codeBase "www.horstmann.com/classes/MyApp.jar" {...};
    • 6.5)代码基是一个URL 且总是 以斜杠作为文件分隔符;如: grant codeBase “file:C://myapps/classes/” {…};

    7) 权限采用下面的结构: permission className targetName, actionList ; (干货——权限的结构定义)

    • 7.1)类名:是权限类的全称类名, 且权限类都继承自 BasicPermission ;
    • 7.2)目标名: 是个与权限相关的值, 如,文件权限中的目录名或者文件名, 或者是 socket 权限中的主机和端口;
    • 7.3)操作列表:同样是与权限相关的,它是一个操作方式的列表,如 read 和 connect 等操作, 用逗号分割;

    8)看个权限荔枝:

    • 8.1) permission java.io.FilePerssion “/temp”, “read, write”;
    • 8.2) socket 权限荔枝: permission java.net.SocketPermission “*.horstmann.com:8000-8999”, “connect”;
    • 8.3)允许程序读取以 java.vm 开头的所有属性: permission java.util.PropertyPermission “java.vm.*”, “read”;
    • 8.4)可以在策略文件中 使用系统属性, 其中的 ${property} 标记会被属性值替代, 如:
      permission java.io.FilePermission “${user.home}”, “read, write”;
    • 8.5) 为了创建平台无关的策略文件,使用 file.separator 属性而不是 使用 显示的 / 或 \ 绝对是个好主意;也可以使用 / {file.separator} 的缩写, 如:
      permission java.io.FilePermission "${user.home}${/}-", "read, write";

    Attention) policytool: JDK提供了policytool的基础工具, 可以用它编辑策略文件;

    【3.3】定制权限

    1)如何把自己的权限类提供给 users, 以使得他们在策略文件中引用这些权限类;
    2)实现自己的权限类: 继承 Permission类, 并提供以下方法(methods):

    • m1) 带有两个String 参数的构造器, 这两个参数分别是目标和操作列表;
    • m2) String getActions();
    • m3) boolean equals();
    • m4) int hashCode();
    • m5) boolean implies(Permission other);
    • 最后一个方法是最重要的。 权限有一个排序, 其中更加泛化的权限隐藏了更加具体的权限。 (干货——最后一个方法implies是最重要的)

    3)看个荔枝(请考虑下面的文件权限):

    p1 = new FilePermission(“/tmp/-“, “read, write”);

    • 3.1)该权限允许读写 /tmp 目录以及子目录中的任何文件。该权限隐含了其他更加具体的权限:

      p2 = new FilePermission(“/tmp/-“, “read”);
      p3 = new FilePermission(“/tmp/aFile”, “read, write”);
      p4 = new FilePermission(“/tmp/aDir/”, “write”);

    • 对以上权限的分析(Analysis):

      • A1) 如果 p1 的目标文件集包含 p2 的目标文件集;
      • A2)如果p1 的操作集包含p2 的操作集;
      • A3)那么, 文件访问权限p1 就隐含了另一个文件访问权限 p2;

    4)执行权限检查: 应该将一个具体的文件权限对象传递给 checkPermission 方法:
    checkPermission(new FilePermission(filename, “read”));
    Attention) 如果你自定义了自己的权限类, 就必须对权限类定义一个合适的隐含法则(通过实现 implies 方法 来实现);

    【3.4】实现权限类

    1)看个荔枝:下面这个JTextArea的子类询问安全管理器是否准备好了去添加新文本。

    class WordCheckTextArea extends JTextArea
    {
        public void append(String text)
        {
           WordCheckPermission p = new WordCheckPermission(text, "insert");
           SecurityManager manager = System.getSecurityManager();
           if (manager != null) manager.checkPermission(p);
           super.append(text);
        }
    }
    • 对上述代码的分析(Analysis):如果安全管理器赋予了WordCheckPermission权限,那么该文本就可以追加。否则,checkPermission方法就会抛出一个异常。

    2)单词检查权限有两个可能的操作: 一个是insert(用于插入特定文本的权限),另一个是avoid(添加不包含某些不良单词的任何文本的权限)。应该用下面的策略文件运行这个程序:

    grant
    {
        permission WordCheckPermission "sex,drugs,C++", "avoid";
    };
    • 对上述代码的分析(Analysis): 这个策略文件赋予的权限,可以插入除不良单词sex,drugs和C++之外的任何文本。

    Attention)当设计WordCheckPermission类时,我们必须特别注意implies方法,下面是控制权限p1是否隐含p2的规则(Rules):

    • R1)如果p1有avoid操作,p2有insert操作,那么p2的目标必须避开p1中的所有单词。例如,下面这个权限:

      WordCheckPermission “sex,drugs,C++”, “avoid”
      隐含了下面这个权限:
      WordCheckPermission “Mary had a little lamb”, “insert”

    • R2)如果p1和p2都有avoid操作,那么p2的单词集合必须包含p1单词集合中的所有单词。例如,下面这个权限:

      WordCheckPermission “sex,drugs”, “avoid”
      隐含了下面这个权限:
      WordCheckPermission “sex,drugs,C++”, “avoid”

    • R3)如果p1和p2都有insert操作,那么p1的文本必须包含p2的文本。例如,下面这个权限:

      WordCheckPermission “Mary had a little lamb”, “insert”
      包含了下面这个权限: WordCheckPermission “a little lamb”, “insert”

    Warning)务必要把你的权限类设为public。策略文件加载器不能加载位于引导类路径之外的可视包的类,并且它会悄悄忽略它无法找到的所有类。 (干货——务必要把你的权限类设为public)

    你可能感兴趣的:(java)