混淆的另一重境界

Mess :https://github.com/JackCho/Mess

Mess介紹

眾所周知,我們開混淆打包后生成的apk里,Activity、自定義View、Service等出現(xiàn)在xml里的相關(guān)Java類默認(rèn)都會被keep住,那么這對于app的保護(hù)是不足夠好的,Mess就是來解決這個(gè)問題,把即使出現(xiàn)在xml文件中的Java類照樣混淆。

使用

dependencies {
   ...
   classpath 'me.ele:mess-plugin:1.0.1'
 }

apply plugin: 'com.android.library'
apply plugin: 'me.ele.mess'

此外,Mess還提供一個(gè)可選配置,ignoreProguard,由于有些依賴庫本身也配置了相關(guān)混淆配置,如com.android.support:recyclerview-v7com.jakewharton:butterknife等,那么這些文件都將會被添加到proguardFiles中,導(dǎo)致依賴庫無法被混淆,所以ignoreProguard配置就是來解決這個(gè)問題的。

比如忽視com.android.support:recyclerview-v7的混淆配置文件,則直接

mess {
    ignoreProguard 'com.android.support:recyclerview-v7'
}

實(shí)現(xiàn)原理

先來看看Android gradle plugin在構(gòu)建時(shí)最后所走的幾個(gè)task:

:app:processReleaseResources
...
:app:transformClassesAndResourcesWithProguardForRelease
:app:transformClassesWithDexForRelease
:app:transformClassesWithShrinkResForRelease
:app:mergeReleaseJniLibFolders
:app:transformNative_libsWithMergeJniLibsForRelease
:app:validateDebugSigning
:app:packageRelease
:app:zipalignRelease
:app:assembleRelease

其中有幾個(gè)關(guān)鍵性的task,可以看到:app:transformClassesAndResourcesWithProguardForRelease是走在:app:packageRelease之前的,那么我們就在打包前對混淆的task做些操作來實(shí)現(xiàn)我們的目的。

  • hook transformClassesAndResourcesWithProguardFor${variant.name.capitalize()}
  • hook ProcessAndroidResources Task,將生成的aapt_rules.txt中內(nèi)容清空
  • 如果需要混淆依賴庫,則刪除依賴庫中的proguard.txt文件
  • 遍歷一遍mapping.txt獲取所有Java類名的的映射關(guān)系得到一個(gè)Map
  • 拿映射Map替換AndroidManifest.xml里的Java原類名
  • 拿映射Map替換layout、menu和value文件夾下的xml的Java原類名
  • 重新跑ProcessAndroidResources Task
  • 恢復(fù)之前刪除依賴庫中的proguard.txt文件

以上就是Mess干的關(guān)鍵性的東西,接下來依次說明。

hook transformClassesAndResourcesWithProguardFor${variant.name}

這個(gè)task是處理類和資源混淆的,也是我們的突破口,Mess中大部分自定義task都是圍繞在這個(gè)task執(zhí)行的,之后會有詳解。

hook ProcessAndroidResources Task,將生成的aapt_rules.txt中內(nèi)容清空

這一步是雖說只是把aapt_rules.txt文件中的內(nèi)容清空,但是確實(shí)Mess Plugin能成功的最關(guān)鍵的一步。

ProcessAndroidResources task會生成一個(gè)aapt_rules.txt,可見源碼ProcessAndroidResources.groovy,aapt_rules.txt里會keep住我們在xml里所書寫的那些Activity、自定義View等Java類名部分,還可以看到JackTask.java里的相關(guān)代碼:

if (config.isMinifyEnabled()) {
    ConventionMappingHelper.map(jackTask, "proguardFiles", new Callable<List<File>>() {
        @Override
        public List<File> call() throws Exception {
            // since all the output use the same resources, we can use the first output
            // to query for a proguard file.
            File sdkDir = scope.getGlobalScope().getSdkHandler().getAndCheckSdkFolder();
            File defaultProguardFile =  new File(sdkDir,
                SdkConstants.FD_TOOLS + File.separatorChar
                    + SdkConstants.FD_PROGUARD + File.separatorChar
                    + TaskManager.DEFAULT_PROGUARD_CONFIG_FILE);

            List<File> proguardFiles = config.getProguardFiles(true /*includeLibs*/,
                ImmutableList.of(defaultProguardFile));
            File proguardResFile = scope.getProcessAndroidResourcesProguardOutputFile();
            proguardFiles.add(proguardResFile);
            // for tested app, we only care about their aapt config since the base
            // configs are the same files anyway.
            if (scope.getTestedVariantData() != null) {
                proguardResFile = scope.getTestedVariantData().getScope()
                    .getProcessAndroidResourcesProguardOutputFile();
                proguardFiles.add(proguardResFile);
            }

            return proguardFiles;
         }
   });

   jackTask.mappingFile = new File(scope.getProguardOutputFolder(), "mapping.txt");
}

