JAVA混淆代码ProGuard

2,675 阅读12分钟

ProGuard 介绍

ProGuard是GuardSquare公司于2002年发布的免费的开源软件。 ProGuard 是一种命令行工具,可通过压缩字节码和混淆类、字段和方法的名称来减小应用程序的大小。 适用于Java/Kotlin 应用程序的免费开源收缩器。 它非常适合主要对 Android 优化器感兴趣的使用 Java 或 Kotlin 的开发人员。 目前已成为Google Android SDKs的一部分,是Google官方认证的免费Android 移动应用程序Applcation的保护工具,ProGuard为Android 应用程序和SDKs提供最基本的保护。

官网

官网地址 www.guardsquare.com

什么是代码混淆,它是如何工作的?

代码混淆的目标是防止任何未经授权的方 访问和深入了解应用程序的逻辑,从而防止他们提取数据、篡改代码、利用漏洞等。 可以使用现成的反汇编器和/或反编译器对移动应用程序进行逆向工程,使黑客可以轻松访问和分析应用程序的源代码。 然后黑客可以:窃取知识产权和克隆应用程序提取敏感信息并收集凭证识别漏洞向应用程序添加恶意代码并重新打包敏感数据可能包括;有价值的知识产权(如自定义算法)、身份验证机制、应用内支付机制、密钥(API 密钥、硬编码加密密钥等)、凭证(数据库密码等)、服务器通信背后的逻辑等等。

解决方案

代码混淆 是使应用程序难以或不可能反编译或反汇编的过程,并且检索到的应用程序代码更难被人类解析。 应用程序开发人员必须强化各个层的代码。这是实现保护移动应用程序中的敏感数据和财产所需的保护级别的唯一方法。混淆是更广泛的移动应用程序屏蔽策略的一部分。 应用程序屏蔽是使黑客更难对应用程序进行逆向工程或修改的过程的广义术语。有多种技术可用于应用程序屏蔽,包括代码混淆和其他代码加固技术,以及运行时移动应用程序自我保护(RASP)

代码混淆的类型

目前有多种技术可用于混淆代码。 其中包括:名称混淆定义:用难以破译的替代品替换代码中可读的名称控制流混淆定义: 修改代码的逻辑结构,使其更不可预测和可追溯算术混淆定义: 将简单的算术和逻辑表达式转换为复杂的等价物代码虚拟化定义: 将方法实现转化为随机生成的虚拟机的指令

工作原理

输入罐子 = 程序编写需要混淆的 jars (or aars, wars, ears, zips, apks, or directories)

库罐 = 依赖其他库(spring、mybatis 等)

image.png

Proguard的工作流程由Shrink(缩小)、Optimize(优化)、Obfuscate(混淆)、Preverify(预先校验)四个步骤组成,每个步骤都是可选的,我们可以通过配置脚本决定执行其中的哪几个步骤。这里引入一个EntryPoint概念,EntryPoint是在ProGuard过程中保存不会被处理的类或方法。在压缩过程中,Proguard从EntryPoint出发,递归检索,删除那些没有使用到的类和类的成员。在优化过程中,那些非EntryPoint的类和方法会被设置成private,static或final,没有使用到的参数会被移除,有些方法可能会被标记为内联的。在混淆过程中,会对非EntryPoint的类和类的成员进行重命名,也就是用其它无意义的名称代替。我们在配置文件中用-keep保留的部分属于EntryPoint,所以不会被重命名

其他混淆器

Allatori Java 混淆器

Allatori(非开源软件)是一个 Java obfuscator 属于第二代 obfuscators 的家庭,因此它的所有频谱的机会保护你的知识产权。 Allatori 具有以下几种保护方式:命名混淆,流混淆,调试信息混淆,字符串编码,以及水印技术。对于教育和非商业项目来说这个混淆器是免费的。 支持jar包与maven pom文件混淆

使用

ProGuard 用法有很多如Java 类文件收缩器、优化器、混淆器和预验证器。因此,ProGuard 处理的应用程序和库更小、更快。这里只介绍混淆使用。

  • 缩小步骤检测并删除未使用的类、字段、方法和属性。
  • 优化器步骤优化字节码并删除未使用的指令。
  • 名称混淆步骤使用无意义的短名称重命名剩余的类、字段和方法。
  • 最后的预验证步骤将预验证信息添加到类中,这是 Java Micro Edition 和 Java 6 及更高版本所必需的。

maven 依赖

官网maven插件 wvengen.github.io/proguard-ma…

1. pom.xml 增加配置

增加proguard混淆插件

