Android混淆進階

前言:
上一次真正更新內容還是在年初,想想在新的公司已經大半年了。在脫離只做Android上層需求的局限后,這半年期間遇到了很多問題也學習到了很多不一樣的東西。比如沙盒技術、Hook、自定義反射Task、反編譯、研究系統源碼解決沙盒bug等。今天就來談一下Android混淆以及自定義gradle Task的相關知識,帶您體驗不一樣的,更加完備(包括四大組件、native)的混淆知識。

說到Android的混淆,隨便百度都是一抓一大把.比如怎么開啟混淆,怎么做到代碼和資源的混淆或是包括一些諸如 -Keep、 -keepclassmembers、 -keepclasseswithmembers、dontwarn等混淆命令。本人覺得這些都是一個Android程序員必須要掌握的基本技能。
但是你是否有想過,為什么目前不需要對四大組件做keep 系統都默認不會去混淆呢? 為什么我沒有對native相關的代碼做keep,系統同樣會幫我們做避免混淆的操作呢?
開始研究前,我們先引入R8混淆編譯器的東西。
Android Gradle插件升級至3.4.0版本之后,帶來一個新特性-新一代混淆工具R8,做為D8的升級版替代Proguard;在應用壓縮、應用優化方面提供更極致的體驗。
R8 一步到位地完成了所有的縮減(shrinking),去糖(desugaring)和 轉換成 Dalvik 字節碼(dexing )過程。

縮減(shrinking)過程實現以下三個重要的功能:

代碼縮減:從應用及其庫依賴項中檢測并安全地移除未使用的類、字段、方法和屬性。
資源縮減:從封裝應用中移除不使用的資源,包括應用庫依賴項中的不使用的資源。
優化:檢查并重寫代碼,以進一步減小應用的 DEX 文件的大小。
混淆:縮短類和成員的名稱,從而減小 DEX 文件的大小。
?R8 和當前的代碼縮減解決方案 Proguard 相比,R8 可以更快地縮減代碼,同時改善輸出大小。
轉至:https://blog.csdn.net/fitaotao/article/details/115083963
簡單來說R8就是Proguard的升級版。

1、四大組件混淆

好了,暫時忘記這個概念。我們來做一個簡單的需求:去除四大組件的混淆。
對于這個需求也許你會有個疑問?我們正常開發不是都是對四大組件做keep操作嗎?為什么要去除呢?如果去除了,那編譯出的apk,pms在解析AndroidnddManifest.xml的時候不就認不出四大組件的名字了嗎?
沒錯,混淆后pms在解析的時候確實會出問題,導致app閃退。Android的混淆確實這點沒做好,就是不會在混淆的同時去同步的把AndroidnddManifest.xml的類名改掉,所以這個需要我們在合適的編譯點創建任務根據mapping混淆表去修改對應的四大組件類名。
OK,去除四大組件的混淆,也許你直接就把四大組件的相關keep刪除,本以為大功告成卻發現為什么明明刪除了配置,四大組件還是沒有達到混淆效果呢?
研究這個問題前,我們執行下打包任務,直接看最后幾個任務

Task :module-api:mergeReleaseJavaResource
Task :module-api:transformClassesAndResourcesWithR8ForRelease
Task :module-api:transformClassesAndResourcesWithSyncLibJarsForRelease
Task :module-api:bundleReleaseAar
Task :module-api:reBundleAarRelease

其中mergeReleaseJavaResource之后系統已經生成了aapt-rules.txt文件(build\intermediates\proguard-rules\release\aapt_rules.txt)
點進去你會驚奇的發現,里面內容是一個混淆表,其中左邊的類名都是四大組件的名字,右邊轉成的名字還是原來的名字,聰明的你或許會有個猜想:難道四大組件的混淆在這里配置的嗎?
沒錯,實際上aapt_rules.txt會去解析AndroidManifest.xml中關于四大組件的類名,aapt_rules.txt所keep住的所有內容都將會添加到最后的混淆配置中,即使你不寫keep配置,系統仍然默認幫你避免這些混淆。
所以這個需求的關鍵點
1、在編譯時候的mergeReleaseJavaResource任務后清空aapt_rules.txt文件的內容,
2、在transformClassesAndResourcesWithR8ForRelease(生成mapping文件)解析混淆表的內容,存儲到map中,然后替換編譯后的(build\intermediates\library_manifest\release\AndroidManifest.xml)AndroidManifest.xml文件中的四大組件的名字。
下面展示下代碼實現

1,清空

open class DontKeepDefaultRulesTask : ProguardDefaultTask() {
    private val nativeKeep = "-keepclasseswithmembernames class * {\n" +
            "    native <methods>;\n" +
            "}"

