0%

JAR包加密class文件防止反编译

前言

通常来说,将代码打成jar包后,可以通过反编译工具得到源码。这对于代码隐蔽性是极大的挑战。常用的加密方式有代码混淆、编译加密运行动态解密、魔改JVM等;

本文采用第二种方式;

编译期加密

前言

首先要知道,代码变为jar包需要经过编译的;maven将编译后的class文件统一打成jar包。但是这样,就等于泄露了代码源文件。所以需要在maven编译时做出调整,将指定的class替换成加密后的class,然后再打进jar包;

maven插件

通过自定义maven插件进行的实现

首先我们来看pom.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<groupId>top.bubuzi</groupId>
<artifactId>encryption-maven-plugin</artifactId>
<!-- 注意,这里的打包模式必须是maven-plugin -->
<packaging>maven-plugin</packaging>
<version>bubuzi.changsha.RS1</version>
<!-- maven插件开发需引入的依赖 -->
<!-- 版本随你的maven版本变动 -->
<dependencies>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>

编写插件加密类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package top.bubuzi;

import top.bubuzi.SafeLoaderUtil;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
* @goal deepcompile
**/
//@Mojo(name = "brakeFramePlugin")
public class FramePlugin extends AbstractMojo {
// 这里前三个成员变量注释请勿修改,这是读取maven配置的固定写法
// 当然也有使用@Mojo的写法,需要引入额外的依赖,这里不做多介绍
/**
* @parameter property="protectClassNames" default-value=""
*/
private List<String> protectClassNames;
/**
* @parameter property="noCompileClassNames" default-value=""
*/
private List<String> noCompileClassNames;
/**
* @parameter property="output" default-value="${project.build.directory}"
*/
private String root;
private File output;
private List<String> protectClassNameList = new ArrayList<>();


@Override
public void execute() throws MojoExecutionException, MojoFailureException {
getLog().info("执行jar包加密操作");
this.output = new File(root);
try {
this.protectCore(this.output);
} catch (IOException e) {
e.printStackTrace();
}
}

private void protectCore(File root) throws IOException {
if (root.isDirectory()) {
for (File file : root.listFiles()) {
protectCore(file);
}
}
if (root.getName().endsWith(".class")) {
String className = root.getName().replace(".class", "");
//class筛选
boolean flag = false;
if (protectClassNames != null && protectClassNames.size() > 0) {
for (String item : protectClassNames) {
if (className.equals(item)) {
flag = true;
}
}
}
if (noCompileClassNames.contains(className)) {
boolean deleteResult = root.delete();
if (!deleteResult) {
System.gc();
deleteResult = root.delete();
}
getLog().info(String.format("[noCompile-deleteResult]:%s", deleteResult));
}
if (flag && !protectClassNameList.contains(className)) {
protectClassNameList.add(className);
getLog().info(String.format("[protectCore]:%s", className));
FileOutputStream fos = null;
try {
final byte[] instrumentBytes = doProtectCore(root);
//加密后的class文件保存路径
String folderPath = output.getAbsolutePath() + "\\" + "classes";
File folder = new File(folderPath);
if (!folder.exists()) {
folder.mkdir();
}
folderPath = output.getAbsolutePath() + "\\" + "classes" + "\\" + "coreclass";
folder = new File(folderPath);
if (!folder.exists()) {
folder.mkdir();
}
String filePath = output.getAbsolutePath() + "\\" + "classes" + "\\" + "coreclass" + "\\" + className + ".class";
getLog().info("[filePath]: " + filePath);
File protectFile = new File(filePath);
if (protectFile.exists()) {
protectFile.delete();
}
protectFile.createNewFile();
fos = new FileOutputStream(protectFile);
fos.write(instrumentBytes);
fos.flush();
} catch (Exception e) {
getLog().info("[protectCore-exception]:" + className);
e.printStackTrace();
} finally {
if (fos != null) {
fos.close();
}
if (root.exists()) {
boolean deleteResult = root.delete();
if (!deleteResult) {
System.gc();
deleteResult = root.delete();
}
getLog().info("[protectCore-deleteResult]:" + deleteResult);
}
}
}
}
}

private byte[] doProtectCore(File clsFile) throws MojoExecutionException {
try {
FileInputStream inputStream = new FileInputStream(clsFile);
byte[] bytes = new byte[(int) clsFile.length()];
getLog().info("origin length:" + bytes.length);
inputStream.read(bytes);
// jdk9+
// byte[] bytes = inputStream.readAllBytes();
// SafeLoaderUtil.encryption加密为JNI调用的C++程序,可以采用不同实现
byte[] encipherByte = SafeLoaderUtil.encryption(bytes);
inputStream.close();
return encipherByte;
} catch (Exception e) {
throw new MojoExecutionException("doProtectCore error", e);
}
}

}

