Android代码混淆

什么是代码混淆

代码混淆就是将代码中的各种元素,如变量,方法,类和包的名字改写成无意义的名字,增加项目反编译后被读懂的难度。
Android代码混淆使用ProGuard工具,ProGuard是一个压缩、优化和混淆Java字节码文件的免费的工具,它可以删除无用的类、字段、方法和属性。

以下是官网对ProGuard的说明:

ProGuard是一个对Java类文件进行压缩,优化,混淆和校验的工具。
压缩过程查找并删除没有使用到的类,字段,方法和属性。优化过程对方法的字节码进行分析和优化。
混淆过程把剩余的元素名字该写成简短且无意义的名字。这些过程会使程序体积更小,运行更高效,更难被反编译。
最后的校验过程为类增加校验信息,但这个过程依赖J2ME和JDK6或以上的编译环境。

  • rom编译
    Android.mk文件中,用LOCAL_PROGUARD_ENABLED来配置混淆的模式;LOCAL_PROGUARD_FLAG_FILES用来指定配置文件。LOCAL_PROGUARD_ENABLED的取值如下:
    • full:使用编译系统默认的配置:压缩但不混淆和优化,默认的混淆配置文件是build/core/proguard.flags
    • custom:和full一样,但不包括aapt生成的resource相关的混淆配置。
    • nosystem:不使用系统的默认配置,但使用aapt生成的resource相关的混淆配置,其他混淆由模块自己负责。
    • disabled:关闭混淆
    • obfuscation:和full一样,并且开启混淆
    • optimization:和full一样,并且开启优化
    • 不设置时,如果是app,默认为full,如果是library,则默认为disabled。

编译userdebug版本时,编译脚本会把app的obfuscation改成full,即不混淆;所以userdebug版本的app是不混淆的。想了解更多信息,可以自行阅读project_src/build/core/下的java.mk,package_internel.mk,java_library.mk,proguard.flags,proguard_base_keeps.flags等文件。

  • Android Studio
    项目目录下的build.gradle文件中minifyEnabled设置为true为开启,false为关闭;proguardFiles用来指定混淆配置文件。使用Build菜单下的Generate Signed APK进行打包即可。记得在Build Type:选项下选择release,否则只打包不会混淆。

  • Eclipse
    项目目录下的project.properties文件中添加配置即可开启混淆:proguard.config=xxx,xxx为混淆配置文件路径,多个配置文件用:分隔。 然后Export APK就可以了,注意直接运行程序生成的安装包是没有经过混淆的。

如何使用混淆

理想的目标是将所有元素都加入混淆,但混淆会另反射无法工作。因此反射以及反射延伸出来的功能使用到的元素都不能混淆。
因为Android开发中有些内容每次都要配置,所以sdk中提供了一份默认配置文件,我们新建项目时可以复制或引用sdk下的默认配置,在此基础上再增加自己的需求。默认配置文件在android_sdk/tools/proguard/proguard-android.txt。

