ClassLoader 与 JAVA类加密技术

Posted on 2010-04-01 22:52 梧桐夜雨 阅读(1002) 评论(0)  编辑  收藏
了解ClassLoader
1,   什么是 ClassLoader?
    Java 程序并不是一个可执行文件,是需要的时候,才把装载到 JVM中。ClassLoader 做的工作就是 JVM 中将类装入内存。 而且,Java ClassLoader 就是用 Java 语言编写的。这意味着您可以创建自己的 ClassLoader
    ClassLoader 的基本目标是对类的请求提供服务。当 JVM 需要使用类时,它根据名称向 ClassLoader 请求这个类,然后 ClassLoader 试图返回一个表示这个类的 Class 对象。 通过覆盖对应于这个过程不同阶段的方法,可以创建定制的 ClassLoader。
2, 一些重要的方法
A)  方法 loadClass
        ClassLoader.loadClass() 是 ClassLoader 的入口点。该方法的定义如下:
        Class loadClass( String name, boolean resolve );
         name  JVM 需要的类的名称,如 Foo 或 java.lang.Object。
         resolve 参数告诉方法是否需要解析类。在准备执行类之前,应考虑类解析。并不总是需要解析。如果 JVM 只需要知道该类是否存在或找出该类的超类,那么就不需要解析。
    
    cool.gif  方法 defineClass
       defineClass 方法是 ClassLoader 的主要诀窍。该方法接受由原始字节组成的数组并把它转换成 Class 对象。原始数组包含如从文件系统或网络装入的数据。defineClass 管理 JVM 的许多复杂、神秘和倚赖于实现的方面 -- 它把字节码分析成运行时数据结构、校验有效性等等。不必担心,您无需亲自编写它。事实上,即使您想要这么做也不能覆盖它,因为它已被标记成final的。

    C)  方法 findSystemClass
       findSystemClass 方法从本地文件系统装入文件。它在本地文件系统中寻找类文件,如果存在,就使用 defineClass 将原始字节转换成 Class 对象,以将该文件转换成类。当运行 Java 应用程序时,这是 JVM 正常装入类的缺省机制。(Java 2 中 ClassLoader 的变动提供了关于 Java 版本 1.2 这个过程变动的详细信息。) 对于定制的 ClassLoader,只有在尝试其它方法装入类之后,再使用 findSystemClass。原因很简单:ClassLoader 是负责执行装入类的特殊步骤,不是负责所有类。例如,即使 ClassLoader 从远程的 Web 站点装入了某些类,仍然需要在本地机器上装入大量的基本 Java 库。而这些类不是我们所关心的,所以要 JVM 以缺省方式装入它们:从本地文件系统。这就是 findSystemClass 的用途。

     D) 方法 resolveClass
   正如前面所提到的,可以不完全地(不带解析)装入类,也可以完全地(带解析)装入类。当编写我们自己的 loadClass 时,可以调用 resolveClass,这取决于 loadClass 的 resolve 参数的值。


   E) 方法 findLoadedClass
      findLoadedClass 充当一个缓存:当请求 loadClass 装入类时,它调用该方法来查看 ClassLoader 是否已装入这个类,这样可以避免重新装入已存在类所造成的麻烦。应首先调用该方法。

3, 怎么组装这些方法
  1) 调用 findLoadedClass 来查看是否存在已装入的类。
  2) 如果没有,那么采用那种特殊的神奇方式来获取原始字节。
  3) 如果已有原始字节,调用 defineClass 将它们转换成 Class 对象。
  4) 如果没有原始字节,然后调用 findSystemClass 查看是否从本地文件系统获取类。
  5) 如果 resolve 参数是 true,那么调用 resolveClass 解析 Class 对象。
  6) 如果还没有类,返回 ClassNotFoundException。

