Android混淆解析

一:混淆的作用

1.1 作用

混淆 并不是让代码无法被反编译,而是将代码中的类、方法、变量等信息进行重命名,把它们改成一些毫无意义的名字。混淆代码可以在不影响程序正常运行的前提下让破解者很头疼,从而大大提升了程序的安全性。

混淆APK build.gradle中minifyEnabled的值是false,这里我们只需要把值改成true,打出来的APK包就会是混淆过的了。

release { 
  minifyEnabled true
  proguardFiles getDefaultProguardFile('proguard-android.txt'),  'proguard-rules.pro'
}

其中minifyEnabled用于设置是否启用混淆,proguardFiles用于选定混淆配置文件。注意这里是在release闭包内进行配置的,因此只有打出正式版的APK才会进行混淆,Debug版的APK是不会混淆的。

二:代码混淆

2.1 默认混淆规则

proguard-android.txt文件:
Android SDK/tools/proguard目录下

# This is a configuration file for ProGuard.
# http://proguard.sourceforge.net/index.html#manual/usage.html

-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.
# Dont 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.**

-dontusemixedcaseclassnames: 表示混淆时不使用大小写混淆类名。
-dontskipnonpubliclibraryclasses:不跳过library中的非public方法。
-verbose: 打印混淆的详细信息。
-dontoptimize: 不进行优化,优化可能会造成一些潜在风险,不能保证在所有版本的Dalvik上都正常运行。
-dontpreverify: 不进行预校验
-keepattributes Annotation: 对注解参数进行保留。
-keep public class com.google.vending.licensing.ILicensingService
-keep public class com.android.vending.licensing.ILicensingService:

表示不混淆上述声明的两个类。

表示不混淆任何包含native方法的类的类名以及native方法名

-keepclasseswithmembernames class * {
    native <methods>;
}  

表示不混淆任何一个View中的setXxx()和getXxx()方法,因为属性动画需要有相应的setter和getter的方法实现

-keepclassmembers public class * extends android.view.View {
   void set*(***);
   *** get*();
}

表示不混淆Activity中参数是View的方法

-keepclassmembers class * extends android.app.Activity {
   public void *(android.view.View);
}

表示不混淆枚举中的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文件中的所有静态字段

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

proguard中一共有三组六个keep关键字的含义

keep  保留类和类中的成员,防止它们被混淆或移除。
keepnames 保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclassmembers  只保留类中的成员,防止它们被混淆或移除。
keepclassmembernames  只保留类中的成员,防止它们被混淆,但当成员没有被引用时会被移除。
keepclasseswithmembers  保留类和类中的成员,防止它们被混淆或移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。
keepclasseswithmembernames  保留类和类中的成员,防止它们被混淆,但当成员没有被引用时会被移除,前提是指名的类中的成员必须存在,如果不存在则还是会混淆。

keepclasseswithmember和keep关键字的区别

如果这个类没有native的方法,那么这个类会被混淆

-keepclasseswithmember class * {
    native <methods>;
}

不管这个类有没有native的方法,那么这个类不会被混淆

-keep class * {
    native <methods>;
}

proguard-rules.pro:
任何一个Android Studio项目在app模块目录下都有一个proguard-rules.pro文件,这个文件就是用于让我们编写只适用于当前项目的混淆规则的

2.2 mapping文件

三:资源混淆

上面介绍的混淆仅仅针对Java代码进行混淆,而不能对资源文件进行混淆,资源文件是指:anim、drawable、layout、menu、values等等。

3.1 背景知识

3.1.1 Android资源的编译和打包过程简介