代码种的SafeLoaderUtil.encryption的JNI调用方法可参考上一篇文章

maven插件打包安装

一定要安装至本地!!!

一定要安装至本地!!!

一定要安装至本地!!!

image-20220809092513798

加密项目配置

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<build>
<plugins>
<!-- 引入maven加密插件 -->
<plugin>
<groupId>top.bubuzi</groupId>
<artifactId>encryption-maven-plugin</artifactId>
<version>bubuzi.changsha.RS1</version>
<executions>
<!-- 必须填写,设置编译器动作 -->
<execution>
<phase>compile</phase>
<goals>
<goal>deepcompile</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 这里可改为全类名,暂时未优化 -->
<!-- 写入需要加密的类名称 -->
<protectClassNames>
<protectClassName>LoginServiceImpl</protectClassName>
</protectClassNames>
</configuration>
</plugin>
</plugins>
</build>

打包

配置好pom后,就可以愉快的打包了

可以看见,日志中正确输出了打包相关内容。

image-20220809093251630

项目结构里,LoginServiceImpl类应该存在于impl包下

image-20220809093411656

查看jar包内结构

使用压缩包的模式打开jar

image-20220809093533570

image-20220809093548178

image-20220809093632308

image-20220809093702549
可以看见,本应在impl包下的LoginServiceImpl类,去了coreclass文件夹下

这就是加密完成

通过idea反编译加密class文件,可以看见根本无法查阅

image-20220809095217583

解密

运行时解密

在这里我查阅了许多资料,始终没能找到使我满意的解答方式;

这与springboot的加载机制有关系,起初我想用classloader的方式动态加载,一直未能成功,感兴趣的同志可以去了解以下springboot的加载机制,还有要注意jdk版本问题,jdk8及以下与jdk8以上的类加载机制有部分变化;

最终采用的一种折中方案;

我们都知道,spring会将beanClass封装成一个BeanDefinition对象,然后再通过bean别名,一一对应的方式注册进IOC容器;

所以,我们只需要在spring容器启动后,手动将加密类注册进容器,后续bean创建时,就会使用我们注入的class;

