Android-混淆学习

混淆的作用

Java代码是很容易反编译的,而Android是使用Java开发的,也容易被反编译出来。一般来说,通过dex2jar和jd-gui就可以反编译出一般的APK了。想要进一步了解的,可以看郭神的这篇文章。为了保护自己的源码,我们需要对编译好的class文件进行加密,就是混淆。

Proguard是一个混淆代码的开源库,配合Gradle构建工具,就可以很简单的在Android中使用了。

Proguard的作用

  • 压缩(Shrink) 检测并移除代码中无用的类、字段、方法和属性(Attribute)
  • 优化(Optimize) 对字节码进行优化,移除无用指令
  • 混淆(Obfuscate) 使用a、b、c这样简短而无意义的字母来对类、方法、字段的名称进行重命名
  • 预检(Preveirfy) 在Java平台上处理后的代码进行预检测,确保加载的class文件时可执行的

总之,使用Proguard能够让代码更精简、更高效,也更难逆向。

Android Studio中开启混淆

在gradle文件中添加如下代码:

android{
     buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

minifyEnabled true表示开启混淆。既然是混淆,那得定义一个规则去定义哪些东西是可以混淆的,哪些是不能混淆的,避免发生异常。

proguard-rules.pro就是用来编写这个规则的。

混淆后默认会在工程目录app/build/outputs/mapping/release下生成一个mapping.txt文件,这就是混淆规则,我们可以根据这个文件把混淆后的代码反推回源本的代码,所以假设该文件暴露出去,在某种程度上来说,等于没混淆。

混淆规则

混淆一般遵循以下规则:

  • 涉及到Java反射不能混淆(反射是通过全类名去找出对应的类,假设混淆了之后,就不能准确地找到自己需要的信息了)
  • 涉及到Json转换的不能混淆(同样是反射)
  • 注解不能混淆(同样是反射)
  • 泛型不能混淆
  • 调用jni的类和路径不能混淆(java对应jni的方法名称就是Java_包名[.换成类名方法名])
  • 四大组件和Application不能混淆(Android Framework会调用到)
  • 内部类一般不能混,外部类会调用到
  • 对于库来说,对外提供的接口不能混
  • 会被外部调用到的代码都不能混,但是可以掌握混淆的粒度

注:

lib的混淆只对其起作用,APP主工程的混淆会影响到lib,导致其内部的变量或方法被混淆,而类名和类路径不会被混淆,这样会出问题的

  • 解决思路

      1、两个工程的混淆交给其中一个同一控制
    
      2、两个工程的混淆配置进行merge
    
  • 具体实施

    两个工程的混淆交给其中一个同一控制:

      1、lib工程的混淆都放进app中,统一由app控制
      
      2、在app的Proguard-rules.pro文件中加入-include xxx,把lib的混淆文件导入进来,前提是需要把lib工程的位置文件拷贝到app目录下
    
      3、在app的build.gradle配置中配置,类似:proguardFiles getDefaultProguardFile(‘proguard-android.txt’), ‘proguard-rules.pro’,‘proguard-lib-rules.pro’,但是proguard-lib-rules.pro要在app工程目录下
    

    连个工程的混淆配置进行merge:

      在lib工程中加入consumerProguardFiles ‘proguard-rules.pro’,这个配置会在lib工程被打包的时候让lib工程中的proguard-rules.pro生效,此时lib工程就会按照它自己的proguard-rules.pro文件配置被正确混淆
    

    例如:在realm中就是用了这种方式,见realm-gradle文件:

      android{
          //多渠道打包
          productFlavors {
              //base版本
              base {
                  dimension 'api'
                  externalNativeBuild {
                      cmake {
                          arguments "-DREALM_FLAVOR=base"
                      }
                  }
              consumerProguardFiles 'proguard-rules-consumer-common.pro', 'proguard-rules-consumer-base.pro'
              proguardFiles 'proguard-rules-build-common.pro'
              }
    
              objectServer {
                  dimension 'api'
                  externalNativeBuild {
                      cmake {
                          arguments "-DREALM_FLAVOR=objectServer"
                      }
                  }
                  consumerProguardFiles 'proguard-rules-consumer-common.pro', 'proguard-rules-consumer-objectServer.pro'
                  proguardFiles 'proguard-rules-build-common.pro', 'proguard-rules-build-objectServer.pro'
              }
          }
      }
    

关于consumerProguardFiles属性的更多解释,见这里

Android APP的基本混淆配置

###############################基本指令###########################
#指定代码的压缩级别,在0-7之间,一般是5,不需要修改
-optimizationpasses 5    

#混淆时不使用大小写混合,混淆后的类名为小写
-dontusemixedcaseclassnames   

# 混淆时是否做预校验(Android不需要preverify,去掉这一步可加快混淆速度)
-dontpreverify    

#有了verbose这句话,混淆后就会生成映射文件
# 包含有类名->混淆后类名的映射关系
# 然后使用printmapping指定映射文件的名称
-verbose
-printmapping proguardMapping.txt

#混淆时所采用的算法,后面的参数是一个过滤器
#这个过滤器是谷歌推荐的算法,一般不改变
-optimizations !code/simplification/arithmetic,!field/*,!class/merging/*  

# 保护代码中的Annotation不被混淆,这在JSON实体映射时非常重要,比如fastJson
-keepattributes *Annotation*

#避免混淆泛型,这在JSON实体映射时非常重要,比如fastJson
-keepattributes Signature

#抛出异常时保留代码行号,在异常分析中可以方便定位
-keepattributes SourceFile,LineNumberTable

#用于告诉ProGuard,不要跳过对非公开类的处理。默认情况下是跳过的,因为程序中不会引用它们,有些情况下人们编写的代码与类库中的类在同一个包下,并且对包中内容加以引用,此时需要加入此条声明
-dontskipnonpubliclibraryclasses

#这个是给Microsoft Windows用户的,因为ProGuard假定使用的操作系统是能区分两个只是大小写不同的文件名,但是Microsoft Windows不是这样的操作系统,所以必须为ProGuard指定-dontusemixedcaseclassnames选项
-dontusemixedcaseclassnames

##################################保留############################

#保持native方法不被混淆
#keepclasseswithmembernames 保留类和该类中所有带native限定符的方法
-keepclasseswithmembernames class * {  
    native ;
}

#保留了继承自Activity、Application这些类的子类
# 因为这些子类,都有可能被外部调用
# 比如说,第一行就保证了所有Activity的子类不要被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.support.v4.**
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService


# 保留在Activity中的方法参数是view的方法,
# 从而我们在layout里面编写onClick就不会被影响
-keepclassmembers class * extends android.app.Activity {
    public void *(android.view.View);
}

# 保留自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View {
    *** get*();
    void set*(***);
    public (android.content.Context);
    public (android.content.Context, android.util.AttributeSet);
    public (android.content.Context, android.util.AttributeSet, int);
}

#枚举enum类不能被混淆
-keepclassmembers enum * {
    //这两个方法用到了反射
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

#保留Parcelable序列化的类不被混淆
-keep class * implements android.os.Parcelable { # 保持 Parcelable 不被混淆
    public static final android.os.Parcelable$Creator *;
}

# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
    java.lang.Object writeReplace();
    java.lang.Object readResolve();
}

# 对于R(资源)下的所有类及其方法,都不能被混淆
-keep class **.R$* {
    *;
}

# 对于带有回调函数onXXEvent的,不能被混淆
-keepclassmembers class * {
    void *(**On*Event);
}

内部类

# 保留内嵌类不被混淆
-keep class com.example.xxx.MainActivity$* { *; }

这个$符号就是用来分割内嵌类与其母体的标志

WebView

# 对WebView的处理
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
    public boolean *(android.webkit.WebView, java.lang.String)
}
-keepclassmembers class * extends android.webkit.webViewClient {
    public void *(android.webkit.webView, java.lang.String)
}

JavaScript

# 保留JS方法不被混淆
-keepclassmembers class com.example.xxx.MainActivity$JSInterface1 {
    ;
}

其中JSInterface是MainActivity的子类

规则解读

**表示把本包和所含子包下的类名都保持
-keep class cn.hadcn.test.**

*表示只是保持该包下的类名,而子包下的类名还是会被混淆
-keep class cn.hadcn.test.*

使用上面两种方式,具体的方法名和变量名还是会变化的

//保持类名和里面的内容
-keep class cn.hadcn.test.* {*;}

//$表示内部类
//表示ScriptFragment的内部类JavaScriptInterface中的所有public成员不能被混淆
-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
    public *;
}

//保护类下的特定内容
;     //匹配所有构造器
;   //匹配所有域
;  //匹配所有方法方法

//使用private 、public、native等来进一步指定不被混淆的内容
//表示One类下面的所有public方法不会被混淆
-keep class cn.hadcn.test.One {
    public ;
}

//还可以对方法加参数以求更精准地指定不被混淆的内容
-keep class cn.hadcn.test.One {
    //保留One类下参数为JSONObject类型的所有构造方法不被混淆
    public (org.json.JSONObject);
}

Google官网给出了便于理解这些规则的表格:

保留 防止被移除或者被重命名 防止被重命名
类和类成员 -keep keepnames
仅类成员 -keepclassmembers -keepclassmembernames
如果拥有某成员,保留类和类成员 -keepclasseswithmembers -keepclasseswithmembernames

移除是指在压缩(Shrinking)时是否会被删除

致谢

Android混淆从入门到精通
5分钟搞定android混淆

感谢各位大牛的分享,这也当是自己的学习笔记!

你可能感兴趣的:(Android-混淆学习)