【转载】Java应用加解密方案

2023-02-12  本文已影响0人  andycheng

成也Class,败也Class

Java应用加解密方案 - 墨天轮 (modb.pro)

Java自95年发布以来,借由“一次编译,到处运行”的特性,在企业级开发中获得了巨大的成功。

而相对的,在桌面应用上却一直没有起色。虽然目前看来桌面应用的需求越来越小,因为web应用的体验越来越接近桌面应用了。

但实际上,即使桌面应用的需求量依然很大,Java也很难有大起色。因为帮助它在J2EE上取得成功的特性,在J2SE上反而成为了绊脚石!

虽然由于上面的种种原因导致Java并不是很适合桌面型软件,但是在实际环境里还是有需要将软件部署到客户端端机器上的情况。为了保护自己辛苦编写的软件不被轻易破解,就需要进行相应的加密处理。

加密思路

实际上,只要时间足够长,任何密码都是可以被破解的。所以如果一个密码在软件有效期内不被破解,这就算是一个成功的加密!比如,你卖的软件有效期十年(有多少软件能用十年的?!),密码破解需要十年零一天,那这种加密方式是合适的!所以,加密的最经济的目的不是为了防止破解,而是为了加大破解难度,使得在软件有效期内不被破解即可!

在Java领域,有不少的加密方式,其中用得比较多的就是混淆了!下面一一进行列举!

解决方案

混淆

最简单的方式就是混淆,市面上有各种混淆工具,开源的、商业的。

混淆的作用就是把class文件中,原本有意义的名字改成a,b,c这种没有意义的名字,来增加逻辑破解的难度。

平台语言

JVM上目前有200多种语言,比如:scala,clojure,kotlin,jruby这些语言,可以使用这些语言进行编写,生成的class自带”混淆”功能!

因为不是标准的Java代码编译的,所以相同的逻辑生成的class和用Java编写所生成的class差异较大,如果不熟悉对应语言,相应的破解也就比较麻烦了。

Java9

Java9里提供了,Jlink 工具和 AOT(预先编译技术)。可以把 java 程序编译成可执行的二进制文件。

二进制文件相对class文件破解难度要大很多,无形中就实现了加密。不过Java9什么时候正式发布,还是个未知数~

Class文件加解密

如果要自己实现Class的加解密,那最先想到的方法就是对Class文件进行加密,然后在执行的时候对Class进行解密了。

加密没什么好说的,选择一种加密方式直接对Class文件进行加密就可以了!

主要的工作在执行时对Class进行解密!网上比较多的是基于ClassLoader的方式,其实也可以通过Instrumentation的方式进行解密。

此方式对于正常的Java应用程序来说没什么问题,但是对于依赖了某些第三方库的程序,由于第三方库实现了自己的类加载机制,需要做额外处理,详见自定义类加载的处理

基于ClassLoader

基于ClassLoader的问题是,你需要做各种适配,对Main的、对Jetty的、对Tomcat的。

如果你的应用是通过Main来执行的,那可以自定义一个ClassLoader,继承URLClassLoader,覆写它的findClass方法。主要就是里面获取Class字节码的逻辑

public Class<?> findClass(final String name) throws ClassNotFoundException {
    ...
    Resource res = ucp.getResource(path, false);   //这里就是Class的数据,先对其做解密,再走后续流程  
    ...
}

编写完成后,通过此ClassLoader来启动你的应用程序。

对于Jetty需要实现一个ClassLoader,继承org.eclipse.jetty.webapp.WebAppClassLoader,同样的覆写findClass方法,方式和上面的相同。

编写完成后,需要将此类打包成jar,放到${jetty_home}/lib/ext下,修改contexts目录下,对应应用的xml文件

<Configure id="mycontext1" class="org.eclipse.jetty.webapp.WebAppContext">
  ...  <Set name="classLoader">
          <New class="ClassLoader全限定名">
          <Arg><Ref id="mycontext1"/></Arg></New>
  </Set>
  ...</Configure>

这样的修改还是有问题的,因为Jetty自己还有一套类加载机制,通过asm直接读取的Class,主要是用来解析annotation,此修改对asm无效,相应修改见自定义类加载的处理

基于Instrumentation

Java5开始提供了Instrumentation功能,使用 Instrumentation,可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。

这里需要做的就是在读取Class的时候进行解密,代码很简单

public class PreMain {    public static void premain(String agentArgs, Instrumentation inst)
            throws ClassNotFoundException, UnmodifiableClassException {
        inst.addTransformer(new Transformer());
    }
}
class Transformer implements ClassFileTransformer {    public byte[] transform(ClassLoader l, String className, Class<?> c,
                            ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {        if (DecryptClass.isEncrypted(b)) {            try {
                b = DecryptClass.decryptClass(b);
            } catch (Throwable t) {
                t.printStackTrace();
            }
        }        return b;
    }
}

配置MANIFEST.MF

Premain-Class: A.B.C.PreMain

在运行应用时,添加-javaagent:path2PreMain.jar即可

自定义类加载的处理(基于Instrumentation)

上面的方案只是处理了JVM自己的Class的加载逻辑,像Tomcat/Jetty这类服务器有一套自己的类加载机制,Jsp的加载也有一套自己的机制,以及Spring,dubbo这样的第三方库,也有自己的一套类加载机制,这里分别给出解决方案。

整体思路是通过javassist[^注1]对相应的加载方法进行增强,增加解密逻辑!

这三个放在一起,是因为它们使用的都是asm的ClassReader来进行类加载的,不过Spring对asm进行了自行封装,实际就只是改了包名而已!

看下ClassReader的代码,大概就知道加载逻辑

//主要逻辑就是这个构造方法,其余的构造方法最后都会调用这个构造方法public ClassReader(byte[] var1, int var2, int var3) {
        ....
}

第一个参数就是Class的字节数组,只要在对这个byte[]进行解密处理就可以了。使用Instrumentation结合javassist[^注1]对其做个增强就可以了。

if (className.endsWith("org/springframework/asm/ClassReader")
                || className.endsWith("org/objectweb/asm/ClassReader")) {            try {
                ClassPool classPool = ClassPool.getDefault();
                ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(b);
                CtClass clazz = classPool.makeClass(byteArrayInputStream);
                CtConstructor[] constructors = clazz.getDeclaredConstructors();                for (CtConstructor ctConstructor : constructors) {
                    CtClass[] ctClasses = ctConstructor.getParameterTypes();                    if (ctClasses.length == 3) {                        return fixConstruct(clazz, ctConstructor);
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
private byte[] fixConstruct(CtClass clazz, CtConstructor constructor) {        try {
            StringBuffer sb = new StringBuffer();            //进行解密
            sb.append("$1=decryptIfNessary($1);\n");
            sb.append("$3=$1.length;\n");
            constructor.insertBefore(sb.toString());            return clazz.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }        return null;
    }

Jsp的读取使用了org.eclipse.jdt.internal.compiler.classfmt.ClassFileReader!

//核心逻辑在这个构造方法里public ClassFileReader(byte[] classFileBytes, char[] fileName, boolean fullyInitialize) throws ClassFormatException {
    ... 
}

和上面一样的方案,对classFileBytes进行解密即可!

dubbo使用了javassist.bytecode.ClassFile!所以需要使用javassist对javassist的ClassFile类进行增强。由于是Web项目,打破了双亲委托模型[^注2],所以可以使用上层的javassist对下层的javassist进行增强。

//这里的流是ClassFile流,进行处理即可public ClassFile(DataInputStream in) throws IOException {
        read(in);
}

处理逻辑依然相同,不过这里处理的是流!

Class文件加解密的问题和解决

眼尖的应该已经看出对Class文件进行加解密的问题了,应该说是致命的!因为加解密本身就是使用Java的,这段Java没法进行加密操作,也就是加解密逻辑是暴露在用户眼底的,只要找到这段逻辑就知道了加解密逻辑了。当然,这段逻辑可以使用JNI来进行本地化.但是对字节数组和流的处理还是在Java里,导致的问题是,用户可以在此处做处理,将解密后的字节数组输出,就可以获取到解密后的Class了!

所以,此方法建议和混淆一起使用,用于进一步增加破解难度!

底层方案

其实,Java9已经给出了方案,就是编译成二进制!但是目前Java9还没正式发布!

所以如果自行处理的话,最安全的做法,就是对JVM的native的defineClass进行处理!

private native Class<?> defineClass0(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd);private native Class<?> defineClass1(String name, byte[] b, int off, int len,
                                         ProtectionDomain pd, String source);private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,                                         int off, int len, ProtectionDomain pd,
                                         String source);

也就是对这三个方法的byte[]或ButeBuffer进行解密操作。这就需要去修改JVM底层C代码了。成本也相对较高!

终极方案

上面的方法均是使用技术手段进行加密操作。而实际终极方案可以不使用技术手段,可以通过法律手段来解决!当然成本就更高了!

总结

就目前来看,Java还是没有一个完美的加密方案,如上的方案都有或多或少的缺陷。或破解容易,或成本较高。

Java9通过模块化、Jlink和AOT应该能给出一个较经济的方法来解决这个问题!

java

上一篇 下一篇

猜你喜欢

热点阅读