美文网首页
浅谈Android混淆

浅谈Android混淆

作者: 言者无知_n4c | 来源:发表于2017-07-21 01:51 被阅读0次

    1.What and why?

    • What?

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

    • Why?

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

    2.How?

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

    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移除的代码

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

    解码混淆过的stack trace


    使用位于 <sdk-root>/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 <methods>;
    }
    

    -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 ...] <fields> |
                                                                          (fieldtype fieldname);
        [@annotationtype] [[!]public|private|protected|static|synchronized|native|abstract|strictfp ...] <methods> |
                                                                                               <init>(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
    <field>     匹配类中的所有字段
    <method>    匹配类中所有的方法
    <init>      匹配类中所有的构造函数
    
    -keep class com.lily.test.** 本包和所包含子包下的类名都保持
    -keep class com.lily.test.* 保持该包下的类名
    -keep class com.lily.test.** {*;} 保持包和子包的类名和里面的内容均不被混淆
    -keepclassmembers class **.R$* { 
        public static <fields>; 
    } 
    

    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 <methods>;
    }
    
    # 保留枚举类不被混淆
    -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.** {
        <fields>;
        <methods>;
    }
    
    #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混淆

          本文链接:https://www.haomeiwen.com/subject/dxojkxtx.html