浅谈Android混淆

1.What and why?

  • What?

代码混淆(Obfuscated code)亦称花指令,是将计算机程序的代码,转换成一种功能上等价,但是难于阅读和理解的形式的行为。

  • Why?

混淆的目的是为了加大反编译的成本,但是并不能彻底防止反编译.

2.How?

ProGuard由shrink、optimize、obfuscate和preverify四个步骤组成,每个步骤都是可选的,需要哪些步骤都可以在脚本中配置。参见ProGuard官方介绍。

浅谈Android混淆_第1张图片
ProGuard_build_process.png

Entry Points(入口点):

为了确定哪些代码应该被保留,哪些代码应该被移除或混淆,需要确定一个或多个Entry Point。Entry Point经常是带有main methods,applets,midlets的classes,它们在混淆过程中会被保留。

What does each step do?


  • shrink: Proguard从上述EntryPoints开始遍历搜索哪些类和类成员被使用。其他没有被使用的类和类成员会移除。

  • optimize: 优化代码,非EntryPoints类会加上private/static/final, 没有用到的参数会被删除,一些方法可能会变成内联代码。

  • obfuscate: 使用短又没有语义的名字重命名非EntryPoints的类名,变量名,方法名。EntryPoints的名字保持不变。

  • preverify: 预校验代码是否符合Java1.6或者更高的规范(唯一一个与入口类不相关的步骤)

3.Usage

要执行proguard,可以直接执行命令:

java -jar proguard.jar options ...

如果有Android SDK的同学可以在{ANDROID_SDK_ROOT}/tools/proguard/lib/目录下找到proguard.jar这个jar包。或者,也可以在{ANDROID_SDK_ROOT}/tools/proguard/bin目录下直接使用脚本执行命令。

我们也可以把proguard的参数写到一个配置文件中,比如说proguard.cfg。那我们的命令可以这样写:

java -jar proguard.jar @proguard.cfg

这个文件也就是我们在Android Studio中经常配置的混淆文件了。我们在编译正式包的时候打包脚本自动帮我们执行了这条命令。通过这个脚本可以避免重复输入参数。

当然,我们也可以配置文件与命令行参数混用,例如:

java -jar proguard.jar @proguard.cfg -verbose

AndroidStudio中开启混淆


参考Android官方文档
如果需要开启混淆,在build.gradle文件中相应的BuildType下将minifyEnabled 设置为true,开启混淆会降低构建速度,因此避免在debug版本中开启混淆。
以下是以release版本为例,开启混淆的gradle脚本片段:

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

其中proguardFiles属性用于定义 ProGuard 规则,与上文中直接使用proguard.jar进行混淆时指定的文件选项是一个意思。

  • getDefaultProguardFile(‘proguard-android.txt’) 方法可从 Android SDK tools/proguard/ 文件夹获取默认的 ProGuard 设置。要想做进一步的代码压缩,请尝试使用位于同一位置的 proguard-android-optimize.txt 文件。它包括相同的 ProGuard 规则,但还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度。
  • proguard-rules.pro 文件用于添加自定义 ProGuard 规则。默认情况下,该文件位于模块根目录(build.gradle 文件旁),内容为空。
    构建输出

构建时Proguard都会输出下列文件:

  • dump.txt 说明APK中所有类文件的内部结构
  • mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换
  • seeds.txt 列出未进行混淆的类和成员
  • usage.txt 列出从APK移除的代码

这些文件保存在 /build/outputs/mapping/release/目录下。
每新发布一个版本,都会产生新的 mapping.txt文件,所以要保存好相应的 mapping.txt文件,方便解码混淆过的stack trace。

解码混淆过的stack trace


使用位于 /tools/proguard/目录下的retrace脚本,将混效果的stack trace 和mapping.txt作为输入,可以使输出已解码的stack trace.
例如:

retrace.bat -verbose mapping.txt obfuscated_trace.txt

proguard-android.txt 解读


