由浅入深 Android 混淆实战

7,055 阅读8分钟

许久没有做混淆相关的工作, 以前存储的知识遗忘得差不多了。停留在很多人的记忆里面混淆还不简单吗?不就是 -keep。这样说也没错,但是要把混淆做得精细精准还是不简单的,今天就一文带你全而透。

混淆的作用

我们为什么要做这个工作,有什么好处?

  • 代码缩减(摇树优化):使用静态代码分析来查找和删除无法访问的代码和未实例化的类型,对规避 64k 引用限制非常有用;

  • 资源缩减:移除不使用的资源,包括应用库依赖项中不使用的资源。

  • 混淆代码:缩短类和成员的名称,从而减小 DEX 文件的大小,增加反编译成本。

  • 优化代码:检查并重写代码,选择性内联,移除未使用的参数和类合并来优化代码大小。

  • 减少调试信息 : 规范化调试信息并压缩行号信息。

混淆的情况

混淆的情况是指你接手混淆时候的状况,大致分两种。

  • 一种是项目刚刚立项,这个时候你跟进混淆,随着你的代码量增多,和引入的第三方库&SDK 增多逐渐增加混淆规则,这是一个应该有的良性的状态,做到精准混淆也容易。
  • 第二种情况是以前的维护者完全没有混淆,有海量的代码和第三方库,里面的反射注解和各种存在混淆风险的问题存在,这样想做到精准混淆并不容易

上文多次提到精准混淆,我理解的精准混淆是最细粒度的白名单,而不是如下反例:

-keep public class * extends java.lang.Object{*;}

混淆基础知识储备

开启和关闭混淆

开启混淆比较简单,一般来讲为了方便开发调试只混淆 release 版本:

buildTypes {
    release {
        shrinkResources true //开启资源压缩
        minifyEnabled true //开启混淆
        zipAlignEnabled true //k对齐
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

minifyEnabled 和 proguardFiles 是必选项其他为可选,关闭混淆的话就比较容易了直接 minifyEnabled 修饰为 false 即可。

proguard-android.txt 和 proguard-android-optimize.txt

我们经常在代码里面看到这样的语句:

image.png proguard-rules.pro 我们知道就在 app/ 目录下,但是这个 getDefaultProguardFile 是什么?在哪里?有什么用?

getDefaultProguardFile 是 Android SDK 为我们提供的一些 Android 内置的混淆规则,一般来将这些是通用的,你要做到精通混淆必选知道它的位置以及他里面包含的内容和含义。

位置:android/sdk/tools/proguard/

image.png

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html
#
# This file is no longer maintained and is not used by new (2.2+) versions of the
# Android plugin for Gradle. Instead, the Android plugin for Gradle generates the
# default rules at build time and stores them in the build directory.

-dontusemixedcaseclassnames #混淆时不会产生形形色色的类名
-dontskipnonpubliclibraryclasses #指定不去忽略非公共类库
-verbose #输出生成信息

# Optimization is turned off by default. Dex does not like code run
# through the ProGuard optimize and preverify steps (and performs some
# of these optimizations on its own).
#-dontoptimize #不优化指定文件
-dontpreverify #不预校验
# Note that if you want to enable optimization, you cannot just
# include optimization flags in your own project configuration file;
# instead you will need to point to the
# "proguard-android-optimize.txt" file instead of this one from your
# project.properties file.

-keepattributes *Annotation*
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService

# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

# We want to keep methods in Activity that could be used in the XML attribute onClick
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}

-keepclassmembers class **.R$* {
    public static <fields>;
}

# The support library contains references to newer platform versions.
# Don't warn about those in case this app is linking against an older
# platform version.  We know about them, and they are safe.
-dontwarn android.support.**

# Understand the @Keep support annotation.
-keep class android.support.annotation.Keep

-keep @android.support.annotation.Keep class * {*;}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}

-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

mapping 文件

image.png

Mapping 非常重要,在 app/build/mapping 中生成的 mapping 文件是我们分析混淆是否生效,混淆后的崩溃寻因的重要依据,通过这个文件的映射我们能够在一堆杂乱无章的 a、 b、 c 中回溯到原始代码。例:

image.png

工具集

工欲善其事必先利其器,两款对混淆有着很大帮助的工具介绍

Android Studio APK Analysis

AS自带简单好用,对比包体积的占比分析也是相当不错,并且随着 AS 的迭代有着官方的支持相信功能会越来越强大,我们只需要简单的将 apk 包拖拽到 AS 中就会自动触发 AS 的 apk 解析功能:

image.png

Jadx

Jadx 的强大之处在于相较于 AS 自带的分析器,它还能直接查看源代码,AS 只能看到类名和方法名,简直是逆向神器。

