美文网首页
使用 ProGuard 进行代码混淆

使用 ProGuard 进行代码混淆

作者: 风雪围城 | 来源:发表于2018-01-27 15:41 被阅读0次

    背景

     一个 apk 包,很容易可以被逆向出源码。逆向的过程也比较简单,最近在 github 上看到一个工具,可以直接从 apk 包中解析出项目源码,如果不对代码做任何处理,你的项目就会像没有穿衣服一样站在别人面前。
     一种可行的办法,就是对代码做映射, 将我们原有的有意义的类名、变量名映射(重命名)为无意义的简短的名称;或者注入一些无意义的代码,以影响别人的窥视。
     Proguard 就是这样一种工具。当然,它的作用也不仅仅是如此。

    ProGuard 是什么

     根据官网的介绍,它是一个针对 Java class 文件 进行 压缩、优化、混淆、预检的工具。
     它会读取 jar(或者 wars、ears、zips或者其他目录),然后依次进行压缩、优化、混淆、预检工作,处理好之后,再将它们输出。输入中的 java 文件,它们的名字以及其中的内容,会被映射为混淆后的名字。
     ProGuard 的处理,总是从含有 entry points (包含有main方法或者需要被系统调用的类)开始的。

    • 首先,进行压缩工作,会检测哪些类和成员会被使用,没有被使用的将被丢弃。
    • 然后,进行优化工作,主要是针对方法的字节码,那些非 entry point 的方法或者类可能会被设置为 private 、static、final;一些不用的参数可能会被移除,一些方法可能会被设置为内联函数。
    • 接着,会进行混淆,ProGuard 会对一些非 entry points 的类进行类名和成员名的重命名。
    • 最后,预检阶段,这是唯一一个不需要知道 entry points 的阶段。

    主要方法

    这里,主要讲一下 keep 和 dontwarn。

    keep

     上面也讲到了 entry point 的概念。对于那些 main 方法,或者需要通过使用类名或者属性名进行操作的地方(系统回调或者反射),我们不能对它们进行混淆,一旦混淆,在需要的时候可能引发找不到的异常。
     举几个例子。比如说,main方法被混淆重命名成了 a ,那么找不到 main 方法,自然也就找不到程序执行的入口;比如说,我们把 Activity 混淆了,在启动该 Activity,同样找不到该组件,因为这个 Activity 已经被混淆后重命名了,而在我们的 manifest.xml(xml文件不会被混淆) 文件中,还留的是未混淆之前的名字。
     因此,我们需要使用 keep 对这些 entry point 进行保护,使得在混淆过程中,跳过这些类或者成员,不进行混淆。
     keep命令有一些变体,但是总体都是围绕着类名和成员名展开的。
     我们比较常用的,可能就是-keep [,modifier,...] class_specification ,下面举几个实例

    //--------------------1---------------------------
    -keep class com.alibaba.fastjson.** { *; }
    //--------------------2---------------------------
    -keep class * implements android.os.Parcelable {
        public static final android.os.Parcelable$Creator *;
    }
    //--------------------3---------------------------
    -keep public class * extends android.view.View {
        *** get*();
        void set*(***);
        public <init>(android.content.Context);
        public <init>(android.content.Context, android.util.AttributeSet);
        public <init>(android.content.Context, android.util.AttributeSet, int);
    }
    
    • class 关键字:class 关键字可以代表任何 classs 或者 interface;但是当使用 interface 的时候,就只能代表接口;使用 enum 时,就只能代表枚举。

    • 类名,必须是完全限定名。比如 String,就必须是 java.lang.String。在指定类名的时候,可以使用一些正则表达式:

      ? 通配符

      可以匹配类名中的某一个字符,但是不包括包分隔符。比如说"mypackage.Test?",可以匹配"mypackage.Test2",但是无法匹配"mypackage.Test12"。

      * 通配符

      该通配符可以表示类的全限定名中的任意一部分字符串,但是不包括包名分隔符(其实就是点".")。比如"mypackage.*Test*"可以匹配"mypackage.Test" 和 "mypackage.YourTestApplication",但是无法匹配"mypackage.subpackage.MyTest"。
      因此,上述代码块中第二个示例,即匹配了所有继承自 Parcelable 的类。

      ** 通配符

      可以通配全限定名中任何长度的字符串,这其中包括包名分隔符。因此,上面代码块中的第一个示例,实际上指的就是com.alibaba.fastjson 包下的所有类以及子包中的类中的所有内容。即,完全不对这个包下面的所有内容进行混淆。

    • extends 和 implements 关键字:它们实际上是对通配符作用的进行限制,代表了继承自或者实现了某个接口的所有类。如上面的示例3部分,表示不混淆所有继承自 View 的类。

    • 成员和方法:可以指定一个属性成员和方法不被混淆,在指定的时候也可以使用一些通配符:

      <init> 通配符

      该通配符会表示所有构造方法,注意,它也可以带参数列表。

      <fields> 通配符

      该通配符表示所有属性成员。

      <methods> 通配符

      该通配符表示所有方法。

      * 通配符

      在这里,它代表所有属性成员和方法。

    • 成员和方法名的通配符。我们在指定成员和方法名时,同样可以使用通配符。
      % 匹配基础类型,但是不包括 void
      ? 匹配类名中的单个字符
      * 匹配类名中的一部分字符,但是同样不包括包名分隔符
      ** 匹配类名中的一部分字符,包括包名分隔符,所匹配类不包括数组
      *** 匹配任意类型类名或数组
      ... 匹配任意数量任意类型

    dontwarn

     混淆过程中,会出现一些警告,导致 build 失败。
     这些警告,有些时候并不会对我们的使用产生任何影响。比如在使用 picasso 的时候,如果在没有进行如下设置:

    #-dontwarn com.squareup.okhttp.**
    

    就会产生这样的警告:

    Warning: com.squareup.picasso.OkHttpDownloader: can't find referenced class com.squareup.okhttp.OkHttpClient
    Warning: com.squareup.picasso.OkHttpDownloader: can't find referenced class com.squareup.okhttp.OkHttpClient
    Warning: com.squareup.picasso.OkHttpDownloader: can't find referenced class com.squareup.okhttp.OkHttpClient
    

     事实上,picasso 并不是一定需要使用 okhttp ,如果我们的项目中使用了 okhttp ,它才会使用 okhttp 作为下载工具。

    注意事项

     混淆处理,实际上就是一个重命名的过程,即实现名字(类名、方法名、属性名)的映射。这种映射关系可以在混淆过程中输出到 mapping.txt 文件中。在混淆处理之后,名字就已经变了,此时如果再试图使用原来的名字去找到它们,必然是找不到的。比如以下几种情况:

    1. 反射使用。比如 setName() 方法通过混淆被映射为了 a() 如果我们希望通过方法名 setName 来调用类中的该方法,在写代码的时候,我们也不会知道这个名字将会被映射为 a ,混淆之后,会找不到方法的。 混淆使得方法名发生改变,而我们还在使用原来的方法名进行反射。

    2. bean 文件使用。对于 bean 文件,很多时候,它们作为和服务器之间的通信实体。如果在这种情况下进行了混淆,当数据发给服务器之后,服务器是看不懂的,因为属性名都变了,而服务端保存的是原来的 bean 文件。

    3. 回调函数。这是一个值得注意的地方。比如在 Activity 中的 onTouchEvent 回调,如果被你混淆了,而系统实际上不知道的,混淆是你的个人行为。它不会知道到该回调的,同样因为找不到。

    4. 枚举。在使用枚举类型的时候,应当注意不要对它们进行混淆。因为枚举会使用反射进行操作。

    5. native 方法不要混淆。

    小结

     以上,是对混淆的一个总结。混淆的目的,在于影响别人反编译之后的阅读,无法做到真正的让别人无法破解。核心部分,还是需要做加密处理,或者干脆使用 so 动态库来实现。
     当然,上面已经提到,ProGuard 的作用不仅在此,还可以用来对 apk 进行瘦身。更多详情,可以在官网上找到。

    相关文章

      网友评论

          本文标题:使用 ProGuard 进行代码混淆

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