文章目录

  • 概述
  • 资源准备
  • 环境准备(简单操作可跳过)
  • 改造编译插件
  • 改造Tomcat源码
  • 改造Spring源码
  • 环境测试


概述

 本文主要是介绍如何通过改造Maven-war-plugin插件,Spring源码,Tomcat容器以达到代码加密解密的效果。这里选择war包+原生Tomcat的部署方式来进行讲解,其他形式可自主实验,原理大致相同。大致流程如下:项目代码通过Maven-war-plugin插件对编译(java compile)完成的项目打包,会对目标目录的资源进行复制,完成war包打包,在复制过程中对其class加密,已达到防止反编译的效果。war包部署到tomcat启动后,tomcat类加载器加载Web应用的class文件到JVM中,在类加载最开始的加载过程,对读取的class文件进行解密。Spring容器类加载过程相同,相应地在Spring读取class文件进行解密。

资源准备

 这里主要以Spring项目来进行讲解,需要准备Maven-war-plugin(2.6版本),Spring(5.2.x版本),Tomcat(8.5.73版本)对应版本的源码,还需下载对应版本的Tomcat安装包(8.5.73版本),版本可自主选择。

Maven,JDK环境默认已安装
Maven-war-plugin:源码下载 Spring:源码下载 Tomcat:源码/软件包下载

环境准备(简单操作可跳过)


jar包防止反编译 java java打包防止反编译_jar包防止反编译 java


 创建的工程比较简单,只是为了方便演示代码加密的效果,通过访问localhost:8080/hello接口前端回显“hello”字样。

 这里主要讲解下Tomcat部署war包的过程。


 1.将下载的Tomcat软件安装包apache-tomcat-8.5.73.zip解压


jar包防止反编译 java java打包防止反编译_java_02


 2.进入/apache-tomcat-8.5.73/bin目录,双击startup.bat启动,访问localhost:8080,能成功访问安装成功!!!


jar包防止反编译 java java打包防止反编译_jar_03

首次启动窗口日志为乱码,可以修改/apache-tomcat-8.5.73/conf/logging.properties文件,将字符编码全部改成GBK即可。

 3.修改配置文件

 修改/apache-tomcat-8.5.73/conf/server.xml


jar包防止反编译 java java打包防止反编译_apache_04


 4.编译CodeEncryption工程,打包成war包部署


jar包防止反编译 java java打包防止反编译_spring_05


 5.将CodeEncryption-1.0-SNAPSHOT.war解压到步骤3配置路径

jar包防止反编译 java java打包防止反编译_jar包防止反编译 java_06


再次启动Tomcat,访问localhost:8080/hello

jar包防止反编译 java java打包防止反编译_apache_07


 到这里部署环境就OK了,下面就进入正题。

改造编译插件

 将下载的插件源码包maven-war-plugin-2.6-source-release.zip解压,导入idea。

 工程结构如下:

jar包防止反编译 java java打包防止反编译_apache_08


 改造插件源码最主要的是找到复制目标目录资源的方法入口。

 下面是Web工程中对Maven插件的配置项:

<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>2.6</version>
                <configuration>
                    <warSourceDirectory>src\main\webapp</warSourceDirectory>
                    <failOnMissingWebXml>false</failOnMissingWebXml>
                </configuration>
            </plugin>
        </plugins>
    </build>

 对项目打包package可以看到日志信息,可以发现日志打印了复制资源的信息,我们在插件源码全局搜索Copying webapp resources定位到复制资源的代码位置:

jar包防止反编译 java java打包防止反编译_jar_09


 到这里我们就找到了插件在复制资源的入口,就是copyFiles方法:

jar包防止反编译 java java打包防止反编译_jar_10


jar包防止反编译 java java打包防止反编译_jar_11


 我们只用看正常复制文件的分支,确定代码改造位置。

jar包防止反编译 java java打包防止反编译_java_12


 进入copyFile方法,