image.png

更多介绍请移步 github.com/skylot/jadx

混淆实战

通过 demo 样例的混淆实战深刻理解每个混淆规则的含义,我们要做到的不是仅仅开启 minifyEnabled 然后应用通过,而是需要知到得更细更透彻,理解每个混淆语句规则的作用范围。

先定义一下基准包以及子包,还有类、内部类、接口、注解、方法、成员,然后我们分部对其进行混淆和 -keep 保持,记住下图的 proguard 开始的包类目录关系,我们后面一直要使用它。

image.png

后续的文章都会以这几个类做样例,所以我们把它罗列出来再加深一下印象:

  • User.java
  • Animal.java
  • MethodLevel.java
  • Student.java
  • Teacher.java
  • Company.java
  • IBehaviour.java

部分样例类:

public class Teacher extends User implements IBehaviour {

  @Override
  public void doSomething() {
    System.out.println("teaching ...");
  }

  @MethodLevel(value = 1)
  private void waking(){

  }
}

混淆中代码调用关系

先开启混淆,不添加任何规则。我们通过 jadx 看下混淆情况

image.png proguard 包和类一个都找不到应该都是被混淆了,进一步印证一下我们的想法,我们去 mapping 文件里面找下映射关系,结果出乎意料,我没有在 mapping 中找到映射关系,只在 usage.txt 中找到了对应的全路径名

image.png

是不是我们的类申明了没有引用导致的呢?我们去 activity 做一下调用

image.png

果然和我们的预想的一样,如果类创建了没有使用,mapping 不会生成映射关系,甚至可能在打包的过程中给优化掉,再看加上调用关系后我们查询 mapping 文件:

image.png

image.png

上图可以得知,我们的 proguard 包和下面的所有内容全部都混淆了。

keep 探寻

网络上的大部分文章都千篇一律,简单的给出了一个 Keep 语句,实际用的时候都是 copy 对其作用范围不是很明确,接下来我们就一个一个来探寻

keep *

-keep class com.zxmdly.proguard.*

我们先加上这句看看打包后的变化

对比之前的结果,我们看到的是 proguard 的包和下面的类名被保留下来了,注意仅仅是包合类名被保留,类中的字段和成员是没有找到的,这是为什么呢?难道是字段没有被使用

image.png

image.png

我们去印证下

image.png

image.png

好了,到现在我们已经能够透彻的知道了 -keep * 的作用,总结作用范围:

  • 能够保持该包和该包下的类、和静态内部类的类名保持,对字段和方法不额外起作用,子包不起作用,字段或者方法没有被调用则直接忽略。

keep **

-keep class com.zxmdly.proguard.**

image.png

通过查看上图和上面 keep * 的经验,我们可以得出结论:

  • keep ** 能够保持该包和其子包的子类的所有类名(含注解、枚举)不被混淆,但是方法和字段不在其作用范围,字段或者方法没有被调用则直接忽略。

值得注意的是, keep ** 对于 keep * 是包含关系,声明了 keep ** 混淆规则就无需再声明 keep * 了。

keep ** {*;}

-keep class com.zxmdly.proguard.* {}

image.png 有了之前的经验,我们可以得出结论:

  • keep ** {*;} 的作用范围特别大,能够保持该包及其子包、子类和其字段方法都不被混淆,相对来讲我们需要慎用这样的语句,因为可能导致混淆不够精准。

单个类名保持

-keep class com.zxmdly.proguard.Company

image.png

  • 仅保持类名,字段和成员被混淆

保持方法

-keep class com.zxmdly.proguard.Company{
  <methods>;
}

image.png

保持字段

-keep class com.zxmdly.proguard.Company{
  <fields>;
}

image.png

实现关系保持

-keep public class * implements com.zxmdly.proguard.IBehaviour

image.png

-keep public class * implements com.zxmdly.proguard.IBehaviour {*;}

image.png

继承关系保持

-keep public class * extends com.zxmdly.proguard.base.User {*;}

image.png

指定保持具体方法或者字段

-keep class com.zxmdly.proguard.Company{
      public java.lang.String address;
      public void printlnAddress();
}

image.png

Tips 小技巧

在 gralde 中,我们可以通过下面配置将我们的混淆规则分门别类,指定多个混淆配置文件。

image.png

例如给第三方的 SDK 专门放到一个 Third 混淆配置文件,使用了这个小技巧加上注释,我们的混淆规则是不是更清晰了呢

结语

通过本文由浅入深的带大家进行混淆实战,相信 99% 的精准混淆工作已经难不倒你,当然混淆还有更深入和更细节的用法,篇幅关系我们下次再探。