移动安全-APK代码混淆

概述

Android代码混淆是让Android项目避免轻易被逆向分析,防止代码安全泄露的手段之一。它将工程中的Android代码用简单抽象的字母或单词代替原有的代码名称。使代码丧失可读性从而使逆向工程师难以阅读,增加逆向成本。当逆向成本大于逆向收益的时候,逆向代码也就失去意义。

除此之外,由于代码混淆用简单抽象的单词代替原有长而通俗易懂的代码,因而减少APK的体积。而且,使用代码混淆后,利用Gradle为Android提供的插件,能将项目中未使用的资源安全移除,大大减少APK体积。

Android SDK 自带了混淆工具Proguard。它位于SDK根目录\tools\proguard下面。如果开启了混淆,Proguard默认情况下会对所有代码,包括第三方包都进行混淆,可是有些代码或者第三方包是不能混淆的,这就需要我们手动编写混淆规则来保持不能被混淆的部分

我们使用Jadx工具反编译APK并查看源码,然后比较下APK进行代码混淆和不进行代码混淆的效果。首先来看看未进行代码混淆的时候:
在这里插入图片描述
接下来看看对该APK进行代码混淆(如何混淆一会说)之后,Jadx反编译出来的代码情况:
在这里插入图片描述

代码混淆

了解了代码混淆的效果和作用,我们来看看如何实现混淆。

开启混淆

使用Android Studio正式打包时默认是不开启代码混淆的,如果需要开启代码混淆,可以在 app 模块下的build.gradle 文件中修改 minifyEnabled false 为 true。代码结构如下:

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.2"
    defaultConfig {
        applicationId "com.wiwide.soldout"
        minSdkVersion 19
        targetSdkVersion 25
        versionCode 6
        versionName "2.0.1"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            // 是否进行混淆
            minifyEnabled true
            // 混淆文件的位置
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug {
            ···
        }
    }
}

这段代码中,minifyEnabled就是我们开启代码混淆的开关,系统默认为false,既不开启代码混淆。proguardFiles是指定项目中所使用的混淆规则配置文件。其中getDefaultProguardFile(‘proguard-android.txt’)指定系统默认的混淆规则,而proguard-rules.pro代表当前项目的混淆规则

proguard-android.txt位于Android SDK目录下/tools/proguard目录下,proguard-rules.pro位于app module下。一般而言,通过修改proguard-rules.pro来改变当前项目的混淆规则。此外,通过添加shrinkResources true能将项目中未使用的资源移除
在这里插入图片描述
【说明】代码混淆后会产生如下几个位于< module-name >/build/outputs/mapping/release/目录下的文件:
在这里插入图片描述

文件名 说明
dump.txt 说明 APK 中所有类文件的内部结构。
mapping.txt 提供原始与混淆过的类、方法和字段名称之间的转换。
seeds.txt 列出未进行混淆的类和成员。
usage.txt 列出从 APK 移除的代码。

【注意】一般而言,项目的debug版本无需开启代码混淆,因为代码混淆会额外增加apk的构建时间。但是,用于测试的版本也要与生产版本一样开启代码混淆,以免在生产版本出现测试版本未曾出现的bug。

配置混淆

一般而言,默认 ProGuard 配置文件 (proguard-android.txt) 足以满足需要,ProGuard 会移除所有(并且只会移除)未使用的代码。但是,ProGuard可能会移除应用真正需要的代码。举例来说,它可能错误移除代码的情况包括:例如:

  • 当应用引用的类只来自 AndroidManifest.xml 文件时;
  • 当应用调用的方法来自 Java 原生接口 (JNI) 时;
  • 当应用在运行时(例如使用反射或自检)操作代码时。

测试应用应该能够发现因不当移除的代码而导致的错误,但您也可以通过查看 < module-name >/build/outputs/mapping/release/ 中保存的 usage.txt 输出文件来检查移除了哪些代码。

默认配置

学习自定义proguard-rules.pro之前,我们先来看看系统默认的代码混淆规则proguard-android.txt文件。假如proguard-rules.pro文件没用配置任何混淆规则,系统将以proguard-android.txt里的规则为准开启混淆

#混淆时不使用大小写混合类名
-dontusemixedcaseclassnames
#不跳过library中非public的类
-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
#不混淆包含native方法的类和native方法
-keepclasseswithmembernames class * {
    native <methods>;
}

