目录
摘要
整体描述
过程分解
1、创建动态链接库
2、 SpringBoot 生成可执行 jar 包
3、 Maven 自定义插件
3.3.1、 自定义插件
3.3.2、 创建Jar 包加密工具
3.3.3、 插件与加密工具整合
3.3.4、插件打成依赖 Jar 包
4、插件应用
参考文献
摘要
由于业务需要,需要将提供给客户的可执行 Jar 包源码加密处理;基本要求:不能改变原有的逻辑和代码结构;团队研发人员无感知。最终实现过程可以概括为:
1、使用Maven工具,执行打包命令,生成加密 jar 包;
2、java -jar 命令直接运行加密后的 jar 包。
本文章参考了 Xjar 项目的加密方式,结合自定义 Maven 插件和JVMTI,最终实现了上述过程。
整体描述1、SpringBoot 项目使用 Maven 工具自动构建可执行 jar 包;
2、(主要过程)实现加密 jar 包的过程本质上是将 Maven 构建的可执行 jar 包进一步封装,对 jar 包中的 class 文件进行加密处理;
Tip: 具体的加密算法根据不同需求场景自由定义
3、C/C++ 创建动态链接库 encrypt.dll(window平台,linux平台为 encrypt.so),通过以下命令参数匹配动态链接库,并实现 class 文件的解密运行过程
过程分解 1、创建动态链接库java -agentlib:encrypt -jar your-executeable-jar-file.jar
windows
gcc -shared encrypt.c -o encrypt.dll -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/win32"linux
gcc -shared encrypt.c -o encrypt.so -I "{JAVA_HOME}/include" -I "{JAVA_HOME}/include/linux"
将通过以上指令生成的动态链接库文件(.dll 或 .so)放到系统路径下;
Java 可通过以下方式查看系统路径:
System.getProperty("java.library.path")
2、 SpringBoot 生成可执行 jar 包encrypt.c 文件:
由 C/C++ 语言编写的代码,用于生成动态链接库,通过 java agent 方式实现 class 文件解密过程
完整代码
#include#include #include "jni.h" #include #include // 加密方法 void encode(char *str) { unsigned int len = strlen(str); for (int i = 0; i < len; i ++) { str[i] = str[i] ^ 0x07; } } // jni 接口实现 JNIEXPORT jbyteArray JNICALL Java_com_wyhw_plugin_encrypt_encrypt_JarEncryptUtil_encrypt(JNIEnv * env, jclass cls, jbyteArray bytes) { char* dst = (char *) ((*env) -> GetByteArrayElements(env, bytes, 0)); encode(dst); (*env) -> SetByteArrayRegion(env, bytes, 0, strlen(dst), (jbyte *) dst); return bytes; } // class 文件解密 void JNICALL ClassDecryptHook(jvmtiEnv *jvmti_env, JNIEnv* jni_env, jclass class_being_redefined, jobject loader, const char* name, jobject protection_domain, jint class_data_len, const unsigned char* class_data, jint* new_class_data_len, unsigned char** new_class_data) { //printf("ClassDecryptHook Files: %sn", (char *) name); *new_class_data_len = class_data_len; (*jvmti_env) -> Allocate(jvmti_env, class_data_len, new_class_data); unsigned char* _data = *new_class_data; if (name && strncmp(name, "com/zk/sdk", 10) == 0 && strstr(name, "$$") == NULL) { //printf("nnhit: %sn", (char *) name); //printf("len=%dnn", class_data_len); for (int i = 0; i < class_data_len; ++i) { _data[i] = class_data[i]; } encode(_data); } else { for (int i = 0; i < class_data_len; ++i) { _data[i] = class_data[i]; } } } JNIEXPORT jint JNICALL Agent_onLoad(JavaVM *vm, char *options, void *reserved) { jvmtiEnv *jvmtiEnv; jint ret = (*vm) -> GetEnv(vm, (void **)&jvmtiEnv, JVMTI_VERSION); if (JNI_OK != ret) { printf("ERROR: Fail get jvmtiEvn!n"); return ret; } jvmtiCapabilities jvmtiCapabilities; (void) memset(&jvmtiCapabilities, 0, sizeof(jvmtiCapabilities)); jvmtiCapabilities.can_generate_all_class_hook_events = 1; jvmtiCapabilities.can_tag_objects = 1; jvmtiCapabilities.can_generate_object_free_events = 1; jvmtiCapabilities.can_get_source_file_name = 1; jvmtiCapabilities.can_get_line_numbers = 1; jvmtiCapabilities.can_generate_vm_object_alloc_events = 1; jvmtiError error = (*jvmtiEnv) -> AddCapabilities(jvmtiEnv, &jvmtiCapabilities); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Unable to AddCapabilities JVMTI!n"); return error; } jvmtiEventCallbacks callbacks; (void) memset(&callbacks, 0, sizeof(callbacks)); callbacks.ClassFileLoadHook = &ClassDecryptHook; error = (*jvmtiEnv) -> SetEventCallbacks(jvmtiEnv, &callbacks, sizeof(callbacks)); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Fail setEventCallBacks!n"); return error; } error = (*jvmtiEnv) -> SetEventNotificationMode(jvmtiEnv, JVMTI_ENABLE, JVMTI_EVENT_CLASS_FILE_LOAD_HOOK, NULL); if (JVMTI_ERROR_NONE != error) { printf("ERROR: Fail SetEventNotificationMode!n"); return error; } return JNI_OK; }
在项目 pom.xml 文件中添加 spring-boot-maven-plugin 插件
org.springframework.boot spring-boot-maven-plugin
之后会在 Maven 插件处出现 spring-boot 插件
运行 mvn clean package 命令,生成可执行 jar 包(一般放在项目 target 目录下)
3、 Maven 自定义插件Tip:Maven 命令 clean 和 package 所涉及的生命周期不同,可以参考相关文档 参考文献
本插件的目标是对 spring-boot-maven-plugin 插件生成的可执行 jar 包进行二次处理,实现 class 字节码文件加密处理。
3.3.1、 自定义插件引入 pom 依赖
org.apache.maven.plugin-tools maven-plugin-annotations3.4 provided org.apache.maven maven-core2.2.1
1、创建自定义 Mojo 类 EncryptMojo;
2、继承 org.apache.maven.plugin.AbstractMojo 抽象类;
3、类文件需要添加 org.apache.maven.plugins.annotations.Mojo 注解
4、实现 execute() 方法(加密过程就是在这里实现的)
3.3.2、 创建Jar 包加密工具EncryptMojo.java 完整代码
package com.wyhw.plugins.plugin.encrypt; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugin.logging.Log; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; @Mojo( name = "encrypt", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME ) public class EncryptMojo extends AbstractMojo { @Parameter(defaultValue = "${project}", readonly = true) private MavenProject project; @Parameter(property = "encrypt.on", defaultValue = "true") private boolean on; @Parameter(property = "encrypt.over", defaultValue = "false") private boolean over; @Parameter(property = "encrypt.suffix", defaultValue = "-entry") private String archiveSuffix; public void setOn(String on) { this.on = Boolean.valueOf(on); } public void setOver(String over) { this.over = Boolean.valueOf(over); } public void setArchiveSuffix(String archiveSuffix) { this.archiveSuffix = archiveSuffix; } public void execute() throws MojoExecutionException, MojoFailureException { Log log = getLog(); if (!on) { log.info("Disable encrypt package !!!"); return; } String originalArchivePath = project.getArtifact().getFile().getAbsolutePath(); log.info("Original file path: " + originalArchivePath); int lastSeparatorIndex = originalArchivePath.lastIndexOf("."); String fileName = originalArchivePath.substring(0, lastSeparatorIndex); String extendName = originalArchivePath.substring(lastSeparatorIndex + 1); String newFileName = fileName + archiveSuffix + "." + extendName; log.info("Encrypt file path: " + newFileName); try { if (over) { log.info("Overwrite original file"); newFileName = originalArchivePath; } JarEncryptUtil.encrypt(originalArchivePath, newFileName); } catch (Exception e) { log.error("自动加密失败", e); } } }
引入 pom 依赖
org.apache.commons commons-compressLATEST
直接上代码
JarEncryptUtil .java
package com.wyhw.plugins.plugin.encrypt; import org.apache.commons.compress.archivers.jar.JarArchiveEntry; import org.apache.commons.compress.archivers.jar.JarArchiveInputStream; import org.apache.commons.compress.archivers.jar.JarArchiveOutputStream; import java.io.*; import java.util.jar.Manifest; import java.util.logging.Logger; public class JarEncryptUtil { static { System.loadLibrary("encrypt"); } private static Logger logger = Logger.getGlobal(); public native static byte[] encrypt(byte[] bytes); public static void encrypt(String src, String desc) throws Exception { File srcFile = new File(src); File descFile = new File(desc); try (FileInputStream fis = new FileInputStream(srcFile); FileOutputStream fos = new FileOutputStream(descFile); JarArchiveInputStream jis = new JarArchiveInputStream(fis); JarArchiveOutputStream jos = new JarArchiveOutputStream(fos);) { UnclosedInputStream nis = new UnclosedInputStream(jis); UnclosedOutputStream nos = new UnclosedOutputStream(jos); JarArchiveEntry jarArchiveEntry; while ((jarArchiveEntry = jis.getNextJarEntry()) != null) { long time = jarArchiveEntry.getTime(); String name = jarArchiveEntry.getName(); JarArchiveEntry descJarEntry = new JarArchiveEntry(name); descJarEntry.setTime(time); if (jarArchiveEntry.isDirectory()) { jos.putArchiveEntry(descJarEntry); } else if (name.equals("meta-INF/MANIFEST.MF")) { Manifest manifest = new Manifest(nis); jos.putArchiveEntry(descJarEntry); manifest.write(nos); } else if (name.startsWith("BOOT-INF/classes/")) { jos.putArchiveEntry(descJarEntry); if (name.contains("com/zk/") && name.endsWith(".class")) { transfer(encryptCus(nis), nos); logger.info(name + " 文件已加密"); } else { transfer(nis, nos); } } else if (name.startsWith("BOOT-INF/lib/")) { descJarEntry.setMethod(JarArchiveEntry.STORED); descJarEntry.setSize(jarArchiveEntry.getSize()); descJarEntry.setCrc(jarArchiveEntry.getCrc()); jos.putArchiveEntry(descJarEntry); transfer(nis, nos); } else { jos.putArchiveEntry(descJarEntry); transfer(nis, nos); } jos.closeArchiveEntry(); } jos.finish(); } } private static InputStream encryptCus(InputStream nis) throws IOException { try (BufferedInputStream bis = new BufferedInputStream(nis); ByteArrayOutputStream bos = new ByteArrayOutputStream();) { byte[] buffer = new byte[4096]; int len; while ((len = bis.read(buffer)) != -1) { bos.write(buffer, 0, len); } bos.flush(); byte[] bytes = bos.toByteArray(); encrypt(bytes); return new ByteArrayInputStream(bytes); } } private static void transfer(InputStream in, OutputStream out) throws IOException { byte[] buffer = new byte[4096]; int length; while ((length = in.read(buffer)) != -1) { out.write(buffer, 0, length); } out.flush(); } }
UnclosedOutputStream.java
package com.wyhw.plugins.plugin.encrypt; import java.io.IOException; import java.io.OutputStream; public class UnclosedOutputStream extends OutputStream { private final OutputStream out; public UnclosedOutputStream(OutputStream out) { this.out = out; } @Override public void write(int b) throws IOException { out.write(b); } @Override public void write(byte[] b) throws IOException { out.write(b); } @Override public void write(byte[] b, int off, int len) throws IOException { out.write(b, off, len); } @Override public void flush() throws IOException { out.flush(); } @Override public void close() { } }
3.3.3、 插件与加密工具整合UnclosedInputStream .java
package com.wyhw.plugins.plugin.encrypt; import java.io.IOException; import java.io.InputStream; public class UnclosedInputStream extends InputStream { private final InputStream in; public UnclosedInputStream(InputStream in) { this.in = in; } @Override public int read() throws IOException { return in.read(); } @Override public int read(byte[] b) throws IOException { return in.read(b); } @Override public int read(byte[] b, int off, int len) throws IOException { return in.read(b, off, len); } @Override public long skip(long n) throws IOException { return in.skip(n); } @Override public int available() throws IOException { return in.available(); } @Override public void mark(int readLimit) { in.mark(readLimit); } @Override public void reset() throws IOException { in.reset(); } @Override public boolean markSupported() { return in.markSupported(); } @Override public void close() { } }
在 execute() 方法中直接调用加密工具方法:
JarEncryptUtil.encrypt(originalArchivePath, newFileName);3.3.4、插件打成依赖 Jar 包
需要加密的项目在使用自定义 Maven 插件时,需要引入插件 Jar 包;所以需要将此插件打包成供第三方项目依赖的 Jar 包
引入打包插件:
maven-plugin-plugin 3.6.0 default-addPluginArtifactmetadata package addPluginArtifactmetadata default-descriptor process-classes descriptor
执行 maven 命令:
mvn clean package
至此,已经完成了加密插件的定义;
4、插件应用在需要加密的第三方项目 pom.xml 文件中引入依赖:
com.wyhw.plugin encrypt-maven-plugin0.0.1-SNAPSHOT
配置打包插件
在 spring-boot-maven-plugin 插件之后配置自定义加密插件(spring-boot-maven-plugin 插件和自定义的加密插件都是处于 Maven 生命周期中 package 阶段的插件,插件执行顺序跟配置的前后顺序一致;只有先执行 spring-boot-maven-plugin 插件,再执行自定义加密插件,加密过程才能正常执行)
org.springframework.boot spring-boot-maven-plugincom.wyhw.plugin encrypt-maven-plugin0.0.1-SNAPSHOT encrypt
执行 package 命令,生成加密 jar 包
mvn clean package
加密成功后,控制台会打印出相应文件加密成功的日志信息
参考文献"C:Program FilesJavajdk1.8.0_211binjava.exe" -Dmaven.multiModuleProjectDirectory=E:spaceideasdk "-Dmaven.home=D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3" "-Dclassworlds.conf=D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3binm2.conf" "-javaagent:D:IntelliJ IDEA 2019.1.2libidea_rt.jar=57987:D:IntelliJ IDEA 2019.1.2bin" -Dfile.encoding=UTF-8 -classpath "D:IntelliJ IDEA 2019.1.2pluginsmavenlibmaven3bootplexus-classworlds-2.5.2.jar" org.codehaus.classworlds.Launcher -Didea.version2019.1.2 -DskipTests=true package [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building sdk 0.0.1-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ sdk --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Using 'UTF-8' encoding to copy filtered properties files. [INFO] Copying 1 resource [INFO] Copying 0 resource [INFO] [INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ sdk --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- maven-resources-plugin:3.2.0:testResources (default-testResources) @ sdk --- [INFO] Using 'UTF-8' encoding to copy filtered resources. [INFO] Using 'UTF-8' encoding to copy filtered properties files. [INFO] skip non existing resourceDirectory E:spaceideasdksrctestresources [INFO] [INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ sdk --- [INFO] Nothing to compile - all classes are up to date [INFO] [INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ sdk --- [INFO] Tests are skipped. [INFO] [INFO] --- maven-jar-plugin:3.2.0:jar (default-jar) @ sdk --- [INFO] Building jar: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT.jar [INFO] [INFO] --- spring-boot-maven-plugin:2.5.3:repackage (repackage) @ sdk --- [INFO] Replacing main artifact with repackaged archive [INFO] [INFO] --- zk-common-plugins:0.0.1-SNAPSHOT:encrypt (default) @ sdk --- [INFO] Original file path: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT.jar [INFO] Encrypt file path: E:spaceideasdktargetsdk-0.0.1-SNAPSHOT-encrypt.jar 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt 信息: BOOT-INF/classes/com/zk/sdk/core/util/CRC32Util.class 文件已加密 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt 信息: BOOT-INF/classes/com/zk/sdk/core/wrapper/RGBTriple.class 文件已加密 一月 04, 2022 7:11:42 下午 com.zk.common.plugins.encrypt.JarEncryptUtil encrypt 信息: BOOT-INF/classes/com/zk/sdk/SdkApplication.class 文件已加密 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.235 s [INFO] Finished at: 2022-01-04T19:11:42+08:00 [INFO] Final Memory: 27M/408M [INFO] ------------------------------------------------------------------------ Process finished with exit code 0
JVMTI 官方文档:JVM(TM) Tool Interface 1.2.3
XJar 开源项目:GitHub - core-lib/xjar: Spring Boot JAR 安全加密运行工具,支持的原生JAR。
Maven: Maven 的生命周期
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)