如何在不修改原始程序的情况下通过加密保护源代码**。
对于 C 或 C++ 等传统语言,只要您不发布源代码,就很容易在 Web 上保护源代码。 不幸的是,j**a 程序的来源很容易被其他人窥视。 只要有反编译器,任何人都可以分析别人的。 J**A 的灵活性使得源代码很容易被窃取,但同时,它也使得通过加密来保护它相对容易,我们唯一需要知道的是 J**A 的类加载器对象。 当然,在加密过程中,有关 J**A 加密扩展 (JCE) 的知识也是必不可少的。
有几种技术可以“混淆”j**a 文件,使反编译器在处理它们时效率降低。 但是,修改反编译器来处理这些混淆的类文件并不难,因此您不能简单地依靠模糊测试来保证源代码的安全。
我们可以使用流行的加密工具(例如 PGP(Pretty Good Privacy)或 GPG(GNU Privacy Guard))对应用程序进行加密。 此时,最终用户必须在运行应用程序之前对其进行解密。 但是在解密后,最终用户有一个未加密的类文件,这与事先不加密没有什么不同。
j**a 运行时隐式输入字节码的机制意味着字节码是可以修改的。 每次 JVM 加载类文件时,它都需要一个名为 ClassLoader 的对象,该对象负责将新类加载到正在运行的 JVM 中。 jvm 为类装入器提供了一个文件,其中包含要装入的类(例如 j**a.)。lang.object),然后类加载器负责查找类文件,加载原始数据,并将其转换为类对象。
我们可以自定义类加载器以在执行类文件之前对其进行修改。 这种技术在这里使用非常广泛,其目的是在加载类文件时对其进行解密,因此可以将其视为即时解密器。 由于解密的字节码文件永远不会保存到文件系统中,因此窃贼很难获得解密的**。
由于将原始字节码转换为类对象的过程完全由系统负责,因此创建自定义类加载器对象并不难,只要先获取原始数据,然后可以进行包括解密在内的任何转换。
J**A 2 在一定程度上简化了自定义类加载器的构造。 在 J**A 2 中,LoadClass 的默认实现仍负责所有必需的步骤,但它也调用了一个新的 FindClass 方法来适应各种自定义类加载过程。
这为我们提供了编写自定义类加载器的快捷方式,省去了更少的麻烦:只需覆盖 findclass,而不是 loadclass。 这种方法避免了重复加载器必须执行的所有常见步骤,因为这些步骤都由 loadclass 处理。
但是,本文中的自定义类装入器不使用此方法。 原因很简单。 如果默认类装入器首先查找加密的类文件,则可以找到它; 但是,由于类文件已加密,因此它将无法识别类文件,并且加载过程将失败。 因此,我们不得不自己实现 loadclass,这增加了一些工作量。
每个正在运行的 JVM 都已经有一个类加载器。 此缺省类装入器根据类路径环境变量的值在本地文件系统中查找相应的字节码文件。
应用自定义类装入器需要对过程有深入的了解。 我们首先必须创建一个自定义类加载器类的实例,然后显式要求它加载另一个类。 这迫使 JVM 将类及其所需的所有类关联到自定义类装入器。 清单 1演示如何使用自定义 C lassloader 加载类文件。
清单 1使用自定义类装入器加载类文件。
首先,创建一个类加载器对象:classloader myclassloader = new myclassloader(); 使用自定义类加载器对象加载类文件,并将其转换为类对象类 myclass = myclassloaderloadclass( "mypackage.myclass" );最后,创建一个类对象 newinstance = myclass 的实例newinstance();请注意,myclass 所需的所有其他类都将通过自定义类加载器自动加载
如前所述,自定义类装入器只是从类文件中获取数据,并将字节码传递给运行时系统,其余工作由运行时系统完成。
有几种重要的方法可以执行类加载器。 在创建自定义类加载器时,我们只需要覆盖其中一个,即 loadclass,即可提供一个 ** 来获取原始类文件数据。 此方法有两个参数:类的名称,以及指示 JVM 是否需要解析类名称(即是否也加载依赖类)的标志。 如果这个标志是真的,我们只需要在返回 JVM 之前调用 resolveclass。
清单 2 classloader.loadClass()。
public class loadclass( string name, boolean resolve ) throws classnotfoundexception 所需的步骤 2:如果上述操作不成功,我们尝试使用默认类加载器加载它 if (clasz == null) clasz = findsystemclass( name ); 必需步骤 3:如有必要,加载相关类 if (resolve &&clasz!= null) resolveclass( clasz );将类返回给调用方 return clasz; }catch( ioexception ie ) catch( generalsecurityexception gse )
清单 2 显示了一个简单的 loadclass 实现。 对于所有类装入器对象,其中的大多数项都是相同的,但一小部分(标有注释)是唯一的。 在处理过程中,还有其他几种用于类装入器对象的帮助程序方法:
findloadedclass:用于检查请求的类当前是否存在。 loadclass 方法应首先调用它。 defineclass:获取到原始类文件的字节码数据后,调用defineclass将其转换为类对象。 任何 loadclass 实现都必须调用此方法。 FindSystemClass:提供对默认类装入器的支持。 如果用于查找类的自定义方法找不到指定的类(或者您没有故意使用自定义方法),则可以调用该方法来尝试默认加载方法。 这很有用,尤其是在从普通 jar 文件加载标准 j**a 类时。 ResolveClass:当 JVM 不仅要加载指定的类,还要加载该类引用的所有其他类时,它会将 loadclass 的 resolve 参数设置为 true。 在这种情况下,我们必须先调用 resolveclass,然后再将刚刚加载的类对象返回给调用方。 J**A 加密扩展 (JCE) 称为 J**A 加密扩展。 它是SUN的加密服务软件,包括加密和密钥生成功能。 JCE 是 JCA(J**A 加密架构)的扩展。
JCE 不指定特定的加密算法,但为可添加为服务提供程序的加密算法的特定实现提供提供了一个框架。 除了 JCE 框架之外,JCE 软件包还包括 SunJCE 服务提供者,其中包括许多有用的加密算法,例如 DES(Data Encryption Standard)和 Blowfish。
为简单起见,在本文中,我们将使用 DES 算法对字节码进行加密和解密。 以下是使用 JCE 加密和解密数据时必须遵循的基本步骤:
1. 生成安全密钥。 在加密或解密任何数据之前,需要密钥。 密钥是与加密应用程序一起发布的一小段数据,清单 3 显示了如何生成密钥。
清单 3生成密钥。
DES 算法需要一个可信的随机数源,securerandom sr = new securerandom(); 为我们选择的 DES 算法生成一个 keygenerator 对象:keygenerator kg = keygeneratorgetinstance( "des" );kg.init( sr );生成密钥密钥 = kggeneratekey();获取密钥数据字节 rawkeydata = keygetencoded();* 然后你可以用密钥加密或解密它,或者保存为文件以备后用 * dosomething(rawkeydata);
2. 加密数据。 获得密钥后,您可以使用它来加密您的数据。 除了解密的类加载器之外,通常还有一个单独的程序来加密要发布的应用程序(参见清单 4)。
清单 4使用密钥加密原始数据。
DES 算法需要一个可信的随机数源,securerandom sr = new securerandom(); byte rawkeydata = * 以某种方式获取密钥数据 * ; 从原始密钥数据创建一个 DesKeySpec 对象 DesKeySpec dks = new deskeySpec(RawKeyData); 创建一个密钥工厂并使用它来将 deskeyspec 转换为 secretkey 对象 secretkeyfactory keyfactory = secretkeyfactorygetinstance( "des" );secretkey key = keyfactory.generatesecret( dks );cipher 对象实际完成加密操作,cipher cipher = ciphergetinstance( "des" );使用密钥初始化密码对象密码init( cipher.encrypt_mode, key, sr );现在,获取数据并加密字节数据 = * 以某种方式获取数据 * 正式执行加密操作 byte encrypteddata = cipherdofinal( data );加密数据的进一步处理 dosomething( encrypteddata );
3. 解密数据。 运行加密应用程序时,类装入器会分析并解密类文件。 该过程如清单 5 所示。
清单 5使用密钥解密数据。
DES 算法需要一个可信的随机数源,securerandom sr = new securerandom(); byte rawkeydata = * 以某种方式获取原始密钥数据 * ; 从原始密钥数据创建一个 deskeyspec 对象 deskeyspec dks = new deskeyspec( rawkeydata ); 创建一个密钥工厂并使用它来将 deskeyspec 对象转换为 secretkey 对象 secretkeyfactory keyfactory = secretkeyfactorygetinstance( "des" );secretkey key = keyfactory.generatesecret( dks );cipher 对象实际完成解密操作,cipher cipher = ciphergetinstance( "des" );使用密钥初始化密码对象密码init( cipher.decrypt_mode, key, sr );现在,获取数据和解密 byte encrypteddata = * 获取加密数据 * 正式执行解密操作 byte decrypteddata = cipherdofinal( encrypteddata );解密数据的进一步处理 dosomething( decrypteddata );
前面我们介绍了如何加密和解密数据。 若要部署加密的应用,请按照下列步骤操作:
1. 创建应用程序。 我们的示例包含一个主应用类和两个帮助程序类(分别称为 foo 和 bar)。 这个应用没有做太多的实际工作,但只要我们可以加密这个应用,我们就可以加密其他应用。
2. 生成安全密钥。 在命令行上,使用 generatekey 工具(请参阅 generatekeyj**a)将密钥写入文件:
% j**a generatekey key.data
3.加密应用程序。 在命令行上,使用 encryptclasses 工具(请参阅 encryptclasses.)。j**a)加密应用程序的类:
% j**a encryptclasses key.data app.class foo.class bar.class
该命令将每个。 类文件将替换为其各自的加密版本。
4. 运行加密的应用程序。 用户通过 decryptstart 程序运行加密的应用程序。 decryptstart 过程如清单 6 所示。
清单 6 decryptstart.j**a,启动加密应用程序的程序。
import j**a.io.*;import j**a.security.*;import j**a.lang.reflect.*;import j**ax.crypto.*;import j**ax.crypto.spec.*;公共类 DecryptStart 扩展了类加载器主过程:这是我们读取密钥并创建 DecryptStart 实例的地方,这是我们的自定义类加载器。 设置好类加载器后,我们用它来加载应用实例,最后通过 j**a 反射 API 调用应用实例的 main 方法 static public void main( string args throws exception ; method main = clasz.getmethod( "main", mainargs );创建一个包含 main() 方法对象参数的数组 argsarray = ; system.err.println( "[decryptstart: running "+appname+".main()]" );调用 main() maininvoke( null, argsarray );public class loadclass( string name, boolean resolve ) 抛出 classNotFoundException }catch( fileNotFoundException fnfe ) 必需的第 2 步:如果上述操作不成功 我们尝试使用默认的类加载器加载它 if (clasz == null) clasz = findsystemclass( name );必需步骤 3:如有必要,加载相关类 if (resolve &&clasz!= null) resolveclass( clasz );将类返回给调用方 return clasz; }catch( ioexception ie ) catch( generalsecurityexception gse )
对于未加密的应用,正常执行如下:
% j**a app arg0 arg1 arg2
对于加密应用,相应的操作为:
% j**a decryptstart key.data app arg0 arg1 arg2
DecryptStart 有两个用途。 decryptstart 的实例是实现即时解密的自定义类加载器; 同时,decryptstart 还包括一个主进程,该进程创建解密器的实例并使用它来加载和运行应用程序。 示例应用包含在应用中j**a、foo.J**A 和 Barj**a. util.j**a 是一个文件 I o 工具,本文中的许多示例都使用了该工具。 如需完整**请从本文末尾**开始。
我们看到,在不修改源代码的情况下加密 J**a 应用程序很容易。 但是,没有完全安全的系统。 本文中的加密提供了一定程度的源保护,但它容易受到某些攻击。
虽然应用本身是加密的,但启动器 decryptstart 不是。 攻击者可以反编译启动程序并对其进行修改,以将解密的类文件保存到磁盘。 降低这种风险的一种方法是以高质量混淆启动过程。 或者,启动程序可以直接编译为机器语言,使启动程序具有传统可执行文件格式的安全性。
同样重要的是要记住,大多数 JVM 本质上并不安全。 狡猾的黑客可以修改 JVM,从类加载器外部获取解密的 **,并将其保存到磁盘,绕过本文的加密技术。 J**A没有为此提供真正有效的补救措施。
但是,应该注意的是,所有这些可能的攻击都有一个前提,即攻击者可以获取密钥。 如果没有密钥,应用程序的安全性完全取决于加密算法的安全性。 虽然这种保护方法**并不完美,但它仍然是保护知识产权和敏感用户数据的有效方法。