    @TaskAction
    fun delete() {
        doTask {
            val aaptFile = File(project.buildDir, "intermediates/proguard-rules/$buildType/aapt_rules.txt")
            if (aaptFile.exists()) {
                aaptFile.writeText("")
            }

            //刪除系統默認的 native keep規則
            val defaultFile = File(project.buildDir, "intermediates/proguard-files")
            defaultFile.listFiles()?.forEach {
                var content = it.readText()
                content = content.replace(nativeKeep,"")
                it.writeText(content)
            }
        }

    }
}

創建任務,到對應的文件中清空內容(忽略下面的native的混淆)

val mergeJavaResource = taskContainer.findByName("merge${buildType}JavaResource")
                    //該任務后刪除aapt_rules.txt
                    val aaptDeleteTask = taskContainer.create("AaptRulesDeleteFor$buildType", DontKeepDefaultRulesTask::class.java)
                    aaptDeleteTask.buildType = buildType
                    mergeJavaResource?.finalizedBy(aaptDeleteTask)

該任務在mergeReleaseJavaResource后執行

2、替換

open class ManifestRebuildTask : ProguardDefaultTask() {

    @TaskAction
    fun rebuild() {
        doTask {
            val mappingFile = File(project.buildDir, "outputs/mapping/$buildType/mapping.txt")
            if (!mappingFile.exists()) {
                println("mapping file not exits")
                return@doTask
            }

            val map = MappingParsingUtil.parsing(mappingFile)

            val manifestFile = File(project.buildDir, "intermediates/library_manifest/$buildType/AndroidManifest.xml")
            if (!manifestFile.exists()) {
                println("androidManifest file not exits")
                return@doTask
            }

            var content = manifestFile.readText()
            content = content.replace("\$", "inner")

            map.forEach {
                val keyStr = it.key.replace("\$", "inner")
                val valueStr = it.value.replace("\$", "inner")
                content = content.replace(keyStr, valueStr)
            }
            content = content.replace("inner", "\$")

            manifestFile.writeText(content)

        }


    }
}

思路:在transformClassesAndResourcesWithR8ForRelease后獲取mapping文件并解析到map臨時變量中,同時替換AndroidManifest.xml中相關的類名(inner的替換操作是因為 java中 $符號比較特殊,他是內部類符號,同時也是正則表達式表示匹配字符串的結尾)

該任務在transformClassesAndResourcesWithR8ForRelease后執行

 val transformClassesAndResourcesWithR8Task = taskContainer.findByName("transformClassesAndResourcesWithR8For$buildType")
                    //該任務后修改androidManifest.xml
                    val manifestRebuildTask = taskContainer.create("ManifestRebuildFor$buildType", ManifestRebuildTask::class.java)
                    manifestRebuildTask.buildType = buildType
                    transformClassesAndResourcesWithR8Task?.finalizedBy(manifestRebuildTask)

2、Native相關的混淆

同四大組件一樣,網上在講到native混淆配置的時候通常要加一句

-keepclasseswithmembernames class * {
             native <methods>; 
 }

保持native的方法不被混淆,否則c端就沒法認識你的類名和方法名了。
但是我想說真正嚴謹的混淆是需要你在Android端做類名及方法名的混淆的,雖然c端肯定有自己的操作對so進行混淆。
或許你此時的處理方式也是毫不猶豫的去除上面的keep配置,然后重新編譯,不出意外同樣沒有任何效果。此時你的心情或許如下圖


微信圖片_20210930171129.jpg

不急,有了上面的經驗或許你會想到:是不是系統又對這塊做了默認操作呢?

沒錯,打開build\intermediates\proguard-files目錄下會發現有三個關于混淆的文件,每個文件中都有這么一句配置:
image.png

坑爹,原來又是系統搞的鬼。不過有了之前去除四大組件混淆的默認操作,同樣我們也可以在mergeReleaseJavaResource任務后遍歷三個文件,去除這句配置。(代碼往上翻)

好了,去除默認的keep操作有了,那么問題來了。不管系統最后會混淆成上面字母,那么c端的開發者要怎么知道到底改成什么名字呢?
對于四大組件,Android有默認的AndroidnddManifest.xml可修改操作,讓系統知道我們改后的名字,從而不至于導致pms解析失敗,那native要怎么辦呢?
不慌,方法都是相通的。同樣,我們可以和c端規定一份頭文件,把類名及方法名混淆后的名字改在該文件中,在編譯他們的native任務的時候,c端會先去引入修改后的改頭文件,從而拿到混淆后的名字。
但是問題來了,這個任務明顯是要在native的任務之前(preReleaseBuild),此時mapping文件根本都還沒生成,連我們自己都不知道最終生成的名字,還怎么給他們改呢?
我們換一種思路,能不能事先給他們規定成想要的名字呢?
我們知道混淆規則中可以添加一條applyMapping的命令,該命令可以做到增量添加mapping的內容,此時剛好達到我們想要的結果。那么接下來,我們只需在preReleaseBuild命令前加一個生成native.mapping文件并填充內容的任務即可