# keep setters in Views so that animations can still work.
# see http://proguard.sourceforge.net/manual/examples.html#beans
#不混淆继承View类的getXX()setXX()方法以保证属性动画正常运行。
-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
#不混淆Activity中public void修饰的含有View参数的方法以保证在xml配置的onclick正常使用
-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations
#不混淆枚举类的values()valueOf()方法
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
#不混淆实现Parcelable接口的类的CREATOR成员
-keepclassmembers class * implements android.os.Parcelable {
  public static final android.os.Parcelable$Creator CREATOR;
}
#不混淆R文件的所有public类成员,如android.R类
-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.
#不对android.support包下的代码警告。例如,app使用了低于某些类的support包版本导致出现警告问题。
-dontwarn android.support.**

# Understand the @Keep support annotation.
#不混淆Keep注解
-keep class android.support.annotation.Keep

#不混淆带Keep注解的类
-keep @android.support.annotation.Keep class * {*;}
#如果类的方法使用了Keep注解,不混淆类与类方法
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <methods>;
}
#如果类的属性使用了Keep注解,不混淆类与类成员
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <fields>;
}
#如果类的构造方法使用了Keep注解,不混淆类与类成员
-keepclasseswithmembers class * {
    @android.support.annotation.Keep <init>(...);
}

通过proguard-android.txt文件,我们大致知道了当开启混淆时,系统默认的混淆规则,当自定义proguard-rule.pro文件时我们就无需重复以上的混淆规则。此外,通过proguard-android.txt我们大致知道如何自定义混淆规则。

配置规则

1、关于keep关键词:

关键词 说明
keep 不混淆类和类成员
keepnames 不混淆类和类成员,但类和类成员未被引用会被移除
keepclassmembers 不混淆类成员
keepclassmembernames 不混淆类成员,但未被引用的类成员会被移除
keepclasseswithmembers 不混淆带有指定条件类成员的类和类成员
keepclasseswithmembernames 不混淆带有指定条件的类和类成员,但未被引用的类和类成员会被移除

或者:
在这里插入图片描述
2、相关通配符

通配符 说明
* 匹配任意长度字符,但不包含包名分隔符”.”
** 匹配任意长度字符,但包含包名分隔符”.”
*** 匹配任意参数类型。例如*** getSchool(***)可匹配String getSchool(String)
匹配任意长度的任意类型的参数。例如void setSchool(…)可匹配void setSchool(String name,int age)
< filed > 匹配类或接口中所有字段
< method > 匹配类、接口的所有方法
< init > 匹配类中所有构造方法

3、具体解析

先看如下两个比较常用的命令,很多童鞋可能会比较迷惑以下两者的区别。

-keep class cn.hadcn.test.**
-keep class cn.hadcn.test.*
  1. 一颗星
    表示只是保持该包下的类名,而子包下的类名还是会被混淆;
  2. 两颗星
    表示把本包和所含子包下的类名都保持;用以上方法保持类后,你会发现类名虽然未混淆,但里面的具体方法和变量命名还是变了。
  3. 这时如果既想保持类名,又想保持里面的内容不被混淆,我们就需要以下方法了:
-keep class cn.hadcn.test.* {*;}

在此基础上,我们也可以使用Java的基本规则来保护特定类不被混淆,比如我们可以用extends,implements等这些Java规则。如下例子就避免所有继承Activity的类被混淆:

-keep public class * extends android.app.Activity

如果我们要保留一个类中的内部类不被混淆则需要用$符号,如下例子表示保持ScriptFragment内部类JavaScriptInterface中的所有public内容不被混淆。

-keepclassmembers class cc.ninty.chat.ui.fragment.ScriptFragment$JavaScriptInterface {
   public *;
}

到这里对混淆配置文件的语法已经基本了解,系统的proguard-android.txt已经为我们完成大部分基础的混淆配置工作。我们只需要写好自己项目的proguard-rules.pro文件,对照 proguard-android.txt照葫芦画瓢就能自定义自己项目的 proguard-rules.pro

对于开启混淆的项目,我们应该从类的创建就要思考这个类是否混淆,大到一个模块,小到一个类的字段,以免到项目庞大以后再来思考混淆就更加难办了。例如,当项目使用了反射机制,对于反射的类及其成员要根据实际情况避免混淆。又例如Gson所要解析成的实体类的字段也要避免混淆。

总结

代码混淆也是一项很有难度的事情,随着项目越来越庞大,混淆的难度也越来越大。一个错误的混淆很可能导致整个APP无法正常运行。因此,学习混淆应该循序渐进,积累更多经验。

通过代码混淆增加逆向的难度和减少APK的体积是成熟的Android应用应该具备的,同时也会增加开发者的开发难度。对于要不要开启代码混淆,应该从项目本身实际情况(成本、收益、开发难度)而定,不要盲目为了混淆而开启混淆项目的可控性下降,当然如果开发人员拥有过硬的混淆代码配置经验,代码混淆对于APP应用本身是利大于弊的。

发布了117 篇原创文章 · 获赞 84 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/weixin_39190897/article/details/98386577