混淆的另一重境界

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

Mess介紹

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

使用

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

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

此外,Mess還提供一個可選配置魔招,ignoreProguard晰搀,由于有些依賴庫本身也配置了相關混淆配置,如com.android.support:recyclerview-v7办斑、com.jakewharton:butterknife等外恕,那么這些文件都將會被添加到proguardFiles中,導致依賴庫無法被混淆乡翅,所以ignoreProguard配置就是來解決這個問題的鳞疲。

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

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

實現(xiàn)原理

先來看看Android gradle plugin在構建時最后所走的幾個task:

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

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

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

以上就是Mess干的關鍵性的東西挣柬,接下來依次說明潮酒。

hook transformClassesAndResourcesWithProguardFor${variant.name}

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

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

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

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

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

其中getProcessAndroidResourcesProguardOutputFile方法所對應的文件就是我們所需要清空的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這個Task執(zhí)行之后清空aapt_rules.txt中的內(nèi)容踏堡,以保證編譯出的main.jar中的所有.class都是混淆后的猎唁。

相關代碼如下:

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文件

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

Mess中直接將proguard.txt文件名最后加上~腐魂,如proguard.txt~蹬屹,在linux中表示備份纵揍,以便之后文件的恢復。

相關代碼如下:

  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類名的的映射關系得到一個Map

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

直接遍歷:

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

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

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

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

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

細心活纬凤,拿到AndroidManifest.xml一行一行讀取,匹配到相關字符串則進行替換撩嚼,但這里有個小坑停士,由于Java內(nèi)部類的類名是用$符號分割的,剛好它又是正則表達式表示匹配字符串的結尾完丽,因此對于內(nèi)部類恋技,我們應該現(xiàn)將$符號先替換成其他字符串,然后再做類名的替換逻族,Mess中是替換成inner蜻底,相關代碼如下:

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中的對應Java類名替換了聘鳞,這一步就是替換layout薄辅、menu和value這三個文件夾下的xml內(nèi)容要拂,感謝groovy語法讓整件事情變得非常簡單。layout站楚、menu文件夾大家能立馬理解脱惰,那么value呢?其實就是behavior引入后才存在的窿春,所以value文件夾千萬別忽視拉一。

相關代碼如下:

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

至此,整個工程的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()

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

有頭有尾抽碌。

尾語

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

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末殴蹄,一起剝皮案震驚了整個濱河市究抓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌袭灯,老刑警劉巖刺下,帶你破解...
    沈念sama閱讀 212,222評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異稽荧,居然都是意外死亡橘茉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,455評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來捺癞,“玉大人夷蚊,你說我怎么就攤上這事∷杞椋” “怎么了惕鼓?”我有些...
    開封第一講書人閱讀 157,720評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長唐础。 經(jīng)常有香客問我箱歧,道長,這世上最難降的妖魔是什么一膨? 我笑而不...
    開封第一講書人閱讀 56,568評論 1 284
  • 正文 為了忘掉前任呀邢,我火速辦了婚禮,結果婚禮上豹绪,老公的妹妹穿的比我還像新娘价淌。我一直安慰自己,他們只是感情好瞒津,可當我...
    茶點故事閱讀 65,696評論 6 386
  • 文/花漫 我一把揭開白布蝉衣。 她就那樣靜靜地躺著,像睡著了一般巷蚪。 火紅的嫁衣襯著肌膚如雪病毡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,879評論 1 290
  • 那天屁柏,我揣著相機與錄音啦膜,去河邊找鬼。 笑死淌喻,一個胖子當著我的面吹牛僧家,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播似嗤,決...
    沈念sama閱讀 39,028評論 3 409
  • 文/蒼蘭香墨 我猛地睜開眼啸臀,長吁一口氣:“原來是場噩夢啊……” “哼届宠!你這毒婦竟也來了烁落?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,773評論 0 268
  • 序言:老撾萬榮一對情侶失蹤豌注,失蹤者是張志新(化名)和其女友劉穎伤塌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體轧铁,經(jīng)...
    沈念sama閱讀 44,220評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡每聪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,550評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片药薯。...
    茶點故事閱讀 38,697評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡绑洛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出童本,到底是詐尸還是另有隱情真屯,我是刑警寧澤,帶...
    沈念sama閱讀 34,360評論 4 332
  • 正文 年R本政府宣布穷娱,位于F島的核電站绑蔫,受9級特大地震影響,放射性物質發(fā)生泄漏泵额。R本人自食惡果不足惜配深,卻給世界環(huán)境...
    茶點故事閱讀 40,002評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望嫁盲。 院中可真熱鬧篓叶,春花似錦、人聲如沸羞秤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,782評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锥腻。三九已至嗦董,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間瘦黑,已是汗流浹背京革。 一陣腳步聲響...
    開封第一講書人閱讀 32,010評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留幸斥,地道東北人匹摇。 一個月前我還...
    沈念sama閱讀 46,433評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像甲葬,于是被迫代替她去往敵國和親廊勃。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,587評論 2 350

推薦閱讀更多精彩內(nèi)容