运用加密技术保护Java源代码

运用加密技术保护 Java 源代码
 
级别 : 初级
俞良松, 软件工程师,独立顾问和自由撰稿人
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想要装入的不仅包括指定的类,而且还包括该类引用的所有其他类时,它会把loadClassresolve参数设置成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.       步骤 1 :创建应用。我们的例子包含一个 App 主类,两个辅助类(分别称为 Foo Bar )。这个应用没有什么实际功用,但只要我们能够加密这个应用,加密其他应用也就不在话下。
2.       步骤 2 :生成一个安全密匙。在命令行,利用 GenerateKey 工具(参见 GenerateKey.java )把密匙写入一个文件:
% java GenerateKey key.data
3.        
4.       步骤 3 :加密应用。在命令行,利用 EncryptClasses 工具(参见 EncryptClasses.java )加密应用的类:
% java EncryptClasses key.data App.class Foo.class Bar.class
5.      
该命令把每一个 .class 文件替换成它们各自的加密版本。
6.       步骤 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()
);
    }
 }
}
7.      
对于未经加密的应用,正常执行方式如下:
% java App arg0 arg1 arg2
8.      
对于经过加密的应用,则相应的运行方式为:
% java DecryptStart key.data App arg0 arg1 arg2
9.        
DecryptStart 有两个目的。一个 DecryptStart 的实例就是一个实施即时解密操作的定制 ClassLoader ;同时, DecryptStart 还包含一个 main 过程,它创建解密器实例并用它装入和运行应用。示例应用 App 的代码包含在 App.java Foo.java Bar.java 内。 Util.java 是一个文件 I/O 工具,本文示例多处用到了它。完整的代码请从本文最后下载。
 
我们看到,要在不修改源代码的情况下加密一个 Java 应用是很容易的。不过,世上没有完全安全的系统。本文的加密方式提供了一定程度的源代码保护,但对某些攻击来说它是脆弱的。
虽然应用本身经过了加密,但启动程序 DecryptStart 没有加密。攻击者可以反编译启动程序并修改它,把解密后的类文件保存到磁盘。降低这种风险的办法之一是对启动程序进行高质量的模糊处理。或者,启动程序也可以采用直接编译成机器语言的代码,使得启动程序具有传统执行文件格式的安全性。
另外还要记住的是,大多数 JVM 本身并不安全。狡猾的黑客可能会修改 JVM ,从 ClassLoader 之外获取解密后的代码并保存到磁盘,从而绕过本文的加密技术。 Java 没有为此提供真正有效的补救措施。
不过应该指出的是,所有这些可能的攻击都有一个前提,这就是攻击者可以得到密匙。如果没有密匙,应用的安全性就完全取决于加密算法的安全性。虽然这种保护代码的方法称不上十全十美,但它仍不失为一种保护知识产权和敏感用户数据的有效方案。
 
众所周知,java开发语言提供了很方便的开发平台,而且开发出来的程序很容易在不同的平台上面进行移植,现在越来越多的人使用它开发软件。 Java有了它方便的一个方面,但是他同时也带给了开发者一个烦恼,这就是保护的办法不多,而且大多数不是很好用,这样自己辛苦开发出来的程序很容易被人反编译过来而据为己有,一般情况下,大多数的人都是用混编器(java obfuscator)来把开发出来的程序进行打乱以达到没有办法来反编译观看源代码,但是这种办法在网上很容易找到相关的软件来重新整理,那么这个混编只能控制一些本来也没有办法动您的软件的人,而对于一些掌握工具的人几乎是透明的,还有就是利用硬件加密锁,但大多数公司提供的硬件加密锁只是提供了一些dll的连接或简单的api调用,只要反编译他们,就很容易把一些api调用去掉,这样硬件加密锁也就不起作用了,但是现在到底有没有好的办法呢? 市场上现有的几款防止反编译工具其实都是改进了加载过程。但是加载到JAVA虚拟机后一样可以得到CLASS字节码的。 只要懂JAVA原理的人其实很用意破解的. JAVACOMPILE 这款工具就不一样了,javacompile 它是修改了虚拟机内核, 用户目录里没有需要的JAVA字节码,而是当JAVA需要调用函数的时候才通过网络下载相对应的已经二次编译的字节码,就算用户通过 CLASS.getClass 也得不到可以用来反编译字节码。 真正意义上保护JAVA知识产权。 个人版限制编译30个CLASS文件,文件打包方式下载。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值