4,Java 2 中 ClassLoader 的变动
1)loadClass 的缺省实现
   定制编写的 loadClass 方法一般尝试几种方式来装入所请求的类,如果您编写许多类,会发现一次次地在相同的、很复杂的方法上编写变量。 在 Java 1.2 中 loadClass 的实现嵌入了大多数查找类的一般方法,并使您通过覆盖 findClass 方法来定制它,在适当的时候 findClass 会调用 loadClass。 这种方式的好处是您可能不一定要覆盖 loadClass;只要覆盖 findClass 就行了,这减少了工作量。

2)新方法:findClass
     loadClass 的缺省实现调用这个新方法。findClass 的用途包含您的 ClassLoader 的所有特殊代码,而无需要复制其它代码(例如,当专门的方法失败时,调用系统 ClassLoader)。

3) 新方法:getSystemClassLoader
     如果覆盖 findClass 或 loadClass,getSystemClassLoader 使您能以实际 ClassLoader 对象来访问系统 ClassLoader(而不是固定的从 findSystemClass 调用它)。
 
4) 新方法:getParent 
    为了将类请求委托给父代 ClassLoader,这个新方法允许 ClassLoader 获取它的父代 ClassLoader。当使用特殊方法,定制的 ClassLoader 不能找到类时,可以使用这种方法。
父代 ClassLoader 被定义成创建该 ClassLoader 所包含代码的对象的 ClassLoader。
运用加密技术保护Java源代码
QUOTE
Java程序的源代码很容易被别人偷看。只要有一个反编译器,任何人都可以分析别人的代码。本文讨论如何在不修改原有程序的情况下,通过加密技术保护源代码。
一、为什么要加密?
对 于传统的C或C++之类的语言来说,要在Web上保护源代码是很容易的,只要不发布它就可以。遗憾的是,Java程序的源代码很容易被别人偷看。只要有一 个反编译器,任何人都可以分析别人的代码。Java的灵活性使得源代码很容易被窃取,但与此同时,它也使通过加密保护代码变得相对容易,我们唯一需要了解 的就是Java的ClassLoader对象。当然,在加密过程中,有关Java Cryptography Extension(JCE)的知识也是必不可少的。

有几种技术可以“模糊”Java类文件,使得反编译器处理类文件的效果大打折扣。然而,修改反编译器使之能够处理这些经过模糊处理的类文件并不是什么难事,所以不能简单地依赖模糊技术来保证源代码的安全。

我们可以用流行的加密工具加密应用,比如PGP(Pretty Good Privacy)或GPG(GNU Privacy Guard)。这时,最终用户在运行应用之前必须先进行解密。但解密之后,最终用户就有了一份不加密的类文件,这和事先不进行加密没有什么差别。

Java 运行时装入字节码的机制隐含地意味着可以对字节码进行修改。JVM每次装入类文件时都需要一个称为ClassLoader的对象,这个对象负责把新的类装 入正在运行的JVM。JVM给ClassLoader一个包含了待装入类(比如java.lang.Object)名字的字符串,然后由 ClassLoader负责找到类文件,装入原始数据,并把它转换成一个Class对象。

我们可以通过定制ClassLoader,在类文件执行之前修改它。这种技术的应用非常广泛——在这里,它的用途是在类文件装入之时进行解密,因此可以看成是一种即时解密器。由于解密后的字节码文件永远不会保存到文件系统,所以窃密者很难得到解密后的代码。

由于把原始字节码转换成Class对象的过程完全由系统负责,所以创建定制ClassLoader对象其实并不困难,只需先获得原始数据,接着就可以进行包含解密在内的任何转换。

Java 2在一定程度上简化了定制ClassLoader的构建。在Java 2中,loadClass的缺省实现仍旧负责处理所有必需的步骤,但为了顾及各种定制的类装入过程,它还调用一个新的findClass方法。

这为我们编写定制的ClassLoader提供了一条捷径,减少了麻烦:只需覆盖findClass,而不是覆盖loadClass。这种方法避免了重复所有装入器必需执行的公共步骤,因为这一切由loadClass负责。

