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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容