`
SunMicro
  • 浏览: 20619 次
社区版块
存档分类
最新评论

JVM and Security primary summary

阅读更多
早期JAVA的安全模型被称作”沙箱(sandbox)“,通过定义这样一个用户可配置的保护域来实现代码的安全性管理,紧接着在JDK1.4中引入了健壮的全功能安全体系,该体系仍然基于沙箱这一概念的,不过新体系的安全策略是向ProtectionDomain授权权限而不是针对单一代码段授权权限

在最初的沙箱定义中,条件过于严格,导致善意的程序运行受限,新的安全平台体系中引入了代码签名和认证的信任模式,在这种模式下,可以根据代码数字签名的可信度给与适当的执行权限,使足够可信度的代码将充分发挥其作用。

沙箱由四个基本部分组成:
  1. 类装载器结构
  2. class文件验证
  3. 内置与jvm的和语言本身的安全特性
  4. 安全管理器和java api
1.类装载器结构

jvm通过类装载器实现类的加载并同时建立不同可信度的命名空间,也叫运行时包,只有被同一类加载器所加载的类相互之间才可见,因为他们在同一命名空间,不同的类加载器会创建不同的命名空间,不同命名空间之间是不允许访问(不可见的。jvm在加载类的过程了使用双亲委托模式,既是发现需要装载某一类的时候,首先将这个任务委托给自己的上级类装载器,依次规律,一般情况下类装载任务最后将委托给启动类装载器,只有在双亲无法完成装载的情况下,才由自己完成装载。双亲委托模式首先降低了恶意代码试图利用系统对java api的信任来达到目的的可能性。设想,一个恶意类被命名为java.lang.String,试图使类加载器误以为该恶意类是java api的一部分,但双亲委托将使用启动类加载器首先寻找java.lang.String,启动类加载器的搜索范围在java api中,当在api中找到这个String类后便会进行加载,而恶意的java.lang.String将不会被加载,也就是说恶意代码无法伪造身份。若恶意代码试图将自己“混"入java API,比如,一个声明为java.lang.IamHack的类,但由于启动类装载器在api中无法找到该类,它只可能被类路径装载器或用户自定义的类装载器装载,所以与api处在不同的命名空间中,仍然无法获得足够的信任


2.class文件验证

这一过程分为四个阶段

⑴文件结构验证

sun定义的class文件具有特定的结构,以0xCAFEBABE开头标识这是一份java特定的class文件(cafe babe,听说是对barista所作贡献的感谢)。class文件的每一个组成部分都声明了其类型和长度,检验器可以以此计算出正确的class文件总长度,从而判断是否有删减或附加额外的代码

下面是一个接口的16进制标识:
  1. 0000000: cafe babe 0000 0032 0009 0700 0707 0008 .......2........
  2. 0000010: 0100 0564 6f53 7468 0100 0328 2956 0100 ...doSth...()V..
  3. 0000020: 0a53 6f75 7263 6546 696c 6501 0009 446f .SourceFile...Do
  4. 0000030: 6572 2e6a 6176 6101 0015 6575 746f 7069 er.java...eutopi
  5. 0000040: 612f 7365 6375 7269 7479 2f44 6f65 7201 a/security/Doer.
  6. 0000050: 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 ..java/lang/Obje
  7. 0000060: 6374 0601 0001 0002 0000 0000 0001 0401 ct..............
  8. 0000070: 0003 0004 0000 0001 0005 0000 0002 0006 ................
  9. 0000080: 0a .

紧接cafe babe的是虚拟机的主次版本号的16进制标识,然后是常量池中的数据个数,常量次的数据在后侧可以看到,这里包括了类名,方法名等,另外还有字符串和特征符。接下来的对类文件中字段和方法的复杂结构表示。


⑵类型数据语义检查

这一过程需要验证class文件中个组成部分的所属实例是否与声明类型相符合,以及该类型是否符合java语言规定的特定条件,比如final类是否被子类化或是final方法是否被覆写等。另外还有java所有类继承自java.lang.Object,常量池中条目是否合法等。


⑶字节码验证

型数据语义检查并不检验class字节码,字节码的检验将在这个阶段完成。JVM使用的是堆栈体系结构,在使用指令操作数(Java为单字节指令序列)必须将其装入堆栈,JVM规范中又将其称作操作码,每一个操作码后面都跟着一个或多个操作数,为JVM执行操作码时提供额外的数据。JVM依次执行每个操作码,这在JVM内部构成执行线程,每个线程被授予一个由不同栈帧构成的JAVA栈,每一个方法调用都对应内存中的一个栈帧,其中保存了方法中的局部变量和中间结果(保存中间结果的为操作数栈)。

这期间要验证操作数栈中的数据是否合法,方法的调用过程是否使用了合法的参数,局部变量的访问是否被允许(访问必须在初始化之后),字节码验证的验证工作量是比较大的,其中还包括了跳转命令的检查等。经过字节码验证后。可以确保class文件的完整性。


⑷符号引用验证

class文件中对其引用的类,方法,变量等是以文字符号描述的,该描述保证准确定位至唯一的目标。类装载器装载一个类的时候会对其中包含的字符引用作出解析,首先是查找被引用的类,但不一定会加载,因为JVM的延迟加载会确保被引用的类在第一次使用前被加载。若找到了对应的类,则将文字描述替换为直接引用,如指针。JVM会记住这些直接引用,所遇到同样的描述,则直接替换为直接引用,从而避免重复查找。

由于JAVA动态链接的特性,可能会导致二进制不兼容,尽管在改动java文件后的重新编译过程中会检查兼容性,但考虑到在运行时加载的类文件在编译时期并不会被检查,所以需要在运行时执行二进制兼容检查。


3.JVM内置安全特性

在执行期,除了符号引用验证,JVM还会对一些内建的安全特性进行检查,大致如下:
  1. 类型安全的引用转化
  2. 结构化的内存访问(非指针算法)
  3. GC
  4. 数组边界检查
  5. 空引用检查(NullPoint)
强制内存的结构话访问,避免了恶意用户在了解内存分布的情况下通过指针对JVM的内部结构进行破外。当然要了解JVM的内存分布也不是易事。JVM对运行时数据空间的分配是一个黑盒过程,完全由JVM自己决定如何分配,在class中没有任何相关的信息。

字节码检查的局限就是对于那些不经过字节码检查的方法(如本地方法:native method)无法验证其安全性,所以这里采用的是对动态链接库的访问控制,对于那些足有足够可信度的代码才被允许访问本地方法。具体的实现是,由安全管理器来决定代码是否有调用动态链接库的权限,因为调用本地方法必须调用动态链接库。这样一来,不可信的代码将无法通过调用本地方法来绕过字节码检查。


4.安全管理器和java api

我们建立的java application基本上可以说是畅通无阻的,其原因是默认情况下所有JAVA API的操作都是被允许的。但

如果稍加限制一下

新建一个policy(实际上默认既是为read,这点从java.lang.Class中定义getProtectionDomain()方法中可以看出的)


java 代码
  1. grant codeBase "file:///D:/JavaTestCode/security/securitymanager/" {
  2. permission java.io.FilePermission "D:/JavaTestCode/security/securitymanager/*", "read";
  3. };


codeBase指定要限制的类所在目录 :URL
FilePermission 中指定了要保护的文件 :资源名称

然后在运行时以
  1. java -Djava.security.manager -Djava.security.policy=policyURL appName
的形式指定

(也可以通过在java.security中以policy.url.n=policyURL的形式指定)。
java 代码
  1. class TrustlessWriter
  2. {
  3. public static void main(String[] args)throws Exception
  4. {
  5. File data=new File("data.txt");
  6. FileWriter writer=new FileWriter(data);
  7. writer.write("this is a test");
  8. writer.close();
  9. }
  10. }

在控制台输入:
  1. java -Djava.security.manager -Djava.security.policy=myaccess.policy TrustlessWriter


结果将是抛出类似java.security.AccessControlException: access denied (java.io.FilePermission data.txt write)的错误信息

这里需要注意policy的格式

policy file syntax
  1. grant signedBy "signer_names", codeBase "URL",
  2. principal principal_class_name "principal_name",
  3. principal principal_class_name "principal_name",
  4. ... {
  5. permission permission_class_name "target_name", "action",
  6. signedBy "signer_names";
  7. permission permission_class_name "target_name", "action",
  8. signedBy "signer_names";
  9. ...
  10. };

codeBase 需用声明协议类型,若为本地文件话,需以file:声明,FilePermission里面则直接使用目标文件名或通配符


在JAVA2平台安全体系中,访问权限被类型化为对象,也既是说访问权限在Java api中被定义,所有访问权限对象继

承自抽象类java.security.Permission;

所以也可以在代码中启动SecurityManager,并执行检查

java 代码
  1. class TrustlessWriter
  2. {
  3. public static void main(String[] args)throws Exception
  4. {
  5. //SecurityManager sm=new SecurityManager();
  6. FilePermission filePermission=new FilePermission("D:/JavaTestCode/security/securitymanager/data.txt","write");
  7. System.out.println(TrustlessWriter.class.getProtectionDomain());
  8. AccessController.checkPermission(filePermission);//sm.checkPermission(filePermission)
  9. File data=new File("data.txt");
  10. Writer writer=new FileWriter(data,true);
  11. writer.write("hello!");
  12. writer.close();
  13. }
  14. }


此时可省略-Djava.security.manage,执行命令变为:

  1. java -Djava.security.policy=myaccess.policy TrustlessWriter

(AccessController是在JAVA2中引入的,不过考虑到前向兼容,新的平台安全体系中仍然可以使用SecurityManager

来做检查,但SecurityManager的所有check方法均调用AccessController的checkPermission)


关于代码签名和认证

新安全平台中对足够信任度的代码放宽了限制,要获得充分信任须通过代码签名来实现,若我们对某一签名团体足够 信任,比如SUN,那么具有该团体签名的代码将被给予充分信任。一个基本的思路大致为,代码发布者创建私钥/公钥对,然后利用私钥对发布代码进行签名,代码使用方在获得代码发布者提供的公钥后对代码进行验证,确认代码确为该提供者提供以及在发布后未经非法修改。这其中存在一些潜在的危险,既是公钥是否是该代码发布者提供的,恶意用户可能替换掉合法的公钥,这会导致用户将给与恶意用户发布的代码以充分信任,目前的常见做法是通过一些权威的证书机构来发布证书而不是公钥,代码发布者被证书发布机构认证合格后,可以将自己的公钥交付证书发布机构,证书发布机构再通过私钥加密该公钥,从而生成证书序列。这样替换公钥的可能性就变得微乎其微。不过世上无绝对安全之事,通过证书机构发布的证书也不是绝对安全的途径。

对代码进行签名需要将所有class文件打包入jar,然后通过jarsigner对其进行散列计算。

一个简单的示例:

1.首先将CredibleDoer.class打包至 credible.jar:

  1. jar cvf credible.jar eutopia/security/CredibleDoer.class

2.创建别名为credible的密钥对,将其保存在至doerkeys:
  1. keytool -genkeypair -alias credible -keypass douyourwant -keystore doerkeys

3.为credible.jar进行签名:
  1. jarsigner -keystore doerkeys -storepass 111111 -keypass douyourwant credible.jar credible
完成对一个jar文件的签名后,会在jar产生两个文件,此处为credible.SF和credible.DSA

.SF文件保存了一个所有类的列表以及了签名过程中使用的摘要算法,大致如下:

credible.sf
  1. Signature-Version: 1.0
  2. SHA1-Digest-Manifest-Main-Attributes: 9QUCJMHnILftziSD8agLig+iUN8=
  3. Created-By: 1.6.0 (Sun Microsystems Inc.)
  4. SHA1-Digest-Manifest: Z1288OdOqrP+Kstqxv0pS10Je7o=
  5. Name: eutopia/security/CredibleDoer.class
  6. SHA1-Digest: ODGgb1aB9Ht0iVwmfhGSU6pRneo=


DSA文件是一个二进制密匙文件。

验证完整性实验:

现在通过jarsigner -verify credible.jar对该文件进行验证。若文件未经改动,一切正常

若签名后再将TrustlessDoer.class加入credible.jar且不更新签名,那么验证结果将会给出警告:

警告:
此 jar 包含尚未进行完整性检查的未签名条目。

若是替换原有class文件,比如这里替换CredibleDoer.class,将会有类似下面的错误提示:

jarsigner: java.lang.SecurityException: SHA1 digest error for eutopia/security/CredibleDoer.class

通过 keytool -exportcert -file credible.cer -alias credible -keystore doerkeys 可以导出cer正式文件,其中保存了签名的一些基本信息以及公钥

创建密钥对的算法有DiffieHellman,DSA ,RSA, EC可以通过-sigalg <算法名>指定算法,比如:

  1. keytool -sigalg DSA -genkeypair -alias credible -keypass douyourwant -keystore doerkeys

参考:JavaTM Cryptography Architecture API Specification & Reference的Appendix A: Standard Names以及Appendix B: Algorithms


Related Data

深入java虚拟机第二版

Permissions in the JavaTM 2 Standard Edition Development Kit (JDK)

JavaTM Cryptography Architecture API Specification & Reference

Java 安全性架构文档

JavaTM 2 Platform Security Architecture


Default Policy Implementation and Policy File Syntax

Java 授权内幕

J2EE 探索者: 用 JAAS 和 JSSE 实现 Java 安全性
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics