美文网首页
【转载】Java应用加解密方案

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

作者: andycheng | 来源:发表于2023-02-12 10:15 被阅读0次

    成也Class,败也Class

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

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

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

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

    • 虚拟机:虚拟机不是Java独创的,实际上在Java之前就有语言使用了虚拟机。但是由于硬件跟不上,速度太慢而放弃了。Java正好赶上了硬件的发展足以支撑虚拟机的时期。但是在很长一段时间里,速度慢依然是Java被诟病的原因。速度慢的一个原因就是因为要先启动虚拟机。这在J2EE领域不算问题,因为应用不会频繁的启动。但是在桌面程序领域就不同了,使用完就关掉了,要使用的时候就再次打开。但是如果每次打开都要很久,谁能受得了?

    • 一次编译,到处运行:这是一个美好的愿望,初期由于awt的失败,导致大家戏谑的称Java为“一次编写,到处修改”,因为awt在各个系统上的展示有差异。后来通过swing,JavaFx1,JavaFx2在桌面展示方面的完善,展示部分的问题算是解决了。但是,有一个硬伤一直无法解决,也是解决不了的。那就是Class文件,为了做到“一次编译,到处运行”,就需要有统一结构,有统一结构就容易破解。Java自己就直接提供了反编译工具。这在J2EE也不算事,可以说是好事。首先,J2EE运行在服务端,用户是看不到源码的,所以也就没法反编译。同时,由于是同一结构且结构紧凑,可以通过网络的形式远程获取Class进行执行。而在J2SE,这就是个大问题了。大家都知道盗版软件在国内猖獗,虽然商业软件有各种加密手段,但还是阻挡不了破解!用C,C++写的二进制程序尚且如此,如果软件是用Java写的,那破解起来就更简单了,因为软件在客户机上,直接把代码反编译就可以了。可能这就是Java写的客户端软件没有市场的一个重要原因!

    虽然由于上面的种种原因导致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

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

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

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

    • Jetty

    对于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]对相应的加载方法进行增强,增加解密逻辑!

    • Tomcat/Jetty/Spring

    这三个放在一起,是因为它们使用的都是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

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

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

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

    • dubbo

    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

    相关文章

      网友评论

          本文标题:【转载】Java应用加解密方案

          本文链接:https://www.haomeiwen.com/subject/rmqdkdtx.html