其中g(shù)etProcessAndroidResourcesProguardOutputFile方法所對應(yīng)的文件就是我們所需要清空的aapt_rules.txt,可以在VariantScope.java中查看。

@NonNull
public File getProcessAndroidResourcesProguardOutputFile() {
    return new File(globalScope.getIntermediatesDir(),
         "/proguard-rules/" + getVariantConfiguration().getDirName() + "/aapt_rules.txt");
}

很明顯,aapt_rules.txt所keep住的所有內(nèi)容都將會添加到最后的混淆配置中,因此,我們需要在ProcessAndroidResources這個(gè)Task執(zhí)行之后清空aapt_rules.txt中的內(nèi)容,以保證編譯出的main.jar中的所有.class都是混淆后的。

相關(guān)代碼如下:

boolean hasProcessResourcesExecuted = false
output.processResources.doLast {
    if (hasProcessResourcesExecuted) {
    return
    }
    hasProcessResourcesExecuted = true

    def rulesPath = "${project.buildDir.absolutePath}/intermediates/proguard-rules/${variant.dirName}/aapt_rules.txt"
    File aaptRules = new File(rulesPath)
    aaptRules.delete()
    aaptRules << ""
}

如果需要混淆依賴庫,則刪除依賴庫中的proguard.txt文件

這一步就是刪除依賴庫中所保護(hù)的內(nèi)容,具體proguard.txt文件位于app目錄下/build/intermediates/exploded-aar/依賴庫maven名/proguard.txt

Mess中直接將proguard.txt文件名最后加上~,如proguard.txt~,在linux中表示備份,以便之后文件的恢復(fù)。

相關(guān)代碼如下:

  public static void hideProguardTxt(Project project, String component) {
    renameProguardTxt(project, component, 'proguard.txt', 'proguard.txt~')
  }

  public static void recoverProguardTxt(Project project, String component) {
    renameProguardTxt(project, component, 'proguard.txt~', 'proguard.txt')
  }

  private static void renameProguardTxt(Project project, String component, String orgName,
      String newName) {
    MavenCoordinates mavenCoordinates = parseMavenString(component)
    File bundlesDir = new File(project.buildDir, "intermediates/exploded-aar")
    File bundleDir = new File(bundlesDir,
        "${mavenCoordinates.groupId}/${mavenCoordinates.artifactId}")
    if (!bundleDir.exists()) return
    bundleDir.eachFileRecurse(FileType.FILES) { File f ->
      if (f.name == orgName) {
        File targetFile = new File(f.parentFile.absolutePath, newName)
        println "rename file ${f.absolutePath} to ${targetFile.absolutePath}"
        Files.move(f, targetFile)
      }
    }
  }

遍歷一遍mapping.txt獲取所有Java類名的的映射關(guān)系得到一個(gè)Map

之前第一步已經(jīng)將生成的main.jar中所有的.class文件做相關(guān)混淆了,那么我們之前所在xml里寫的還是原來的Java類名,因此,我們想要替換xml里的Java類名,就得先知道原先的類名被替換成什么了,這個(gè)時(shí)候就得依賴mapping.txt了。

直接遍歷:

File mappingFile = apkVariant.mappingFile
println mappingFile.toString()
mappingFile.eachLine { line ->
    //方法名的混淆前面是會有空格的,我們這里只需要拿類名的映射關(guān)系
    if (!line.startsWith(" ")) {
        // 如me.ele.mess.SecondActivity -> me.ele.mess.z:
        // -> 作為分割符號
        String[] keyValue = line.split("->")
        // 原始文件名
        String key = keyValue[0].trim()
        // 混淆后文件名,去掉最后一個(gè)":"
        String value = keyValue[1].subSequence(0, keyValue[1].length() - 1).trim()
        // 添加進(jìn)map
        if (!key.equals(value)) {
            map.put(key, value)
        }
    }
}

這樣后map里就存有所有類名的映射關(guān)系了,但是有個(gè)小問題要注意,假如存在這種情況,me.ele.foo -> me.ele.a,me.ele.fooNew -> me.ele.b,也就是恰巧有類名是另一個(gè)類名的開始部分,那么這樣對我們之后的替換是會有bug的,會導(dǎo)致fooNew被替換成了aNew。因此,拿到map后需要對map做一次原類名長度的降序排序(也就是map中的key),以避免這個(gè)bug發(fā)生。相關(guān)代碼如下:

  public static Map<String, String> sortMapping(Map<String, String> map) {
    List<Map.Entry<String, String>> list = new LinkedList<>(map.entrySet());
    Collections.sort(list, new Comparator<Map.Entry<String, String>>() {
      public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
        return o2.key.length() - o1.key.length()
      }
    });

    Map<String, String> result = new LinkedHashMap<>();
    for (Iterator<Map.Entry<String, String>> it = list.iterator(); it.hasNext();) {
      Map.Entry<String, String> entry = (Map.Entry<String, String>) it.next();
      result.put(entry.getKey(), entry.getValue());
    }

    return result;
  }