不 过,本文的定制ClassLoader并不使用这种方法。原因很简单。如果由默认的ClassLoader先寻找经过加密的类文件,它可以找到;但由于类 文件已经加密,所以它不会认可这个类文件,装入过程将失败。因此,我们必须自己实现loadClass,稍微增加了一些工作量。

二、定制类装入器
每一个运行着的JVM已经拥有一个ClassLoader。这个默认的ClassLoader根据CLASSPATH环境变量的值,在本地文件系统中寻找合适的字节码文件。

应 用定制ClassLoader要求对这个过程有较为深入的认识。我们首先必须创建一个定制ClassLoader类的实例,然后显式地要求它装入另外一个 类。这就强制JVM把该类以及所有它所需要的类关联到定制的ClassLoader。Listing 1显示了如何用定制ClassLoader装入类文件。

【Listing 1:利用定制的ClassLoader装入类文件】

  // 首先创建一个ClassLoader对象
  ClassLoader myClassLoader = new myClassLoader();

  // 利用定制ClassLoader对象装入类文件
  // 并把它转换成Class对象
  Class myClass = myClassLoader.loadClass( "mypackage.MyClass" );

  // 最后,创建该类的一个实例
  Object newInstance = myClass.newInstance();

  // 注意,MyClass所需要的所有其他类,都将通过
  // 定制的ClassLoader自动装入






如前所述,定制ClassLoader只需先获取类文件的数据,然后把字节码传递给运行时系统,由后者完成余下的任务。

ClassLoader 有几个重要的方法。创建定制的ClassLoader时,我们只需覆盖其中的一个,即loadClass,提供获取原始类文件数据的代码。这个方法有两个 参数:类的名字,以及一个表示JVM是否要求解析类名字的标记(即是否同时装入有依赖关系的类)。如果这个标记是true,我们只需在返回JVM之前调用 resolveClass。

【Listing 2:ClassLoader.loadClass()的一个简单实现】

      public Class loadClass( String name, boolean resolve )
      throws ClassNotFoundException {
    try {
      // 我们要创建的Class对象
       Class clasz = null;

      // 必需的步骤1:如果类已经在系统缓冲之中,
      // 我们不必再次装入它
      clasz = findLoadedClass( name );

      if (clasz != null)
        return clasz;

      // 下面是定制部分
      byte classData[] = /* 通过某种方法获取字节码数据 */;
      if (classData != null) {
        // 成功读取字节码数据,现在把它转换成一个Class对象
        clasz = defineClass( name, classData, 0, classData.length );
      }

      // 必需的步骤2:如果上面没有成功,
      // 我们尝试用默认的ClassLoader装入它
      if (clasz == null)
        clasz = findSystemClass( name );

      // 必需的步骤3:如有必要,则装入相关的类
      if (resolve && clasz != null)
        resolveClass( clasz );

      // 把类返回给调用者
      return clasz;

    } catch( IOException ie ) {
      throw new ClassNotFoundException( ie.toString() );
    } catch( GeneralSecurityException gse ) {
      throw new ClassNotFoundException( gse.toString() );
    }
  }






Listing 2显示了一个简单的loadClass实现。代码中的大部分对所有ClassLoader对象来说都一样,但有一小部分(已通过注释标记)是特有的。在处理过程中,ClassLoader对象要用到其他几个辅助方法:

findLoadedClass:用来进行检查,以便确认被请求的类当前还不存在。loadClass方法应该首先调用它。
defineClass:获得原始类文件字节码数据之后,调用defineClass把它转换成一个Class对象。任何loadClass实现都必须调用这个方法。
findSystemClass:提供默认ClassLoader的支持。如果用来寻找类的定制方法不能找到指定的类(或者有意地不用定制方法),则可以调用该方法尝试默认的装入方式。这是很有用的,特别是从普通的JAR文件装入标准Java类时。
resolveClass:当JVM想要装入的不仅包括指定的类,而且还包括该类引用的所有其他类时,它会把loadClass的resolve参数设置成true。这时,我们必须在返回刚刚装入的Class对象给调用者之前调用resolveClass。



三、加密、解密
Java加密扩展即Java Cryptography Extension,简称JCE。它是Sun的加密服务软件,包含了加密和密匙生成功能。JCE是JCA(Java Cryptography Architecture)的一种扩展。

JCE 没有规定具体的加密算法,但提供了一个框架,加密算法的具体实现可以作为服务提供者加入。除了JCE框架之外,JCE软件包还包含了SunJCE服务提供 者,其中包括许多有用的加密算法,比如DES(Data Encryption Standard)和Blowfish。

为简单计,在本文中我们将用DES算法加密和解密字节码。下面是用JCE加密和解密数据必须遵循的基本步骤:

步骤1:生成一个安全密匙。在加密或解密任何数据之前需要有一个密匙。密匙是随同被加密的应用一起发布的一小段数据,Listing 3显示了如何生成一个密匙。 【Listing 3:生成一个密匙】

  // DES算法要求有一个可信任的随机数源
  SecureRandom sr = new SecureRandom();

  // 为我们选择的DES算法生成一个KeyGenerator对象
  KeyGenerator kg = KeyGenerator.getInstance( "DES" );
  kg.init( sr );

  // 生成密匙
  SecretKey key = kg.generateKey();

  // 获取密匙数据
  byte rawKeyData[] = key.getEncoded();

  /* 接下来就可以用密匙进行加密或解密,或者把它保存
     为文件供以后使用 */
  doSomething( rawKeyData );




步骤2:加密数据。得到密匙之后,接下来就可以用它加密数据。除了解密的ClassLoader之外,一般还要有一个加密待发布应用的独立程序(见Listing 4)。 【Listing 4:用密匙加密原始数据】

    // DES算法要求有一个可信任的随机数源
    SecureRandom sr = new SecureRandom();

    byte rawKeyData[] = /* 用某种方法获得密匙数据 */;

    // 从原始密匙数据创建DESKeySpec对象
    DESKeySpec dks = new DESKeySpec( rawKeyData );

    // 创建一个密匙工厂,然后用它把DESKeySpec转换成
    // 一个SecretKey对象
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
    SecretKey key = keyFactory.generateSecret( dks );

    // Cipher对象实际完成加密操作
    Cipher cipher = Cipher.getInstance( "DES" );

    // 用密匙初始化Cipher对象
    cipher.init( Cipher.ENCRYPT_MODE, key, sr );

    // 现在,获取数据并加密
    byte data[] = /* 用某种方法获取数据 */

    // 正式执行加密操作
    byte encryptedData[] = cipher.doFinal( data );

    // 进一步处理加密后的数据
    doSomething( encryptedData );



步骤3:解密数据。运行经过加密的应用时,ClassLoader分析并解密类文件。操作步骤如Listing 5所示。 【Listing 5:用密匙解密数据】

    // DES算法要求有一个可信任的随机数源
    SecureRandom sr = new SecureRandom();

    byte rawKeyData[] = /* 用某种方法获取原始密匙数据 */;

    // 从原始密匙数据创建一个DESKeySpec对象
    DESKeySpec dks = new DESKeySpec( rawKeyData );

    // 创建一个密匙工厂,然后用它把DESKeySpec对象转换成
    // 一个SecretKey对象
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
    SecretKey key = keyFactory.generateSecret( dks );

    // Cipher对象实际完成解密操作
    Cipher cipher = Cipher.getInstance( "DES" );

    // 用密匙初始化Cipher对象
    cipher.init( Cipher.DECRYPT_MODE, key, sr );

    // 现在,获取数据并解密
    byte encryptedData[] = /* 获得经过加密的数据 */

    // 正式执行解密操作
    byte decryptedData[] = cipher.doFinal( encryptedData );

    // 进一步处理解密后的数据
    doSomething( decryptedData );