<build>
    <plugins>
        <!--proguard混淆插件-->
        <plugin>
            <groupId>com.github.wvengen</groupId>
            <artifactId>proguard-maven-plugin</artifactId>
            <version>2.5.3</version>
            <executions>
                <execution>
                    <!--打包的时候开始混淆-->
                    <phase>package</phase>
                    <goals>
                        <goal>proguard</goal>
                    </goals>
                </execution>
            </executions>

            <configuration>
                <proguardVersion>7.2.1</proguardVersion>
                <!-- 指定要混淆的jar -->
                <injar>${project.build.finalName}.jar</injar>
                <!-- 混淆后输出的jar -->
                <outjar>${project.build.finalName}.jar</outjar>
                <!-- 是否混淆 -->
                <obfuscate>true</obfuscate>
                <!-- 指定混淆内容。两种方式(1.在pom文件内增加。2.通过配置文件配置 proguard.cfg) -->
                <proguardInclude>${basedir}/proguard.cfg</proguardInclude>
                <!--java 11-->
                <!-- <libs>
                     <lib>${java.home}/lib/rt.jar</lib>
                 </libs>-->
                <!--java 8-->
                
                <!-- 第一种方式 pom指定混淆内容 -->
                <!-- 第二种方式 读取文件 proguard.cfg -->
                <!-- 
                <options>
                    <!-- 不做优化(变更代码实现逻辑)-->
                    <option>-dontoptimize</option>
                    <!--不路过非公用类文件及成员-->
                    <option>-dontskipnonpubliclibraryclasses</option>
                    <option>-dontskipnonpubliclibraryclassmembers</option>
                    <!--优化时允许访问并修改有修饰符的类和类的成员-->
                    
                    <option>-keep public class * {
                        public protected *;
                        }
                    </option>
                </options>
                -->
                <libs>
                    <lib>${java.home}/lib/rt.jar</lib>
                    <lib>${java.home}/lib/jsse.jar</lib>
                </libs>
            </configuration>
            <dependencies>
                <dependency>
                    <groupId>com.guardsquare</groupId>
                    <artifactId>proguard-base</artifactId>
                    <version>7.2.1</version>
                </dependency>
            </dependencies>
        </plugin>
    </plugins>
</build>

混淆方式

1. 第一种方式 pom指定混淆内容
<configuration>
    <proguardVersion>7.2.1</proguardVersion>
    <!-- 指定要混淆的jar -->
    <injar>${project.build.finalName}.jar</injar>
    <!-- 混淆后输出的jar -->
    <outjar>${project.build.finalName}.jar</outjar>
    <!-- 是否混淆 -->
    <obfuscate>true</obfuscate>
    <options>
        <!-- 不做优化(变更代码实现逻辑)-->
        <option>-dontoptimize</option>
        <!--不路过非公用类文件及成员-->
        <option>-dontskipnonpubliclibraryclasses</option>
        <option>-dontskipnonpubliclibraryclassmembers</option>
        <!--优化时允许访问并修改有修饰符的类和类的成员-->
        
        <option>-keep public class * {
            public protected *;
            }
        </option>
    </options>
</configuration>
2. 第二种方式 读取文件 proguard.cfg

根据 与 pom.xml 同级 proguard.cfg

#关闭压缩,默认打开,不做收缩(删除注释、未被引用代码)-->
-dontshrink

#关闭优化默认打开,这里关闭字节码级别的优化-->
-dontoptimize

#保持目录结构,否则spring的自动注入无法使用-->
-keepdirectories

#对于类成员的命名的混淆采取唯一策略-->
-useuniqueclassmembernames

#混淆时不生成大小写混合的类名,默认是可以大小写混合-->
-dontusemixedcaseclassnames

#混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
-adaptclassstrings

#指定保留保留的参数名称和方法类型 ,controller如果参数也混淆会导致传参映射不上  -->
-keepparameternames

#此选项将保存接口中的所有原始名称(不混淆)-->
-keepnames interface * { *; }

#此选项将保存所有软件包中的所有原始接口文件(不进行混淆)-->
-keep interface * extends * { *; }

#保留枚举成员及方法
-keepclassmembers enum * { *; }

# 删除了执行代码通常不需要的属性。不混淆所有特殊的类:对异常、注解信息在runtime予以保留,不然影响springboot启动
# Signature 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
# Annotation* 保护代码中的Annotation不被混淆
# InnerClasses 指定类与其内部类和外部类之间的关系。
# MethodParameters 指定方法参数的名称和访问标志, 所有类(包括接口)的方法参数不混淆(包括没被keep的),如果参数混淆了,mybatis的mapper参数绑定会出错(如#{id})
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,LocalVariable*Table,*Annotation*,SyntheticjEnclosingMethod,MethodParameters