open class ProguardFileCreateTask : ProguardDefaultTask() {
    var isMinifyEnabled = false

    @TaskAction
    fun create() {
        doTask {
            //創建 NamedGen頭文件
            val copyFile = File(project.rootDir, "library-native${File.separator}src${File.separator}main${File.separator}cpp${File.separator}NamedGen.h")
            
            val random = getRandomLetter(3)


            //創建native.mapping文件用于指定native方法的混淆并修改復制的模板文件
            val nativeMappingFile = File(project.projectDir, "native.mapping")
            //..\mxxxx_PluginGame\library-native\src\main\cpp\Named.h
            val nameHFile = File(project.rootDir, "library-native${File.separator}src${File.separator}main${File.separator}cpp${File.separator}Named.h")

            val fileStringBuffer = StringBuffer()
            val mappingStringBuffer = StringBuffer()

            if (isMinifyEnabled) {

                nameHFile.readLines().forEach { line ->
                    when {
                        line.startsWith("O_CLASS") -> {
                            //O_CLASS(com___mxxxx___library_native___NativeUtils, com/mxxxx/library_native/NativeUtils);
                            val randomClassName = "${random}/${getRandomLetter(2)}"
                            fileStringBuffer.append("O_CLASS(com___mxxxx___library_native___NativeUtils, $randomClassName)\n")

                            mappingStringBuffer.append("com.mxxxx.library_native.NativeUtils -> ${randomClassName.replace("/", ".")}:\n")

                        }
                        line.startsWith("O_FUNC") -> {
                            //O_FUNC(boolean, nativeHookOpenDexFileNative(java.lang.reflect.Method), nativeHookOpenDexFileNative)
                            val randomFunName = getRandomLetter(2)
                            val tempStr = line.replace(line.substring(line.lastIndexOf(",") + 1, line.lastIndexOf(")") + 1), "${randomFunName})")
                            fileStringBuffer.append("${tempStr}\n")

                            mappingStringBuffer.append("\t${resolveFuncName(line, randomFunName)}\n")
                        }
                        else -> {
                            fileStringBuffer.append("${line}\n")
                        }
                    }
                }

                copyFile.writeText(fileStringBuffer.toString())
                nativeMappingFile.writeText(mappingStringBuffer.toString())
            } else {
                copyFile.writeText(nameHFile.readText())
                mappingStringBuffer.append("com.mxxxx.library_native.NativeUtils -> com.mxxxx.library_native.NativeUtils:")
            }

        }
    }
}

對應的混淆規則


image.png

總結

最后說一下,本文的干貨除了分析混淆規則外還有就是關于Gradle插件相關的內容,另外我們開發的Android項目都會引入一個com.android.tools.build:gradle.xxxx,每個版本都會自帶一個默認的R8編譯器的版本,經過測試自帶的1.5.69版本存在很多問題,比如混淆后的文件有些kt的伴生對象找不到,原因是@MetaData的注解莫名被刪除,導致我又創建了一個修改字節碼的任務(利用javaassit將每個類頭部的注解@MetaData去除,使它變成java類),諸如此類的問題還很多,最終利用r8jar的命令去測試,發現2.0.100是最終的版本,越高的版本貌似去除了apply mapping的依賴,換句話說就是不管你加不加-applymapping,他都不起作用。
除了以上兩大需求,其實也可以創建動態生成混淆配置文件(proguard-rules.pro)的任務以及動態生成壓縮后的代碼包字母等,感興趣的可以自己研究自定義Gradle插件的相關知識。

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

推薦閱讀更多精彩內容

  • 一、瘦身優化及 Apk 分析方案介紹 1.1 瘦身優勢 我們首先來介紹下,為什么我們需要做 APK 的瘦身優化? ...
    凱玲之戀閱讀 827評論 0 0
  • 前言 最近項目開發中遇個Crash, 說refresh沒有實現找不到,但是本地都是可以的,然后對照了下發現是混淆出...
    無名指666閱讀 1,464評論 1 1
  • 1.混淆的作用 利用Proguard或者R8工具,對代碼進行重命名,并刪掉沒有被引用的類、字段或者方法。對無用資源...
    taoyyyy閱讀 6,597評論 0 4
  • 什么是混淆 代碼壓縮通過 ProGuard 提供,ProGuard 會檢測和移除封裝應用中未使用的類、字段、方法和...
    6He閱讀 2,958評論 0 0
  • 什么是代碼混淆 代碼混淆就是將代碼中的各種元素,如變量,方法,類和包的名字改寫成無意義的名字,增加項目反編譯后被讀...
    蝸牛家族史閱讀 5,193評論 1 4