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

保持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插件的相关知识。

你可能感兴趣的:(Android混淆进阶)