四、应用实例
前面介绍了如何加密和解密数据。要部署一个经过加密的应用,步骤如下:

步骤1:创建应用。我们的例子包含一个App主类,两个辅助类(分别称为Foo和Bar)。这个应用没有什么实际功用,但只要我们能够加密这个应用,加密其他应用也就不在话下。
步骤2:生成一个安全密匙。在命令行,利用GenerateKey工具(参见GenerateKey.java)把密匙写入一个文件: % java GenerateKey key.data



步骤3:加密应用。在命令行,利用EncryptClasses工具(参见EncryptClasses.java)加密应用的类: % java EncryptClasses key.data App.class Foo.class Bar.class


该命令把每一个.class文件替换成它们各自的加密版本。
步骤4:运行经过加密的应用。用户通过一个DecryptStart程序运行经过加密的应用。DecryptStart程序如Listing 6所示。 【Listing 6:DecryptStart.java,启动被加密应用的程序】

import java.io.*;
import java.security.*;
import java.lang.reflect.*;
import javax.crypto.*;
import javax.crypto.spec.*;

public class DecryptStart extends ClassLoader
{
  // 这些对象在构造函数中设置,
  // 以后loadClass()方法将利用它们解密类
  private SecretKey key;
  private Cipher cipher;

  // 构造函数:设置解密所需要的对象
  public DecryptStart( SecretKey key ) throws GeneralSecurityException,
      IOException {
    this.key = key;

    String algorithm = "DES";
    SecureRandom sr = new SecureRandom();
    System.err.println( "[DecryptStart: creating cipher]" );
    cipher = Cipher.getInstance( algorithm );
    cipher.init( Cipher.DECRYPT_MODE, key, sr );
  }

  // main过程:我们要在这里读入密匙,创建DecryptStart的
  // 实例,它就是我们的定制ClassLoader。
  // 设置好ClassLoader以后,我们用它装入应用实例,
  // 最后,我们通过Java Reflection API调用应用实例的main方法
  static public void main( String args[] ) throws Exception {
    String keyFilename = args[0];
    String appName = args[1];

     // 这些是传递给应用本身的参数
    String realArgs[] = new String[args.length-2];
    System.arraycopy( args, 2, realArgs, 0, args.length-2 );

    // 读取密匙
    System.err.println( "[DecryptStart: reading key]" );
    byte rawKey[] = Util.readFile( keyFilename );
    DESKeySpec dks = new DESKeySpec( rawKey );
    SecretKeyFactory keyFactory = SecretKeyFactory.getInstance( "DES" );
    SecretKey key = keyFactory.generateSecret( dks );

    // 创建解密的ClassLoader
    DecryptStart dr = new DecryptStart( key );

    // 创建应用主类的一个实例
    // 通过ClassLoader装入它
    System.err.println( "[DecryptStart: loading "+appName+"]" );
    Class clasz = dr.loadClass( appName );

    // 最后,通过Reflection API调用应用实例
    // 的main()方法

    // 获取一个对main()的引用
    String proto[] = new String[1];
    Class mainArgs[] = { (new String[1]).getClass() };
    Method main = clasz.getMethod( "main", mainArgs );

    // 创建一个包含main()方法参数的数组
    Object argsArray[] = { realArgs };
    System.err.println( "[DecryptStart: running "+appName+".main()]" );

    // 调用main()
    main.invoke( null, argsArray );
  }