jar包防止反编译 java java打包防止反编译_jar_13

 对于文件插件会直接对其复制到指定目录,所以我们需要在这之前对文件进行改造。改造文件的对象就是class文件。怎么对指定class文件进行改造处理?这里选择异或运算对其处理。原因:A ^ 0XFF ^ 0XFF = A,两次异或后的结果为它本身。这里就选定对0XFF做异或运算。


jar包防止反编译 java java打包防止反编译_jar包防止反编译 java_14


 改造后的代码如下图,这里只是简易的处理,主要是演示效果,可自行优化。

protected boolean copyFile(WarPackagingContext context, File source, File destination, String targetFilename,
                               boolean onlyIfModified)
            throws IOException {
        if (onlyIfModified && destination.lastModified() >= source.lastModified()) {
            context.getLog().debug(" * " + targetFilename + " is up to date.");
            return false;
        } else {
            if (source.isDirectory()) {
                context.getLog().warn(" + " + targetFilename + " is packaged from the source folder");

                try {
                    JarArchiver archiver = context.getJarArchiver();
                    archiver.addDirectory(source);
                    archiver.setDestFile(destination);
                    archiver.createArchive();
                } catch (ArchiverException e) {
                    String msg = "Failed to create " + targetFilename;
                    context.getLog().error(msg, e);
                    IOException ioe = new IOException(msg);
                    ioe.initCause(e);
                    throw ioe;
                }
            } else {
                // 只对工程源码编译的class文件处理
                if (source.getAbsolutePath().indexOf("youngqinger") != -1 && source.getName().endsWith(".class")) {
                    context.getLog().info("Read class file ==> " + source.getName());
                    long len = source.length();
                    byte[] data = new byte[(int) len];
                    try (FileInputStream fileInputStream = new FileInputStream(source)) {
                        int read = fileInputStream.read(data);
                        if (read != len) {
                            throw new IOException("Read class file:" + source + " length error");
                        }
                    }
                    byte[] result = new byte[(int) len];

                    for (int i = 0; i < data.length; i++) {
                        byte value = data[i];
                        // A ^ 0xAC ^ 0XAC = A
                        result[i] = (byte) (value ^ 0XFF);
                    }

                    try (FileOutputStream fileOutputStream = new FileOutputStream(source)) {
                        fileOutputStream.write(result);
                    }
                }
                FileUtils.copyFile(source.getCanonicalFile(), destination);
                // preserve timestamp
                destination.setLastModified(source.lastModified());
                context.getLog().debug(" + " + targetFilename + " has been copied.");
            }
            return true;
        }
    }

 将源码编译打包,得到改造后的插件jar包,重命名防止与中央仓库的jar包混淆:maven-war-plugin-2.6-encrypt.jar。手动将改造后的jar包发布到Maven本地仓库:mvn install:install-file -DgroupId=org.apache.maven.plugins -DartifactId=maven-war-plugin -Dversion=2.6 -Dpackaging=jar -Dfile=maven-war-plugin-2.6-encrypt.jar。

之前是想改下坐标以区别原坐标,但是发现执行命令时,会校验插件坐标和版本名称,导致不能使用改造插件,故与原坐标同名。

jar包防止反编译 java java打包防止反编译_java_15


 导入改造的插件,编译打包后:

jar包防止反编译 java java打包防止反编译_java_16

 重新部署war包,启动Tomcat,可以看到一场:It is not a Java .class file,在class文件格式校验时报错,说明加密后的class文件已经不符合class文件规范了。这时就需要对称的改造Tomcat,来解密加密后的class文件。

jar包防止反编译 java java打包防止反编译_jar包防止反编译 java_17

改造Tomcat源码

编译Tomcat源码->可参考文档:

 这里默认编译环境OK,不再介绍。就不带大家去找程序入口了,直接改造。org.apache.catalina.webresource.FileResource是Tomcat去加载class文件或资源的类。

jar包防止反编译 java java打包防止反编译_jar_18