#入口程序类不能混淆,混淆会导致springboot启动不了
-keep class com.wind.springproguard.SpringProguardApplication {
        public static void main(java.lang.String[]);
     }

# 不混淆 aop 因为 aop 会根据完整包名查找
# ControllerAspect 进行切面
-keeppackagenames com.wind.springproguard.web

 # 不混淆 get set 方法, 如果混淆了,会找不到对一个的方法,浏览器访问接口有可能出现这个异常 HttpMediaTypeNotAcceptableException
-keepclassmembers public class *{void set*(***);*** get*();}
-keep class com.wind.springproguard.service.impl.*

# entity 保留全部类
-keep class com.wind.springproguard.entity.**

# aop aspect 这个方法名不能修改,修改后找不到此方法了
-keep class com.wind.springproguard.config.ControllerAspect {
        public void aspect();
     }
3. 图形界面操作混淆

混淆工具ProGuard GUI 图形化混淆操作 下载地址 github.com/Guardsquare…

2. 修改启动类

由于需要混淆代码,混淆后类都是A B C,spring 默认是把A B C当成BeanName,BeanName又不能重复导致报错。需要修改bean生成策略

public class SpringProguardApplication {
    public static void main(String[] args) {
        SpringApplicationBuilder sab;
        try {
            SpringApplication app = new SpringApplication(SpringProguardApplication.class);
            /**
             * spring中的DefaultListableBeanFactory类有个属性:allowBeanDefinitionOverriding,
             * 默认情况下为true,即允许重名的bean可以被覆盖。
             */
            app.setAllowBeanDefinitionOverriding(true);
            app.setBeanNameGenerator(new UniqueNameGenerator());
            app.run(args);
        } catch (Exception e) {
            throw e;
        }
    }

    /**
     * 由于需要混淆代码,混淆后类都是A B C,spring 默认是把A B C当成BeanName,BeanName又不能重复导致报错
     * 所以需要重新定义BeanName生成策略
     * 不能重写generateBeanName方法,因为有些Bean会自定义BeanName,所以这些情况还需要走原来的逻辑
     */
    @Component("UniqueNameGenerator")
    public static class UniqueNameGenerator extends AnnotationBeanNameGenerator {
        /**
         * 重写buildDefaultBeanName
         * 其他情况(如自定义BeanName)还是按原来的生成策略,只修改默认(非其他情况)生成的BeanName带上包名
         */
        @Override
        public @NotNull String buildDefaultBeanName(BeanDefinition definition) {
            //全限定类名
            return Objects.requireNonNull(definition.getBeanClassName());
        }
    }
}    

3. 混淆基础

官方混淆文档 www.guardsquare.com/manual/conf…

1. 类名

对类名进行keep操作只是将类名keep住,但方法和变量仍然会被混淆

# 一颗星表示keep当前本包下的类名,子包下的类名是会被混淆的
-keep class com.example.hensen.*
# 两颗星表示keep当前本包下的类名和子包下的类名
-keep class com.example.hensen.**
# 表示keep当前类名
-keep class com.example.hensen.net.NetWorkCache
# 表示keep当前类的内部类的类名
-keep class com.example.hensen.net.NetWorkCache$NetWorkBean

2. 内容

对内容进行keep操作不仅可以将类名keep住,还可以对方法和变量keep住

# 一颗星表示keep当前本包下的类名、类的内容
-keep class com.example.hensen.*{*;}
# 两颗星表示keep当前本包下的类名、类的内容和子包下的类名、类的内容
-keep class com.example.hensen.**{*;}
# 表示keep当前类名、类的内容
-keep class com.example.hensen.net.NetWorkCache{*;}
# 表示keep当前类的内部类的类名、内部类的内容
-keep class com.example.hensen.net.NetWorkCache$NetWorkBean{*;}

3. 特定内容

对特定的内容进行keep操作

-keep class com.example.hensen.net.NetWorkCache{
    <init>;# 匹配所有构造器
    <fields>;# 匹配所有变量
    <methods>;# 匹配所有方法

    public <methods>;# 匹配所有共有的方法
    private <methods>;# 匹配所有私有的方法
    public *;# 匹配所有共有的内容
    private *;# 匹配所有私有的内容
    public <init>(java.lang.String);# 匹配特定参数的构造函数
    public void getCache(...);# 匹配任意长度类型参数的方法
}

4. 类成员

对类名不需要keep,只需要对类下的方法进行keep操作