下面介绍一些常用配置以及Android开发中哪些元素不应该混淆。常用配置:

  • -keep
    keep用来指定哪些元素不进行混淆,它有很多变种,比如:
  • -keep 保留指定的包,类和类成员不被混淆。
  • -keepclassmembers 保留指定的类成员不被混淆,但包名类名会被混淆。
  • -keepclasseswithmembers 保留指定的类成员及其类不被混淆。
    当未配置-dontshrink(该配置是关闭压缩功能,也就是不会删除未使用的元素,未配置时,也即是开启压缩功能)时,以上3个配置指定的元素即使未使用过,也不会被删除。 以下3个命令与以上3个命令对应,区别是在上述情况中,指定的元素未使用过就会被删除。
  • -keepnames 也可以写成-keep,allowshrinking
  • -keepclassmembernames 也可以写成-keepclassmembers,allowshrinking
  • -keepclasseswithmembernames 也可以写成-keepclasseswithmembers,allowshrinking
    示例:
    保留Util类名,但内部成员会被混淆

    -keep public class com.test.proguard.util.Util

    保留Util类名及其内部成员

    -keep public class com.test.proguard.util.Util {;}

    保留util包及其下级包的类和内部成员

    -keep public class com.test.proguard.util.
    * {;}

    保留第三方lib库及继承自第三方的类:

    ======= Sina Weibo SDK =========
    -dontwarn com.sina.
    *
    -keep class com.sina.{;}
    -keep interface com.sina.
    {;}
    -keep public class * extends com.sina.**

    保留util包下的所有类成员不被混淆,但包名类名会被混淆

    -keepclassmembers public class com.test.proguard.util.** {*;}

    保留所有名为showText并且是public void的方法不被混淆

    -keepclassmembers class * {
    public void showText(...);
    }

    保留Serializable的所有子孙类中所有的private String的属性。

    -keepclassmembers class * extends java.io.Serializable {
    private java.lang.String *;
    }

    保留Serializable的所有子孙类中所有的private String的属性以及该类名。

    -keepclasseswithmembers class * extends java.io.Serializable {
    private java.lang.String *;
    }
  • -dontwarn
    dontwarn和keep可以说是形影不离,尤其是处理引入的lib库时.引入的lib库可能存在一些无法找到的引用和其他问题,在build时可能会发出警告,如果我们不进行处理,通常会导致build中止.因此为了保证build继续,我们需要使用dontwarn忽略这些我们无法解决的lib库的警告.
    示例:
    忽略com.google.zxing包相关的警告

    -dontwarn com.google.zxing.**

  • 其他配置

  • -dontshrink 不压缩,作用于全局
  • -dontoptimize 不优化,作用于全局
  • -dontobfuscate 不混淆,作用于全局
  • -dontwarn 忽略所有警告,使混淆不会因为警告而停止运行,但会打印警告信息
  • -useuniqueclassmembernames 类和成员都使用唯一的名字,如果没有这个选项,会有很多变量或方法或类名都叫‘a’,‘b’
  • -dontusemixedcaseclassnames 不使用大小写混合类名
  • -verbose 混淆过程中打印更多信息,如果因为异常停止混淆,则会输出stack trace,而不仅仅是异常信息
  • -keepattributes [attribute_filter] Class文件中包含一些与运行无关的信息,比如SourceFile(从哪个源文件编译而来),SourceDir(源文件的文件目录),LineNumberTable(代码行),Exceptions,InnerClasses,Signature,Deprecated,Annotation等等,混淆过程会默认移除掉这些信息,但可以用keepattributes来指定保留那类信息,比如-keepattributes SourceFile,LineNumberTable可以保留代码行和源文件信息。
  • -include 引入其他的配置文件

更多用法可以参考官方文档:https://www.guardsquare.com/en/proguard/manual/usage

不应该混淆的元素

  • 需要反射的元素
    由于反射是通过元素名字来查找的,因此当名字改写后,无法找到目标,会导致出现ClassNotFoundException,NoSuchFiledException,NoSuchMethodException等异常。
    例如如下代码会抛出ClassNotFoundException:

    try {
    String str = "com.test.proguard.util.Util";
    Class clazz = Class.forName(str);
    Object object = clazz.newInstance();
    } catch (Exception e) {
    e.printStackTrace();
    }

    有趣的是,上面这段代码如果改写成下面这样,则会顺利找到Util类:

    try {
    Class clazz = Class.forName("com.test.proguard.util.Util");
    Object object = clazz.newInstance();
    } catch (Exception e) {
    e.printStackTrace();
    }

    这两段代码的区别在于forName传入的参数是常量还是变量,传入常量的调用方式被ProGuard混淆处理了,所以可以正常运行。
    ProGuard还对其他一些反射用法进行了处理。例如:

    Class.forName("SomeClass")
    SomeClass.class
    SomeClass.class.getField("someField")
    SomeClass.class.getDeclaredField("someField")
    SomeClass.class.getMethod("someMethod", new Class[] {})
    AtomicIntegerFieldUpdater.newUpdater(SomeClass.class, "someField")

