从零开发基于ASM字节码的Java代码混淆插件XHood
项目背景
因在公司负责基础框架的开发设计,所以针对框架源代码的保护工作比较重视,之前也加入了一系列保护措施
例如自定义classloader加密保护,授权license保护等,但都是防君子不防小人,安全等级还比较低
经过调研各类加密混淆措施后,决定自研混淆插件,自主可控,能够贴合实际情况进行定制化,达到框架升级后使用零感知,零影响。
快速开始
项目地址:https://gitee.com/code2roc/xhood
在线文档:https://code2roc.gitee.io/xhood/#/
<build>
<plugins>
<plugin>
<groupId>com.code2roc</groupId>
<artifactId>xhood</artifactId>
<version>1.0.0</version>
<executions>
<execution>
<goals>
<goal>obscure</goal>
</goals>
<phase>compile</phase>
</execution>
</executions>
<configuration>
<!--配置项目根包名 -->
<packageName>com.xxx.xxx</packageName>
<!--配置忽略项目启动类 -->
<obscureIgnoreClasss>com.xxx.xxx.Application</obscureIgnoreClasss>
</configuration>
</plugin>
</plugins>
</build>
方案设计
我们首先要清除代码混淆要实现什么,就是将原代码名称结构和内容使用一系列的规则码替换
达到阅读困难,理解困难,恢复困难的作用
混淆的事项包括方法,成员变量,临时变量,方法参数,常量,类,包,枚举
这些事项的混淆还需要遵循固定的顺序,因为事项之间还存在相互引用的情况
在完成结构混淆(类文件,包名)后,需要删除对应的原class文件
混淆前后的效果如下图所示
方案实现
pom引用
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm</artifactId>
<version>9.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-commons</artifactId>
<version>9.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-util</artifactId>
<version>9.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-tree</artifactId>
<version>9.0</version>
</dependency>
<dependency>
<groupId>org.ow2.asm</groupId>
<artifactId>asm-analysis</artifactId>
<version>9.0</version>
</dependency>
名称混淆
名称混淆指的是把类名,方法名,参数名,变量名等定义的名称进行规则码替换,以混淆方法名为例
-
混淆方法定义
自定义ClassVisitor重写visit方法
过滤枚举类的方法
过滤main方法,过滤lambda表达式方法,过滤构造函数方法
过滤非混淆范围内接口的实现方法
过滤非混淆范围内父类的重写方法
-
混淆方法调用
自定义MethodVisitor重写visitMethodInsn,visitInvokeDynamicInsn方法
visitMethodInsn修混淆方法定义中的方法
visitInvokeDynamicInsn修改接口的实现方法和父类的重写方法(混淆范围内且混淆方法定义中的方法)
结构混淆
结构混淆指的是修改类名,包名时对实体class文件和文件夹的重命名,以混淆类名为例
-
混淆类定义
自定义ClassVisitor重写visit方法
过滤非混淆范围内的class
重写visitSource,visitField,visitMethod,visitInnerClass,visitOuterClass等方法
-
混淆类定义调用
自定义MethodVisitor重写visitMethodInsn,visitFieldInsn,visitFrame,visitInvokeDynamicInsn等方法
-
混淆类重命名
定义ClassWriter获取class文件byte数组重新写入文件
注解混淆
注解的混淆比较特殊,需要继承AnnotationVisitor类来重写visit方法实现
针对注解中有枚举需要重写visitEnum方法
针对嵌套注解需要重写visitAnnotation方法
针对注解有参数有数组的,需要重写visitArray方法
visitAnnotation和visitArray方法需要返回AnnotationVisitor对象,调用super方法后返回自定义AnnotationVisitor对象递归处理即可
混淆规则
无论混淆哪一部分,我们总是要根据一个名称例如abc混淆后得到一个固定的规则码例如123
这时候我们会想到md5这种固定输入对应固定输出的信息摘要算法
md5内容太长,我们需要截取某几位进行简化
简化后的规则码在待混淆内容越多时越容易碰撞,需要需要动态调整,简单递归即可,最坏结果就是完整的md5表示
public static String getTakeName(String name, int takeLimit, HashMap<String, String> typeMap) {
String obscureName = getObscureName(name);
if (takeLimit <= 16) {
obscureName = obscureName.substring(8, 24);
}
String takeName = obscureName.substring(0, takeLimit);
if (typeMap.containsValue(takeName)) {
takeName = getTakeName(name, takeLimit + 1, typeMap);
}
return takeName;
}
注意事项
开发事项
在混淆过程中,需要针对不同的情况进行细节处理,举例如下
- 混淆名称中有相同部分的优先排序替换长度最长的部分
例如方法名HandleMethod和Handle两部分,Handle对应的规则码为123,我先替换Handle部分变成了123Method和123,那么123Method这个方法混淆后就会定义错误
- 临时变量和方法变量都会调用MethodVisitor的visitLocalVariable方法,需要区分
先定义ParamterAdapter继承MethodVisitor重写visitParameter记录方法变量
使用事项
在springboot项目中,我们需要进行一些配置避免导致项目无法运行或运行错误**
-
所有需要通过接口返回的实体类需要忽略,例如数据库实体DO
-
通过ConfigurationProperties映射的yml文件配置项类需要忽略
-
通过类名/字段名反射调用的类需要忽略
-
针对@Aspect注解切面进行了兼容,参照如下写法则混淆无影响
PointCut注解/类需要指定全名,Around注解指定方法名
@Aspect @Component public class RepeatSubmitAspect { /** * 切面点 指定注解 */ @Pointcut("@annotation(com.code2roc.fastboot.framework.submitlock.SubmitLock) " + "|| @within(com.code2roc.fastboot.framework.submitlock.SubmitLock)") public void repeatSubmitAspect() { } /** * 拦截方法指定为 repeatSubmitAspect */ @Around("repeatSubmitAspect()") public Object around(ProceedingJoinPoint point) throws Throwable { return point.proceed(); } }