至此,一個(gè)正確的map已經(jīng)拿到,接下來就是靠這個(gè)map來對相關(guān)的xml文件做替換了。

拿映射Map替換AndroidManifest.xml里的Java原類名

細(xì)心活,拿到AndroidManifest.xml一行一行讀取,匹配到相關(guān)字符串則進(jìn)行替換,但這里有個(gè)小坑,由于Java內(nèi)部類的類名是用$符號分割的,剛好它又是正則表達(dá)式表示匹配字符串的結(jié)尾,因此對于內(nèi)部類,我們應(yīng)該現(xiàn)將$符號先替換成其他字符串,然后再做類名的替換,Mess中是替換成inner,相關(guān)代碼如下:

File f = new File(path)
StringBuilder builder = new StringBuilder()
f.eachLine { line ->
    //<me.ele.base.widget.LoadingViewPager -> <me.ele.aaa
    // app:actionProviderClass="me.ele.base.ui.SearchViewProvider" -> app:actionProviderClass="me.ele.bbv"
    if (line.contains("<${oldStr}") || line.contains("${oldStr}>") || line.contains("${oldStr}\"")) {
        if (line.contains("\$") && oldStr.contains("\$")) {
             oldStr = oldStr.replaceAll("\\\$", "inner")
             line = line.replaceAll("\\\$", "inner").replaceAll(oldStr, newStr)
        } else {
            line = line.replaceAll(oldStr, newStr)
        }
    }
    builder.append(line);
    builder.append("\n")
}

f.delete()
f << builder.toString()

拿映射Map替換layout、menu和value文件夾下的xml的Java原類名

前一步已經(jīng)把AndroidManifest.xml中的對應(yīng)Java類名替換了,這一步就是替換layout、menu和value這三個(gè)文件夾下的xml內(nèi)容,感謝groovy語法讓整件事情變得非常簡單。layout、menu文件夾大家能立馬理解,那么value呢?其實(shí)就是behavior引入后才存在的,所以value文件夾千萬別忽視。

相關(guān)代碼如下:

File layoutDir = new File(getLayoutPath())
File menuDir = new File(getMenuPath())
File valueDir = new File(getValuePath())
[layoutDir, menuDir, valueDir].each {File dir ->
    if (dir.exists()) {
        dir.eachFileRecurse(FileType.FILES) { File file ->
            String orgTxt = file.text
            String newTxt = orgTxt
            map.each { k, v ->
                newTxt = newTxt.replace(k, v)
            }
            if (newTxt != orgTxt) {
            println 'rewrite file: ' + file.absolutePath
            file.text = newTxt
            }
        }
    }
}

至此,整個(gè)工程的main.jar中的.class文件以及資源文件都替換成相互匹配的混淆后的名稱了。

重新跑ProcessAndroidResources Task

前些步驟hook后ProcessAndroidResources Task之后我們已經(jīng)把靜態(tài)的文件都替換好了,那么接下來就還得依靠Android gradle plugin的原有tasks了,于是乎我們重新執(zhí)行ProcessAndroidResources Task。

ProcessAndroidResources processTask = variantOutput.processResources
processTask.state.executed = false
processTask.execute()

恢復(fù)之前刪除依賴庫中的proguard.txt文件

有頭有尾。

尾語

想要寫出Mess這樣的plugin,對Android整個(gè)打包流程是要相當(dāng)熟悉的,這樣才能知道什么時(shí)候該hook什么task,平常開發(fā)過程中盡量不要直接點(diǎn)擊run按鈕,應(yīng)該直接通過gradle assemble** 構(gòu)建,這樣無數(shù)次的看構(gòu)建過程中經(jīng)歷哪些task,然后去閱讀相關(guān)task源碼,這樣對整個(gè)打包流程才會越來越胸有成竹。

Mess有個(gè)小遺憾,那就是ButterKnife這個(gè)庫在絕大多數(shù)app中都使用了,但是ButterKnife的混淆規(guī)則中有對使用注解的方法名和變量名做保護(hù),這樣就比較尷尬了,會導(dǎo)致Mess對使用ButterKnife庫的app而言是沒多大作用的。

-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }

但是不要灰心,ButterMess這個(gè)Lib就來解決這個(gè)問題,接下來會寫篇詳解ButterMess的文章,先放個(gè)ButterMess的鏈接:https://github.com/peacepassion/ButterMess

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容