浅谈Android混淆

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混淆的例子

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容