Java程序的源代码很容易被别人偷看
只要有一个反编译器
任何人都可以分析别人的代码
本文讨论如何在不修改原有程序的情况下
通过加密技术保护源代码
一为什么要加密?
对于传统的C或C++之类的语言来说要在Web上保护源代码是很容易的只要不发布它就可以遗憾的是Java程序的源代码很容易被别人偷看只要有一个反编译器任何人都可以分析别人的代码Java的灵活性使得源代码很容易被窃取但与此同时它也使通过加密保护代码变得相对容易我们唯一需要了解的就是Java的ClassLoader对象当然在加密过程中有关Java Cryptography Extension(JCE)的知识也是必不可少的
有几种技术可以模糊Java类文件使得反编译器处理类文件的效果大打折扣然而修改反编译器使之能够处理这些经过模糊处理的类文件并不是什么难事所以不能简单地依赖模糊技术来保证源代码的安全
我们可以用流行的加密工具加密应用比如PGP(Pretty Good Privacy)或GPG(GNU Privacy Guard)这时最终用户在运行应用之前必须先进行解密但解密之后最终用户就有了一份不加密的类文件这和事先不进行加密没有什么差别
Java运行时装入字节码的机制隐含地意味着可以对字节码进行修改JVM每次装入类文件时都需要一个称为ClassLoader的对象这个对象负责把新的类装入正在运行的JVMJVM给ClassLoader一个包含了待装入类(比如javalangObject)名字的字符串然后由ClassLoader负责找到类文件装入原始数据并把它转换成一个Class对象
我们可以通过定制ClassLoader在类文件执行之前修改它这种技术的应用非常广泛??在这里它的用途是在类文件装入之时进行解密因此可以看成是一种即时解密器由于解密后的字节码文件永远不会保存到文件系统所以窃密者很难得到解密后的代码
由于把原始字节码转换成Class对象的过程完全由系统负责所以创建定制ClassLoader对象其实并不困难只需先获得原始数据接着就可以进行包含解密在内的任何转换
Java 在一定程度上简化了定制ClassLoader的构建在Java 中loadClass的缺省实现仍旧负责处理所有必需的步骤但为了顾及各种定制的类装入过程它还调用一个新的findClass方法
这为我们编写定制的ClassLoader提供了一条捷径减少了麻烦只需覆盖findClass而不是覆盖loadClass这种方法避免了重复所有装入器必需执行的公共步骤因为这一切由loadClass负责
不过本文的定制ClassLoader并不使用这种方法原因很简单如果由默认的ClassLoader先寻找经过加密的类文件它可以找到;但由于类文件已经加密所以它不会认可这个类文件装入过程将失败因此我们必须自己实现loadClass稍微增加了一些工作量
二定制类装入器
每一个运行着的JVM已经拥有一个ClassLoader这个默认的ClassLoader根据CLASSPATH环境变量的值在本地文件系统中寻找合适的字节码文件
应用定制ClassLoader要求对这个过程有较为深入的认识我们首先必须创建一个定制ClassLoader类的实例然后显式地要求它装入另外一个类这就强制JVM把该类以及所有它所需要的类关联到定制的ClassLoaderListing 显示了如何用定制ClassLoader装入类文件
【Listing 利用定制的ClassLoader装入类文件】
以下是引用片段
// 首先创建一个ClassLoader对象
ClassLoader myClassLoader = new myClassLoader();
// 利用定制ClassLoader对象装入类文件
// 并把它转换成Class对象
Class myClass = myClassLoaderloadClass( mypackageMyClass );
// 最后创建该类的一个实例
Object newInstance = myClassnewInstance();
// 注意MyClass所需要的所有其他类都将通过
// 定制的ClassLoader自动装入
如前所述定制ClassLoader只需先获取类文件的数据然后把字节码传递给运行时系统由后者完成余下的任务
ClassLoader有几个重要的方法创建定制的ClassLoader时我们只需覆盖其中的一个即loadClass提供获取原始类文件数据的代码这个方法有两个参数类的名字以及一个表示JVM是否要求解析类名字的标记(即是否同时装入有依赖关系的类)如果这个标记是true我们只需在返回JVM之前调用resolveClass
【Listing ClassLoaderloadClass()的一个简单实现】
以下是引用片段
public Class loadClass( String name boolean resolve )
throws ClassNotFoundException {
try {
// 我们要创建的Class对象
Class clasz = null;
// 必需的步骤如果类已经在系统缓沖之中
// 我们不必再次装入它
clasz = findLoadedClass( name );
if (clasz != null)
return clasz;
// 下面是定制部分
byte classData[] = /* 通过某种方法获取字节码数据 */;
if (classData != null) {
// 成功读取字节码数据现在把它转换成一个Class对象
clasz = defineClass( name classData classDatalength );
}
// 必需的步骤如果上面没有成功
// 我们尝试用默认的ClassLoader装入它
if (clasz == null)
clasz = findSystemClass( name );
// 必需的步骤如有必要则装入相关的类
if (resolve && clasz != null)
resolveClass( clasz );
// 把类返回给调用者
return clasz;
} catch( IOException ie ) {
throw new ClassNotFoundException( ietoString() );
} catch( GeneralSecurityException gse ) {
throw new ClassNotFoundException( gsetoString() );
}
}
Listing 显示了一个简单的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加密和解密数据必须遵循的基本步骤
步骤生成一个安全密匙在加密或解密任何数据之前需要有一个密匙密匙是随同被加密的应用一起发布的一小段数据Listing 显示了如何生成一个密匙 【Listing 生成一个密匙】
以下是引用片段
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
// 为我们选择的DES算法生成一个KeyGenerator对象
KeyGenerator kg = KeyGeneratorgetInstance( DES );
kginit( sr );
// 生成密匙
SecretKey key = kggenerateKey();
// 获取密匙数据
byte rawKeyData[] = keygetEncoded();
/* 接下来就可以用密匙进行加密或解密或者把它保存
为文件供以后使用 */
doSomething( rawKeyData );
步骤加密数据得到密匙之后接下来就可以用它加密数据除了解密的ClassLoader之外一般还要有一个加密待发布应用的独立程序(见Listing ) 【Listing 用密匙加密原始数据】
以下是引用片段
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
byte rawKeyData[] = /* 用某种方法获得密匙数据 */;
// 从原始密匙数据创建DESKeySpec对象
DESKeySpec dks = new DESKeySpec( rawKeyData );
// 创建一个密匙工厂然后用它把DESKeySpec转换成
// 一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactorygetInstance( DES );
SecretKey key = keyFactorygenerateSecret( dks );
// Cipher对象实际完成加密操作
Cipher cipher = CiphergetInstance( DES );
// 用密匙初始化Cipher对象
cipherinit( CipherENCRYPT_MODE key sr );
// 现在获取数据并加密
byte data[] = /* 用某种方法获取数据 */
// 正式执行加密操作
byte encryptedData[] = cipherdoFinal( data );
// 进一步处理加密后的数据
doSomething( encryptedData );
步骤解密数据运行经过加密的应用时ClassLoader分析并解密类文件操作步骤如Listing 所示 【Listing 用密匙解密数据】
// DES算法要求有一个可信任的随机数源
SecureRandom sr = new SecureRandom();
byte rawKeyData[] = /* 用某种方法获取原始密匙数据 */;
// 从原始密匙数据创建一个DESKeySpec对象
DESKeySpec dks = new DESKeySpec( rawKeyData );
// 创建一个密匙工厂然后用它把DESKeySpec对象转换成
// 一个SecretKey对象
SecretKeyFactory keyFactory = SecretKeyFactorygetInstance( DES );
SecretKey key = keyFactorygenerateSecret( dks );
// Cipher对象实际完成解密操作
Cipher cipher = CiphergetInstance( DES );
// 用密匙初始化Cipher对象
cipherinit( CipherDECRYPT_MODE key sr );
// 现在获取数据并解密
byte encryptedData[] = /* 获得经过加密的数据 */
// 正式执行解密操作
byte decryptedData[] = cipherdoFinal( encryptedData );
// 进一步处理解密后的数据
doSomething( decryptedData );
四应用实例
前面介绍了如何加密和解密数据要部署一个经过加密的应用步骤如下
步骤创建应用我们的例子包含一个App主类两个辅助类(分别称为Foo和Bar)这个应用没有什么实际功用但只要我们能够加密这个应用加密其他应用也就不在话下
步骤生成一个安全密匙在命令行利用GenerateKey工具(参见GenerateKeyjava)把密匙写入一个文件 % java GenerateKey keydata
步骤加密应用在命令行利用EncryptClasses工具(参见EncryptClassesjava)加密应用的类 % java EncryptClasses keydata Appclass Fooclass Barclass
该命令把每一个class文件替换成它们各自的加密版本
步骤运行经过加密的应用用户通过一个DecryptStart程序运行经过加密的应用DecryptStart程序如Listing 所示 【Listing DecryptStartjava启动被加密应用的程序】
以下是引用片段
import javaio*;
import javasecurity*;
import javalangreflect*;
import javaxcrypto*;
import javaxcryptospec*;
public class DecryptStart extends ClassLoader
{
// 这些对象在构造函数中设置
// 以后loadClass()方法将利用它们解密类
private SecretKey key;
private Cipher cipher;
// 构造函数设置解密所需要的对象
public DecryptStart( SecretKey key ) throws GeneralSecurityException
IOException {
thiskey = key;
String algorithm = DES;
SecureRandom sr = new SecureRandom();
Systemerrprintln( [DecryptStart: creating cipher] );
cipher = CiphergetInstance( algorithm );
cipherinit( CipherDECRYPT_MODE key sr );
}
// main过程我们要在这里读入密匙创建DecryptStart的
// 实例它就是我们的定制ClassLoader
// 设置好ClassLoader以后我们用它装入应用实例
// 最后我们通过Java Reflection API调用应用实例的main方法
static public void main( String args[] ) throws Exception {
String keyFilename = args[];
String appName = args[];
// 这些是传递给应用本身的参数
String realArgs[] = new String[argslength];
Systemarraycopy( args realArgs argslength );
// 读取密匙
Systemerrprintln( [DecryptStart: reading key] );
byte rawKey[] = UtilreadFile( keyFilename );
DESKeySpec dks = new DESKeySpec( rawKey );
SecretKeyFactory keyFactory = SecretKeyFactorygetInstance( DES );
SecretKey key = keyFactorygenerateSecret( dks );
// 创建解密的ClassLoader
DecryptStart dr = new DecryptStart( key );
// 创建应用主类的一个实例
// 通过ClassLoader装入它
Systemerrprintln( [DecryptStart: loading +appName+] );
Class clasz = drloadClass( appName );
// 最后通过Reflection API调用应用实例
// 的main()方法
// 获取一个对main()的引用
String proto[] = new String[];
Class mainArgs[] = { (new String[])getClass() };
Method main = claszgetMethod( main mainArgs );
// 创建一个包含main()方法参数的数组
Object argsArray[] = { realArgs };
Systemerrprintln( [DecryptStart: running +appName+main()] );
// 调用main()
maininvoke( null argsArray );
}
public Class loadClass( String name boolean resolve )
throws ClassNotFoundException {
try {
// 我们要创建的Class对象
Class clasz = null;
// 必需的步骤如果类已经在系统缓沖之中
// 我们不必再次装入它
clasz = findLoadedClass( name );
if (clasz != null)
return clasz;
// 下面是定制部分
try {
// 读取经过加密的类文件
byte classData[] = UtilreadFile( name+class );
if (classData != null) {
// 解密
byte decryptedClassData[] = cipherdoFinal( classData );
// 再把它转换成一个类
clasz = defineClass( name decryptedClassData
decryptedClassDatalength );
Systemerrprintln( [DecryptStart: decrypting class +name+] );
}
} catch( FileNotFoundException fnfe )
// 必需的步骤如果上面没有成功
// 我们尝试用默认的ClassLoader装入它
if (clasz == null)
clasz = findSystemClass( name );
// 必需的步骤如有必要则装入相关的类
if (resolve && clasz != null)
resolveClass( clasz );
// 把类返回给调用者
return clasz;
} catch( IOException ie ) {
throw new ClassNotFoundException( ietoString()
);
} catch( GeneralSecurityException gse ) {
throw new ClassNotFoundException( gsetoString()
);
}
}
}
对于未经加密的应用正常执行方式如下 % java App arg arg arg
对于经过加密的应用则相应的运行方式为 % java DecryptStart keydata App arg arg arg
DecryptStart有两个目的一个DecryptStart的实例就是一个实施即时解密操作的定制ClassLoader;同时DecryptStart还包含一个main过程它创建解密器实例并用它装入和运行应用示例应用App的代码包含在AppjavaFoojava和Barjava内Utiljava是一个文件I/O工具本文示例多处用到了它完整的代码请从本文最后下载
五注意事项
我们看到要在不修改源代码的情况下加密一个Java应用是很容易的不过世上没有完全安全的系统本文的加密方式提供了一定程度的源代码保护但对某些攻击来说它是脆弱的
虽然应用本身经过了加密但启动程序DecryptStart没有加密攻击者可以反编译启动程序并修改它把解密后的类文件保存到磁盘降低这种风险的办法之一是对启动程序进行高质量的模糊处理或者启动程序也可以采用直接编译成机器语言的代码使得启动程序具有传统执行文件格式的安全性
另外还要记住的是大多数JVM本身并不安全狡猾的黑客可能会修改JVM从ClassLoader之外获取解密后的代码并保存到磁盘从而绕过本文的加密技术Java没有为此提供真正有效的补救措施
不过应该指出的是所有这些可能的攻击都有一个前提这就是攻击者可以得到密匙如果没有密匙应用的安全性就完全取决于加密算法的安全性虽然这种保护代码的方法称不上十全十美但它仍不失为一种保护知识产权和敏感用户数据的有效方案