1、 基本沙箱
(1) 类加载器
a) 它防止恶意代码干涉善意代码。为由不同的类加载器加载的代码提供不同的命名空间,
在java虚拟机中,在同一个命名空间的类可以直接交互,但是不同命名空间的类根本无法
知道对方的存在,当然也可以通过显示地提供允许它们交互的,下面的例子就是一种显示
访问另一个命名空间的方式:
/*LoaderSample2.java*/
import java.net.*;
import java.lang.reflect.*;
public class LoaderSample2 {
public static void main(String[] args) {
try {
String path = System.getProperty("user.dir");
URL[] us = {new URL("file://" + path + "/sub/")};
ClassLoader loader = new URLClassLoader(us);
Class c = loader.loadClass("LoaderSample3");
Object o = c.newInstance();
Field f = c.getField("age");
int age = f.getInt(o);
System.out.println("age is " + age);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/*sub/Loadersample3.java*/
public class LoaderSample3 {
static {
System.out.println("LoaderSample3 loaded");
}
public int age = 30;
}
编译:
javac LoaderSample2.java;
javac sub/LoaderSample3.java
运行:java LoaderSample2
LoaderSample3 loaded
age is 30
下图也直接说明了不同命名空间的类的装载方式:
两个类加载器虽然加载的Volcano类型一样,但是在方法区中却不一样。
b) 类装载器的体系结构守护了被信任的类库的边界。通过分别使用不同的类加载器加载
可靠和不可靠的包实现,它会剔除装作被信任的不可靠类。在双亲委派机制下,启动类加载
器可以抢在标准扩展类加载器之前去装载类(这样java核心类就不可替代),而标准扩张类
加载器可以抢在类路径加载器之前去装载那个类,类路径加载器又可以抢在网络类加载器之
前去加载它。如果网络类加载器试图从网络加载一个Java API中的某个类如
java.lang.Integer,因为该类已经在java API核心包中存在,启动类加载器会从核心包中加载
java.lang.Integer,而不会从网络中加载java.lang.Integer;再比如网络类加载器试图加载一个
新的类java.lang.Virus,因为Java允许在同一个包中的类具有彼此访问的特殊权限,暗示着
java.lang.Virus是java api的一部分,但虚拟机需要确认他们是不是同一个运行时包,即由同
一个类加载器加载,因为javaapi中的类由启动类加载器加载,而java.lang.Virus是有网络
类加载器加载,他们依然不能彼此访问。
c) 类装载器会加每一个加载类都放置在一个保护域中,它定义了每个类在运行时的权限。
(2) Class文件校验器
它保证了类装载器装载的class文件是具备正确的内部结构,并且这些class文件相互间
协调一致。它总共有4趟校验步骤:
a) 第一趟:class文件的结构检查。它发生在类被装载时,会校验文件是不是以四个同样的
字节开始:魔数0xCAFEBABE;校验声明的主版本号和次版本号是否在java虚拟机可以
支持的范围内;有没有被删节,文件默认有没有被追加字节,因为文件的每个组成部分都声
明了它的长度和类型,校验器可以通过组成部分声明的类型和长度来计算文件的总长度。等等。。。它的校验目的就是保证这个字节序列正确定义了一个新类型,它必须遵循java的class
文件的固定格式。
b) 第二趟:类型数据的语义检查,它校验类文件中的每个组成部分,确认它们是否是所属
类型的实例,它们的结构是否正确,是否符合Java语言应该在编译时遵守的强制规则。如
除了Object类以外其他所有的类是否都有超类,final类有没有被子类化,final方法有没有
被覆盖等。
c) 第三趟:字节码验证,确保采用任何路径在字节码流中都得到一个确定的操作码,操作
数栈总是包含正确的数值以及类型等等,总之通过字节码的验证保证这个字节码流在java
虚拟机中能被安全地执行。
d) 第四趟:符号引用的验证,在动态链接过程汇总,如果一个符号引用被解析,将会启动
符号应用的验证,它会验证它所有引用的类是否存在。大多数虚拟机的实现采用延迟加载
的策略,所以在预先装载时不会包NoClassDefFoundError错误,而是直到这个引用类首次
被程序使用时才抛出。这样第四趟可能发生在第三趟之后,也可能发生在当字节码被执行
的时候。除了校验引用是否存在,还是校验引用的类版本是否兼容。
(3) 内置的安全特性
a) 类型安全的引用转换
b) 结构化的内存访问(无指针算法)
使用类型安全的、结构化的方法访问内存,使得java程序更健壮。同时java虚拟机也不指
明运行时数据空间在java虚拟机内部是怎么分布的,在class文件内部查到不到任何内存地
址,当然这种限制可以通过本地方法来突破,而安全管理器提供了一个方法来确定一个程序
是否可以状态动态链接库,因为调用本地方法是动态链接库是必需的。
c) 自动垃圾收集
d) 数组越界检查
e) 空引用检查
f) 异常的结构化处理:
一个异常抛出不会导致整个系统崩溃,而是某几个线程死亡。
(4) 安全管理器和Java API
它的作用和之前的3个恰好相反,它主要用于保护虚拟机的外部资源不被虚拟机内运行的
恶意或有漏洞的代码侵犯。
当java程序启动时,它还没有安全管理器,但是应用程序通过将一个指向
java.lang.SecurityManager或是其子类的实例传给setSecurityManager(),以此来安装安全管理
器,它不是必选的。如果在应用程序中安装了安全管理器,在1.0/1.1版本中是不可更改的,
在1.2版本中通过调用System.setSecurityManager()实现。
如果一个程序安装了安全管理器,当Java API执行操作时,如下图调用所示:
2、 代码签名和认证
认证的作用是可以是用户确认,由某些团体担保的一些class文件是值得信任的,并且这些
class文件在到达用户虚拟机的途中没有被改变过。下图是一个签名的流程图:
散列普遍采用64或128为,产生重复的概率理论是2的64或128次幂才可能产生一个重复
的散列值,所以要黑客要想在没有私钥的情况下,产生相同的散列值来破解签名是很困难的。
下图是一个对签名文件进行验证的流程:
虽然认证技术使用了可信赖的数学原理,但数学不能解决所有问题,如它没有说明该信任谁,
以及该信任到什么程度。而且认证技术是建立在私钥是被秘密保管的前提之下,一旦私钥
丢失,不仅无效,还且还带来危险,因为它给出了一个错误的安全意义。同时,公钥的发布
也是一个问题,因为公钥是公开的,有公钥的人不用担心公钥被别人拿走,但是拿公钥的
人就需要担心自己拿到的公钥是不是来自自己想要得到公钥的个人或者团体,比如你想
从A那里拿公钥,可能拿到B的公钥(B利用某些非法手段替换了A的公钥),这时B就
可以利用你对A的签名信任来侵入你的系统。为了解决公钥的问题,建立了许多证书机构
来为这些公钥做担保,如A可以到一个证书机构给出他的一些信任证明以及他的公钥,然
后证书机构验证OK之后利用自己的私钥对A的公钥进行签名,最后得到的数字序列被称
为证书,A就可以不公布公钥,而只公布证书,当你得到证书之后,可以用证书机构的公钥
对证书进行解密得到公钥,然后使用公钥解密文件签名。B要想替换A的公钥,还必须知
道证书机构的私钥,要困难很多了。当然,这样证书机构又成为了安全瓶颈,但这已经大大
降低了安全风险,安全性是一种代价和安全之间的折中:安全风险越小,安全的代价越高。
3、 其他
(1) 策略
一个应用程序只会有一个Policy对象,可以通过Policy.setPolicy方法在运行时修改。
每个Policy对象包含了CodeSource与PermissionCollection的映射,它记录了每个装载的
类型具备哪些权限。在一个类加载器装载类时,会创建CodeSource实例,初始化PermissionCollection,同时利用这两个对象创建ProtectDomain,然后调用defineClass方法
将类装载到jvm中,这也就是说类加载器决定了哪些类型具备哪些权限,可以在装载的时
候增加或删减已定义的权限或者直接不进行权限验证。要启用策略需要启动安全管理器同时
指定策略文件:
java -Djava.security.manger-Djava.security.policy=policy.txt
一定策略文件定义的例子:
keystore "ijvmkeys";
grant signBy "friend" {
permissionjava.io.FilePermission "question.txt","read";
}
grant signBy "stranger" {
permissionjava.io.FilePermission "question.txt","write";
}
grant codeBase "file:${com.nearme.tools.security.*}"{
permissionjava.io.FilePermission "answer.txt","read";
permissionjava.io.FilePermission "answer.txt","write";
}
第一行指定了签名文件的文件名,2-4行指定了friend签名的代码对question.txt具备读权限,
5-7行指定了stranger签名的代码对question.txt具备写权限,8-11行指定
com.nearme.tools.security代码库下面的代码对answer.txt有读写权限。
(2) 保护域
每一个类型都有一个保护域,在类装载的时候指定。下图说明了保护域在权限验证方面
所处的地位。
当一个类加载器吧Friend和Friend$1导入方法区时,将把创建好的ProtectionDomain实例
引用和这些class文件的字节传递给defineClass()方法,这个defineClass()方法将Friend和
Friend$1所在的方法区中的类型数据和被传递的ProtectionDomain对象相关联。
(3) 访问控制器
AccessController提供了一个默认的安全策略执行机制,他使用栈检查来决定潜在不安全的
操作是否被允许。它的checkPermission实现的基本算法决定了调用栈中的每一帧是否有权
执行潜在不安全的操作。