# 表示keep特定类下的特定参数的方法,但类名不会被keep
-keepclassmembernames class com.example.hensen.net.NetWorkCache{
    public void getCache(java.lang.String);
}

5. 类和类成员

作用范围keep所指定类、成员keep所指定类、成员(前提是在压缩阶段没有被删除)
类和类成员-keep-keepnames
仅类成员-keepclassmembers-keepclassmembernames
类和类成员(前提是成员都存在)-keepclasseswithmembers-keepclasseswithmembernames

6. 其他场景

# 指定文件为映射文件,包括类和类成员的混淆名称,文件未提及的类和类成员将生成新的名称
-applymapping mapping.txt
# 指定一个文本文件,其中所有有效字词都用作混淆字段和方法名称
-obfuscationdictionary obfuscationdictionary.txt
# 指定一个文本文件,其中所有有效词都用作混淆类名
-classobfuscationdictionary obfuscationdictionary.txt

# 混淆时不生成大小写混合的类名
-dontusemixedcaseclassnames
# 不忽略指定jars中的非public calsses
-dontskipnonpubliclibraryclasses
# 不忽略指定类库的public类成员(变量和方法)
-dontskipnonpubliclibraryclassmembers

# 混淆过程中打印详细信息,如果异常终止则打印整个堆栈信息
-verbose
# 忽略警告继续处理
-ignorewarnings

# 不对指定的类、包中的不完整的引用发出警告
-dontwarn android.support.v4.**
-dontwarn All

# 避免混淆内部类、泛型、匿名类
-keepattributes InnerClasses,Signature,EnclosingMethod
# 抛出异常时保留代码行号    
-keepattributes SourceFile,LineNumberTable
# 保留注释成员变量,如Activity被@Override注释的方法onCreate、onDestroy方法
-keepattributes *Annotation*
# 资源类变量需要保留
-keep public class **.R$*{
   public static final int *;
}

#此选项将保存接口中的所有原始名称(不混淆)-->
-keepnames interface * { *; }

#此选项将保存所有软件包中的所有原始接口文件(不进行混淆)-->
-keep interface * extends * { *; }

#保留枚举成员及方法
-keepclassmembers enum * { *; }

# 删除了执行代码通常不需要的属性。不混淆所有特殊的类:对异常、注解信息在runtime予以保留,不然影响springboot启动
# Signature 避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
# Annotation* 保护代码中的Annotation不被混淆
# InnerClasses 指定类与其内部类和外部类之间的关系。
# MethodParameters 指定方法参数的名称和访问标志, 所有类(包括接口)的方法参数不混淆(包括没被keep的),如果参数混淆了,mybatis的mapper参数绑定会出错(如#{id})
-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,LocalVariable*Table,*Annotation*,SyntheticjEnclosingMethod,MethodParameters

遇到问题

1. 打包找不到主类

执行 mvn clean install -Dmaven.test.skip=true 进行混淆的时候

出现下面异常
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:repackage (repackage) on project xx-data-center: Execution repackage of goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:repackage failed: Unable to
find main class -> [Help 1]

原因是没有指定 启动入口 找不到主类 main class
解决方法
增加下面代码 重新执行命令
# 入口程序类不能混淆,混淆会导致springboot启动不了
-keep class com.xxx.xxx.DataCenterApplication {
        public static void main(java.lang.String[]);
     }

2. 启动jar包异常

执行 java -jar xx.jar

出现类似异常
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cache.annotation.ProxyCachingConfiguration': BeanPostProcessor before instantiation of bean failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.transaction.annotation.ProxyTransactionManagementConfiguration': 
Initialization of bean failed; nested exception is java.lang.IllegalArgumentException: error at ::0 can't find referenced pointcut aspect

解决方法
增加配置
备注:增加这个配置后,混淆程度就特别弱、基本包名、类名、方法名都不会进行混淆。
不增加这个就需要根据业务代码一一排除哪些不需要混淆的情况。
-keep public class * {
 public protected *;
 }

3. 浏览器访问接口 406

 服务报错 Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]
 解决方法
 增加配置
 # 这是因为混淆了对应的赋值信息。
 # 不混淆 get set 方法, 如果混淆了,浏览器访问接口有可能出现这个异常 HttpMediaTypeNotAcceptableException
-keepclassmembers public class *{void set*(***);*** get*();}

源码

源码-点击下载

详情查看项目 README.md

样例

样例-点击下载

此文件内包含三个文件

  1. 混淆后的jar包 spring-proguard-0.0.1-SNAPSHOT.jar
  2. 原包没有混淆 spring-proguard-0.0.1-SNAPSHOT_proguard_base.jar
  3. jd-gui.exe java反编译工具