背景
一个 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 文件中。在混淆处理之后,名字就已经变了,此时如果再试图使用原来的名字去找到它们,必然是找不到的。比如以下几种情况:
-
反射使用。比如 setName() 方法通过混淆被映射为了 a() 如果我们希望通过方法名 setName 来调用类中的该方法,在写代码的时候,我们也不会知道这个名字将会被映射为 a ,混淆之后,会找不到方法的。 混淆使得方法名发生改变,而我们还在使用原来的方法名进行反射。
-
bean 文件使用。对于 bean 文件,很多时候,它们作为和服务器之间的通信实体。如果在这种情况下进行了混淆,当数据发给服务器之后,服务器是看不懂的,因为属性名都变了,而服务端保存的是原来的 bean 文件。
-
回调函数。这是一个值得注意的地方。比如在 Activity 中的 onTouchEvent 回调,如果被你混淆了,而系统实际上不知道的,混淆是你的个人行为。它不会知道到该回调的,同样因为找不到。
-
枚举。在使用枚举类型的时候,应当注意不要对它们进行混淆。因为枚举会使用反射进行操作。
-
native 方法不要混淆。
小结
以上,是对混淆的一个总结。混淆的目的,在于影响别人反编译之后的阅读,无法做到真正的让别人无法破解。核心部分,还是需要做加密处理,或者干脆使用 so 动态库来实现。
当然,上面已经提到,ProGuard 的作用不仅在此,还可以用来对 apk 进行瘦身。更多详情,可以在官网上找到。
网友评论