jar包防止反编译 java java打包防止反编译_jar_19


 简单的改造代码:

@Override
    protected InputStream doGetInputStream() {
        if (needConvert) {
            byte[] content = getContent();
            if (content == null) {
                return null;
            } else {
                return new ByteArrayInputStream(content);
            }
        }
        try {
            if(resource.getAbsolutePath().indexOf("youngqinger") != -1 && resource.getName().endsWith(".class")){
                byte[] content = getContent();
                return new ByteArrayInputStream(content);
            }
            return new FileInputStream(resource);
        } catch (FileNotFoundException fnfe) {
            // Race condition (file has been deleted) - not an error
            return null;
        }
    }
@Override
    public final byte[] getContent() {
        // Use internal version to avoid loop when needConvert is true
        long len = getContentLengthInternal(false);

        if (len > Integer.MAX_VALUE) {
            // Can't create an array that big
            throw new ArrayIndexOutOfBoundsException(sm.getString(
                    "abstractResource.getContentTooLarge", getWebappPath(),
                    Long.valueOf(len)));
        }

        if (len < 0) {
            // Content is not applicable here (e.g. is a directory)
            return null;
        }

        int size = (int) len;
        byte[] result = new byte[size];

        int pos = 0;
        try (InputStream is = new FileInputStream(resource)) {
            while (pos < size) {
                int n = is.read(result, pos, size - pos);
                if (n < 0) {
                    break;
                }
                pos += n;
            }
        } catch (IOException ioe) {
            if (getLog().isDebugEnabled()) {
                getLog().debug(sm.getString("abstractResource.getContentFail",
                        getWebappPath()), ioe);
            }
            return null;
        }

        byte[] decrypt = new byte[size];
        if(resource.getAbsolutePath().indexOf("youngqinger") != -1 && resource.getName().endsWith(".class")){
            for (int i = 0; i < result.length; i++) {
                byte value = result[i];
                decrypt[i] = (byte) (value ^ 0XFF);
            }
            result = decrypt;
        }

        if (needConvert) {
            // Workaround for certain files on platforms that use
            // EBCDIC encoding, when they are read through FileInputStream.
            // See commit message of rev.303915 for original details
            // https://svn.apache.org/viewvc?view=revision&revision=303915
            String str = new String(result);
            try {
                result = str.getBytes(StandardCharsets.UTF_8);
            } catch (Exception e) {
                result = null;
            }
        }
        return result;
    }

 通过Ant编译打包,这个class文件打包后的目标jar包就是catalina.jar,所以我们只用替换掉Tomcat安装包,lib目录下得catalina.jar即可,现在我们替换掉原来的jar包,重新部署启动Tomcat。启动完之后发现,仍然有异常。观察日志发现是由于Spring容器去解析class文件出错,而且直接指明了是ASM ClassReader去解析的。继续改造Spring…


jar包防止反编译 java java打包防止反编译_java_20

jar包防止反编译 java java打包防止反编译_apache_21

改造Spring源码

 编译Spring源码需要Grandle环境,默认已安装成功。全局搜下ClassReader(asm包下),定位到改造位置,看到在SimpleMetadataReader类实例化了ClassReader,Spring通过Resource接口获取输入流,定位到FileSystemResource获取输入流的位置,对class文件的字节进行解密。改造代码后,编译打包到目标jar(spring-core-5.2.x.BUILD-SNAPSHOT.jar),加载到本地仓库中:mvn install:install-file -DgroupId=com.youngqinger.springframework -DartifactId=spring-core -Dversion=decrypt -Dpackaging=jar -Dfile=spring-core-5.2.x.BUILD-SNAPSHOT.jar。
 主要改造两个类:

  1. org.springframework.core.io.FileSystemResource
  2. org.springframework.cglib.core.ReflectUtils

org.springframework.core.io.FileSystemResource改造代码:

/**
	 * 获取读取资源的输入流
	 */
	@Override
	public InputStream getInputStream() throws IOException {
		try {
			// TODO 改造位置
			String paths = this.filePath.toFile().getAbsolutePath();
			if (paths.indexOf("youngqinger") != -1 && paths.endsWith("class")) {
				InputStream input = new FileInputStream(this.filePath.toFile());
				byte[] byt = new byte[input.available()];
				byte[] value = new byte[input.available()];
				input.read(byt);

				for (int i = 0; i < byt.length; i++) {
					value[i] = (byte)(byt[i] ^ 0XFF);
				}
				return new ByteArrayInputStream(value);
			}
			return Files.newInputStream(this.filePath);
		}
		catch (NoSuchFileException ex) {
			throw new FileNotFoundException(ex.getMessage());
		}
	}

org.springframework.cglib.core.ReflectUtils改造代码:

@SuppressWarnings("deprecation")  
	public static Class defineClass(String className, byte[] b, ClassLoader loader,
			ProtectionDomain protectionDomain, Class<?> contextClass) throws Exception {

		Class c = null;
		Throwable t = THROWABLE;

		// Preferred option: JDK 9+ Lookup.defineClass API if ClassLoader matches
		if (contextClass != null && contextClass.getClassLoader() == loader &&
				privateLookupInMethod != null && lookupDefineClassMethod != null) {
			try {
				MethodHandles.Lookup lookup = (MethodHandles.Lookup)
						privateLookupInMethod.invoke(null, contextClass, MethodHandles.lookup());
				c = (Class) lookupDefineClassMethod.invoke(lookup, b);
			}
			catch (InvocationTargetException ex) {
				Throwable target = ex.getTargetException();
				if (target.getClass() != LinkageError.class && target.getClass() != IllegalArgumentException.class) {
					throw new CodeGenerationException(target);
				}
				// in case of plain LinkageError (class already defined)
				// or IllegalArgumentException (class in different package):
				// fall through to traditional ClassLoader.defineClass below
				t = target;
			}
			catch (Throwable ex) {
				throw new CodeGenerationException(ex);
			}
		}

		// Classic option: protected ClassLoader.defineClass method
		if (c == null && classLoaderDefineClassMethod != null) {
			if (protectionDomain == null) {
				protectionDomain = PROTECTION_DOMAIN;
			}

			// TODO 改造位置
			if(className.indexOf("youngqinger") != -1){
				byte[] value = new byte[b.length];
				for (int i = 0; i < b.length; i++) {
					value[i] = (byte) (b[i] ^ 0XFF);
				}
				b = value;
			}

			Object[] args = new Object[]{className, b, 0, b.length, protectionDomain};
			try {
				if (!classLoaderDefineClassMethod.isAccessible()) {
					classLoaderDefineClassMethod.setAccessible(true);
				}
				c = (Class) classLoaderDefineClassMethod.invoke(loader, args);
			}
			catch (InvocationTargetException ex) {
				throw new CodeGenerationException(ex.getTargetException());
			}
			catch (Throwable ex) {
				// Fall through if setAccessible fails with InaccessibleObjectException on JDK 9+
				// (on the module path and/or with a JVM bootstrapped with --illegal-access=deny)
				if (!ex.getClass().getName().endsWith("InaccessibleObjectException")) {
					throw new CodeGenerationException(ex);
				}
				t = ex;
			}
		}

Spring版本不一样,获取class文件输入流的入口也不同,根据具体版本来改造。

<dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>5.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <!-- 使用自定义core依赖 -->
        <dependency>
            <groupId>com.youngqinger.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>decrypt</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <version>5.2.5.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework</groupId>
                    <artifactId>spring-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

环境测试

 重新刷新pom导入本地仓库改造jar包后,编译打包,重新部署。

可能会遇到下面问题,在示例工程导入commons-logging依赖可解决

jar包防止反编译 java java打包防止反编译_jar包防止反编译 java_22

能够成功访问:

jar包防止反编译 java java打包防止反编译_apache_23

 感谢浏览,欢迎指正!!!