不使用大小写混写类名,默认情况下混淆的类名可以包含大小写字符的混合,以防止在大小写不敏感的系统,比如windows上出现问题。

-dontusemixedcaseclassnames

不忽略公共类库

-dontskipnonpubliclibraryclasses

关闭optimize和preverify选项,因为Android的dex并不像Java虚拟机需要optimize(优化)和previrify(预检)两个步骤。

-dontoptimize
-dontpreverify

指定哪个属性不要混淆,可一次指定多个属性

-keepattributes [attribute_filter]

通常Exceptions, Signature, Deprecated, SourceFile, SourceDir, LineNumberTable, LocalVariableTable, LocalVariableTypeTable, Synthetic, EnclosingMethod, RuntimeVisibleAnnotations, RuntimeInvisibleAnnotations, RuntimeVisibleParameterAnnotations, RuntimeInvisibleParameterAnnotations, and AnnotationDefault属性需要被保留,根据项目具体使用情况保留。

这里需要特别注意的一点是,gradle默认的keepattributes属性不全,只保留了Annotation,Signature,InnerClasses,EnclosingMethod,为了混淆之后定位csh代码方便,我们需要在proguard_rules.pro中手动添加抛出异常时保留代码行号,并且重命名抛出异常时的文件名称,这样能方便定位问题:

抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile

Keep配置


***-keep [,modifier, ...] class_specification ***
指定类和类成员(变量和方法)不被混淆

指定类名不被改变
-keep public class com.google.vending.licensing.ILicensingService

指定使用了Keep注解的类和类成员都不被改变
-keep @android.support.annotation.Keep class * {*;}

-keepclassmembers
指定类成员不被混淆,类名会被混淆
eg.keep setters in views 使得animations仍然能够工作

-keepclassmembers public class * extends android.view.View {
    void set*(***);
    *** get*();
}

***-keepclasseswithmembers ***
指定类和类成员都不被混淆
eg.包含native方法的类名和native方法都不能被混淆,如果native方法未被调用,则被移除。由于native方法与对应so库中的方法名称对应,方法名被混淆会导致调用出现问题,所以native方法不能被混淆。

-keepclasseswithmembernames class * {
   native ;
}

-keepnames
是 -keep,allowshrinking class_pecification 的简写。指定一些类名受到保护,前提是他们在shrink这一阶段没有被去掉。也就是说没有被入口节点直接或间接引用的类还是会被删除。
-keepclassmembernames
与-keepclassmember相似。保护指定的类成员,前提是这些成员在shrink阶段没有被删除。
-keepclasseswithmembernames
与-keepclasseswithmembers类似。保护指定的类,如果它们没有在shrink阶段被删除。
注意

If you specify a class, without class members, ProGuard only preserves the class and its parameterless constructor as entry points. It may still remove, optimize, or obfuscate its other class members.

以上六种keep配置类型,以names结尾的配置不保证被Keep的类或者成员不被删除,只有在obfuscation 这一阶段有效,如果不确定使用哪种,只需要使用不带names结尾的Keep配置即可,因为不带names的keep在shrink阶段有效,可以保证被Keep的类或者属性不被删除。

通用Options:

-verbose 打印混淆详细信息
-dontnote:指定不去输出打印该类产生的错误或遗漏

-dontnote com.android.vending.licensing.ILicensingService

-dontnote android.support.**

-dontwarn:指定不去warn unresolved references和其他重要的problem

-dontwarn android.support.**

自定义混淆文件

Keep配置后面要如何写类的信息?
[@annotationtype] [[!]public|final|abstract|@ ...] [!]interface|class|enum classname
    [extends|implements [@annotationtype] classname]
[{
    [@annotationtype] [[!]public|private|protected|static|volatile|transient ...]  |
                                                                      (fieldtype fieldname);
    [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...]  |
                                                                                           (argumenttype,...) |
                                                                                           classname(argumenttype,...) |
                                                                                           (returntype methodname(argumenttype,...));
    [@annotationtype] [[!]public|private|protected|static ... ] *;
    ...
}]