  public Class loadClass( String name, boolean resolve )
      throws ClassNotFoundException {
    try {
      // 我们要创建的Class对象
      Class clasz = null;

      // 必需的步骤1:如果类已经在系统缓冲之中
      // 我们不必再次装入它
      clasz = findLoadedClass( name );

      if (clasz != null)
        return clasz;

      // 下面是定制部分
      try {
        // 读取经过加密的类文件
        byte classData[] = Util.readFile( name+".class" );

        if (classData != null) {
          // 解密...
          byte decryptedClassData[] = cipher.doFinal( classData );

          // ... 再把它转换成一个类
          clasz = defineClass( name, decryptedClassData,
            0, decryptedClassData.length );
          System.err.println( "[DecryptStart: decrypting class "+name+"]" );
        }
      } catch( FileNotFoundException fnfe ) {
      }

      // 必需的步骤2:如果上面没有成功
      // 我们尝试用默认的ClassLoader装入它
      if (clasz == null)
        clasz = findSystemClass( name );

      // 必需的步骤3:如有必要,则装入相关的类
      if (resolve && clasz != null)
        resolveClass( clasz );

      // 把类返回给调用者
      return clasz;
    } catch( IOException ie ) {
      throw new ClassNotFoundException( ie.toString()
);
    } catch( GeneralSecurityException gse ) {
      throw new ClassNotFoundException( gse.toString()
);
    }
  }
}


对于未经加密的应用,正常执行方式如下: % java App arg0 arg1 arg2


对于经过加密的应用,则相应的运行方式为: % java DecryptStart key.data App arg0 arg1 arg2



DecryptStart 有两个目的。一个DecryptStart的实例就是一个实施即时解密操作的定制ClassLoader;同时,DecryptStart还包含一个 main过程,它创建解密器实例并用它装入和运行应用。示例应用App的代码包含在App.java、Foo.java和Bar.java内。 Util.java是一个文件I/O工具,本文示例多处用到了它。完整的代码请从本文最后下载。

五、注意事项
我们看到,要在不修改源代码的情况下加密一个Java应用是很容易的。不过,世上没有完全安全的系统。本文的加密方式提供了一定程度的源代码保护,但对某些攻击来说它是脆弱的。

虽 然应用本身经过了加密,但启动程序DecryptStart没有加密。攻击者可以反编译启动程序并修改它,把解密后的类文件保存到磁盘。降低这种风险的办 法之一是对启动程序进行高质量的模糊处理。或者,启动程序也可以采用直接编译成机器语言的代码,使得启动程序具有传统执行文件格式的安全性。

另外还要记住的是,大多数JVM本身并不安全。狡猾的黑客可能会修改JVM,从ClassLoader之外获取解密后的代码并保存到磁盘,从而绕过本文的加密技术。Java没有为此提供真正有效的补救措施。

不过应该指出的是,所有这些可能的攻击都有一个前提,这就是攻击者可以得到密匙。如果没有密匙,应用的安全性就完全取决于加密算法的安全性。虽然这种保护代码的方法称不上十全十美,但它仍不失为一种保护知识产权和敏感用户数据的有效方案。

参考资源

在运行时刻更新功能模块。介绍了一个利用类库加载器ClassLoader 实现在运行时刻更新部分功能模块的Java程序,并将其与C/C++中实现同样功能的动态链接库方案进行了比较。
Java 技巧 105:利用 JWhich 掌握类路径。展示一个简单的工具,它可以清楚地确定类装载器从类路径中载入了什么 Java 类。
要了解更多的 Java 安全信息,请阅读 java.sun.com的 Java Security API 页。
如何封锁您的(或打开别人的) Java 代码。Java 代码反编译和模糊处理的指南。
使您的软件运行起来:摆弄数字。真正安全的软件需要精确的随机数生成器。
下载本文代码:EncryptedJavaClass_code.zip


关于作者
俞良松,软件工程师,独立顾问和自由撰稿人。最初从事PB和Oracle开发,现主要兴趣在于Internet开发。您可以通过 javaman@163.net 和我联系。

只有注册用户登录后才能发表评论。


网站导航:
 

posts - 9, comments - 0, trackbacks - 0, articles - 0

Copyright © 梧桐夜雨