这些资源文件是通过Android资源打包工具aapt(Android Asset Package Tool)打包到APK文件里面的。在打包之前,大部分文本格式的XML资源文件还会被编译成二进制格式的XML资源文件。
具体流程可以参照,本文只给出一个大致的流程介绍。
Android应用程序资源的编译和打包过程分析

  • 解析AndroidManifest.xml: 获取包名。

  • 添加被引用资源包: apk中至少会有两个资源包:apk本身的,系统通用资源包有一些系统资源如android:orientation等。

  • 收集资源文件:Package、Type、Config。
    Package: 资源所属包名
    Type: 资源类型; anim、drawable、layout、menu、values.
    Config: 资源配置:hdpi,xhdpi,zh_rCN , land。分辨率和语言等有18个维度.
    资源ID 最高字节表示Package ID,次高字节表示Type ID,最低两字节表示Entry ID。

  • 将收集到的资源增加到资源表

  • 编译values类资源

  • 给Bag资源分配ID: values的资源除了是string之外,还有其它很多类型的资源,其中有一些比较特殊,如bag、style、plurals和array类的资源。这些资源会给自己定义一些专用的值,这些带有专用值的资源就统称为Bag资源。

  • 编译Xml资源文件

  • 生成资源符号:

收集到的资源项都按照类型来保存在一个资源表中,即保存在一个 
 ResourceTable对象。因此,Android资源打包工具aapt只要遍历每一个   
 Package里面的每一个Type,然后取出每一个Entry的名称,并且根据这 
 个 Entry在自己的Type里面出现的次序来计算得到它的资源ID,那么就可 
 以 生成一个资源符号了,这个资源符号由名称以及资源ID所组成。 例 
 如, 对于strings.xml文件中名称为“start_in_process”的Entry来说,它是 
 一个类  型为string的资源项,假设它出现的次序为第3,那么它的资源符 
 号就等于  R.string.start_in_process,对应的资源ID就为0x7f050002,其 
 中,高字 节0x7f表示Package ID,次高字节0x05表示string的Type ID, 
 而低两字节  0x02就表示“start_in_process”是第三个出现的字符串。   
  • 生成资源索引表 resources.arsc
  • 编译AndroidManifest.xml文件
  • 生成R.java文件
  • 打包APK文件


    资源文件存储

3.1.2 AAPT

AAPT是Android Asset Packaging Tool的缩写,它存放在SDK的tools/目录下,AAPT的功能很强大,可以通过它查看查看、创建、更新压缩文件(如 .zip文件,.jar文件, .apk文件), 它也可以把资源编译为二进制文件,并生成resources.arsc, AAPT这个工具在APK打包过程中起到了非常重要作用,在打包过程中使用AAPT对APK中用到的资源进行打包

AAPT

3.1.3 资源索引表-resouce.arsc文件

Android资源打包工具aapt在编译和打包资源的过程中,会执行以下两个额外的操作:

  • 赋予每一个非assets资源一个ID值,这些ID值以常量的形式定义在一个R.Java文件中。

  • 生成一个resources.arsc文件,用来描述那些具有ID值的资源的配置信息,它的内容就相当于是一个资源索引表。

  • 有了资源ID以及资源索引表之后,Android资源管理框架就可以迅速将根据设备当前配置信息来定位最匹配的资源了。

    3.2 美团资源混淆方案

Hook: Resource.cpp中makeFileResources()。
makeFileResources()的作用:将收集到的资源文件加到资源表(ResourceTable)对res目录下的各个资源子目录进行处理,函数为makeFileResources:makeFileResources会对资源文件名做合法性检查,并将其添加到ResourceTable内。
Hook后的方法:主要增加了getObfuscationName(resPath, obfuscationName)来获取混淆后的名称和路径

