1. 程式人生 > >美團Android資源混淆保護實踐

美團Android資源混淆保護實踐

前言

Android應用中的APK安全性一直遭人詬病,市面上充斥著各種被破解或者漢化的應用,破解者可以非常簡單的通過破解工具就能對一個APK進行反編譯、破解、漢化等等,這樣就可以修改原有程式碼的邏輯、新增新程式碼、新增或修改資源、或者更有甚者植入病毒等等,從而破壞原有APK的安全和使用者體驗,最終傷害到使用者和原有的開發者。
而事物都是有兩方面的,有矛就有盾,針對Android應用安全的各種方案應運而生,大家比較熟悉一般是各類加殼加固的工具,我們可以使用這些工具來保護我們的APK,加殼加固是另外一個話題了,我們這裡不對加殼加固進行介紹,後續如果有機會會單獨開一個話題討論,我們在開發過程中可以通過ProGuard或者DexGuard來保護我們的程式碼,從而實現相對的程式碼安全,但我們的資源呢?我們往往忽略對資原始檔的保護,那這裡將要分享的是如果採用常規方式對APK中的資原始檔進行保護。

資源安全

資源安全這個話題目前大家關注度不算太高,相比較而言大家更關注程式碼安全,目前市面上各類APP基本都使用了ProGuard來保護程式碼的安全,但對資原始檔的保護力度都不大,其實資原始檔是存在比較大的安全隱患,那資源會有哪些安全隱患呢?下面我們通過一個比較簡單的例子來說明下保護資原始檔的重要性。

我們先用最常見的apktool工具來反編譯一個應用來看看,通過執行下面命令就能進行反編譯;

    apktool d -s xxx.apk

反編譯成功後我們來看下反編譯得到的檔案結構(見下圖);

通過上圖中的目錄結構,我們可以看到這個應用的資原始檔大概有:anim、drawable、layout、menu、values等等,我們可以通過修改這些資料夾下的資原始檔,並通過apktool進行回編譯(apktool b 命令)就能建立一個經過修改過的APK應用,例如我們修改下圖中紅色橫線所標示的layout檔案,就能往原有APK的支付資訊(根據資源名稱猜測這個layout的意圖)中新增一些我們自己的東西;

這個問題主要是因為我們在開發過程中倡導命名的規範性,一般都要求在命名時做到見名知意,這樣能夠方便我們自己的理解和維護,但同時這也方便了破解者,破解者可以輕鬆的根據檔名稱來猜測這個檔案的意圖和作用,從而做破壞性的修改。

通過這個例子我們可以看出目前資源安全的重要性,那如何做到資源安全呢?安全都是相對的,沒有絕對的安全,我們接下來要討論的是類似Proguard方式的對我們的資源進行保護。我們主要是通過修改AAPT工具來對資源進行保護,為了方便理解,下面先講一下Android應用是怎麼查詢資源的。

Android查詢資源的流程

在Android系統中,每一個應用程式一般都會配置很多資源,用來適配不同密度、大小和方向的螢幕,以及適配不同的國家、地區和語言等等。這些資源是在應用程式執行時自動根據裝置的當前配置資訊進行適配的。這也就是說,給定一個相同的資源ID,在不同的裝置配置之下,查詢到的可能是不同的資源。
這個查詢過程對應用程式來說,是完全透明的,這個過程主要是靠Android資源管理框架來完成的,而Android資源管理框架實際是由AssetManager和Resources兩個類來實現的。其中,Resources類可以根據ID來查詢資源,而AssetManager類根據檔名來查詢資源。事實上,如果一個資源ID對應的是一個檔案,那麼Resources類是先根據ID來找到資原始檔名稱,然後再將該檔名稱交給AssetManager類來開啟對應的檔案的。基本流程如下圖:

通過上圖我們可以看到Resources是通過resources.arsc把Resource的ID轉化成資原始檔的名稱,然後交由AssetManager來載入的。
而Resources.arsc這個檔案是存放在APK包中的,他是由AAPT工具在打包過程中生成的,他本身是一個資源的索引表,裡面維護者資源ID、Name、Path或者Value的對應關係,AssetManager通過這個索引表,就可以通過資源的ID找到這個資源對應的檔案或者資料。

AAPT介紹

AAPT是Android Asset Packaging Tool的縮寫,它存放在SDK的tools/目錄下,AAPT的功能很強大,可以通過它檢視檢視、建立、更新壓縮檔案(如 .zip檔案,.jar檔案, .apk檔案), 它也可以把資源編譯為二進位制檔案,並生成resources.arsc, AAPT這個工具在APK打包過程中起到了非常重要作用,在打包過程中使用AAPT對APK中用到的資源進行打包,這裡不對AAPT這個工具做過多的討論,只看一下AAPT這個工具在打包過程中起到的作用,下圖是AAPT打包的流程:

AAPT這個工具在打包過程中主要做了下列工作:

  1. 把"assets"和"res/raw"目錄下的所有資源進行打包(會根據不同的檔案字尾選擇壓縮或不壓縮),而"res/"目錄下的其他資源進行編譯或者其他處理(具體處理方式視檔案字尾不同而不同,例如:".xml"會編譯成二進位制檔案,".png"檔案會進行優化等等)後才進行打包;
  2. 會對除了assets資源之外所有的資源賦予一個資源ID常量,並且會生成一個資源索引表resources.arsc;
  3. 編譯AndroidManifest.xml成二進位制的XML檔案;
  4. 把上面3個步驟中生成結果儲存在一個*.ap_檔案,並把各個資源ID常量定義在一個R.java中;

.ap_這個檔案會在生成APK時放入APK包中, .ap_ 這個檔案本身是一個ZIP包,他裡面包含resources.arsc、AndroidManifest.xml、assets以及所有的資原始檔,下圖是UNZIP後的截圖:

可以看出*.ap_這個檔案中包含的內容,這個檔案存放在build/intermediates/res的目錄下,下圖是這個檔案存放的路徑截圖:

資源保護

我們這裡參考Proguard Obfuscator方式,對APK中資原始檔名使用簡短無意義名稱進行替換,給破解者製造困難,從而做到資源的相對安全;通過上面分析,我們可以看出通過修改AAPT在生成resources.arsc和*.ap_時把資原始檔的名稱進行替換,從而保護資源。
通過閱讀AAPT編譯資源的程式碼,我們發現修改AAPT在處理資原始檔相關的原始碼是能夠做到資原始檔名的替換,下面是Resource.cpp中makeFileResources()的修改的程式碼片段:

    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();

            String8 obfuscationName;
            String8 obfuscationPath = getObfuscationName(resPath, obfuscationName);

            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;
    }

上述程式碼是在ResourceTable和Assets中新增資原始檔時, 對資原始檔名稱進行修改,這就能夠做到資原始檔名稱的替換,這樣通過使用修改過的AAPT編譯資源並進行打包,我們再用上面講到的apktool這個工具進行反編譯,下圖是反編譯後的截圖:

發現什麼變化了嗎?在res目錄下熟悉的layout、drawable、anim、menu等資料夾不見了,那他們去哪了呢?因為apktool工具把它們放到了unknown資料夾下了,見下圖:

讓我們來看一下unknown資料夾,你會發現資原始檔名已經被簡短無意義名稱進行替換了,這樣會給反編譯者製造理解上的困難,反編譯者需要消耗一定的時間來搞清楚這些資原始檔的作用,資源混淆帶來的另外一個好處是能明顯減小APK的大小,資源混淆既能保護資原始檔的安全又能減小安裝包的大小,那我們何樂而不為呢?

這樣通過修改AAPT,我們可以在程式碼零修改的基礎下就能做到相對的資源安全,當然安全是相對的,沒有絕對的安全。