解密类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
public class StaticEncryptLoader extends ClassLoader {
private static StaticEncryptLoader INSTANCE;

// spring bean 默认bean工厂
@Setter
private DefaultListableBeanFactory beanFactory;
// 用于存储class类
private static final Map<String, Class> classes = new ConcurrentHashMap();

static {
// jni调用,请查看上一章
System.loadLibrary("EncryptionLoader");
}

private StaticEncryptLoader(ClassLoader parent){
super(parent);
}



public static StaticEncryptLoader getInstance(ClassLoader parent){
if(INSTANCE == null){
synchronized (StaticEncryptLoader.class){
if(INSTANCE == null){
INSTANCE = new StaticEncryptLoader(parent);
}
}
}
return INSTANCE;
}
public static StaticEncryptLoader getInstance() {
return INSTANCE;
}

/**
* 用于加载所有加密类
*/
public void start(){
try {
loadClasses2Cache();
// checkPool.scheduleWithFixedDelay(() -> {
// try {
// loadClasses2Cache();
// } catch (IOException e) {
// e.printStackTrace();
// }
// }, 60, 60, TimeUnit.SECONDS);
} catch (IOException e) {
e.printStackTrace();
}
}

public void loadClasses2Cache() throws IOException {
Resource[] resources = new PathMatchingResourcePatternResolver().getResources("/coreclass/**/*.class");
for(Resource resource : resources) {
byte pack[] = null;
InputStream inputStream = resource.getInputStream();
int count = 0;
while(count == 0){
count = inputStream.available();
}
pack = new byte[count];
int readCount = 0;
while (readCount < count) {
readCount += inputStream.read(pack, readCount, count - readCount);
}
byte[] decryption = SafeLoaderUtil.decryption(pack);
Class<?> aClass = null;
try {
aClass = defineClass("top.bubuzi.service.impl.LoginServiceImpl", decryption, 0, decryption.length);
} catch (ClassFormatError classFormatError) {
classFormatError.printStackTrace();
}
// 由于现在只写了一个加密类,直接全类名注入了,实际操作时,需要将全路径匹配为全类名
// 如,现在为coreclass/LoginServiceImpl,后续调整为,coreclass
classes.put("top.bubuzi.service.impl.LoginServiceImpl",aClass);

}
}

public void registryBeans() {
if (!CollectionUtils.isEmpty(classes) && beanFactory != null) {
classes.forEach(
(k, v) -> {
registryBean(k);
}
);
}
}
/**
* 向spring容器注入beanDefinition
*
* @param className 全限定名 (com.xxx.xx.XXXX)
*/
private void registryBean(String className) {
Class cla = classes.get(className.replace(".class", "").replaceAll("/", "."));
if (SpringUtil.isSpringBeanClass(cla) && beanFactory != null) {
BeanDefinition beanDefinition = SpringUtil.buildBeanDefinition(cla);
//将变量首字母置小写
String beanName = StringUtils.uncapitalize(className);
beanName = beanName.substring(beanName.lastIndexOf(".") + 1);
beanName = StringUtils.uncapitalize(beanName);
// 注意,这里的beanName为ioc容器自动注入提供服务,请确保你的容器名称是否匹配
safeRegistry(className, beanDefinition);
safeRegistry(beanName, beanDefinition);
if(beanName.endsWith("Impl") ){
safeRegistry(beanName.replace("Impl",""), beanDefinition);
}
}
}
private void safeRegistry(String name,BeanDefinition beanDefinition){
if(!beanFactory.containsBean(name)){
beanFactory.registerBeanDefinition(name,beanDefinition);
}else{
Object bean = beanFactory.getBean(name);
System.out.println(name);
}
}
}

容器加载类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class SpringRegistry implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
StaticEncryptLoader instance = StaticEncryptLoader.getInstance();
if(instance==null){
return;
}
instance.setBeanFactory((DefaultListableBeanFactory) beanFactory);
if(!(beanFactory.getBeanClassLoader() instanceof StaticEncryptLoader)){
beanFactory.setBeanClassLoader(instance);
}
instance.registryBeans();
}
}

启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication(scanBasePackages = "top.bubuzi")
@MapperScan("top.bubuzi.mapper")
public class PlatformApplication {

/**
* 加载加密类元信息
*/
static {
StaticEncryptLoader instance = StaticEncryptLoader.getInstance(PlatformApplication.class.getClassLoader());
instance.start();
}

public static void main(String[] args) {
SpringApplication.run(PlatformApplication.class, args);
}
}

启动后正常无报错即加密成功

后文

本方法只能避免jar包直接被反编译,不能实现完全的加密防护,实际上百分百防护是做不到的;

但是这里依旧有优化项点:

*    不采用beanFactory,而采用classLoader的方式
*    加密classLoader,隐藏解密程序,classLoader采用分片注入程序的方式
*    核心代码用安全性高的语言实现,如c++实现后jni调用
*    魔改JVM加载机制

希望本文能帮到你

Donate comment here.