Filters

?    matches any single character in a name.(匹配一个字符)
*    matches any part of a name not containing the directory separator.(匹配一个名字,除了目录分隔符外的任意部分)
**    matches any part of a name, possibly containing any number of directory separators.(匹配任意名,可能包含任意路径分隔符)
!  exclude
     匹配类中的所有字段
    匹配类中所有的方法
      匹配类中所有的构造函数
-keep class com.lily.test.** 本包和所包含子包下的类名都保持
-keep class com.lily.test.* 保持该包下的类名
-keep class com.lily.test.** {*;} 保持包和子包的类名和里面的内容均不被混淆
-keepclassmembers class **.R$* { 
    public static ; 
} 

assumenosideeffects选项
指定一些方法被删除也没有影响(尽管这些方法可能有返回值),在optimize阶段,如果确定这些方法的返回值没有使用,那么就会删除这些方法的调用。proguard会自动的分析你的代码,但不会分析处理类库中的代码。例如,可以指定System.currentTimeMillis(),这样在optimize阶段就会删除所有的它的调用。还可以用它来删除打印Log的调用。这条配置选项只在optimizate阶段有用。
注意:Only use this option if you know what you’re doing!
eg:

# 删除代码中Log相关的代码
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

下面是自定义混淆文件的一个范例,四大组件,native方法,反射用到的类,一些引入的第三方库等,都不能进行混淆:

# 代码混淆压缩比,在0~7之间
-optimizationpasses 5# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames

# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses

# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度。
-dontpreverify

-verbose

#google推荐算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*

# 避免混淆Annotation、内部类、泛型、匿名类
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod

# 重命名抛出异常时的文件名称
-renamesourcefileattribute SourceFile

# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable

# 处理support包
-dontnote android.support.**
-dontwarn android.support.**

# 保留四大组件,自定义的Application等这些类不被混淆
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-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.preference.Preference
-keep public class com.android.vending.licensing.ILicensingService

# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
    native ;
}

# 保留枚举类不被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

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

#第三方jar包不被混淆
-keep class com.github.test.** {*;}

#保留自定义的Test类和类成员不被混淆
-keep class com.lily.Test {*;}
#保留自定义的xlog文件夹下面的类、类成员和方法不被混淆
-keep class com.test.xlog.** {
    ;
    ;
}

#assume no side effects:删除android.util.Log输出的日志
-assumenosideeffects class android.util.Log {
    public static *** v(...);
    public static *** d(...);
    public static *** i(...);
    public static *** w(...);
    public static *** e(...);
}

#保留Keep注解的类名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
    @android.support.annotation.Keep *;
}

下面的Proguard的思路可以参考:5分钟搞定android混淆

主要将自定义Proguard分成几个区域:

#--------------------------------定制化区域------------------------------
#---------------------------------1.实体类--------------------------------

#-------------------------------------------------------------------------

#---------------------------------2.第三方包-------------------------

#-------------------------------------------------------------------------

#---------------------------------3.与js互相调用的类----------------

#-------------------------------------------------------------------------

#---------------------------------4.反射相关的类和方法-----------------

#----------------------------5.基本不用动区域(可参考上文进行区分)-------------

4.资源文件的混淆

上面讲述了如何进行代码混淆,再来讲讲如何对资源文件进行混淆。对资源文件进行混淆操作本质上是通过修改resources.arsc(参见文末链接详见resources.arsc作用及文件格式)。现针对两种资源混淆方案进行简要说明。第一种是微信的资源混淆方案,第二种是美团的资源混淆方案,两篇文章中都对原理进行了详细的阐述。

5.混淆时常见的问题解决

TroubleShooting
Error:Uncaught translation error: com.android.dex.util.ExceptionWithContext: name already added: string{"a"}
参考:
Proguard官方文档中的一些关于Android混淆的例子

你可能感兴趣的:(浅谈Android混淆)