static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
                                      ResourceTable* table,
                                      const sp<ResourceTypeSet>& set,
                                      const char* resType)
    {  
       //定义两个字符串
        String8 type8(resType);
        String16 type16(resType);

        bool hasErrors = false;
        //迭代遍历ResourceTypeSet。
        ResourceDirIterator it(set, String8(resType));
        ssize_t res;
        while ((res=it.next()) == NO_ERROR) {
            if (bundle->getVerbose()) {
                printf("    (new resource id %s from %s)\n",
                       it.getBaseName().string(), it.getFile()->getPrintableSource().string());
            }
            String16 baseName(it.getBaseName());
            const char16_t* str = baseName.string();
            const char16_t* const end = str + baseName.size();
            while (str < end) {
                //正则匹配 [a-z0-9_.]
                if (!((*str >= 'a' && *str <= 'z')
                        || (*str >= '0' && *str <= '9')
                        || *str == '_' || *str == '.')) {
                    fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n",
                            it.getPath().string());
                    hasErrors = true;
                }
                str++;
            }
            String8 resPath = it.getPath();
            resPath.convertToResPath();

            String8 obfuscationName;
           //获取混淆后名称
            String8 obfuscationPath = getObfuscationName(resPath, obfuscationName);
            //添加到ResourceTable
            table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
                            type16,
                            baseName, // String16(obfuscationName),
                            String16(obfuscationPath), // resPath
                            NULL,
                            &it.getParams());
            assets->addResource(it.getLeafName(), obfuscationPath/*resPath*/, it.getFile(), type8);
        }

        return hasErrors ? UNKNOWN_ERROR : NO_ERROR;
    }

系统方法

static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
                                  ResourceTable* table,
                                  const sp<ResourceTypeSet>& set,
                                  const char* resType)
{
    String8 type8(resType);
    String16 type16(resType);
    bool hasErrors = false;
    ResourceDirIterator it(set, String8(resType));
    ssize_t res;
    while ((res=it.next()) == NO_ERROR) {
        if (bundle->getVerbose()) {
            printf("    (new resource id %s from %s)\n",
                   it.getBaseName().string(), it.getFile()->getPrintableSource().string());
        }
        String16 baseName(it.getBaseName());
        const char16_t* str = baseName.string();
        const char16_t* const end = str + baseName.size();
        while (str < end) {
            if (!((*str >= 'a' && *str <= 'z')
                    || (*str >= '0' && *str <= '9')
                    || *str == '_' || *str == '.')) {
                fprintf(stderr, "%s: Invalid file name: must contain only [a-z0-9_.]\n",
                        it.getPath().string());
                hasErrors = true;
            }
            str++;
        }
        String8 resPath = it.getPath();
        resPath.convertToResPath();
        table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
                        type16,
                        baseName,
                        String16(resPath),
                        NULL,
                        &it.getParams());
        assets->addResource(it.getLeafName(), resPath, it.getFile(), type8);
    }
    return hasErrors ? UNKNOWN_ERROR : NO_ERROR;
}

3.3 微信资源混淆方案

修改resources.arsc文件
ApkDecoder.decode();

 public void decode() throws AndrolibException, IOException, DirectoryException {
        if (hasResources()) {
            ensureFilePath();
            // read the resources.arsc checking for STORED vs DEFLATE compression
            // this will determine whether we compress on rebuild or not.
            System.out.printf("decoding resources.arsc\n");
            RawARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"));
            ResPackage[] pkgs = ARSCDecoder.decode(mApkFile.getDirectory().getFileInput("resources.arsc"), this);

            //把没有纪录在resources.arsc的资源文件也拷进dest目录
            copyOtherResFiles();

            ARSCDecoder.write(mApkFile.getDirectory().getFileInput("resources.arsc"), this, pkgs);
        }
    }

修改的主要逻辑在ARSCDecoder:

readTable-readPackage-readType-readConfig-readEntry;

资源混淆核心处理过程如下:

  • 生成新的资源文件目录,里面对资源文件路径进行混淆(其中涉及如何复用旧的mapping文件),例如将res/drawable/hello.png混淆为r/s/a.png,并将映射关系输出到mapping文件中。
  • 对资源id进行混淆(其中涉及如何复用旧的mapping文件),并将映射关系输出到mapping文件中。
  • 生成新的resources.arsc文件,里面对资源项值字符串池、资源项key字符串池、进行混淆替换,对资源项entry中引用的资源项字符串池位置进行修正、并更改相应大小,并打包生成新的apk。

四:参考文献

微信资源混淆AndResGuard原理
安装包立减1M--微信Android资源混淆打包工具
美团Android资源混淆保护实践
Android应用程序资源的编译和打包过程分析-luoshengyang
Android应用程序资源的编译和打包过程分析
微信资源混淆AndResGuard原理
AndResGuard

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

推荐阅读更多精彩内容