更多信息请查看官网:https://www.guardsquare.com/en/proguard/manual/introduction
以下列出的内容都是反射延伸出来的用法,同样不能进行混淆。

  • 枚举:Enum.valueOf(String)用到反射,不能混淆
  • 四大组件:四大组件必须在manifest中注册,混淆后类名被改写将无法被找到,会抛出异常。
  • aidl:aidl
  • GSON:GSON是一个利用反射进行序列化的第三方lib库。
  • 实现Parcelable接口的可序列化类:进程间通信的话,要保证两端类名相同,进程内传递时反序列化时需要反射CREATOR对象。
  • 注解:很多场景下注解被用作在运行时反射来确定一些元素的特征。
  • 自定义View
  • native方法
  • jni调用的java方法
  • js调用的java方法

如何恢复被混淆的trace

Proguard进行混淆时会生成一个映射表,文件名是mapping.txt,通过sdk下的retrace.sh脚本和mapping.txt就可以把混淆的trace恢复到原来的样子

示例:
trace.txt文件:

java.lang.Exception
at com.test.proguard.a.b.a(Util.java:39)
at com.test.proguard.a.a.a(TestStart.java:14)
at com.test.proguard.MainActivity.a(MainActivity.java:32)
at com.test.proguard.MainActivity.a(MainActivity.java:31)
at com.test.proguard.b.onClick(MainActivity.java:26)
at android.view.View.performClick(View.java:5217)
at android.view.View$PerformClick.run(View.java:21278)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5547)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:935)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726)

运行命令:

./retrace.sh ~/mapping.txt ~/trace.txt

输出:

java.lang.Exception
at com.test.proguard.util.Util.showText(Util.java:39)
at com.test.proguard.util.TestStart.start(TestStart.java:14)
at com.test.proguard.MainActivity.test(MainActivity.java:32)
at com.test.proguard.MainActivity.access$0(MainActivity.java:31)
at com.test.proguard.MainActivity$1.onClick(MainActivity.java:26)
at android.view.View.performClick(View.java:5217)
at android.view.View$PerformClick.run(View.java:21278)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5547)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:935)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:726)

常见问题

1.反射导致找不到类、方法、属性
当反射时抛出:ClassNotFoundException,NoSuchMethodException,NoSuchFieldException时请检查反射目标是否被混淆了。

2.进程间通信传递Parcelable序列化类时报异常

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.test.parcel/com.test.parcel.MainActivity}:
android.os.BadParcelableException: ClassNotFoundException when unmarshalling: com.test.model.Student
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2514)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2575)
at android.app.ActivityThread.access$900(ActivityThread.java:160)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1415)

原因:序列化类被混淆后,与另一端的序列化类名称匹配不上,导致抛出ClassNotFoundException异常。
解决:序列化类不应该被混淆。

注意,在Android7.0上Parcelable类的keep需要跟之前的不一样,如下的做法很常见(android本身在proguard_basic_keeps.flags中也是这样写的):

// Parcelable CREATORs must be kept for Parcelable functionality
-keep class * implements android.os.Parcelable {
public static final ** CREATOR;
}

但是这样的写法在Android7上不管用。需要如下写法:

-keepclasseswithmembers class * implements android.os.Parcelable {*;}

或者:

-keepclassmembers class * implements android.os.Parcelable {
public static ;
}

3.Intent传递Parcelable序列化类时报异常

java.lang.RuntimeException: Unable to start service com.smartisan.feedbackhelper.upload.ReliableUploader@431b6290
with Intent { cmp=com.smartisan.gamestore/com.smartisan.feedbackhelper.upload.ReliableUploader (has extras) }:
android.os.BadParcelableException: Parcelable protocol requires a Parcelable.Creator object called CREATOR
on class com.smartisan.feedbackhelper.utils.e

原因:序列化类被混淆后,CREATOR对象变量名被改写,无法被找到,导致抛出异常。
解决:序列化类不应该被混淆。

4.aidl相关类不应该混淆

Parcel : **** enforceInterface() expected 'com.xy.bizport.service.aidl.IXyRemoteCallable' but read 'com.xy.bizport.a.a.a'

原因:混淆后两端类名无法匹配,导致异常。

5.js和java不能互相调用,提示找不到方法
原因:混淆后方法名无法匹配。
解决:增加如下配置:

-keepattributes Annotation, JavascriptInterface

-keepattributes 建议只写一行,因为在odin上配置-keepattributes时,前面的会被后面的覆盖。

你可能感兴趣的:(Android代码混淆)